客户端服务端通信——传统 IO 方式

任务:客户端每隔两秒发送一个带有时间戳的 “hello, world!” 消息给服务端,服务端收到消息之后打印该信息。

我们新建两个类:IOServerIOClient 来分别表示服务端和客户端。

IOServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class IOServer {

public static void main(String[] args) throws Exception {

ServerSocket serverSocket = new ServerSocket(3333);

new Thread(() -> {
while (true) {
try {
Socket socket = serverSocket.accept();
new Thread(() -> {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();

} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}

}

IOServer 中我们做了几件事:

  • 创建了一个 ServerSocket 来监听 3333 端口并创建一个线程(第5行),线程里面不断调用阻塞方法 serversocket.accept(); 获取新的连接(第10行)
  • 一旦获取到新的连接,给每个连接创建一个新的线程,这个线程负责从该连接中读取数据(第11行),以字节流(InputStream)的方式读取数据(15-18行)。

IOClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class IOClient {

public static void main(String[] args) {
new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1", 3333);
while (true) {
try {
socket.getOutputStream().write((new Date().getTime() + ": hello, world!").getBytes());
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}

}

IOClient 中我们每隔2秒向服务端发送一条格式为 timestamp: hello,world! 的消息。

通过 IOServerIOClient 我们基于传统 IO 模型成功完成了任务,但仅限于完成了任务。

由于针对每一个 IOClient 实例,IOServer 都会创建一个新线程来维护连接和处理数据,这在客户端(IOClient)实例较少的时候可以正常工作。一旦客户端增多,服务端(IOServer)可能要支撑成千上万的连接,传统 IO 模型在此时就会显得比较吃力:

  • 每一个客户端都需要一个新线程,会导致线程资源受限;而线程资源是操作系统中极其重要的资源,客户端每隔2秒才发送一次消息,这意味着大量的服务端线程处理阻塞状态,这是非常严重的资源浪费
  • 一旦客户端数据激增,在服务端实例一定的情况下,将会导致服务端线程爆炸。而单机可认为 CPU 数是固定的,因此大量 CPU 时钟会被用来进行线程切换,而实际处理工作的时钟减少,造成性能下降
  • 另一方面,数据的读写是以字节流(InputStream、OutputStream 为单位进行的),存在效率问题

如果要在客户实例比较多的场景完成这个任务,我们需要解决上面的三个问题。