一、什么是java的nio?
- Java NIO(Non-blocking I/O)是从Java 1.4版本开始引入的一个新的I/O API,它提供了一种与标准Java I/O API不同的操作方式。NIO的主要特性是非阻塞I/O操作,这意味着在等待I/O操作完成时,线程不会被阻塞,而是可以继续执行其他任务。
二、NIO 的核心组件
- Buffer(缓冲区) :用于存放数据。所有数据在被读取或写入时都要经过缓冲区。
- Channel(通道) :负责将数据从一个地方传输到另一个地方。通道可以读取或写入缓冲区。
- Selector(选择器) :用于监听多个通道的事件(如可读、可写等),允许在一个线程中处理多个通道。
三,如何使用?
下面是一个简单的java NIO服务端示例,演示如何使用nio来处理客户端的连接请求和数据交换
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;
import java.util.Set;
public class NioServer {
private static final int PORT = 8080;
private Selector selector;
public void startServer() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port " + PORT);
while (true) {
if (selector.select() == 0) {
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
acceptNewConnection(serverSocketChannel);
} else if (key.isReadable()) {
readData(key);
}
// 重要:删除已处理的 SelectionKey
iterator.remove();
}
}
}
private void acceptNewConnection(ServerSocketChannel serverSocketChannel) throws IOException {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
private void readData(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
socketChannel.close();
} else {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data, "UTF-8");
System.out.println("Received data from client: " + message);
sendResponse(socketChannel, message);
}
}
private void sendResponse(SocketChannel socketChannel, String message) throws IOException {
ByteBuffer buffer = ByteBuffer.wrap(("Hello world from server: " + message).getBytes());
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
}
public static void main(String[] args) throws IOException {
new NioServer().startServer();
}
}
下边是一个简单的客户端示例,用来向服务端发送消息
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NioClient {
private static final String DEFAULT_HOST = "localhost";
private static final int DEFAULT_PORT = 8080;
private void connect() throws Exception {
try (SocketChannel socketChannel = SocketChannel.open()) {
socketChannel.configureBlocking(false);
socketChannel.connect(new java.net.InetSocketAddress(DEFAULT_HOST, DEFAULT_PORT));
while (!socketChannel.finishConnect()) {
// 等待连接完成
}
ByteBuffer sendBuffer = ByteBuffer.wrap("Hello world from client!".getBytes("UTF-8"));
socketChannel.write(sendBuffer);
// 清空缓冲区以准备好接收服务器的响应
ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
// 等待服务器的响应
boolean receivedResponse = false;
while (!receivedResponse) {
receiveBuffer.clear(); // 准备接收数据
int bytesRead = socketChannel.read(receiveBuffer);
if (bytesRead > 0) {
receiveBuffer.flip(); // 切换到读模式
byte[] data = new byte[receiveBuffer.remaining()];
receiveBuffer.get(data);
String response = new String(data, "UTF-8");
System.out.println("Received response: " + response);
receivedResponse = true;
} else if (bytesRead < 0) {
// 如果客户端关闭了连接
System.out.println("Connection closed by the server.");
break;
}
}
}
}
public static void main(String[] args) throws Exception {
new NioClient().connect();
}
}
代码解释
-
服务端:
- 打开一个
ServerSocketChannel并绑定到端口。 - 创建一个
Selector并注册ServerSocketChannel以便监听新的连接。 - 循环监听选择器的事件,并根据事件类型(接受新连接或读取数据)处理。
- 打开一个
-
客户端:
- 打开一个
SocketChannel并连接到服务器。 - 向服务器发送消息,并读取服务器的响应。
- 打开一个
其中有几个关键的地方需要注意:
- 第39行的iterator.remove();,需要删除已处理的selectionkey,否则服务端会重复处理消息进而引发死循环。
- sendResponse响应客户端方法中的while (buffer.hasRemaining()) { socketChannel.write(buffer); },很多资料中都直接使用socketChannel.write(buffer);这时你可能会遇到一个问题,客户端发送过来的数据收不到响应值,原因可能是缓存区中的数据读取不完整,导致数据发送不完全,客户端无法解析。
- 客户端while (!socketChannel.finishConnect()) { // 等待连接完成 },客户端尝试非阻塞地连接到服务端,此处一定要确保连接完成,循环中可以添加一些其他操作,添加日志等,如果添加此段代码,可能会出现客户端发送的数据服务端收不到,原因是连接还未完成就去执行后续的发送数据操作。
- 客户端中有如下一段代码
// 等待服务器的响应
boolean receivedResponse = false;
while (!receivedResponse) {
receiveBuffer.clear(); // 准备接收数据
int bytesRead = socketChannel.read(receiveBuffer);
if (bytesRead > 0) {
receiveBuffer.flip(); // 切换到读模式
byte[] data = new byte[receiveBuffer.remaining()];
receiveBuffer.get(data);
String response = new String(data, "UTF-8");
System.out.println("Received response: " + response);
receivedResponse = true;
} else if (bytesRead < 0) {
// 如果客户端关闭了连接
System.out.println("Connection closed by the server.");
break;
}
}
此段代码中大部分教程都不会等待服务器的响应,此时如果你的机器性能好,网络带宽好,可能没有感受,但是如果机器差点就会发现客户端无法收到服务端返回的响应消息。
响应示例
1.首先启动服务端NioServer
2.然后启动客户端NioClient
3.然后返回服务端查看客户端请求
三张图片对比可以看到客户端发送请求,服务端收到了请求并做出响应,最后客户端收到了服务端返回的响应。
- 至此一个简单的nio示例代码已完成,对于初学者来说尤其要注意上边提到的几点,少踩坑。