netty由浅入深 – 阻塞i/o

阻塞式I/O应该算是从Java学习以来用的最多的功能了, 无论是文件读写, 网络I/O, HttpServlet的response, 都是靠java.io包下的API完成的. 而java.io包, 便是阻塞式I/O的实现. 在了解Netty之前, 可以先了解为什么不用阻塞式I/O, 阻塞式I/O的缺点是什么, 毕竟没有对比就没有优劣之分.

阻塞式Socket I/O

代码示例

在Java中, 创建一个Socket服务端的代码通常是这样的:

1
2
3
ServerSocket server = new ServerSocket(8080);
Socket socket = server.accept();

读取数据通常是这样的:

1
2
3
4
5
6
byte[] data = byte[1024];
int len = 0;
while(len >= 0){
    len = socket.read(data);
    
}

分析

系统资源损耗

ServerSocket.accept()执行后, 线程会陷入阻塞, 直到监听端口接收到socket连接请求. 如果要执行其他任务, 就需要新开线程, 这自然会消耗系统资源. 在量少的时候可能没太多问题. 但是, 如果访问量大了呢. 这就需要进行优化了.

  1. 将接收到的Socket放入子线程处理.
    但这在高访问量的时候并没有什么用, 每个访问开启一个线程, 系统资源占用会非常严重.
  2. 将接收到的Socket封装成任务放入线程池处理.
    将任务放入一个队列中排队, 让线程池中可复用的线程来执行任务.这可以缓解一下系统资源占用的问题. 但是这只是解决了线程数量带来的系统开销问题, 还是没有从根本上解决效率问题.

线程阻塞损耗

这里针对上述的方案2讨论.
在这之前, 可以举个现实中的例子来感受一下.

一个服务窗口在办业务, 需要提供一些证件.
有的人不知道, 在窗口前一张一张地从包里拿出来递过去.
有的人早就知道, 手持证件一直在排队.

例子讲完了, 现在来解释一下吧. 这个例子里面, 服务窗口是线程池中的线程, 证件是传输的数据. 前者在窗口前挤牙膏, 后者的明明能马上处理却不得不等待. 于是后面的所有人都被浪费了前者取证件的时间. 如果想提高效率, 那可以让拿证件的人先在旁边拿, 后面准备好证件的人先办业务, 等拿齐了证件再办业务. 这样可以保证任务快速地被处理, 减少队伍中所有任务的等待时间.

为什么要讲这个例子呢? 我们先看InputStream.read()系列函数的描述, 其中有这么一句:

This method blocks until input data is available, …

其实不止read如此, Stream API大部分方法都是会造成阻塞的.

如果流中暂时没有数据, 这个函数会阻塞, 这就相当于是在窗口前面掏证件了. 如果在连接的网络情况不好的情况下, 是有可能出现大量的丢包, 流中长时间接收不到数据的情况的. 这样一阻塞, 有可能后面排队的Socket中的数据已经发送到了, 正等待接收. 这根上面讲的排队例子很相似, 后面的人准备好了材料却还要等. 虽然有线程池, 但线程池也是有线程上限的, 如果线程池中有一个阻塞, 那就是浪费了资源, 因为流中有数据的任务可以保证在read方法上不发生阻塞. 然而线程池是做不到让阻塞的任务中途休息, 让其他线程来执行的, 所以只能让阻塞线程一直阻塞下去, 浪费线程资源.

线程模型

总结

由上面一些问题可以看出, 网络I/O的效率问题主要就出在网络上. 向外写数据是很简单的事, 因为数据全都在自己的手上, 只要将其写到连接中即可. 但是, 读数据确是有些麻烦的, 主要在它的不稳定上, 由于网络原因, 不一定能及时收到数据, 所以在没有数据时的处理方式是使用的关键. 阻塞式的处理就是在那干等, 算是最差的处理方式了.