高性能聊天系统

  作者:板桥banq

上页

1.2.6  非堵塞I/O

传统网络系统运行的性能瓶颈通常在I/O读写,包括对端口和文件的操作。过去,当打开一个SocketI/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个重要的类:SelectorSelectionKey Channel

Selector 实际是一个Reactor类,主要监视管理一系列SelectionKey,每个SelectionKey代表一种ChannelSelector之间的关系。在某个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发生联系的ChannelSelectableChannel常用的有两种:SocketChannelServerSocketChannel,这两种SelectableChannel的区别是可注册的事件不一样:

·       ServerSocketChannel 对应事件:OP_ACCEPT

·       SocketChannel 对应事件:OP_CONNECTOP_READOP_WRITE

后者一般使用在服务器端,可以从中知道有无可以接受的客户端连接。创建ServerSocketChannel如下:

ServerSocketChannel  sc = ServerSocketChannel.open();

创建一个ServerSocketChannel后,需要将其和主机端口进行绑定,例如和192.168.0.18009端口绑定:

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所示。

nio

1-7  非堵塞I/O原理图

1-7基本展示了非堵塞I/O的原理结构这部分结构主要实现了Reactor模式中的事件到达部分当有读或写等任何实现注册的事件发生时可以从Selector中获得相应的SelectionKeySelectionKey可以找到相应的Channel从而能获得客户端发送过来的数据。

 

下页