为什么我的纯 NIO selectKey 仍然选择了事件

Why my pure NIO selectKey still has event selected

我是蔚来新手

假设我有一个像这样的 NIO 服务器:

package org.example.nio.selectordemo2;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        Selector selector = Selector.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        int loopCount = 0;
        while (true) {
            if (++loopCount == 20) break;
            if(selector.select(2000) == 0) {
                continue;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                if(key.isAcceptable()) {
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if(key.isReadable()) {
                    SocketChannel channel = (SocketChannel)key.channel();
                    ByteBuffer buffer = (ByteBuffer)key.attachment();
                    channel.read(buffer);
                    System.out.println("Receive message:" + new String(buffer.array()));
                }
                keyIterator.remove();
            }
        }
    }
}

我先运行上面的服务器代码,然后运行下面的客户端代码。假设服务端代码会收到两个事件,一个是连接,一个是读取消息。因此,假设一旦客户端连接并发送消息,服务器将 运行 完成。然而,结果是,服务器似乎一直通知相同的事件。

package org.example.nio.selectordemo2;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOClient {
    public static void main(String[] args) throws Exception{
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        if (!socketChannel.connect(inetSocketAddress)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("Doing other job");
            }
        }
        String str = "Sending A Message....";
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        socketChannel.write(buffer);
    }
}

然而,让我奇怪的是,结果一直是这样的:

显然问题出在

Set<SelectionKey> selectionKeys = selector.selectedKeys();

但我不知道为什么,或者我应该如何处理。

您需要阅读 documentation for the Buffer class, which is the superclass of ByteBuffer. You are completely ignoring one of the most important properties of all buffers: the position

每个Buffer,包括ByteBuffers,都维护一个位置。位置是 ByteBuffer 中的索引,它确定数据将添加到缓冲区的位置,以及从缓冲区中的何处读取数据。

当你调用channel.read(buffer);时,它从通道中读取字节,并将它们添加到到ByteBuffer的当前位置。 然后更新位置添加到最后一个字节后的索引。

所以,这是你第一次调用后 ByteBuffer 的状态 channel.read:

Sending A Message....␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀
          1         2         3         4         5         6
0123456789012345678901234567890123456789012345678901234567890123456789
                     ↑
                     position = 21

这是第二次调用 channel.read 后 ByteBuffer 的状态:

Sending A Message....Sending A Message....␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀
          1         2         3         4         5         6
0123456789012345678901234567890123456789012345678901234567890123456789
                                          ↑
                                          position = 42

这是你第三次调用后 ByteBuffer 的状态 channel.read:

Sending A Message....Sending A Message....Sending A Message....␀␀␀␀␀␀␀
          1         2         3         4         5         6
0123456789012345678901234567890123456789012345678901234567890123456789
                                                               ↑
                                                               position = 63

如您所见,该数组始终包含您的原始消息。 不要使用 buffer.array();它忽略缓冲区的位置。

从通道读取到 ByteBuffer 后,获取刚刚放入 ByteBuffer 中的数据的正确方法是 flip it. In the case of character data, you will then need to decode 它。

flip() 会将 ByteBuffer 的位置移动到 ByteBuffer 的开头,因此下次您从该 ByteBuffer 读取时,您将读取刚刚由 channel.read 放入其中的数据.

channel.read之后:

Sending A Message....␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀
          1         2         3         4         5         6
0123456789012345678901234567890123456789012345678901234567890123456789
                     ↑
                     position = 21

buffer.flip()之后:

Sending A Message....␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀␀
          1         2         3         4         5         6
0123456789012345678901234567890123456789012345678901234567890123456789
↑                    ↑
position = 0         limit = 21

如果在调用 flip() 之后调用 buffer.get(),它将读取字节 'S' 并将 ByteBuffer 的位置前进一位。如果不调用 flip(),下一次读取字节的尝试将是 return 0,因为这是位置 21 处的字节值。

但是你不想直接从ByteBuffer中读取;你想把它的内容变成一个字符串。当前,您正在将字节传递给 String 构造函数,这是不明智的——它将使用默认字符集。如果您的服务器是 Windows 上的 运行 而客户端不是 运行 Windows(反之亦然),任何非 ASCII 字符都将被破坏。

字节和字符之间正确的转换方式是使用Charset, specifically its decode method. Java defines several common Charsets in the StandardCharsets class. Most of the time, UTF-8是最好的选择。

因此,使用标准字符集的 decode 方法如下所示:

channel.clear();
channel.read(buffer);
channel.flip();
String message = StandardCharsets.UTF_8.decode(buffer).toString();

您需要为下一次读取将 ByteBuffer 的值设置回零,因为您总是希望下一次 channel.read 操作将字节放置在 ByteBuffer 中的位置 0。最简单的方法是使用 ByteBuffer 的 clear() 方法,如您在上面的代码中所见。 clear() 还将限制缓冲区的当前容量(即最大允许位置)。