一直很奇怪,为啥没有close事件,终于在一次实验的时候发现:
1.启动一个客户端和服务端
2.关闭客户端,服务端会发生一个read事件,并且在read的时候抛出异常,来表示关闭
另外,这个事件会不断发生,就算从已准备好的集合移除也没有,必须将该channel关闭或者调用哪个该key的cancel方法,因为SelectionKey代表的是Selector和Channel之间的联系,所以在Channel关闭了之后,对于Selector来说,这个Channel永远都会发出关闭这个事件,表明自己关闭了,直到从该Selector移除去
3.服务端关闭,client端在write的时候会抛出异常
java.io.IOException: 远程主机强迫关闭了一个现有的连接。
at sun.nio.ch.SocketDispatcher.write0(Native Method)
at sun.nio.ch.SocketDispatcher.write(Unknown Source)
NIO的SelectableChannel关闭的一个问题
如果在取消SelectionKey(这时候只是加入取消的键集合,下一次select才会执行)后没有调用到selector的select方法(因为Client一般在取消key后,我们都会终止调用select的循环,当然,server关闭一个注册的channel我们是不会终止select循环的),那么本地socket将进入CLOSE-WAIT状态(等待本地Socket关闭)。
简单的解决办法是在 SelectableChannel.close方法之后调用Selector.selectNow方法
Netty在超过256连接关闭的时候主动调用一次selectNow
NIO就绪处理之OP_WRITE
一开始很多人以为write事件,表示在调用channel的write方法之后,就会发生这个事件,然后channel再会把数据真正写出,但是实际上,写操作的就绪条件为底层缓冲区有空闲空间,而写缓冲区绝大部分时间都是有空闲空间的,所以当你注册写事件后,写操作一直是就绪的,选择处理线程全占用整个CPU资源。所以,只有当你确实有数据要写时再注册写操作,并在写完以后马上取消注册,一般的,Client端需要注册OP_CONNECT,OP_READ;Server端需要注册OP_ACCEPT并且连接之后注册OP_READ
当有数据在写时,将数据写到缓冲区中,并注册写事件。
publicvoid write(byte[] data) throws IOException {
writeBuffer.put(data);
key.interestOps(SelectionKey.OP_WRITE);
}
注册写事件后,写操作就绪,这时将之前写入缓冲区的数据写入通道,并取消注册。
channel.write(writeBuffer);
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
大部分情况下,其实直接用write方法写就好了,没必要用写事件。
读不满
因为我们的数据都是偏业务性的,比如使用开头一个字节来表示后面数据的长度,接着就会等待读取到那么多数据,但是TCP是流式的协议,100字节的数据可能是一段段发送过来的,所以在没有读到完整的数据前需要等待。
这时候可以将buffer attach到key上,下次read发生的时候再继续读取
写不出去
在发送缓冲区空间不够的情况下,write方法可能会返回能够写出去的字节数,比如只剩50字节,你写入100字节,这时候write会返回50,即往缓冲区写入了50字节
在网络较好的情况下,这应该是不太可能发生的,一般都是网络有问题,重传率很高
详细的情况可以参考:java nio对OP_WRITE的处理解决网速慢的连接
while (bb.hasRemaining()) {
int len = socketChannel.write(bb);
if (len < 0) {
throw new EOFException();
}
}
由于缓冲区一直蛮,下面的代码会一直执行,占用CPU100%,因此推荐的方式如下
while (bb.hasRemaining()) {
int len = socketChannel.write(bb);
if (len < 0){
throw new EOFException();
}
if (len == 0) {
selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE);
mainSelector.wakeup();
break;
}
}
如果返回0,表示缓冲区满,那么注册WRITE事件,缓冲区不满的情况下,就会触发WRITE事件,在那时候再写入,可以避免不要的消耗。
Selector返回的key集合非线程安全
Selector.selectedKeys/keys 返回的集合都是非线程安全的
Selector.selectedKeys返回的可移除
Selector.keys 不可变
对selected keys的处理必须单线程处理或者适当同步
正确注册Channel和更新interest
直接注册不可吗?
channel.register(selector, ops, attachment);
不是不可以,效率问题
至少加两次锁,锁竞争激烈
Channel本身的regLock,竞争几乎没有
Selector内部的key集合,竞争激烈
更好的方式:加入缓冲队列,等待注册,reactor单线程处理
同样,SelectionKey.interest(ops)
在linux上会阻塞,需要获取selector内部锁做同步
在win32上不会阻塞
屏蔽平台差异,避免锁的激烈竞争,采用类似注册channel的方式:
if (this.isReactorThread()) {
key.interestOps(key.interestOps() | SelectionKey.OP_READ);
}
else {
this.register.offer(new Event(key,SelectionKey.OP_READ));
selector.wakeup();
}
正确处理OP_WRITE
OP_WRITE处理不当很容易导致CPU 100%
OP_WRITE触发条件:
前提:interest了OP_WRITE
触发条件:
socket发送缓冲区可写
远端关闭
有错误发生
正确的处理方式:
仅在已经连接的channel上注册
仅在有数据可写的时候才注册
触发之后立即取消注册,否则会继续触发导致循环
处理完成后视情况决定是否继续注册
没有完全写入,继续注册
全部写入,无需注册
正确取消注册channel
SelectableChannel一旦注册将一直有效直到明确取消
怎么取消注册?
channel.close(),内部会调用key.cancel()
key.cancel();
中断channel的读写所在线程引起的channel关闭
但是这样还不够!
key.cancel()仅仅是将key加入cancelledKeys
直到下一次select才真正处理
并且channel的socketfd只有在真正取消注册后才会close(fd)
后果是什么?
服务端,问题不大,select调用频繁
客户端,通常只有一个连接,关闭channel之后,没有调用select就关闭了selector
sockfd没有关闭,停留在CLOSE_WAIT状态
正确的处理方式,取消注册也应当作为事件交给reactor处理,及时wakeup做select
适当的时候调用selector.selectNow()
Netty在超过256连接关闭的时候主动调用一次selectNow