C++手搓WebSocket服务器:从协议解析到高并发架构实战
1. 项目概述为什么用C手搓WebSocket服务器如果你正在看这篇文章大概率是厌倦了那些“一键部署”的WebSocket服务或者对Java的Netty、Node.js的ws库背后的黑盒感到好奇想从更底层的地方搞清楚一个实时双向通信的服务器到底是怎么转起来的。用C来搭建WebSocket服务器听起来有点“硬核”但它带来的好处是实实在在的极致的性能控制、最小的资源开销以及对网络协议栈的透彻理解。这不仅仅是完成一个功能更像是一次深入网络编程腹地的探险。在实际项目中无论是高频交易系统、大型多人在线游戏的实时交互还是物联网设备海量连接的管理C WebSocket服务器都是核心基础设施之一。它能让你精确控制每一个字节的收发、每一个连接的生存周期以及面对成千上万并发连接时内存和CPU的每一分消耗。这次我们就抛开那些厚重的框架从Socket API开始一步步构建一个属于我们自己的、能跑起来的C WebSocket服务器。2. 核心原理与协议握手拆解WebSocket协议本身并不复杂它的核心魅力在于在TCP这个可靠的字节流协议之上建立了一套轻量级的、基于帧Frame的双向消息通信机制。整个流程可以概括为“一次HTTP握手长久WebSocket通信”。2.1 从HTTP升级到WebSocket一切的开始都是一次普通的HTTP请求但这次请求带着特殊的“意图”。客户端握手请求客户端比如浏览器会向服务器发送一个类似下面的HTTP GET请求GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13这里有几个关键点Upgrade: websocket和Connection: Upgrade明确告知服务器客户端希望将协议升级到WebSocket。Sec-WebSocket-Key一个由客户端随机生成的Base64编码的16字节值。它是后续服务器生成应答密钥的原料。Sec-WebSocket-Version: 13指明使用的WebSocket协议版本13是目前RFC 6455定义的稳定版本。服务器握手响应服务器收到请求后需要验证Upgrade头并生成正确的响应。如果同意升级则返回HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbKxOo其中Sec-WebSocket-Accept的值是服务器对客户端挑战的应答。它的计算方式是将客户端传来的Sec-WebSocket-Key与一个固定的GUID字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”拼接然后计算这个拼接字符串的SHA-1哈希值最后将这个哈希值进行Base64编码。注意这个计算过程必须严格遵循RFC标准。很多自己实现的服务器栽在第一步就是因为哈希或编码出错导致浏览器一直报“连接失败”。我建议在开发初期用一个已知可用的库如OpenSSL来计算SHA-1和Base64确保这一步绝对正确。2.2 WebSocket数据帧Frame解析握手成功后通信便脱离了HTTP进入WebSocket数据帧的交换。一个WebSocket帧的结构是理解所有通信的基础。一个帧的二进制布局大致如下0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -------------------------------------------------------- |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len126/127) | | |1|2|3| |K| | | ------------------------- - - - - - - - - - - - - - - - | Extended payload length continued, if payload len 127 | - - - - - - - - - - - - - - - ------------------------------- | |Masking-key, if MASK set to 1 | -------------------------------------------------------------- | Masking-key (continued) | Payload Data | -------------------------------- - - - - - - - - - - - - - - - : Payload Data continued ... : - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | Payload Data continued ... | ---------------------------------------------------------------你需要关注的核心字段FIN (1 bit): 标识这是否是消息的最后一个帧。一个消息Message可能被分割成多个帧Frames发送。Opcode (4 bits): 定义帧的类型。0x0: 延续帧Continuation0x1: 文本帧Text0x2: 二进制帧Binary0x8: 连接关闭Close0x9: Ping心跳检测0xA: Pong心跳响应MASK (1 bit): 指示负载数据Payload Data是否被掩码Mask处理。根据RFC 6455所有从客户端发往服务器的帧必须被掩码MASK1而从服务器发往客户端的帧则不能掩码MASK0。这是安全设计的一部分防止缓存污染攻击。Payload len (7/716/764 bits): 负载数据的长度。这是一个变长字段如果值在0-125之间它就是实际长度。如果是126则后面2个字节16位表示长度。如果是127则后面8个字节64位表示长度。Masking-key (0 or 4 bytes): 如果MASK位为1则紧跟长度字段后是4字节的掩码密钥。Payload Data: 实际的应用数据。如果被掩码了需要先用掩码密钥进行解码transformed-octet-i original-octet-i XOR masking-key[i mod 4]。实操心得手动解析帧头是最容易出错的地方特别是处理变长长度字段时。我的建议是先写一个健壮的read_exact函数确保能从Socket中精确读取指定字节数。解析时先将前2个字节读入按位解析出操作码、掩码位和基础长度再根据基础长度决定是否要继续读取额外的长度字节和掩码密钥。这个过程务必小心字节序网络字节序是大端。3. 服务器架构设计与核心类实现一个健壮的服务器不能是面条代码。我们需要一个清晰的架构来管理连接、处理I/O和解析协议。这里我采用经典的 Reactor 模式配合非阻塞I/O和多路复用如epoll或kqueue这是C高性能网络服务器的常见选择。3.1 核心类设计我们设计几个核心类来分工合作WebSocketServer: 服务器的主类负责监听端口、接受新连接、管理事件循环Event Loop。WebSocketSession: 代表一个WebSocket连接会话。每个连接的Socket、读写缓冲区、状态握手中、已连接、关闭中都由它管理。它负责该连接上的所有数据帧的解析、组帧和业务处理。Frame: 一个纯粹的数据结构表示一个解析后的WebSocket帧包含操作码、掩码、负载数据等字段。Message: 表示一个完整的应用层消息可能由多个Frame组装而成当FIN0时。3.2 事件循环与连接管理服务器的核心是一个无限循环在这个循环中我们使用epollLinux或kqueueBSD/macOS来监听所有Socket文件描述符上的事件可读、可写、错误。// 伪代码展示主循环逻辑 void WebSocketServer::run() { int epoll_fd epoll_create1(0); // 将监听socket加入epoll struct epoll_event ev; ev.events EPOLLIN; ev.data.fd listen_socket_; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_socket_, ev); while (is_running_) { int nfds epoll_wait(epoll_fd, events_, MAX_EVENTS, -1); for (int i 0; i nfds; i) { int fd events_[i].data.fd; if (fd listen_socket_) { // 接受新连接 acceptNewConnection(); } else { // 处理已有连接上的I/O事件 auto session findSessionByFd(fd); if (session) { if (events_[i].events EPOLLIN) { session-handleRead(); } if (events_[i].events EPOLLOUT) { session-handleWrite(); } if (events_[i].events (EPOLLERR | EPOLLHUP)) { session-handleError(); } } } } // 处理定时任务如心跳检测 checkHeartbeat(); } }关键点非阻塞Socket创建的所有客户端Socket都必须设置为非阻塞模式fcntl(fd, F_SETFL, O_NONBLOCK)。这是实现高并发的基石避免一个慢连接阻塞整个服务器。边缘触发(ET) vs 水平触发(LT)epoll支持两种模式。边缘触发EPOLLET效率更高但编程更复杂要求你必须一次性读完或写完所有数据直到返回EAGAIN。对于新手我建议先从水平触发开始更不容易出错。Session生命周期管理WebSocketSession对象通常通过std::shared_ptr管理并存储在一个std::unordered_mapint, std::shared_ptrWebSocketSession中键是文件描述符fd。当连接关闭时需要从map中移除并确保资源正确释放。3.3 WebSocketSession 的关键方法实现WebSocketSession是业务逻辑发生的地方。handleRead()流程循环调用recv读取数据到该Session的读缓冲区。如果连接还处于握手阶段则检查读缓冲区中是否有完整的HTTP请求并处理握手。如果握手已完成则调用parseFrames()尝试从读缓冲区中解析出一个或多个完整的WebSocket帧。对于每一个解析出的Frame根据其opcode进行分发处理Text/Binary Frame: 如果FIN1则这是一个完整的消息可以传递给上层业务逻辑如广播给其他客户端。如果FIN0则需要缓存该帧等待后续的Continuation帧。Close Frame (0x8): 收到关闭帧应按照协议规范可能回送一个Close Frame然后开始关闭连接流程。Ping Frame (0x9): 必须立即回送一个包含相同应用数据Payload Data的Pong Frame (0xA)。Pong Frame (0xA): 通常是对我们之前发送的Ping的回应用于更新该连接的最后活跃时间实现心跳保活。sendMessage(const std::string msg)流程根据消息长度和类型文本/二进制构造一个或多个WebSocket帧。记住从服务器发出的帧MASK位必须为0。将构造好的帧数据放入该Session的写缓冲区。如果该Socket在epoll中未监听可写事件EPOLLOUT则添加监听。因为写缓冲区可能满需要等待可写事件再继续发送。在handleWrite()中尝试将写缓冲区的数据通过send发送出去。如果一次没发完就等待下次可写事件如果发完了就从epoll中移除对可写事件的监听避免忙等待。避坑指南写缓冲区与EPOLLOUT一个常见的错误是一有数据就立刻调用send如果TCP发送缓冲区已满send在非阻塞模式下会返回EAGAIN或EWOULDBLOCK。这时你必须将剩余数据存入应用层的写缓冲区并监听该Socket的EPOLLOUT事件。等可写事件触发时再继续发送。发送完成后一定要记得取消对EPOLLOUT的监听否则只要TCP发送缓冲区有空闲就会一直触发可写事件导致CPU空转。4. 从零开始的详细搭建步骤理论说再多不如动手做一遍。下面我们抛开大型库用最基础的Socket API和标准库在Linux环境下搭建一个最小可用的WebSocket服务器。我们选择在Linux上开发因为它的网络编程接口epoll是高性能服务器的标准。4.1 环境准备与项目结构首先确保你的开发环境有GCC/Clang和CMake。我们不需要额外的WebSocket库但需要OpenSSL库来计算握手时的SHA-1哈希。# 在Ubuntu/Debian上安装依赖 sudo apt-get update sudo apt-get install build-essential cmake libssl-dev # 项目目录结构建议 websocket_server_cpp/ ├── CMakeLists.txt ├── include/ │ ├── server.h │ ├── session.h │ └── frame.h ├── src/ │ ├── server.cpp │ ├── session.cpp │ ├── frame.cpp │ └── main.cpp └── third_party/ # 空目录如果需要可以放一些基础工具函数CMakeLists.txt的基础配置cmake_minimum_required(VERSION 3.10) project(WebSocketServer LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 查找 OpenSSL find_package(OpenSSL REQUIRED) # 包含目录 include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) include_directories(${OPENSSL_INCLUDE_DIR}) # 添加可执行文件 add_executable(ws_server src/main.cpp src/server.cpp src/session.cpp src/frame.cpp ) # 链接库 target_link_libraries(ws_server ${OPENSSL_LIBRARIES} pthread)4.2 核心代码实现握手与帧解析第一步实现握手响应 (session.cpp中的关键函数)#include openssl/sha.h #include openssl/evp.h #include base64.h // 需要一个简单的Base64编码实现可以网上找或自己写 bool WebSocketSession::handleHandshake(const char* buffer, size_t len) { // 1. 将buffer解析为HTTP请求头这里简化实际需要解析多行 std::string request(buffer, len); if (request.find(Upgrade: websocket) std::string::npos || request.find(Connection: Upgrade) std::string::npos) { return false; // 不是WebSocket升级请求 } // 2. 提取 Sec-WebSocket-Key std::string key; size_t key_start request.find(Sec-WebSocket-Key: ); if (key_start ! std::string::npos) { key_start 19; // 字符串长度 size_t key_end request.find(\r\n, key_start); key request.substr(key_start, key_end - key_start); } if (key.empty()) return false; // 3. 计算 Sec-WebSocket-Accept std::string magic_guid 258EAFA5-E914-47DA-95CA-C5AB0DC85B11; std::string combined key magic_guid; unsigned char hash[SHA_DIGEST_LENGTH]; // SHA-1结果是20字节 SHA1(reinterpret_castconst unsigned char*(combined.c_str()), combined.size(), hash); // 4. Base64编码 std::string accept_key base64_encode(hash, SHA_DIGEST_LENGTH); // 5. 构造并发送HTTP 101响应 std::string response HTTP/1.1 101 Switching Protocols\r\n Upgrade: websocket\r\n Connection: Upgrade\r\n Sec-WebSocket-Accept: accept_key \r\n\r\n; sendRawData(response.c_str(), response.size()); state_ State::CONNECTED; return true; }第二步实现WebSocket帧解析 (frame.cpp)#include arpa/inet.h // 用于ntohs, ntohll等字节序转换 bool parseFrameHeader(const char* buffer, size_t len, Frame frame, size_t header_len) { if (len 2) return false; // 至少需要前2个字节 const unsigned char* data reinterpret_castconst unsigned char*(buffer); frame.fin (data[0] 0x80) ! 0; frame.opcode data[0] 0x0F; frame.masked (data[1] 0x80) ! 0; uint64_t payload_len data[1] 0x7F; size_t index 2; if (payload_len 126) { if (len index 2) return false; uint16_t len_16; memcpy(len_16, buffer index, 2); payload_len ntohs(len_16); // 网络字节序转主机字节序 index 2; } else if (payload_len 127) { if (len index 8) return false; uint64_t len_64; memcpy(len_64, buffer index, 8); payload_len be64toh(len_64); // 大端转主机字节序 index 8; } if (frame.masked) { if (len index 4) return false; memcpy(frame.masking_key, buffer index, 4); index 4; } frame.payload_length payload_len; header_len index; // 帧头总长度 return true; } void unmaskPayload(Frame frame, const char* masked_data, char* unmasked_data) { if (!frame.masked) { memcpy(unmasked_data, masked_data, frame.payload_length); return; } for (uint64_t i 0; i frame.payload_length; i) { unmasked_data[i] masked_data[i] ^ frame.masking_key[i % 4]; } }第三步在Session中整合帧解析与处理在WebSocketSession::handleRead()中握手成功后循环解析帧void WebSocketSession::processData() { while (read_buffer_.size() 0) { Frame frame; size_t header_len 0; // 尝试解析一个帧头 if (!parseFrameHeader(read_buffer_.data(), read_buffer_.size(), frame, header_len)) { break; // 数据不够一个完整的帧头等待下次读取 } // 检查是否有完整的帧数据头负载 if (read_buffer_.size() header_len frame.payload_length) { break; // 数据不够一个完整的帧等待下次读取 } // 提取负载数据并解码 std::vectorchar payload(frame.payload_length); const char* payload_start read_buffer_.data() header_len; unmaskPayload(frame, payload_start, payload.data()); // 处理不同类型的帧 handleFrame(frame, payload.data()); // 从读缓冲区中移除已处理的数据 read_buffer_.erase(read_buffer_.begin(), read_buffer_.begin() header_len frame.payload_length); } } void WebSocketSession::handleFrame(const Frame frame, const char* payload) { switch (frame.opcode) { case 0x1: // 文本帧 case 0x2: { // 二进制帧 if (frame.fin) { // 这是一个完整的消息 std::string message(payload, frame.payload_length); onMessage(message); // 回调给上层业务逻辑 } else { // 是消息片段需要缓存 cacheFragment(frame, payload, frame.payload_length); } break; } case 0x8: { // 关闭帧 // 可以解析关闭帧中的状态码和原因前2字节是状态码 sendCloseFrame(); startClosing(); break; } case 0x9: { // Ping帧 // 必须回送一个Pong帧负载数据与Ping相同 sendPongFrame(payload, frame.payload_length); break; } case 0xA: // Pong帧通常忽略或用于更新心跳时间 updateLastActiveTime(); break; case 0x0: { // 延续帧 appendFragment(payload, frame.payload_length); if (frame.fin) { // 所有片段接收完毕组装成完整消息 std::string full_message assembleFragments(); onMessage(full_message); clearFragments(); } break; } default: // 收到未知操作码按协议应关闭连接 sendCloseFrame(1002); // 协议错误 startClosing(); break; } }4.3 编译与运行测试在项目根目录下mkdir build cd build cmake .. make -j4编译成功后会生成ws_server可执行文件。运行服务器默认监听8080端口./ws_server 8080现在你可以使用任何WebSocket客户端进行测试。最简单的方法是写一个HTML页面用JavaScript连接!DOCTYPE html html body script const ws new WebSocket(ws://你的服务器IP:8080); ws.onopen () { console.log(Connected!); ws.send(Hello C Server!); }; ws.onmessage (event) { console.log(Message from server:, event.data); }; ws.onerror (error) { console.error(WebSocket Error:, error); }; /script /body /html或者使用命令行工具如wscat(npm install -g wscat)wscat -c ws://localhost:80805. 进阶优化与生产环境考量一个能跑通的Demo只是起点要用于实际项目还有大量的细节需要打磨。5.1 心跳机制与连接保活网络环境复杂中间的路由器或防火墙可能会清除长时间空闲的连接。WebSocket协议通过Ping/Pong帧提供了心跳机制。实现思路在WebSocketSession中记录最后一次收到有效数据任何帧或发送Ping的时间 (last_active_)。在服务器的checkHeartbeat()定时任务中比如每秒或每30秒一次遍历所有已连接的Session。如果某个Session的last_active_距离现在超过设定的超时时间如60秒则向该连接发送一个Ping帧。同时记录这次Ping的发送时间。如果发送Ping后在另一个更短的超时时间内如30秒没有收到Pong回应则认为连接已失效主动关闭它。void WebSocketServer::checkHeartbeat() { auto now std::chrono::steady_clock::now(); for (auto [fd, session] : sessions_) { auto idle_duration now - session-getLastActiveTime(); if (idle_duration std::chrono::seconds(60)) { // 发送Ping session-sendPing(); session-setPingSentTime(now); } // 检查Ping是否超时未回复 auto ping_sent_time session-getPingSentTime(); if (ping_sent_time.time_since_epoch().count() 0) { if (now - ping_sent_time std::chrono::seconds(30)) { // Ping超时关闭连接 session-forceClose(); } } } }5.2 多线程与资源竞争单线程Reactor模式虽然清晰但可能无法充分利用多核CPU。常见的优化模式是One Loop Per Thread创建多个事件循环I/O线程每个线程运行一个独立的epoll实例。主监听Socket采用SO_REUSEPORT选项让多个线程可以同时accept新连接操作系统内核会进行负载均衡。线程池处理业务逻辑I/O线程只负责数据的收发和协议的解析编解码解析出的完整应用层消息可以包装成任务投递到一个独立的业务逻辑线程池中进行处理。这样可以避免耗时的业务计算阻塞网络I/O。资源竞争当多个线程可能操作同一个Session时比如一个线程在收数据另一个线程的业务逻辑要发数据必须加锁。一个简单的做法是每个WebSocketSession自带一个互斥锁 (std::mutex)任何对内部状态如写缓冲区的修改都需要先加锁。但要注意锁的粒度避免长时间持有锁影响性能。5.3 流量控制与背压Backpressure在高负载下发送数据的速度可能超过对端接收或网络传输的能力。这就是背压问题。处理策略写缓冲区监控如前所述当send返回EAGAIN时数据被存入应用层写缓冲区。你需要设置一个写缓冲区的上限如1MB。暂停读取如果某个Session的写缓冲区已经满了说明对端消费太慢。此时你应该从epoll中暂时移除对该Socket可读事件EPOLLIN的监听停止从该连接读取更多数据直到写缓冲区被清空到一定程度后再恢复监听。这相当于TCP的滑动窗口在应用层的体现防止服务器内存被慢客户端拖垮。连接级限流对于广播场景如果某个Session发送持续缓慢可以考虑将其标记为“慢客户端”降低向其发送数据的优先级甚至断开连接。5.4 安全性增强TLS/SSL加密 (wss://)生产环境必须使用WSS。你需要集成OpenSSL或类似的TLS库。流程是先完成TCP连接和TLS握手再进行WebSocket的HTTP升级握手。这会使WebSocketSession的读写接口从操作Socket变为操作SSL对象。输入验证与限速对接收到的消息大小进行限制防止恶意客户端发送超大消息耗尽内存。对消息速率进行限制防止洪水攻击。Origin校验在握手阶段可以检查HTTP头中的Origin字段只允许来自信任域名的连接。子协议协商通过Sec-WebSocket-Protocol头客户端和服务器可以协商使用哪种上层应用协议例如chat,soap,wamp。服务器应根据自身支持的情况进行响应。6. 常见问题排查与调试技巧自己实现协议遇到问题是家常便饭。这里记录几个我踩过的坑和解决方法。问题1连接握手失败浏览器报“WebSocket connection failed”排查步骤抓包分析使用tcpdump或 Wireshark 抓取TCP流量这是最直接的方法。过滤tcp.port 8080查看三次握手是否成功以及HTTP升级请求和响应的原始数据。检查响应头确保服务器返回的HTTP状态码是101而不是200或其它。检查Upgrade和Connection头是否拼写正确大小写不敏感但最好按规范来。验证Sec-WebSocket-Accept这是最容易出错的地方。将你计算出的accept_key与抓包看到的或使用在线工具计算的结果进行比对。确保SHA-1和Base64的编码完全正确。检查响应结尾HTTP响应头必须以\r\n\r\n结束多一个空格或少一个换行都会导致握手失败。问题2可以连接但收不到消息或消息乱码排查步骤检查掩码Mask牢记铁律客户端发来的帧必须被掩码MASK1服务器发出的帧必须无掩码MASK0。很多乱码问题都是因为掩码处理反了或者忘记解码。验证帧头解析特别是负载长度Payload Length的解析。当长度是126或127时后面跟的长度值是网络字节序大端。在x86/x64小端机器上必须用ntohs或be64toh进行转换。分片消息处理检查你的FIN位处理逻辑。如果客户端发送的消息被分成了多个帧你的服务器是否能正确地将它们缓存并组装对于延续帧opcode0x0的处理是否正确打印调试在handleRead和sendMessage中将收发的原始字节以十六进制打印出来与标准的WebSocket协议分析工具如浏览器开发者工具的Network标签下的WebSocket帧查看器进行对比。问题3服务器在高并发连接下内存缓慢增长或崩溃排查步骤检查Session泄漏确保在连接关闭时收到Close帧、读写出错、超时对应的WebSocketSession对象被正确销毁并从管理它的Map中移除。使用valgrind工具检测内存泄漏。检查写缓冲区膨胀实现前面提到的背压控制。如果没有慢客户端会导致服务器为它堆积大量待发送数据耗尽内存。检查文件描述符泄漏每个Socket都是一个文件描述符。关闭连接时除了调用close(fd)还要记得将其从epoll实例中移除 (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, nullptr))。系统对进程能打开的文件描述符数量是有限制的。压力测试使用工具如websocket-bench或autobahn|testsuite进行并发连接和消息吞吐测试。autobahn尤其重要它能对你的服务器实现进行全面的协议合规性测试。问题4如何调试复杂的帧交互使用autobahn|testsuite这是一个用Python编写的WebSocket协议测试套件。启动它的测试客户端让它连接你的服务器它会自动运行成百上千个测试用例检查你的服务器对各种正常和异常情况如分片、控制帧穿插、非法数据等的处理是否符合RFC标准。这是让服务器变得健壮的最佳途径。# 安装并运行autobahn测试 pip install autobahn wstest -m fuzzingserver # 启动测试服务器用于客户端测试 # 或者运行测试客户端来测试你的服务器 wstest -m fuzzingclient -s fuzzingclient.json你需要编写一个fuzzingclient.json配置文件指定你的服务器地址和端口。从头实现一个C WebSocket服务器是一次对网络编程基本功的全面锻炼。你会深刻理解TCP的流特性、非阻塞I/O的边角情况、协议设计的精巧之处以及高性能服务器编程中的各种权衡。虽然过程充满挑战但当你看到自己编写的服务器稳定地处理成千上万的实时连接时那种成就感是使用现成框架无法比拟的。这个项目可以作为你深入网络世界的基石在此基础上你可以轻松地扩展出支持房间、广播、RPC等各种功能的实时通信系统。