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

2020-03-22 00:10:33 +08:00
 amiwrong123

我开始以为 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

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

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

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

可以发现:

2236 次点击
所在节点    Java
6 条回复
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
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
2020-03-22 12:08:01 +08:00
@amiwrong123

你可以认为

socketChannel.register(selector, SelectionKey.OP_READ);

其实就是

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

第三个参数就是 attachment
amiwrong123
2020-03-22 12:19:40 +08:00
@az467
纳尼,这怎么说,看文档也没看出有这个意思啊。注册 读 或 写 表现还不一样昂
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 。
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
2020-03-22 12:39:35 +08:00
@az467
好吧,懂了。怪我没仔细看代码~~

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

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

也不所谓哪种是正确的

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

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

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

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

© 2021 V2EX