简介:这是一份基于C++实现的WebSocket服务器端简易Demo,面向具备基础C++与网络编程经验、希望系统理解WebSocket服务端开发流程的工程师与学习者。资源压缩包约15MB,共57个文件,以cpp源文件、h头文件与Visual Studio工程文件为主体,包含WebSocketServer.sln工程与websocket-server.cpp主程序,可直接打开构建并运行本地服务。Demo完整覆盖了WebSocket协议的关键实现环节,包括基于HTTP Upgrade的握手认证与Sec-WebSocket-Key校验、数据帧的编解码与分片处理、PING/PONG心跳维持、CLOSE帧关闭流程,以及异步事件驱动的多连接并发管理,并预留了业务逻辑扩展层,可在此基础上快速添加聊天室、实时行情推送、在线协作等自定义功能。包内还附带SHA1、MD5等加密工具源码与websocket-client.html测试页面,便于本地联调验证握手响应与消息收发流程;服务器端对异常断开、无效帧等场景也给出了基础容错写法,对理解RFC 6455规范和服务端工程化实践均有一定参考价值。目前已有1224人学习下载,是快速掌握C++ WebSocket服务端开发的实用入门范例。
1. 为什么我建议你直接抄一个 websocket C++ Server 端的简单 demo
先说结论:websocket 的 C++ 服务端实现没有你想象的那么神秘,但它也绝对不是一个下午能徒手写完的东西。我自己见过太多人上来就啃 RFC 6455,结果被掩码、分片、心跳握手这些细节卡住,写了一周还在 DEBUG 各种字节错位。如果你的目标是快速验证一个原型、给现有 C++ 服务加上实时推送能力,或者是第一次接触 WebSocket 协议想搞懂状态机的流转,那么一个最小的、能跑通的 server 端 demo 就是性价比最高的学习路径。它足够简单,能让你看清楚一条消息从客户端发出到服务端解析出来的完整链路;它又足够完整,能直接当项目骨架用。
这篇笔记我按照自己平时做方案的顺序来写:先讲选型与协议里最容易出错的几个点,再给出一个可以用 Bazel 直接拉起来的最小工程,然后逐个参数解释,再单独开一章讲我踩过的坑,最后给一个带心跳和广播的进阶版回显服务。读者里新手可以直接照着第二章的代码跑,熟手直接跳去第四章看注意项就行。
2. 选型逻辑与协议边界:为什么我选 IXWebSocket 而不是 Boost.Beast
2.1 库选型的三个现实理由
C++ 生态里 WebSocket 服务端库主要有两条路线:一条是 Boost.Beast,另一条是 IXWebSocket 这类独立库,再就是自己按 RFC 6455 手写协议。我推荐用 IXWebSocket 做 demo,原因很直白:Beast 的设计是建立在“你懂 ASIO 的异步模型”这个前提上的,而入门 demo 要解决的是业务验证问题,不是异步框架学习问题。IXWebSocket 封装了底层 socket 监听、连接管理、HTTP 握手升级这些脏活,对外暴露的接口是“连接建立”“消息到达”“连接断开”三个回调,这让一个 200 行的服务端程序变得可读。
第二个理由是跨平台。IXWebSocket 是 CMake 工程,可以直接 FetchContent 引进来,Windows、Linux、macOS 都能编。我自己曾经在一个模拟项目里被迫从 Linux 切到 Windows 验证某种第三方接入,Boost.Beast 的工程配置折腾了半天,而 IXWebSocket 的 CMake FetchContent 直接过了。
第三个理由是调试友好。它自带一个trace日志开关,能打印出每个包的类型和操作码。协议调试这种活儿,很多时候不是逻辑不对,而是帧格式错了自己看不见,有日志至少能定位到是服务端还是客户端的问题。
2.2 WebSocket 握手阶段:最容易理解的帧格式
WebSocket 连接可以分为两个阶段:HTTP 协议升级握手和后续的数据帧传输。握手是纯文本的 HTTP 请求,服务端要做的事情是校验客户端发来的Sec-WebSocket-Key,加上一个固定的 GUID 字符串做 SHA-1 哈希,然后把结果 Base64 编码后通过Sec-WebSocket-Accept头返回。这个流程在 IXWebSocket 里是自动完成的。你不需要手写这一步,但必须知道它的存在:如果你发现客户端一直卡在连接建立阶段,九成是这个握手校验没通过。
数据帧的格式需要你有一次印象,因为后面排查问题离不开它。RFC 6455 规定客户端发送给服务端的帧必须做掩码处理,服务端发往客户端的帧不需要。如果你自己拆包解析时发现收到的 payload 前面四个字节是掩码键,千万别忘了对 payload 逐字节异或。这个错位问题是我见过最多人翻车的地方。
2.3 demo 的边界定义
一个用于快速验证的 websocket C++ Server 端 demo,我认为它的功能边界应该是:能监听一个指定端口、能接受客户端连接、能收到文本消息并原样回显(或者打印到终端)、能检测到连接断开并自动清理资源。以下几个功能是 demo 阶段不应该做的:一是认证和鉴权,这会干扰你对连接状态的判断;二是复杂的二进制帧分包,因为那需要维护分片状态机;三是高并发性能测试,那已经属于生产优化范畴了。边界划清楚之后,你的学习曲线才会平缓。
3. 最小可运行服务端:完整代码与逐步解析
3.1 用 Bazel 引入依赖的最小工程
我平时最常用的构建方案是 Bazel,因为http_archive可以精确锁定版本,不需要依赖系统里面装了什么库。这里提供一个最小工程结构:
# WORKSPACE 文件 load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") http_archive( name = "ixwebsocket", urls = ["https://github.com/machinezone/IXWebSocket/archive/refs/tags/v11.4.0.tar.gz"], strip_prefix = "IXWebSocket-11.4.0", )然后是对应的 BUILD 文件:
cc_binary( name = "ws_server_demo", srcs = ["ws_server_demo.cpp"], deps = [ "@ixwebsocket//:ixwebsocket", ], linkopts = ["-lpthread"], )这里的参数说明:name是目标二进制名称,deps里面的@ixwebsocket告诉 Bazel 去WORKSPACE里找这个外部依赖,linkopts里加上-lpthread是因为多线程环境下ixwebsocket内部线程需要解析 pthread 符号。如果你用的是 CMake,那就不需要显式管这个,CMake 会自动传递链接选项。
3.2 核心回调逻辑:连接、消息、断开
下面是最小可运行的服务端核心代码。它的逻辑非常直白:创建一个WebSocketServer对象,设置三个回调,然后启动监听:
#include <ixwebsocket/IXWebSocketServer.h> #include <iostream> int main() { ix::WebSocketServer server(8080, "0.0.0.0"); server.setOnClientMessageCallback([](const std::shared_ptr<ix::ConnectionState>& connectionState, ix::WebSocket& webSocket, const ix::WebSocketMessagePtr& msg) { if (msg->type == ix::WebSocketMessageType::Message) { std::cout << "收到消息: " << msg->str << std::endl; webSocket.send(msg->str); } }); server.setOnConnectionCallback([](const std::shared_ptr<ix::ConnectionState>& connectionState, ix::WebSocketServer* server, ix::WebSocket& webSocket) { if (connectionState->getTerminated()) { std::cout << "连接断开" << std::endl; } else { std::cout << "新连接" << std::endl; } }); auto res = server.listen(); if (!res.first) { std::cerr << "监听失败: " << res.second << std::endl; return 1; } server.start(); std::cout << "服务已启动,监听端口 8080" << std::endl; std::cin.get(); server.stop(); return 0; }代码里的三个回调就是整个 demo 的骨架。setOnClientMessageCallback里的msg->str取的是文本消息的 payload,webSocket.send(msg->str)做回显。setOnConnectionCallback中通过connectionState->getTerminated()来判断这个回调是被连接建立触发的还是连接断开触发的。
关键的接口参数是server.listen()之前的server(8080, "0.0.0.0"):第一个参数是端口,第二个是绑定的 IP 地址,0.0.0.0表示监听所有网卡,如果你只想本机调试,改成127.0.0.1更安全。server.start()会启动后台线程处理连接,所以主线程用std::cin.get()阻塞住防止进程退出。server.stop()是优雅退出,会先关闭所有客户端连接再停止监听。
需要说明的一点是:如果你把这个 demo 服务的端口改成一个小于 1024 的端口,在 Linux 下必须用 root 权限运行,否则 listen 会直接报权限错误。这不是代码问题,是操作系统限制。
3.3 编译与运行验证命令
假设你已经在ws_server_demo.cpp所在目录执行了bazel run :ws_server_demo,你会看到类似下面的输出:
服务已启动,监听端口 8080这时候用浏览器打开一个简单的 HTML 测试页面,或者用现成的在线 websocket 测试工具连接ws://127.0.0.1:8080,然后发送文本"hello",服务端终端会打印出:
收到消息: hello同时浏览器端会立刻收到回显的消息。至此,一个最小可运行的 demo 就算验证通过了。如果这一步没问题,说明你的协议栈、网络监听、端口绑定都正常工作。用它作为起点,后续的业务逻辑就都是在这个回调框架里加代码了。
我可以给你一个直接能用的第八个 WebSocket 客户端调试工具,它其实不需要是真实服务端:很多浏览器 F12 控制台里可以直接写new WebSocket("ws://127.0.0.1:8080")出来。但注意:浏览器会自动带 Origin 头,如果你的服务端在校验 Origin,本地连接可能会失败。
4. 常见避坑记录:现象、原因、解决
4.1 回调里直接 sleep 导致消息堆积
现象:我在一个模拟项目里给setOnClientMessageCallback加了一段耗时 1 秒的逻辑,然后客户端连续发 100 条消息,发现服务端响应变得巨慢。
原因:ix::WebSocketServer的回调默认在接收消息的线程里执行。你 sleep 一秒,整个接收线程就卡住一秒。因为消息是按连接顺序处理的,前一条的回调没返回,后面的消息就在缓冲区排队。
解决:如果你确定回调逻辑比较重,不要在回调函数本身里做。把消息内容拷出来,丢到一个线程池或者std::async里去异步处理。demo 阶段最快的填法是:std::thread([&webSocket, msg]() { webSocket.send(msg->str); }).detach();,但注意这里webSocket是引用,如果连接断开,对象生命周期需要额外管理。
4.2 客户端断连后 send 直接崩溃
现象:客户端强关浏览器,服务端在某个业务线程里还在往这个webSocket对象send数据,程序直接抛异常或者退出。
原因:WebSocket 对象在多线程环境下不是线程安全的,而且连接关闭后底层 socket 已经不可写。回调里捕获到的shared_ptr<ConnectionState>能告诉你连接是否终止,但很多人图省事就在 lambda 里裸传了webSocket引用。
解决:最稳妥的做法是在收到连接断开回调时,把该连接的所有待发送任务取消掉。IXWebSocket 的webSocket.getUrl()可以拿到连接标识,你用一个unordered_set维护活跃连接的 URL,断线回调里 erase,发送前先count检查一下。
4.3 TCP_NODELAY 不设置导致回显延迟
现象:客户端发一条消息,服务端立刻回显,但客户端要等 40ms 左右才显示出来。
原因:这是 Nagle 算法和 TCP 延迟确认机制相互作用的结果。小包攒在缓冲区里不发送。
解决:IXWebSocket 提供了setSocketOptions接口。在启动前加一行:
server.setSocketOptions([](int sock) { ix::setTCPNoDelay(sock, true); });这句的意思是告诉 TCP 协议栈不要缓冲小包,有数据就发。日志类、聊天类实时应用基本都应该开这个。
4.4 局域网另一端连不上
现象:服务端监听127.0.0.1,结果手机连不上;改成0.0.0.0之后手机还是连不上。
原因:可能是防火墙拦了端口,也可能是路由隔离。第二点尤其在公司网络里常见,纯软件方面是防火墙。
解决:先用netstat -ant | grep 8080看端口是否处于LISTENING状态。如果监听地址是0.0.0.0,那就是防火墙的问题。Linux 下可以用sudo ufw allow 8080/tcp临时放行。另外,如果客户端是从浏览器页面发起的连接,页面本身的 HTTP 服务如果是 https 的,那么 websocket 必须用 wss,否则浏览器会直接拒绝。
4.5 用浏览器调试时收不到二进制消息
现象:用ws.onmessage = function(e) { console.log(e.data) }调试,服务端发来的二进制帧在控制台输出一个Blob对象,看不到内容。
原因:浏览器的 WebSocket API 默认把二进制帧当作 Blob 处理,不是 ArrayBuffer。你的服务端send(msg->binary)发的是二进制帧,浏览器接收端没有设置binaryType。
解决:在页面连接建立后加一行ws.binaryType = "arraybuffer";。这样你就可以用new TextDecoder().decode(new Uint8Array(e.data))把内容打印成文本。这个坑在调试阶段很容易让你误以为是服务端发错格式了。
5. 进阶为你自己的回显服务:心跳、广播、配置文件
5.1 心跳检测的必要性与实现
demo 阶段你可能会跳过心跳,但真实场景下网络断线自己根本感知不到。一个常见的场景是客户端连着 wifi 突然断网,TCP 连接不会立刻报错,服务端里这条连接就一直挂着。IXWebSocket 服务端的连接状态对象上有心跳接口,我会在连接建立回调里直接启动:
if (!connectionState->getTerminated()) { ix::WebSocketServerConfiguration config; config.pingIntervalMs = 30000; server.setServerConfiguration(config); }if (!connectionState->getTerminated()) { webSocket.ping(); }这段代码的意思是每 30 秒主动发一个 ping 帧给客户端,客户端如果还活着,会自动回 pong。如果客户端死了,TCP 层会在几次重试之后超时断开,服务端就能触发断开回调做清理。注意:这里webSocket.ping()应该放在定时器里,上面代码只是一个回调内部的启动示意,实际工程我会把定时器放在另一个线程。
5.2 频率限制
setServerConfiguration里的pingIntervalMs是全局配置。如果你需要,直接放在listen()之前调用。网上很多资料会把 ping 放在业务回调里,那其实是不对的,它会阻塞消息处理线程。
5.3 广播消息:向所有活跃连接推送
广播是 WebSocket 服务端最典型的应用场景。比如一个状态监控页面,后端采集到数据后要实时推给所有在线客户端。IXWebSocket 用getClients()直接拿连接列表:
server.getClients()[i]->send("boardcast: " + msg->str);这段代码的含义是遍历当前所有客户端连接,逐个发送消息。发送失败时,send返回值里有getSuccess布尔值,通过它可以找出失效连接并踢掉:
auto client = server.getClients()[i]; bool success = client->send("...").success; if (!success) client->close();高并发生产环境里广播需要加锁,demo 阶段不需要。但你要知道:如果你在业务线程里广播,同时另一个连接断开导致容器被修改,迭代器可能失效。所以我一般把广播逻辑放在主循环里统一处理,而不是嵌在连接回调里。
5.4 负载均衡
没有负载均衡。WebSocket 的广播在单机几十个连接上没有压力,IP 层的组播反而不适合业务筛选。作为一个进阶 demo,掌握以上两点:频控周期和失效清理,就足够覆盖大部分消息推送场景了。
6. 验证连通性与压测:本地小流量测试的几个小技巧
最后一个实操大招是教你怎么用一条命令验证你的服务端是不是真的稳定。有人会打开浏览器收脉冲,有人会写个 Java 客户端来敲。但作为 C++ 服务端,我更喜欢用轻量级的命令行工具直接压一把,这样能快速暴露连接数和消息频次的上限。
技巧一是用 Python 快速写一个压测小脚本,模拟多个客户端同时连接和发消息。技巧二是查看服务端进程的资源占用,确认有没有句柄泄漏。技巧三是我个人的习惯:关闭服务端时用server.stop()手动触发,然后观察进程是否正常退出,如果卡住,说明某个回调线程没有正确结束。
for i in {1..50}; do python3 -c "import websocket; ws=websocket.create_connection('ws://127.0.0.1:8080'); ws.send('hello'); print(ws.recv())" & done; wait这行命令的含义是用 Python 的websocket-client库同时创建 50 个连接,每个连接发送一条消息并阻塞等待回显。&让这些进程并行执行,wait等待全部结束。如果服务端回显逻辑正确,你应该看到 50 行hello输出。这一步要是能过,说明回调模型本身没有串行处理问题,至少并发连接能力是达标的。
跑完之后我一般还会看一眼服务端进程的状态:ps -eLf | grep ws_server_demo里线程数是否正常。这个检查是我吃过亏的教训——早期一个版本在连接断开时没有释放线程资源,跑了 20 分钟线程数涨到一千多,内存跟着飙。所以现在我做 demo 必做两件事:一是并发连接压一下,二是关掉之后确认线程数能归零。希望这些经验能帮到你,至少能让你第一次跑通 websocket C++ Server 端的时候少一点折腾。
本文还有配套的精品资源,点击获取