在上一篇中,我们学会了如何用 io_uring
读取文件。今天,我们将进入更复杂的领域:网络编程。我们将实现一个经典的
TCP Echo
Server:它接收客户端连接,读取发送的数据,然后原样返回。
1. 异步网络编程模型
与同步阻塞模型(One Thread Per Connection)或
epoll Reactor 模型不同,io_uring
的网络编程更像是一个状态机。
我们需要维护每个连接的状态,并通过 user_data
在提交(Submission)和完成(Completion)之间传递上下文。
1.1 状态流转图
stateDiagram-v2
[*] --> ACCEPT: 提交 Accept 请求
ACCEPT --> READ: 连接建立 (cqe->res = client_fd)
ACCEPT --> ACCEPT: 重新提交 Accept (处理下一个连接)
READ --> WRITE: 收到数据 (cqe->res > 0)
READ --> CLOSE: 客户端断开 (cqe->res <= 0)
WRITE --> READ: 发送完成,继续读取
CLOSE --> [*]
2. 核心设计
2.1 上下文管理 (Context)
在 epoll 中,我们通常用
epoll_data.ptr 指向一个结构体。在
io_uring 中,我们使用
io_uring_sqe_set_data 和
io_uring_cqe_get_data。
我们需要定义一个结构体来区分当前完成的事件类型:
enum {
EVENT_ACCEPT,
EVENT_READ,
EVENT_WRITE
};
struct conn_info {
int fd; // 文件描述符
int type; // 事件类型
char buf[1024]; // 数据缓冲区
struct iovec iov; // io_uring 需要的 iovec 结构
};2.2 链式处理 (Chaining)
异步编程的核心在于在回调中发起下一个异步操作。
- Accept 完成时:
- 获取新连接
client_fd。 - 发起
io_uring_prep_read(client_fd)。 - 重要:立即再次发起
io_uring_prep_accept(server_fd),否则服务器将无法接受新连接。
- 获取新连接
- Read 完成时:
- 如果
res > 0:发起io_uring_prep_write(client_fd)将数据回写。 - 如果
res <= 0:关闭连接close(client_fd)。
- 如果
- Write 完成时:
- 发起
io_uring_prep_read(client_fd)等待下一条消息。
- 发起
3. 代码实现
下面是 02-echo-server.c 的核心逻辑。
/* examples/io_uring/02-echo-server.c */
// ... (头文件与结构体定义) ...
int main() {
// ... (Socket 初始化与 bind/listen) ...
// 初始化 io_uring
struct io_uring ring;
io_uring_queue_init(4096, &ring, 0);
// 提交第一个 Accept 请求
add_accept_request(&ring, server_fd, ...);
io_uring_submit(&ring);
while (1) {
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
struct conn_info *user_data = (struct conn_info *)io_uring_cqe_get_data(cqe);
if (user_data->type == EVENT_ACCEPT) {
int client_fd = cqe->res;
// 1. 为新连接准备 Read
add_read_request(&ring, client_fd);
// 2. 重新 Arm Accept
add_accept_request(&ring, server_fd, ...);
}
else if (user_data->type == EVENT_READ) {
int bytes = cqe->res;
if (bytes <= 0) {
close(user_data->fd); // 断开
} else {
// 收到数据,准备 Write (Echo)
add_write_request(&ring, user_data->fd, user_data->buf, bytes);
}
}
else if (user_data->type == EVENT_WRITE) {
// 发送完毕,继续 Read
add_read_request(&ring, user_data->fd);
}
// 释放旧的 user_data (注意内存管理策略)
free(user_data);
io_uring_cqe_seen(&ring, cqe);
io_uring_submit(&ring);
}
}完整代码: 02-echo-server.c
4. 内存管理注意事项
在示例代码中,为了简化逻辑,我们在每次请求时都
malloc 一个新的
conn_info,并在处理完 CQE 后 free
掉。
在高并发生产环境中,频繁的 malloc/free
是不可接受的。通常的优化策略包括: 1. 内存池 (Memory
Pool):预分配大量的 conn_info。 2.
嵌入式结构:将 conn_info
嵌入到连接对象中,整个生命周期复用。 3. Provided
Buffers:使用 io_uring 的高级特性
IOSQE_BUFFER_SELECT,让内核自动选择缓冲区,避免为每个连接预分配读缓冲。(我们将在下一篇详细介绍)。
5. 总结
通过这个 Echo Server,我们掌握了 io_uring
处理网络并发的基本模式。相比
epoll,我们不再需要手动调用
read/write,而是将这些操作全部交给内核。
下一篇,我们将探讨 io_uring
的高级特性,看看如何通过
SQPOLL 和 Fixed Buffers
进一步榨干硬件性能。
上一篇: 03-liburing-api.md - liburing 基础 API 下一篇: 05-advanced-features.md - 高级特性