NIO 中 检测到 channel 连接断开后的处理方法?

2020-03-21 15:29:06 +08:00
 amiwrong123

如果检测到连接断开,那么 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();
2753 次点击
所在节点    Java
5 条回复
arloor
2020-03-21 15:32:12 +08:00
异常也是可读事件的一种,如果出现异常了,那么可以关闭 channel 并且取消对该 channel 可读事件的监听
guyeu
2020-03-21 16:01:17 +08:00
程序退出是因为运行到了终点。。。你可以起一个线程池来处理事件。。
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 之前一定要根据协议知道这一次要读的长度,等到满足长度以后再做相关操作。
amiwrong123
2020-03-21 22:10:10 +08:00
@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,集合永远不会删除掉元素呗。 所以不如先这样呗。
aguesuka
2020-03-22 01:51:24 +08:00
第一种情况如 2 楼所说,第二点:Selector#select() 返回的是更新的 key 的数量。
我错了,取消 key 会在下一次 select 的时候执行这段代码
if (!ch.isOpen() && !ch.isRegistered())
((SelChImpl)ch).kill();
不能保证一定会关闭连接。不过关闭连接的时候,AbstractSelectableChannel#implCloseChannel()这个方法就是把所有键取消了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/654868

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX