我是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);
}
}
显然问题出在
Set<SelectionKey> selectionKeys = selector.selectedKeys();
但是我不知道为什么,或如何处理。
您需要阅读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()还将限制缓冲区的当前容量(即最大允许位置)。