如果检测到连接断开,那么 select 循环就会不断有 read 过来。但我现在对这种情况,有点疑问。
package NonBlocking;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class TestDisconnectClient {
static SocketChannel socketChannel = null;
static Selector selector = null;
public static void main(String[] args) throws IOException {
socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8888));
socketChannel.configureBlocking(false);
selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_READ);
int result = 0; int i = 1;
while((result = selector.select()) > 0) {
System.out.println(String.format("selector %dth loop, ready event number is %d", i++, result));
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
if (sk.isReadable()) {
System.out.println("有数据可读");
SocketChannel canReadChannel = (SocketChannel)sk.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
while (canReadChannel.read(buf) > 0) {
buf.flip();
System.out.println(new String(buf.array()));
buf.clear();
}
} catch (IOException e) {
//canReadChannel.close();
//sk.cancel();
System.out.println("检测到远程连接断开");
e.printStackTrace();
//continue;
}
}
iterator.remove();
}
}
}
}
} catch (IOException e) {
//canReadChannel.close();
//sk.cancel();
System.out.println("检测到远程连接断开");
e.printStackTrace();
continue;
}
}
iterator.remove();
} catch (IOException e) {
canReadChannel.close();
sk.cancel();
System.out.println("检测到远程连接断开");
e.printStackTrace();
continue;
}
}
iterator.remove();
如果改成如上这种,select 循环检测到一次 read 事件并抛出异常后,下一次循环继续,但会阻塞在 select 那里。
主要想请教下上面三种情况的差异的原因。
还有就是,正确处理连接断开的方法就是:canReadChannel.close();sk.cancel(); 吗
1
arloor 2020-03-21 15:32:12 +08:00
异常也是可读事件的一种,如果出现异常了,那么可以关闭 channel 并且取消对该 channel 可读事件的监听
|
2
guyeu 2020-03-21 16:01:17 +08:00
程序退出是因为运行到了终点。。。你可以起一个线程池来处理事件。。
|
3
aguesuka 2020-03-21 19:45:49 +08:00
第一种情况
read()的注释 Returns: The number of bytes read, possibly zero, or -1 if the channel has reached end-of-stream read 返回-1 的时候就是已经读完了,下一次 read 就会抛已经关闭的异常。同时你忘了考虑返回 0 的情况。 第二种情况 select()的注释 Returns: The number of keys, possibly zero, whose ready-operation sets were updated select()返回 0 是退出的原因。为什么返回 0 我没法复现。 两个方法都隐式调用了对方,而且有标志位会只调用一次。不过 SelectionChanncel#cancel()是懒式,建议只调用 SocketChannel#close()。 客服端新建连接要用无参的 opne 方法,先设置为非阻塞然后注册再连接。 Selector#select()可能返回 0,你需要用 while true 。 建议不要用 iterator 而是用 jdk11 的 Selector#select(Consumer<SelectionKey> action),或者模仿写一个:拷贝原来的 set,把原来的 set clear,然后循环被拷贝的 set 。 记得考虑 read 返回 0 和-1 的情况,返回-1 记得关闭连接。 不要用 while read,如果出现数据比 buffer 大,多半是 buffer 太小了。 read 之前一定要根据协议知道这一次要读的长度,等到满足长度以后再做相关操作。 |
4
amiwrong123 OP @aguesuka
非常感谢回复。 我用的服务端是这个代码 https://paste.ubuntu.com/p/hS9cQBdFyz/ 发现每次都得手动停止掉服务端,来测试,还挺麻烦。。 第一种情况: 程序上呢,是每次 iterator 循环最后都 remove 掉 key 了。效果上呢,循环不断执行,每次都是 read 事件,每次 read 都抛异常。(抛异常说明对方已经断连接了,所以我也应该断连接,但我没有。所以,即使我每次 iterator 循环最后都 remove 掉 key,下一次 select 还是会选出 read 事件。 不知道我的理解对不) 第二种情况:程序上呢,是如果发现 read 抛出异常就 continue 循环,即不执行 remove key 了。效果上呢,第一次抛出异常,然后直接退出循环。(这个真没想通,为毛不 remove key,反而会导致下次 select 会直接返回 0 ) 第三种情况:程序上呢,是如果发现 read 抛出异常就 continue 循环,并且 continue 之前,close channel,cancel key 。效果上呢,第一次抛出异常,然后阻塞在下一次 select 。(最后一种情况,看起来是最正常的情况,或者说,是想要的效果) 你说 SelectionKey#cancel 和 SocketChannel#close 这两个都会隐式调对方,这看源码能看出来吗。。SelectionKey#cancel 是懒汉模式的意思,只是说不会马上执行呗。 拷贝原来的 set,把原来的 set clear,然后循环被拷贝的 set 。之所以这么做,是因为 select 出来的集合 如果不显示 remove,集合永远不会删除掉元素呗。 所以不如先这样呗。 |
5
aguesuka 2020-03-22 01:51:24 +08:00 1
第一种情况如 2 楼所说,第二点:Selector#select() 返回的是更新的 key 的数量。
我错了,取消 key 会在下一次 select 的时候执行这段代码 if (!ch.isOpen() && !ch.isRegistered()) ((SelChImpl)ch).kill(); 不能保证一定会关闭连接。不过关闭连接的时候,AbstractSelectableChannel#implCloseChannel()这个方法就是把所有键取消了 |