关于套接字编程,我们可以使用它来完成网络通信,而关于使用socket来实现多客户端连接服务器,我记录了以下东西。

基本思路

实例化并等待连接

服务器实例化serversocket,并监听本机自定义端口,等待客户端的连接,在这里,当然我们可以为其开一个线程来完成其工作。
使用线程?比方说,我们写的是一个web项目,需要如tomcat等服务器启动时就开启socket服务器,我们应该:

  1. 在web.xml添加监听器

    1
    2
    3
    <listener>   
    <listener-class>com.util.InitServerUtil</listener-class>
    </listener>
  2. 编写监听器,让其实现ServletContextListener接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void contextDestroyed(ServletContextEvent sce) {  
    Server.stopServer();
    }
    public void contextInitialized(ServletContextEvent sce) {
    if (Server.startServer())
    System.out.println("\nRun Success!\n");
    else
    System.out.println("Run failed!");
    }
  3. Server.startServer()方法即是开启服务器的代码,而在这里,即是线程意义所在,如果不使用线程,那么startServer()方法代码即是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void startServer(){   
    serverSocket = new ServerSocket(22222);
    while (true) {
    socket = serverSocket.accept();
    String Name = "Mac" + ++count;// 服务器给用户暂定硬件名
    ClientConnection ccon = new ClientConnection(socket, macName);//为socket分配线程接收服务器消息
    Hmap.put(Name, ccon);// 将该客户端连接加入哈希map
    }
    }
  4. 很明显,该方法里面存在一个while的死循环,那么导致的后果是,该方法一直执行while循环而永不返回,它不返回,那么Server.startServer()代码永远阻塞在这儿,导致于tomcat服务器一直停在这儿,而间接开启失败。而如果我们使用了线程:

    1
    2
    3
    4
    public static void startServer(){   
    listen = new Thread(listenTask);//listenTask-线程执行的任务,用于开启socket服务器,而代码即是上面的while循环内容
    listen.start();
    }
  5. 那么,startServer()方法仍然返回,它的任务交给线程去执行了,即是异步。这样,tomcat也就启动成功了。

进行连接

客户端实例化socket并设定要连接的服务器地址和端口(IP+port),进行连接。

保存连接

客户端连接上服务器,服务器执行连接成功后的代码(即accept()方法之后),这时应该将accept()返回的socket对象连同对应的客户端标识信息,保存起来(如使用ConcurrentHashMap来保存)。同时,应该开一个线程用来完成一个任务—①时刻准备接收客户端发送来的消息,也就是说在服务器为每个客户端都开一个线程用来完成接收客户端消息的任务。
为何使用ConcurrentHashMap?可查看http://blog.csdn.net/xuefeng0707/article/details/40834595

数据收发

对应地,每个客户端也应该开一个线程用来时刻接收服务器发送来的消息。
客户端使用连接服务器的socket,调用socket的输出流进行写操作向服务器写数据
客户端的接收服务器消息线程完成服务器数据读操作
服务器为每个客户端开的线程用来完成客户端向服务器写数据的任务
服务器将保存的对应客户端的socket取出,调用socket的输出流进行写操作向客户端写数据

数据格式

关于上面的“①时刻准备接收客户端发送来的消息”

对于socket,有的人采用字符流进行数据传送,有的人采用字节流进行传输。各自的优劣好坏,我浅谈一下:

字符流传输

1
2
3
4
5
6
7
while (true) {  
in = new DataInputStream(socket.getInputStream());
String str;
if ((str = in.readUTF()) != null) {
message= str;
}
}

这里将socket的inputstream进一步封装为DataInputStream,进而调用其readUTF()方法。此时,在while死循环中,程序执行到in.readUTF()时,如果客户端发送的数据为空,那么程序会阻塞到该位置,直到socket中有数据为止,而不会继续执行下面的代码了,即不会再一直while循环了,使用这样的方式,有一个好处,那就是因为这个阻塞会导致程序能时刻知道你客户端是否一直和服务器连上的,一旦断开,会引发异常 ,这样,我们就可以通过捕捉异常的方式来时刻知道,客户端是否处于连接状态。

字节流传输

1
2
3
4
5
6
7
while (true) {  
in = socket.getInputStream();
buff = new byte[in.available()];
if (in.read(buff) > 0) {
message= new String(buff);
}
}

此时,在while无限循环的情况下,while循环体中的代码会被无限循环,而每次循环时都进行一次if判断,如果客户端传送的字节数据>0,即将字节数据组合成字符串,即是客户端发送的消息,否则继续循环。这里,如果用户没有传送数据过来,in.read(buff)返回的是0,这样的一直判断会导致cpu将所有的时间片用于这个while循环,即连上一个客户端,cpu就爆满了,当然如果是四核处理器,那么连上四个客户端,同样GG。对于此,可使用线程休眠来解决:

1
Thread.sleep(700);

一般休眠时间在700毫秒及以下即可保证时刻都能接收到客户端的消息。而对应想要时刻知道客户端是否处于连接状态,就只能另辟蹊径,比如使用心跳机制:

1
2
3
4
5
6
7
8
9
10
public Boolean isRemoteClose(Socket socket) {   
try {
socket.sendUrgentData(0);// 发送1个字节的紧急数据,默认情况下,服务器端没有开启紧急数据处理,不影响正常通信
return false;
} catch (SocketException se) {
return true;
} catch (IOException e) {
return true;
}
}

需要注意的是:对于windows系统,处于安全考虑,对于心跳机制是持有不支持态度的,即对于时刻发生心跳包是会被阻止的,一般发生了17个心跳包左右,就不在允许你再发送了。因此,只能在每次进行通信的时候,进行一次发送数据的试探,这个数据最好是一个没用的数据,如果可以发送成功,表示客户端是连接上的。

相关demo在:http://download.csdn.net/detail/localhost01/9538007