NIO 服务器端不阻塞的一个Bug解决
@author:Jingdai
@date:2021.06.27
今天用 NIO 时出现了一个Bug,搞了半天,现在记录一下。
使用 NIO 进行通信时,在客户端正常关闭时(即调用close() 方法关闭),会触发服务器的一个读事件,此时服务器的 read 方法会返回 -1,根据此我们就可以做一些处理,如下代码。
int n = socketChannel.read(byteBuffer);
if (n == -1) {
selectionKey.cancel();
}
但是今天写代码发现客户端正常关闭后服务器端总是读不到 -1,无法取消这个 key,导致服务器端无法阻塞,一直空转。示例代码如下。
Server.java
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.nio.charset.Charset;
import java.util.Iterator;
public class Server {
public static void main(String[] args) {
int port = 8888;
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(port));
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
SocketChannel sc = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer;
if (selectionKey.attachment() == null) {
byteBuffer = ByteBuffer.allocate(512);
selectionKey.attach(byteBuffer);
} else {
byteBuffer = (ByteBuffer) selectionKey.attachment();
}
int n = sc.read(byteBuffer);
System.out.println(n);
if (n == -1) {
selectionKey.cancel();
} else {
byteBuffer.flip();
System.out.println(Charset.defaultCharset().decode(byteBuffer));
// byteBuffer.clear();
}
}
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Client.java
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
String ip = "localhost";
int port = 8888;
try (Socket socket = new Socket(ip, port)) {
OutputStream os = socket.getOutputStream();
os.write("Hello Server!".getBytes());
os.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
最后发现在客户端正常关闭时,如果 read(ByteBuffer dst) 方法的 dst 里面没有数据,则 read() 方法就会正常的返回 -1;而如果 dst 里面已经有了数据的话,那么 read() 方法就会返回 0 。所以上面代码中就无法进入到 n == -1 的那个分支,即无法取消这个 key,这就导致 select 无法阻塞,一直空转。知道了原因,也就很好解决了,只要将上面代码中那行注释的代码去掉注释就行,读取完之后 clear 会重置ByteBuffer ,就可以使 read 方法返回 -1了,当然每次读取都使用一个新的 ByteBuffer 也是可以的。
结论
当客户端正常关闭时,会触发服务器的读事件。如果服务器端的 read(ByteBuffer dst) 方法的 dst 里面没有数据,则 read() 方法就会正常的返回 -1;而如果 dst 里面已经有了数据的话,那么 read() 方法就会返回 0 。