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

如何理解 NIO 中 attach 以及如何正确注册 write 事件?

  •  
  •   amiwrong123 · 2020-03-22 00:10:33 +08:00 · 2144 次点击
    这是一个创建于 1500 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我开始以为 attach 是分读的附件和写的附件的,但写了测试代码发现并不是。 服务端代码:

    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.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    
    public class NonBlockingServer {
        public static void main(String[] args) throws IOException {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(new InetSocketAddress(8888));
    
            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);  //首先注册 ACCEPT 事件
    
            int result = 0; int i = 1;
            while(true) {  //遍历获得就绪事件
                result = selector.select();
                System.out.println(String.format("selector %dth loop, ready event number is %d", i++, result));
                if (result == 0) {
                    continue;
                }
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while(iterator.hasNext()){  //就绪事件可能不止一个
                    SelectionKey sk=iterator.next();
    
                    if(sk.isAcceptable()){  //如果是 ACCEPT,那么与之关联的 channel 肯定是个 ServerSocketChannel
                        System.out.println("服务端有 ACCEPT 事件就绪");
                        ServerSocketChannel ss = (ServerSocketChannel)sk.channel();
                        SocketChannel socketChannel = ss.accept();
                        socketChannel.configureBlocking(false);  //也切换非阻塞
                        socketChannel.register(selector, SelectionKey.OP_READ);  //注册 read 事件
                    }
                    else if(sk.isReadable()){    //如果是 READ,那么与之关联的 channel 肯定是个 SocketChannel
                        System.out.println("服务端有 READ 事件就绪");
                        SocketChannel socketChannel = (SocketChannel)sk.channel();
                        ByteBuffer buf=ByteBuffer.allocate(1024);
                        int len=0;
                        StringBuilder sb = new StringBuilder();
                        while((len=socketChannel.read(buf))>0){
                            buf.flip();
                            String s  = new String(buf.array(),0,len);
                            sb.append(s);
                            buf.clear();
                        }
                        //服务端开始响应消息
                        ByteBuffer readAtta = (ByteBuffer)sk.attachment();
                        if (readAtta != null) {
                            System.out.println("lasttime readAtta string is: "+new String(readAtta.array()));
                        } else {
                            System.out.println("lasttime readAtta is null ");
                        }
                        sk.attach(ByteBuffer.wrap(sb.toString().getBytes()));
                        sk.interestOps(sk.interestOps() | SelectionKey.OP_WRITE);
                        String sendStr = "您的消息'"+sb.toString()+"'我已经收到了";
                        System.out.println("接下来 attach 的是:"+sendStr);
                        sk.attach(ByteBuffer.wrap(sendStr.getBytes()));
                    }
                    else if(sk.isWritable()){
                        System.out.println("服务端有 WRITE 事件就绪");
                        SocketChannel socketChannel = (SocketChannel)sk.channel();
    
                        ByteBuffer writeAtta = (ByteBuffer) sk.attachment();
                        if (writeAtta != null) {
                            System.out.println("lasttime writeAtta string is: "+new String(writeAtta.array()));
                        } else {
                            System.out.println("lasttime writeAtta is null ");
                        }
    
                        sk.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE);
                    }
                    iterator.remove();
                    System.out.println("after remove key");
                }
            }
        }
    }
    
    

    客户端代码(这个不重要,就放链接里了): https://paste.ubuntu.com/p/58P39BQQTm/ 使用方法:客户端在控制台每输入一句话,再去服务端看执行效果:

    selector 1th loop, ready event number is 1
    服务端有 ACCEPT 事件就绪
    after remove key
    selector 2th loop, ready event number is 1
    服务端有 READ 事件就绪
    lasttime readAtta is null 
    接下来 attach 的是:您的消息'你好'我已经收到了
    after remove key
    selector 3th loop, ready event number is 1
    服务端有 WRITE 事件就绪
    lasttime writeAtta string is: 您的消息'你好'我已经收到了
    after remove key
    
    selector 4th loop, ready event number is 1
    服务端有 READ 事件就绪
    lasttime readAtta string is: 您的消息'你好'我已经收到了
    接下来 attach 的是:您的消息'他好吗'我已经收到了
    after remove key
    selector 5th loop, ready event number is 1
    服务端有 WRITE 事件就绪
    lasttime writeAtta string is: 您的消息'他好吗'我已经收到了
    after remove key
    
    • 可以发现,attach 其实针对的是一个对象。换句话说,它是针对的同一个 channel 的附件。这样理解对吗?
    • 是否可以认为,sk.interestOps(sk.interestOps() | SelectionKey.OP_WRITE);sk.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE);是正确的注册和反注册 write 事件的姿势?

    然后我进行服务端代码修改:

    • sk.interestOps(sk.interestOps() | SelectionKey.OP_WRITE);换成socketChannel.register(selector, SelectionKey.OP_WRITE)
    • sk.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE);换成socketChannel.register(selector, SelectionKey.OP_READ)

    再看服务端效果则是这样了:

    selector 1th loop, ready event number is 1
    服务端有 ACCEPT 事件就绪
    after remove key
    selector 2th loop, ready event number is 1
    服务端有 READ 事件就绪
    lasttime readAtta is null 
    接下来 attach 的是:您的消息'你好'我已经收到了
    after remove key
    selector 3th loop, ready event number is 1
    服务端有 WRITE 事件就绪
    lasttime writeAtta string is: 您的消息'你好'我已经收到了
    after remove key
    
    
    selector 4th loop, ready event number is 1
    服务端有 READ 事件就绪
    lasttime readAtta is null 
    接下来 attach 的是:您的消息'他好吗'我已经收到了
    after remove key
    selector 5th loop, ready event number is 1
    服务端有 WRITE 事件就绪
    lasttime writeAtta string is: 您的消息'他好吗'我已经收到了
    after remove key
    

    可以发现:

    • 经过socketChannel.register(selector, SelectionKey.OP_READ);后,下一次再执行 attachment,附件就丢了,变成 null 了。
    • 但经过socketChannel.register(selector, SelectionKey.OP_WRITE);后,下一次再执行 attachment,附件却不会丢。 这是为什么呢?
    6 条回复    2020-03-22 12:39:35 +08:00
    az467
        1
    az467  
       2020-03-22 11:33:37 +08:00
    读文档呀。

    Selector 持有两个 Set,Keys 和 SelectedKeys,后者只是前者的子集。
    每次调用 select(),会把前者中的部分添加到后者中,而不是重复注册生成新的 SelectionKey 。
    register()也是一样的,如果已经在给定的 selector 注册过,那么方法只会对 interestOps 和 attachment 进行覆盖。

    key.attach()会设置 key 的 attachment 字段,而一个 key 和一个 channel 绑定,所以你的理解是正确的。

    socketChannel.register(selector, SelectionKey.OP_WRITE); 👈

    String sendStr = "您的消息'"+sb.toString()+"'我已经收到了";

    System.out.println("接下来 attach 的是:"+sendStr);

    sk.attach(ByteBuffer.wrap(sendStr.getBytes())); 👈

    可以看出,你调用 attach()在 register()之后,所以 attachment 不会“丢失”。
    amiwrong123
        2
    amiwrong123  
    OP
       2020-03-22 11:59:23 +08:00
    @az467
    好吧,大概懂了。等会我再去仔细看文档。
    但有个地方没想通,就是第二种运行效果,为啥
    ```
    selector 4th loop, ready event number is 1
    服务端有 READ 事件就绪
    lasttime readAtta is null
    接下来 attach 的是:您的消息'他好吗'我已经收到了
    after remove key
    ```
    为什么经过 socketChannel.register(selector, SelectionKey.OP_READ);后(因为上一次执行了 WRITE 事件,然后在 WRITE 事件里,执行了 socketChannel.register(selector, SelectionKey.OP_READ);),下一次再执行 attachment,附件就丢了,变成 null 了呢
    az467
        3
    az467  
       2020-03-22 12:08:01 +08:00
    @amiwrong123

    你可以认为

    socketChannel.register(selector, SelectionKey.OP_READ);

    其实就是

    socketChannel.register(selector, SelectionKey.OP_READ, null);

    第三个参数就是 attachment
    amiwrong123
        4
    amiwrong123  
    OP
       2020-03-22 12:19:40 +08:00
    @az467
    纳尼,这怎么说,看文档也没看出有这个意思啊。注册 读 或 写 表现还不一样昂
    az467
        5
    az467  
       2020-03-22 12:33:29 +08:00   ❤️ 1
    @amiwrong123

    * Registers this channel with the given selector, returning a selection
    * key.
    *
    * <p> An invocation of this convenience method of the form
    *
    * <blockquote><tt>sc.register(sel, ops)</tt></blockquote>
    *
    * behaves in exactly the same way as the invocation
    *
    * <blockquote>
    <tt>sc.{@link
    * #register(java.nio.channels.Selector,int,java.lang.Object)
    * register}(sel, ops, null)</tt></blockquote>

    还是我原来说的,你仔细看你的代码:

    //sk.interestOps(sk.interestOps() | SelectionKey.OP_WRITE);
    socketChannel.register(selector, SelectionKey.OP_WRITE); 👈

    sk.attach(ByteBuffer.wrap(sendStr.getBytes())); 👈

    注册写,attachment 置 null,然后调用了 attach(),attachment 为 sendStr 。

    //sk.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE);
    socketChannel.register(selector, SelectionKey.OP_READ); 👈

    注册读,attachment 置空,没有调用 attach(),所以 attachment 依然为 null 。
    az467
        6
    az467  
       2020-03-22 12:33:29 +08:00
    @amiwrong123

    * Registers this channel with the given selector, returning a selection
    * key.
    *
    * <p> An invocation of this convenience method of the form
    *
    * <blockquote><tt>sc.register(sel, ops)</tt></blockquote>
    *
    * behaves in exactly the same way as the invocation
    *
    * <blockquote>
    <tt>sc.{@link
    * #register(java.nio.channels.Selector,int,java.lang.Object)
    * register}(sel, ops, null)</tt></blockquote>

    还是我原来说的,你仔细看你的代码:

    //sk.interestOps(sk.interestOps() | SelectionKey.OP_WRITE);
    socketChannel.register(selector, SelectionKey.OP_WRITE); 👈

    sk.attach(ByteBuffer.wrap(sendStr.getBytes())); 👈

    注册写,attachment 置 null,然后调用了 attach(),attachment 为 sendStr 。

    //sk.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE);
    socketChannel.register(selector, SelectionKey.OP_READ); 👈

    注册读,attachment 置空,没有调用 attach(),所以 attachment 依然为 null 。
    amiwrong123
        7
    amiwrong123  
    OP
       2020-03-22 12:39:35 +08:00
    @az467
    好吧,懂了。怪我没仔细看代码~~

    所以 注册什么事件的时候,不想弄丢附件,就 sk.interestOps(sk.interestOps() | SelectionKey.OP_WRITE);呗

    而想直接清空附件,就 socketChannel.register(selector, SelectionKey.OP_WRITE);呗

    也不所谓哪种是正确的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2356 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 15:32 · PVG 23:32 · LAX 08:32 · JFK 11:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.