之前的一篇文章中,我们的代码其实已经实现了简单的多路复用,由一个线程来管理多个连接和通道数据,但是我们同时也看到了自己实现的程序的缺陷,无用轮询过多,导致响应和数据接收过慢。所以才有了选择器Selector。这一篇可能更偏理论。
什么是选择器?先分析字面意思,首先它是一个容器,作用是选择。这么理解没毛病吧,那么既然是容器,那么就相当于是一个集合,我们需要将东西放进去,然后根据条件拿出我们想要的,这样才叫做选择。而操作系统提供的选择器提供了选择执行已经就绪的任务的能力,这使得多元 I/O 成为可能。所以牛的不是NIO,牛的是操作系统,原本这个选择,轮询工作是我们自己来实现的,现在交给了操作系统,并且操作系统实现的很好。
我们需要将之前创建的一个或多个可选择的通道注册到选择器对象中,一个表示通道和选择器对应关系的键将被返回,选择器会记住你所关心的通道的具体的操作,然后进行追踪,针对性的对符合条件的通道进行选择。
那么我们想要知道那些通道已经准备好,有两种方式可以选择,之前我们自己实现的是第一种,对所有通道进行周期性的轮询,然后找出已经准备好的通道,这也是选择器的一种实现方式,而操作系统层面的选择器实现的明显是另外一种:线程挂起,就绪激发。
选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道,线程才会被激发,并处理对应的通道,当然这一切有个前提,那就是通道必须是非阻塞的。
那么实现选择器的三大基石
①Selector选择器对象,用于管理需要被选择的通道对象,监听需要被选择的操作条件。
②SelectableChannel,只有现实了SelectableChannel接口的通道对象才能被注册到选择器Selector上并指定需要被监听的操作。翻看之前的类图可以发现,套接字相关的通道都实现了这一接口。而文件字节通道并没有实现,以为文件通道无法做到非阻塞。
③选择键(SelectionKey),选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被
SelectableChannel.register( ) 返回并提供一个表示这种注册关系的标记。该注册关系包含所关心的通道操作,以及通道已经准备好的操作。
为了更好理解,这三者的关系可以理解为是三张表,选择器可以有多个,通道可以有多个,同一个通道可以被多个选择器监听,一个选择器也可以监听多个通道。而SelectionKey存储的则是双方的对应关系,selector_id 和 channel_id是联合主键。