目录
当使用Java NIO(New I/O)创建一个服务端时,你需要使用ServerSocketChannel
和Selector
两个关键类。
整体代码
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 BUFFER_SIZE = 1024;
private static final int TIMEOUT = 3000;
public static void main(String[] args) {
// 创建一个Selector实例
try (Selector selector = Selector.open();
// 创建一个ServerSocketChannel实例
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
// 绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
// 注册到Selector,监听ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 阻塞等待就绪的事件
if (selector.select(TIMEOUT) == 0) {
System.out.print(".");
continue;
}
// 获取所有已就绪的事件的SelectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 处理事件
handleKey(key);
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handleKey(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
// 处理ACCEPT事件
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(BUFFER_SIZE));
} else if (key.isReadable()) {
// 处理READ事件
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
// 客户端关闭连接
socketChannel.close();
} else if (bytesRead > 0) {
// 处理接收到的数据
buffer.flip();
byte[] data = new byte[bytesRead];
buffer.get(data);
String message = new String(data);
System.out.println("Received message: " + message);
buffer.clear();
}
}
}
}
代码详解
导入必要的类:
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;
设置常量:
这里定义了一个缓冲区大小和一个超时时间,可以根据需要进行调整。
private static final int BUFFER_SIZE = 1024;
private static final int TIMEOUT = 3000;
创建Selector和ServerSocketChannel实例:
try (Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
使用try-with-resources
语句创建了Selector
和ServerSocketChannel
的实例。这样可以确保在使用完后自动关闭它们。
配置ServerSocketChannel:
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
将ServerSocketChannel
配置为非阻塞模式,并绑定到指定的端口(这里使用的是8080)
注册到Selector:
将ServerSocketChannel
注册到Selector
上,以便监听ACCEPT
事件。
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
进入事件循环:
这是服务端的主要事件循环。在每次循环中,使用selector.select(TIMEOUT)
阻塞等待就绪的事件。如果没有任何事件就绪,将继续下一次循环。
while (true) {
if (selector.select(TIMEOUT) == 0) {
System.out.print(".");
continue;
}
处理就绪的事件:
获取就绪的事件的SelectionKey
集合,并遍历处理每个就绪的事件。
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
handleKey(key);
iterator.remove();
}
处理ACCEPT事件:
如果就绪的事件是ACCEPT
事件,表示有新的客户端连接请求。通过ServerSocketChannel.accept()
方法接受连接,并将SocketChannel
注册到Selector
上,以便监听READ
事件。
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(BUFFER_SIZE));
}
处理READ事件:
如果就绪的事件是READ
事件,表示有数据可读取。使用SocketChannel.read()
方法读取数据,并进行相应的处理。在示例中,将读取的数据存储到ByteBuffer
中,并打印接收到的消息。
SocketChannel
的read
方法在处理READ
事件时返回的整数值有以下含义:
- 大于0:表示读取到了数据。返回的值表示实际读取到的字节数。
- 等于0:表示没有读取到任何数据。这通常发生在非阻塞模式下,当没有可读取的数据时,
read
方法立即返回0。 - 小于0:表示通道已经到达了流的末尾(End of Stream)。这通常意味着客户端关闭了连接或发生了异常。返回的值依赖于底层操作系统和网络协议的实现,通常为-1。
在处理read
事件时,需要根据返回值进行适当的处理。以下是对这些返回值的典型处理方式:
- 大于0的情况:根据实际读取到的字节数,从
ByteBuffer
中获取数据并进行相应的处理。可以将读取到的数据转换为字符串、处理数据包等操作。 - 等于0的情况:可以选择继续等待下一次
READ
事件,或进行其他操作。 - 小于0的情况:通常表示连接已关闭或发生了错误。可以关闭相关的通道,释放资源,并进行适当的异常处理。
else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
socketChannel.close();
} else if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[bytesRead];
buffer.get(data);
String message = new String(data);
System.out.println("Received message: " + message);
buffer.clear();
}
}
注意事项
在实现Java NIO服务端时,以下是一些需要注意的关键事项,并附带一些示例说明:
非阻塞模式:
- 通过调用
configureBlocking(false)
将ServerSocketChannel
和SocketChannel
配置为非阻塞模式。 - 示例代码:
serverSocketChannel.configureBlocking(false);
socketChannel.configureBlocking(false);
Selector的正确使用:
- 注册通道时选择正确的事件类型,如
OP_ACCEPT
、OP_READ
等。 - 示例代码:
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
socketChannel.register(selector, SelectionKey.OP_READ);
- 在处理就绪事件时,检查事件类型并采取相应的操作。
- 示例代码:
if (key.isAcceptable()) {
// 处理ACCEPT事件
} else if (key.isReadable()) {
// 处理READ事件
}
适当的缓冲区管理:
- 合理分配和管理
ByteBuffer
缓冲区的大小,避免过小或过大的缓冲区。 - 示例代码:
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
事件处理的错误处理:
- 在处理读写操作时,捕获可能的异常,并进行适当的错误处理。
- 示例代码:
try {
// 读取数据
} catch (IOException e) {
// 处理读取异常
}
并发访问的线程安全性:
- 在多线程环境下共享Selector和通道时,确保正确的线程同步和保护共享资源的访问。
- 示例代码:
synchronized (selector) {
// 同步访问共享资源
}
性能优化:
- 使用适当的缓存策略,如使用
DirectByteBuffer
代替HeapByteBuffer
,以减少数据复制和内存拷贝。 - 示例代码:
ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
进程退出和资源释放:
- 在程序退出时,确保正确关闭通道、释放资源,避免资源泄漏。
- 示例代码:
serverSocketChannel.close();
socketChannel.close();
selector.close();