土法炼钢兴趣小组的算法知识备份

实战:基于 io_uring 的 TCP Echo Server

目录

在上一篇中,我们学会了如何用 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_dataio_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)

异步编程的核心在于在回调中发起下一个异步操作

  1. Accept 完成时
    • 获取新连接 client_fd
    • 发起 io_uring_prep_read(client_fd)
    • 重要:立即再次发起 io_uring_prep_accept(server_fd),否则服务器将无法接受新连接。
  2. Read 完成时
    • 如果 res > 0:发起 io_uring_prep_write(client_fd) 将数据回写。
    • 如果 res <= 0:关闭连接 close(client_fd)
  3. 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高级特性,看看如何通过 SQPOLLFixed Buffers 进一步榨干硬件性能。


上一篇: 03-liburing-api.md - liburing 基础 API 下一篇: 05-advanced-features.md - 高级特性

返回 io_uring 系列索引


By .