NIO非阻塞通信简介及基于TCP的非阻塞通信
通信方式简述
阻塞式的通信
以基于TCP的通信方式来说明:创建ServerSocket
(或ServerSocketChannel
)、调用accept()
方法获取客户端的Socket
连接、通过Socket
通信、关闭Socket
连接。
对于阻塞式的通信,最影响效率的其实是SocketChannel.read()
或通过Socket
获得的输入流的InputStream.read()
。当服务端调用这些输入方法时,线程就会进入阻塞状态,直到客户端发来数据才被唤醒。
如果用单线程的方式实现上述流程,资源的利用率极低。线程调用read()
方法后,进入阻塞状态,如果客户端迟迟没有数据,服务端就会一直被阻塞,而没办法进行其他操作。
第一种解决方法就是使用多线程的方式,一个线程仅负责accept()
接收请求,将接收到的请求分配给其他线程,这样,调用read()
时,处理请求的线程不会被阻塞,这样就还能接收新的请求,提高了并发量与资源利用率。
第二种解决方法就是使用非阻塞的通信方式。
非阻塞式的通信
先来考虑一下为什么会有阻塞通信?和本地I/O一样,本地I/O也是需要阻塞,当通过系统调用I/O完成后,唤醒被阻塞的进程(线程)。问题就在于:什么时候I/O完成,对于阻塞式的I/O来说,当进程被唤醒时就说明I/O完成。
现在再来想一下本地I/O最低效的方式,循环的访问I/O完成的状态标志,当状态标志指示I/O完成后,说明I/O结束。这就是一个非阻塞的过程,当调度到这个进程(线程),CPU在循环的访问一个标志位来判断I/O是否完成
把这个思想迁移到网络通信中,现在有多个连接,怎样判断哪个连接有数据过来?给这些连接一个标志,用来表示是否有数据,服务端循环的访问每个连接的这个标志,就可以知道哪个连接的数据已经准备好了
问题貌似解决了,但是想一下,如果所有连接都没有数据,那CPU一直再做“无用功”,浪费资源。
为了避免这种资源浪费,可以使用一个选择器
,这个选择器的作用就是把有数据的连接都挑出来,轮询的时候保证每个连接都是有数据需要处理的;如果所有连接都没有数据,选择器
被阻塞,直到有某个连接的数据准备好。
关于非阻塞式的通信的基本思想,已经阐述完成,带着这些简单的理解,继续了解NIO提供的非阻塞通信。
Java提供的抽象
上面分析非阻塞I/O时,有两个很重要的点:被选择器选择的连接
和选择器
。
NIO提供了它们的抽象:java.nio.channels.SelectableChannel
和Selector
SelectableChannel
下面给出一段官方文档描述的翻译:
SelectableChannel
可以通过Selector
复用。
SelectableChannel
的实例为了可以被Selector
使用,必须使用register()
方法注册到Selector
的实例;该方法返回一个SelectionKey
对象,这个对象代表了这个通道注册到一个选择器中。
SelectableChannel
是线程安全的。
看完这些解释如果不懂,可以先接着往下看,看到后面就会理解。
Selector
再谈谈向Selector
注册,在注册时,有两个参数,一个是Selector
,另一个是一个int
类型,它表示事件。
在上面一直用“读”/“输入”举例子,如果要让Selector
判断一个Selectable
是否可读,注册时仅需要调用SelectableChannel.register(Selector, SelectionKey.OP_READ)
SelectionKey.OP_READ
表示一个读事件,或者说让选择器判断通道是否可读。
非阻塞通信的使用-TCP
SelectableChannel
是一个抽象类,在使用时,没办法直接实例化,查看一下它的子类,巧了,有ServerSocketChannel
和SocketChannel
看到这两个,就想到了它们可以进行阻塞的通信,那怎么进行非阻塞通信?
1. 创建ServerSocketChannel
这一步和阻塞式的通信一致。
severSocketChannel = ServerSocketChannel.open();
2. 设置使用非阻塞式通信
severSocketChannel.configureBlocking(false);
默认情况下,为阻塞通信,也就是参数为true的情况。
3. 绑定端口
这一步和阻塞式的通信一致。
severSocketChannel.bind(new InetSocketAddress(PORT));
4. 创建Selector
selector = Selector.open();
5. 将ServerSocketChannel
的OP_Accept
事件注册到Selector
severSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
当有连接到来(accept队列中有连接请求),事件被触发,选择器选