高性能聊天系统
作者:板桥banq
1.2.6 非堵塞I/O
传统网络系统运行的性能瓶颈通常在I/O读写,包括对端口和文件的操作。过去,当打开一个Socket的I/O通道后,使用下列语句:
Socket socket = new Socket(url,port);
InputStream in = socket.getInputStream();
while(!Thread.interrupted()){
int byteRead = in.read();
…
}
其中,read()守候在端口边不断读取传输过来的字节内容,如果读取不到任何字节内容,read()也只能等待,这会使得整个程序系统被锁住,影响程序系统继续做其他事情。一个普遍的改进办法就是开设线程,让线程去等待或轮询。但是,这种做法相当耗费资源的。更主要的是,当线程轮询后,若有事件发生,只有等到线程下次轮询时才会知道。即没有一种这样的途径:当有事件发生时,能够主动发出通知。
在前面讨论中,非堵塞I/O 作为Reactor模式的实现,实际上提供了一种事件发生、自我激活、主动通知的机制。因此使用非堵塞I/O将大大提高系统的I/O处理性能。
非堵塞I/O中有3个重要的类:Selector、SelectionKey 和Channel。
Selector 实际是一个Reactor类,主要监视管理一系列SelectionKey,每个SelectionKey代表一种Channel和Selector之间的关系。在某个Channel上如果发生某种连接事件,Selector将会自动激活产生一个SelectionKey对象。即SelectionKey属于事件对象(Event Object),是动态的,每发生一个连接事件就产生一个SelectionKey对象。
从被激活的SelectionKey中,外界可以知道每个Channel发生的具体事件类型,这些事件包括:是否发生连接、是否可以读或者是否可以写等。
从以上分析可以看出,Selector有自我激活的能力。使用Selector时,只要告诉它需要关注的特定事件,Selector将会一直监视这种特定事件,一旦发生,就发出通知。类似火警警报器,一旦发现失火事件,立即会主动激活报警。
由于Selector只负责事件发生,不负责事件处理,事件处理是由开发者编制程序实现,因此,使用者需要自己建立一套获取发生事件的机制。
总之,非堵塞I/O的使用包括两大部分:注册事件和获取事件。下面将简单演示一下这两部分的使用实现。
首先,需要向Selector注册外界感兴趣的事件,创建Selector对象如下:
Selector selector = Selector.open();
或
Selector
selector = SelectorProvider.provider().openSelector();
Selector是一个观察者,那么它观察谁?当然是连接通道Channel,这种Channel是一种SelectableChannel,即可以和Selector发生联系的Channel。SelectableChannel常用的有两种:SocketChannel和ServerSocketChannel,这两种SelectableChannel的区别是可注册的事件不一样:
· ServerSocketChannel 对应事件:OP_ACCEPT。
· SocketChannel 对应事件:OP_CONNECT、OP_READ、OP_WRITE。
后者一般使用在服务器端,可以从中知道有无可以接受的客户端连接。创建ServerSocketChannel如下:
ServerSocketChannel sc = ServerSocketChannel.open();
创建一个ServerSocketChannel后,需要将其和主机端口进行绑定,例如和192.168.0.1的8009端口绑定:
InetSocketAddress address = new InetSocketAddress("192.168.0.1", 8009);
sc.socket().bind(address);
sc.configureBlocking(false); //设置为非堵塞
现在如果需要从这个ServerSocketChanne了解有无可接受的客户端连接,语法如下:
SelectionKey acceptKey
= sc.register( selector, SelectionKey.OP_ACCEPT )
上面一条语句是用Selector注册ServerSocketChannel实例,返回一个Key实例,通常SelectionKey对象都是线程安全型的,但是修改感兴趣的事件操作时,这个方法是被标记为同步的,即在调用interestOps()方法时会锁定一段时间,因此,实际应用中,如果有一个以上的线程来调用同一个Selector对象时,需要使用Selector.wakeup()来解锁。
以上非堵塞I/O的注册事件工作已经准备就绪,那么,在正常运行中,如果Selector发现了事先注册的事件,如何传递出来呢? 这就需要建立一个事件获取通道。
其实,获取事件时,只要执行语句selector.select(),这将触发系统内部自动检查所有使用这个Selector注册的Channel状态。如果在某个Channel发现有感兴趣事件发生,这条语句的返回结果将大于0,通过这个信息外界可以知道有网络连接事件发生了,那么又如何知道是哪个具体Channel发生的呢?
使用selector.selectedKeys()获取一个SelectionKey结果集,遍历这个结果集,通过每个SelectionKey就可以找到发生事件的Channel,这样可以从这个Channel进行读写数据,如图1-7所示。
图1-7 非堵塞I/O原理图
图1-7基本展示了非堵塞I/O的原理结构,这部分结构主要实现了Reactor模式中的事件到达部分,当有读或写等任何实现注册的事件发生时,可以从Selector中获得相应的SelectionKey,从SelectionKey可以找到相应的Channel,从而能获得客户端发送过来的数据。