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

问题描述 投票:0回答:1

我是NIO初学者。

假设我有一个类似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);
    }
}

但是,令我感到奇怪的是,结果保持如下:enter image description here

显然问题出在

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

但是我不知道为什么,或如何处理。

java nio
1个回答
-1
投票

您需要阅读documentation for the Buffer class,它是ByteBuffer的超类。您完全忽略了所有缓冲区的最重要属性之一:position

每个缓冲区(包括ByteBuffer)保持位置。位置是ByteBuffer中的索引,它确定将数据添加到缓冲区的位置以及从中读取数据的位置。

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

所以,这是您第一次呼叫channel.read之后的ByteBuffer的状态:

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

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

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

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

从通道读取到ByteBuffer后,获取刚刚放置在ByteBuffer中的数据的正确方法是对其进行flip。如果是字符数据,则需要将其decode

[flip()将把ByteBuffer的位置移到ByteBuffer的开头,因此,下次您从该ByteBuffer读取数据时,您将读取由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的位置提高1。如果您不调用flip(),则下一次读取字节的尝试将返回0,因为这是位置21处的字节值。]

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

在字节和字符之间进行转换的正确方法是使用Charset,特别是其decode方法。 Java在StandardCharsets类中定义了几个常见的字符集。大多数时候,UTF-8是最佳选择。

因此,使用标准Charset的decode方法如下:

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

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

© www.soinside.com 2019 - 2024. All rights reserved.