一、引言:为何用协程写网络服务?
传统 C++ 网络服务器一般使用如下方式:
-
select() 或 epoll() + 状态机
-
多线程 per-connection(线程爆炸问题)
-
Boost.Asio(复杂回调嵌套)
C++20 协程带来非阻塞、无回调、简洁清晰的异步 IO 编程方式。我们可以构建一个轻量的协程式 TCP 网络服务器框架,支持数百甚至上千并发连接,逻辑简洁,性能优异。
二、本文目标
实现一个协程驱动的网络服务框架,具备以下特性:
协程处理客户端连接 | 每个连接由一个协程处理,避免线程堆叠 |
非阻塞读写封装 | 使用协程挂起等待 socket 读写 |
Echo 模式 | 客户端发什么就回什么 |
聊天室模式(支持切换) | 所有连接共享广播消息 |
连接生命周期管理 | 自动关闭、错误处理、防崩溃 |
支持 Linux 平台原生 epoll | 使用 epoll 驱动 socket 与协程调度 |
三、整体架构图
lua
复制编辑
+—————————+ | TCP 监听器 | +—————————+ | +—–v—–+ | 新连接协程 | —> 创建 Client 协程 +—–+—–+ | +———v———-+ +———+ | Client 协程 | <—> | socket | | 读写消息 + 广播 | +———+ +——————–+
四、基础模块一览
socket.hpp | socket API 封装 |
awaitable.hpp | 协程等待封装(read/write) |
server.hpp | 主服务器控制器 |
client.hpp | 客户端协程控制器 |
epoll_loop.hpp | epoll 驱动事件分发与 resume |
五、epoll 协程等待实现核心思想
cpp
复制编辑
// 伪代码结构 co_await socket_read(sockfd, buffer, len); // 1. 注册 sockfd 到 epoll // 2. 协程挂起,等待 IO 就绪 // 3. epoll 返回后 resume 协程
这类似于 async/await 的异步 IO 模型,但是我们自己用 C++20 原语构建的。
六、Awaitable 封装(以 read 为例)
cpp
复制编辑
struct ReadAwaiter { int fd; char* buffer; size_t length; ssize_t result; ReadAwaiter(int fd, char* buf, size_t len) : fd(fd), buffer(buf), length(len), result(0) {} bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle<> h) { register_epoll_read(fd, h); // 注册读事件 } ssize_t await_resume() const noexcept { return result; } };
你需要维护一个 epoll_loop 循环来监听所有挂起的 fd 与协程 resume。
七、Socket API 简化封装
cpp
复制编辑
int create_server_socket(int port) { int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); int opt = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); sockaddr_in addr = {AF_INET, htons(port), INADDR_ANY}; bind(sockfd, (sockaddr*)&addr, sizeof(addr)); listen(sockfd, SOMAXCONN); return sockfd; }
八、客户端协程逻辑:Echo 模式
cpp
复制编辑
Task<> handle_client(int client_fd) { char buf[1024]; try { while (true) { ssize_t n = co_await ReadAwaiter(client_fd, buf, sizeof(buf)); if (n <= 0) break; std::cout << "[client] 收到: " << std::string(buf, n) << "\\n"; ssize_t m = write(client_fd, buf, n); // 简化写 if (m <= 0) break; } } catch (…) { std::cout << "[client] 异常断开\\n"; } close(client_fd); co_return; }
九、聊天室支持:共享广播池
cpp
复制编辑
std::set<int> all_clients; std::mutex client_mtx; void broadcast_to_all(const std::string& msg, int exclude = -1) { std::lock_guard<std::mutex> lock(client_mtx); for (int fd : all_clients) { if (fd == exclude) continue; write(fd, msg.c_str(), msg.size()); } }
替换上面的 write() 为:
cpp
复制编辑
broadcast_to_all(std::string(buf, n), client_fd);
十、协程主监听器
cpp
复制编辑
Task<> server_loop(int listen_fd) { while (true) { int client_fd = accept(listen_fd, nullptr, nullptr); if (client_fd >= 0) { fcntl(client_fd, F_SETFL, O_NONBLOCK); { std::lock_guard<std::mutex> lock(client_mtx); all_clients.insert(client_fd); } handle_client(client_fd).start(); // 协程化处理 } else { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } }
十一、epoll 协程事件调度核心(伪码)
你需要维护如下结构:
cpp
复制编辑
std::unordered_map<int, std::coroutine_handle<>> read_waiters; void register_epoll_read(int fd, std::coroutine_handle<> h) { read_waiters[fd] = h; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, …); }
在 epoll_wait() 中:
cpp
复制编辑
for (each ready fd) { auto h = read_waiters[fd]; if (h) h.resume(); read_waiters.erase(fd); }
十二、主函数入口
cpp
复制编辑
int main() { int port = 9000; int listen_fd = create_server_socket(port); std::cout << "服务器监听端口:" << port << "\\n"; server_loop(listen_fd).start(); // 启动 epoll 主循环(需要另开线程) epoll_loop(); // 持续调度挂起协程 return 0; }
十三、运行测试
你可以使用多个 telnet 客户端测试:
bash
复制编辑
telnet 127.0.0.1 9000
Echo 模式:
复制编辑
客户端发送:hello 服务器回应:hello
聊天室模式:
复制编辑
客户端1:hello 客户端2:hello(来自客户端1)
十四、性能与优势
低开销 | 协程调度无需线程切换 |
高并发 | 可轻松支持上千连接 |
可读性强 | 逻辑结构顺序直观,避免回调地狱 |
易于扩展 | 模块清晰,可加入加密、验证、协议处理等 |
十五、扩展建议
WebSocket 支持 | 封装帧协议并改写 read/write 协程 |
用户身份识别 | 在连接协程中添加认证阶段 |
JSON 协议封装 | 结合 nlohmann/json 解析客户端消息 |
REST + Chat 服务 | HTTP 与聊天室服务共用端口,路径区分 |
TLS 加密支持 | 使用 OpenSSL 配合非阻塞 BIO 封装协程 I/O |
十六、总结
本项目实现了:
-
一个基于 C++ 协程的 TCP 网络服务框架
-
每个客户端由协程独立处理
-
支持 Echo 模式与聊天室广播模式
-
使用 epoll 驱动异步协程调度
-
构建了基础的协程式高并发网络模型
评论前必须登录!
注册