分类:
2010-09-15 23:19:12
nio的select()的时候,只要数据通道允许写,每次select()返回的OP_WRITE都是true。所以在nio的写数据里面,我们在每次需要写数据之前把数据放到缓冲区,并且注册OP_WRITE,对selector进行wakeup(),这样这一轮select()发现有OP_WRITE之后,将缓冲区数据写入channel,清空缓冲区,并且反注册OP_WRITE,写数据完成。
这里面需要注意的是,每个SocketChannel只对应一个SelectionKey,也就是说,在上述的注册和反注册OP_WRITE的时候,不是通过channel.register()和key.cancel()做到的,而是通过key.interestOps()做到的。代码如下:
public void write(MessageSession session, ByteBuffer buffer) throws ClosedChannelException {
SelectionKey key = session.key();
if((key.interestOps() & SelectionKey.OP_WRITE) == 0) {
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
}
try {
writebuf.put(buffer);
} catch(Exception e) {
System.out.println("want put:"+buffer.remaining()+", left:"+writebuf.remaining());
e.printStackTrace();
}
selector.wakeup();
}
.....
while(true) {
selector.select();
.....
if(key.isWritable()) {
MessageSession session = (MessageSession)key.attachment();
//System.out.println("Select a write");
synchronized(session) {
writebuf.flip();
SocketChannel channel = (SocketChannel)key.channel();
int count = channel.write(writebuf);
//System.out.println("write "+count+" bytes");
writebuf.clear();
key.interestOps(SelectionKey.OP_READ);
}
}
......
}
要点一:不推荐直接写channel,而是通过缓存和attachment传入要写的数据,改变interestOps()来写数据;
要点二:每个channel只对应一个SelectionKey,所以,只能改变interestOps(),不能register()和cancel()。
2、nio的临时selector的使用,了解grizzly的都知道,Grizzly框架有一个比较与众不同的地方在于使用临时selector注册channel进行读或者写。这个带来什么好处呢?一个是,通常我们可能将read派发到其他线程中去,如果一次没有读完,那么就得继续注册OP_READ到主selector上;注意,nio在一些平台上有个问题,就是SelectionKey.interestOps方法跟Selector.select方法会有并发冲突,产生奇怪的现象,因此,你会看到大多数的nio框架都会保证SelectionKey.interestOps跟Selector.select的调用在同一个线程;在没有读完继续注册这个场景下,免不了线程间的context switch,如果采用一个临时selector注册并读取,就可以避免这个切换开销。另外,对于write调用,通常你可能这样写:
在负载比较高的时候,write返回0的次数会越来越多,while循环将空耗多次导致CPU占用偏高,这个问题在win32上比较严重,同样可以采用临时selector的解决(Cindy2.x是留在队列,等待下次写)。下例是采用临时Selector进行读的例子:
<> 这样的方式,某种意义上可以认为是non-blocking模式下的阻塞读,在网络条件稳定的情况下(比如内网),能带来比较高的效率。