基于前文对 I/O 多路复用原理、Epoll 机制及 Java NIO 的讨论,实现 I/O 多路复用的核心在于利用操作系统提供的系统调用(如 Linux 的 epoll)或语言层面的封装(如 Java 的 Selector)。
以下分别以 Java NIO(跨平台通用) 和 Linux C (epoll,高性能底层) 为例,展示如何实现。
一、 Java NIO 实现(基于 Selector)
Java 通过 java.nio.channels 包屏蔽了底层 OS 差异,在 Linux 下自动映射为 epoll,在 Windows 下映射为 IOCP/select。
1. 核心步骤
创建 Selector:打开选择器。
注册 Channel将 ServerSocketChannel 设置为非阻塞,并注册 OP_ACCEPT 事件。
循环监听:调用 selector.select() 阻塞等待事件。
处理事件:遍历 selectedKeys,根据事件类型(接受连接、读取数据)执行逻辑。
2. 代码示例
java
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 {
// 1. 打开 Selector
Selector selector = Selector.open();
// 2. 创建 ServerSocketChannel 并配置为非阻塞
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(8080));
// 3. 注册 OP_ACCEPT 事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port 8080...");
while (true) {
// 4. 阻塞等待就绪事件
selector.select();
// 5. 获取就绪的 SelectionKey 集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove(); // 必须手动移除,防止重复处理
try {
if (key.isAcceptable()) {
// 处理新连接
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = ssc.accept();
clientChannel.configureBlocking(false);
// 注册 OP_READ 事件,关注客户端数据
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("New connection accepted: " + clientChannel.getRemoteAddress());
} else if (key.isReadable()) {
// 处理数据读取
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
String message = new String(buffer.array(), 0, buffer.remaining());
System.out.println("Received: " + message);
// 简单回声
clientChannel.write(ByteBuffer.wrap(("Echo: " + message).getBytes()));
} else if (bytesRead == -1) {
// 连接关闭
clientChannel.close();
System.out.println("Connection closed");
}
}
} catch (Exception e) {
key.cancel();
key.channel().close();
}
}
}
}
}
二、 Linux C 实现(基于 Epoll)
这是高性能服务器(如 Nginx、Redis)的底层实现方式,直接操作内核数据结构,效率最高。
1. 核心步骤
epoll_create:创建 epoll 实例。
epoll_ctl:将监听 socket 添加到 epoll 树中,设置 EPOLLIN 事件。
epoll_wait:等待事件发生,返回就绪的 fd 数组。
处理事件:遍历就绪数组,区分是监听 socket(接受新连接)还是客户端 socket(读取数据)。
2. 代码示例(伪代码/关键片段)
java
#include <sys/epoll.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#define MAX_EVENTS 1024
#define PORT 8080
int main() {
int listen_fd, epoll_fd, client_fd;
struct sockaddr_in addr;
struct epoll_event ev, events[MAX_EVENTS];
// 1. 创建监听 socket
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// ... bind 和 listen 省略 ...
// 2. 创建 epoll 实例
epoll_fd = epoll_create1(0);
// 3. 注册监听 socket
ev.events = EPOLLIN; // 关注读事件(包括新连接)
ev.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
while (1) {
// 4. 等待事件,超时时间 -1 表示无限阻塞
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == listen_fd) {
// 5. 处理新连接
client_fd = accept(listen_fd, NULL, NULL);
// 设置非阻塞
fcntl(client_fd, F_SETFL, O_NONBLOCK);
// 注册新连接到 epoll
ev.events = EPOLLIN | EPOLLET; // ET 模式
ev.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
} else {
// 6. 处理数据读取
int fd = events[i].data.fd;
char buf;
int bytes = read(fd, buf, sizeof(buf));
if (bytes > 0) {
write(fd, buf, bytes); // 回声
} else if (bytes <= 0) {
close(fd); // 连接关闭或出错
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
}
}
}
}
return 0;
}
三、 实现关键点总结
非阻塞设置:无论是 Java 的 configureBlocking(false) 还是 C 的 fcntl(O_NONBLOCK),必须将 Channel/FD 设置为非阻塞模式,否则读写操作仍会阻塞线程,导致多路复用失效。
事件移除:在 Java 中,selectedKeys 不会自动清空,必须手动 iterator.remove(),否则下次循环会重复处理同一事件。
ET 模式处理:若使用 Edge Triggered (ET) 模式(如 C 示例中的 EPOLLET),必须一次性读完所有数据(循环 read 直到返回 EAGAIN),否则剩余数据将不会再次触发通知,导致数据滞留。
异常处理:网络环境复杂,必须妥善处理连接断开、读写异常,及时关闭资源并从 Selector/Epoll 中注销,防止内存泄漏或无效事件堆积。
通过上述实现,单线程即可高效管理成千上万个并发连接,这正是高并发网络服务的核心基础。