V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
amiwrong123
V2EX  ›  Java

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

  •  
  •   amiwrong123 · 2020-03-21 15:29:06 +08:00 · 2764 次点击
    这是一个创建于 1744 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如果检测到连接断开,那么 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();
                }
            }
        }
    }
    
    
    • 如上这种,先运行服务端、客户度,再停止服务端。发现服务端一直有 read 事件过来,循环不停执行。
                        } catch (IOException e) {
                            //canReadChannel.close();
                            //sk.cancel();
                            System.out.println("检测到远程连接断开");
                            e.printStackTrace();
                            continue;
                        }
                    }
    
                    iterator.remove();
    
    • 但如果改成如上这种,select 循环检测到一次 read 事件并抛出异常后,循环就退出了。然后整个程序都退出了。这是为啥啊
                        } catch (IOException e) {
                            canReadChannel.close();
                            sk.cancel();
                            System.out.println("检测到远程连接断开");
                            e.printStackTrace();
                            continue;
                        }
                    }
    
                    iterator.remove();
    
    • 如果改成如上这种,select 循环检测到一次 read 事件并抛出异常后,下一次循环继续,但会阻塞在 select 那里。

    • 主要想请教下上面三种情况的差异的原因。

    • 还有就是,正确处理连接断开的方法就是:canReadChannel.close();sk.cancel(); 吗

    5 条回复    2020-03-22 01:51:24 +08:00
    arloor
        1
    arloor  
       2020-03-21 15:32:12 +08:00
    异常也是可读事件的一种,如果出现异常了,那么可以关闭 channel 并且取消对该 channel 可读事件的监听
    guyeu
        2
    guyeu  
       2020-03-21 16:01:17 +08:00
    程序退出是因为运行到了终点。。。你可以起一个线程池来处理事件。。
    aguesuka
        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 之前一定要根据协议知道这一次要读的长度,等到满足长度以后再做相关操作。
    amiwrong123
        4
    amiwrong123  
    OP
       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
        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()这个方法就是把所有键取消了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1365 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 17:06 · PVG 01:06 · LAX 09:06 · JFK 12:06
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.