资讯动态

可直接编译的C++ WebSocket Demo:从跑通到理解协议

发布时间:2026/10/15 16:55:11 来源:尧图企业网站定制
简介面向C开发者的WebSocket协议实战代码包基于websocketpp、Boost与jsoncpp实现客户端/服务端全双工通信内附CMake构建配置可跨平台直接编译运行适合初学者快速掌握C WebSocket集成和JSON数据交换。压缩包共14553个文件约154.62MB内置完整websocketpp与Boost头文件库主体为hpp文件超14000个、ipp与h头文件另有少量cpp示例、sample样例和CMake配置这些头文件涵盖线程、网络I/O、智能指针等基础能力省去手动匹配依赖版本同时也便于查看第三方库内部实现以辅助调试。其中的test-websocket目录包含可直接运行的Demo源码与CMakeLists.txt覆盖握手、收发消息、事件处理等关键流程并演示如何用jsoncpp完成对象与JSON格式互转开发者可在本地环境直接测试和修改验证。目前已有1076人学习下载对于需要低成本获得可运行C WebSocket模板的开发者是一份实用参考资料。1. C WebSocket Demo为什么我劝你先跑通再啃协议做C实时通信几乎所有人都会卡在同一个地方WebSocket协议看着不复杂但真要从零写握手、掩码、分片、关闭帧一套下来没个三五天根本立不住。这个标题说可直接编译意味着它已经把协议细节包好你把代码拿下来、装好依赖、敲一条编译命令就能看到消息从客户端发到服务端再推回来的完整链路。对想验证方案能不能用、想给嵌入式设备或服务端加实时通道的开发者来说这是成本最低的入场方式。我见过太多人一上来就研究RFC 6455结果两个星期过去还在调握手。反向操作反而快先把这个Demo编译起来跑通再对着代码看协议细节哪里发帧、哪里回Pong一目了然。这篇文章就围绕这个Demo讲清楚选型理由、服务端和客户端的最小实现、以及那些会让可直接编译变成直接报错的隐藏坑。2. 先选路线自研、通用库还是高并发框架2.1 为什么Demo首选成熟库而不是手写握手WebSocket的连接建立不是普通的TCP收发它要求客户端先发一个HTTP Upgrade请求服务端计算Sec-WebSocket-Accept并返回101状态码之后才是帧数据传输。这个握手过程涉及SHA1摘要和Base64编码看似简单但边界情况不少请求头大小写不敏感、扩展协商、Cookie携带、子协议选择任何一个环节出错浏览器控制台直接报WebSocket connection failed。更麻烦的是帧格式。服务端发往客户端的帧不需要掩码客户端发往服务端的帧必须带掩码这个细节漏了对端会直接断开连接。还有分片一个大消息会被拆成多个帧首帧opcode是0x1或0x2后续帧opcode是0x0需要等FIN置1才能拼完整。自己写这些逻辑不是不行而是调试成本高——你很难判断是协议问题还是业务问题。所以常见做法是Demo直接用成熟库先把链路跑通等业务验证通过后再评估是否需要为性能或资源占用换成自研实现。多数情况下成熟库的性能已经够用不必重复造轮子。2.2 两个主流方案的选型对比当前C里最常见的两个选择一个是基于Boost.Asio的websocketppheader-only设计依赖清晰API贴近业务逻辑另一个是Boost.Beast同为Boost生态底层更贴近Asio性能和灵活性都更好但模板嵌套深新手看编译报错容易懵。我一般建议Demo用前者理由很实际出错信息能看懂回调模型直观从示例改成自己的业务代码只需要动几个handler。对比项websocketppBoost.Beast依赖Boost.Asio、OpenSSLBoost.Asio、OpenSSL安装复杂度header-only直接include需要Boost库头文件API风格高层回调面向业务低层流操作面向协议性能足够支撑万级连接更高适合网关类编译排错报错相对清晰模板错误信息冗长适用场景Demo、业务服务器高性能代理、网关选型时还要考虑团队里其他人的维护成本。如果项目后续要交给不熟悉Boost的人维护websocketpp的代码结构更容易上手如果团队本身就是Boost深度用户Beast更顺手。这个没有绝对答案但Demo阶段选websocketpp通常能最快看到结果。2.3 裸写最小握手理解协议的最低成本路径哪怕用库我也建议你亲手走一遍握手逻辑否则调试网络问题时协议栈就是个黑匣子。核心就两步读请求头里的Sec-WebSocket-Key拼接固定GUID后做SHA1再Base64编码返回给客户端。#include openssl/sha.h #include openssl/base64.h #include map #include string // 输入: 客户端请求头中的 Sec-WebSocket-Key // 输出: 响应头中的 Sec-WebSocket-Accept std::string calc_accept(const std::string client_key) { // GUID 是 RFC 6455 规定死的字符串不能改 const std::string GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11; std::string raw client_key GUID; unsigned char digest[SHA_DIGEST_LENGTH] {0}; SHA1(reinterpret_castconst unsigned char*(raw.data()), raw.size(), digest); // base64 编码需要申请足够缓冲区 char* out nullptr; int len EVP_EncodeBlock(reinterpret_castunsigned char*(out), digest, SHA_DIGEST_LENGTH); std::string result(out, len); return result; }这段代码只是握手计算的核心实际网络收发还是得靠socket。这么一段足够让你理解握手不是魔法是HTTP升级协议加上一个固定格式的应答头。库帮你封装了这些但出了问题你能顺着抓包定位是密钥算错了还是响应格式错了。提示抓包时重点看Upgrade头是否等于websocket以及状态码是否为101。如果看到200说明服务端把WebSocket请求当普通HTTP处理了。3. 服务端Demo从单连接回显到广播推送3.1 用websocketpp搭一个最小可用服务端服务端的核心逻辑可以拆成四件事初始化Asio、注册三个回调打开、关闭、收消息、监听端口、启动事件循环。以下是可直接编译的最小版本依赖Boost和OpenSSL编译命令后面单独讲。#include websocketpp/config/asio_no_tls.hpp #include websocketpp/server.hpp #include iostream #include set #include mutex typedef websocketpp::serverwebsocketpp::config::asio WsServer; typedef websocketpp::connection_hdl ClientHandle; class BroadcastServer { public: void run(uint16_t port) { // set_reuse_addr 避免端口处于 TIME_WAIT 时重启失败 m_server.set_reuse_addr(true); // 这四个回调是业务入口后续逻辑都在这里扩展 m_server.set_open_handler([this](ClientHandle hdl) { on_open(hdl); }); m_server.set_close_handler([this](ClientHandle hdl) { on_close(hdl); }); m_server.set_message_handler( [this](ClientHandle hdl, WsServer::message_ptr msg) { on_message(hdl, msg); }); m_server.init_asio(); m_server.listen(port); m_server.start_accept(); // run() 会阻塞当前线程生产环境通常放到独立线程 m_server.run(); } private: void on_open(ClientHandle hdl) { std::lock_guardstd::mutex lock(m_mutex); m_clients.insert(hdl); std::cout client connected, total: m_clients.size() std::endl; } void on_close(ClientHandle hdl) { std::lock_guardstd::mutex lock(m_mutex); m_clients.erase(hdl); std::cout client disconnected, total: m_clients.size() std::endl; } void on_message(ClientHandle hdl, WsServer::message_ptr msg) { std::cout recv: msg-get_payload() std::endl; // 默认把消息原样回给发送者这是 echo 模式 m_server.send(hdl, msg-get_payload(), msg-get_opcode()); } WsServer m_server; std::mutex m_mutex; std::setClientHandle, std::owner_lessClientHandle m_clients; }; int main(int argc, char* argv[]) { uint16_t port 9002; if (argc 1) port static_castuint16_t(std::atoi(argv[1])); BroadcastServer server; server.run(port); return 0; }这个代码里有几个关键点。std::owner_less是必须的因为connection_hdl是weak_ptr的别名std::set默认用operator无法排序weak_ptr。msg-get_opcode()能拿到帧类型文本帧是0x1二进制帧是0x2回显时保持原样就不会出现类型错乱。3.2 广播改造让一个客户端的消息推给所有人回显只能验证链路通不通真实业务场景更多是一个客户端发指令服务端推送给所有订阅者。改法也简单把on_message里的send从发给单个hdl换成遍历m_clients逐个发。注意遍历时要持锁但send本身不能持锁否则某个客户端慢会导致全频道阻塞。void on_message(ClientHandle hdl, WsServer::message_ptr msg) { // 先把负载取出来避免在锁内访问消息对象 std::string payload msg-get_payload(); websocketpp::frame::opcode::value opcode msg-get_opcode(); // 复制一份连接列表持有锁但只做拷贝 std::setClientHandle, std::owner_lessClientHandle targets; { std::lock_guardstd::mutex lock(m_mutex); targets m_clients; } // 遍历发送发送失败只打印错误不影响其他客户端 for (auto target : targets) { try { m_server.send(target, payload, opcode); } catch (const websocketpp::lib::error_code e) { std::cerr send failed: e.message() std::endl; } } }复制的开销在客户端数量不多时可以忽略。如果到了上万连接就要考虑分片批量发送或改用共享队列加工作线程的模式。Demo阶段先保证正确性性能优化留到压测之后这是我一贯的节奏。3.3 CMake配置与编译命令可直接编译这四个字里藏着最多的坑。websocketpp是header-only不需要单独编译但它的调用链依赖Boost和OpenSSLCMake里必须正确声明。以下这个CMakeLists.txt在Ubuntu 20.04、22.04以及Windows vcpkg环境下都能用。cmake_minimum_required(VERSION 3.10) project(ws_demo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 必须找到 Boost 的 system 组件asio 依赖它 find_package(Boost REQUIRED COMPONENTS system) find_package(OpenSSL REQUIRED) find_package(Threads REQUIRED) # 服务端 add_executable(ws_server server.cpp) target_link_libraries(ws_server PRIVATE Boost::system OpenSSL::SSL OpenSSL::Crypto Threads::Threads ) # 客户端 add_executable(ws_client client.cpp) target_link_libraries(ws_client PRIVATE Boost::system OpenSSL::SSL OpenSSL::Crypto Threads::Threads )编译时如果报找不到Boost先在系统里装依赖。Ubuntu/Debian执行sudo apt install libboost-system-dev libssl-devmacOS用brew install boost opensslWindows建议走vcpkg。这里要特别提醒Boost版本不能太老也不用追新1.66以上基本都能编过OpenSSL则需要1.1.1或3.x太老的1.0.2在握手计算里会有兼容问题。4. 客户端Demo连接、收发与断线重连4.1 最小客户端连上本地服务端并完成一轮收发服务端跑通之后客户端决定了你的业务能否真正运转。下面的客户端代码连到本地9002端口发一条文本消息收到服务端回显后打印。很多人的第一个半成品就是卡在client.run()上——这个函数会阻塞后续代码永远不执行。#include websocketpp/config/asio_client.hpp #include websocketpp/client.hpp #include iostream #include chrono #include thread typedef websocketpp::clientwebsocketpp::config::asio_client WsClient; int main() { WsClient client; // 日志和异常处理尽量早打开排查问题时能看到内部状态 client.set_access_channels(websocketpp::log::alevel::all); client.clear_access_channels(websocketpp::log::alevel::frame_payload); client.init_asio(); client.set_message_handler([](websocketpp::connection_hdl hdl, WsClient::message_ptr msg) { // 帧类型和负载都要打印二进制帧 payload 可能不可读 std::cout opcode msg-get_opcode() payload msg-get_payload() std::endl; }); websocketpp::lib::error_code ec; auto con client.get_connection(ws://127.0.0.1:9002, ec); if (ec) { std::cerr connection string error: ec.message() std::endl; return 1; } client.connect(con); // 开一个线程发消息主线程跑事件循环 std::thread sender([client, con]() { for (int i 0; i 3; i) { client.send(con, hello from c client, websocketpp::frame::opcode::text, ec); if (ec) { std::cerr send failed: ec.message() std::endl; } std::this_thread::sleep_for(std::chrono::seconds(1)); } }); client.run(); sender.join(); return 0; }这里有个新手很容易踩的坑send要在run()启动之后调用吗答案是不需要但发送队列是异步的立刻run()才能触发实际写入。我把发送放到独立线程并循环三次就是为了演示这种事件循环 业务线程的经典结构。如果你在run()之前调send且立刻退出main消息大概率发不出去因为没有事件循环来处理IO事件。4.2 在线程安全与回调上下文哪些操作不能在回调里做websocketpp的消息回调默认跑在IO线程上也就是说你在set_message_handler里执行阻塞操作整个服务端会卡住。这是一个让很多人头疼的行为服务端连了几个客户端一个客户端发来超大消息服务端在回调里做字符串处理其余所有客户端都等这个处理结束。解决办法有两种思路一是回调里只做入队操作把消息push到线程安全的队列让业务工作线程去消费二是用stream模式让每个连接拥有独立线程但websocketpp这个配置在原生版里支持不完善实际项目更多用前一种。#include queue #include mutex #include condition_variable class MsgQueue { public: void push(websocketpp::connection_hdl hdl, std::string payload) { std::lock_guardstd::mutex lock(m_mutex); m_queue.push({hdl, std::move(payload)}); m_cv.notify_one(); } // 阻塞式弹出工作线程循环调用 std::pairwebsocketpp::connection_hdl, std::string pop() { std::unique_lockstd::mutex lock(m_mutex); m_cv.wait(lock, [this] { return !m_queue.empty(); }); auto item std::move(m_queue.front()); m_queue.pop(); return item; } private: std::mutex m_mutex; std::condition_variable m_cv; std::queuestd::pairwebsocketpp::connection_hdl, std::string m_queue; };这样回调里只做一次加锁入队耗时全在业务线程里。换成连接客户端数量大的场景这是必须的控制流。Demo里直接处理消息没问题但如果服务端要做数据库查询、文件IO或调用其他后端服务务必改成消息队列模式否则并发一上来就露馅。4.3 心跳与重连线上必踩的细节WebSocket长连接断开的时机往往不是你主动关的——网络闪断、服务器重启、防火墙空闲超时都会让连接假死。TCP本身没有应用层心跳所以WebSocket的ping/pong帧是保活核心。客户端定时发ping服务端回pong如果在超时时间内没收到pong就主动断开重连。websocketpp里设置这部分参数很直接// 设置心跳间隔毫秒在连接建立后生效 con-set_ping_interval(30000); // 每30秒发一次ping con-set_ping_timeout(10000); // 10秒没收到pong就断开 // 断线后重连在close_handler里重新get_connection并connect client.set_close_handler([client](websocketpp::connection_hdl hdl) { std::cerr connection closed, retry in 3s std::endl; std::this_thread::sleep_for(std::chrono::seconds(3)); websocketpp::lib::error_code ec; auto new_con client.get_connection(ws://127.0.0.1:9002, ec); if (ec || !new_con) { std::cerr reconnect failed: ec.message() std::endl; return; } client.connect(new_con); });sleep重连是简化版生产环境建议用指数退避加随机抖动避免所有断线的客户端同时重连把服务端打崩。这就是典型的惊群问题停电恢复后几百个客户端同时发起重连服务端瞬时连接数飙升表现为连接被拒。注意ping_interval和ping_timeout是websocketpp扩展的便捷接口底层仍然走的是标准ping/pong帧服务端和浏览器都认这个机制。5. 直接编译的常见问题与避坑清单5.1 链接错误Boost版本和OpenSSL导致现象编译时报undefined reference to boost::system::...或者openssl/ssl.h: No such file or directory。原因大部分是依赖没装全或者CMake没有正确传递链接库。Boost早期版本里system库的头文件路径不同OpenSSL 1.0.2和3.x的API也有差异。还有一类隐蔽情况pkg-config找到了OpenSSL但CMake的find_package用了不同路径。解决先确认依赖确实安装了再检查CMake里链接顺序。Linux下Boost::system和OpenSSL::SSL/Crypto的顺序不要颠倒因为boost的ssl streambuf依赖openssl。最简单排错手段是编译时加上-Wl,--no-as-needed避免动态库被丢弃。还有一个实用习惯单独先编一个仅含Boost头文件的hello world验证Boost环境本身没问题再引入WebSocket代码。5.2 握手404 / 403路径和子协议没对上现象服务端能收到请求但客户端报错Invalid HTTP status或者服务端日志显示404、403。原因websocketpp的服务端默认不关心路径但如果你设置了set_http_handler或者用了消息处理器里的get_uri做鉴权路径不匹配就会拒绝。403通常来自validate_handler默认实现允许所有连接但业务代码里可能加了来源检查。解决日志打开access channel看握手请求的原始uri。客户端连接地址写成ws://127.0.0.1:9002时uri是/如果写成ws://127.0.0.1:9002/ws服务端收到的uri是/ws。鉴权逻辑必须和服务端预期一致。调试期最简单办法是把validate_handler留空先跑通再补安全检查。5.3 可以直接编译但一运行就崩溃现象程序启动后立即Segmentation fault甚至没有打印任何日志。原因最常见的是set_message_handler之后消息处理器持有了局部变量的引用回调触发时该变量已经析构。另一个高发场景是多个WsServer实例共享同一个Asio io_context但websocketpp的init_asio每个实例都会创建新的io_context逻辑上符合预期但若手动传入io_service指针并在另一线程run生命周期管理要格外小心。解决检查所有注册到handler里的lambda捕获列表。如果捕获了this要保证服务器对象的析构晚于run()返回如果捕获了局部变量值确保是副本而非引用。全局变量和静态实例能规避一部分生命周期问题但滥用全局变量会让后续维护变得痛苦。更稳妥的做法是让server对象在main栈上创建run结束之前不销毁。5.4 中文消息乱码现象客户端发中文服务端收到后打印成乱码。原因WebSocket文本帧的payload必须符合UTF-8编码。C的std::string只是字节序列不强制编码。如果客户端是浏览器JavaScript发的是UTF-8没问题但如果是C客户端输入源来自Windows控制台或旧版代码的GBK编码字符串直接发出去就成乱码。解决统一全链路UTF-8。一个常用方法是接收方做编码转换但发送方从源头转更干净。Windows下用MultiByteToWideChar配合WideCharToMultiByte把GBK转成UTF-8Linux下如果不是UTF-8环境也要先转码再放入std::string。判断谁是乱码源头的方法服务端把收到的payload写到一个UTF-8编码的文本文件浏览器打开看是否正常——正常说明服务端收到了正确数据问题在服务端打印环节不正常说明发送端编码就没对。5.5 服务端重启后立刻bind失败现象服务端进程崩溃或者CtrlC退出后紧接着重启提示bind: Address already in use。原因TCP连接关闭后端口进入TIME_WAIT状态默认要等约2个最大段生存期才能复用。开发时这种等待很影响迭代节奏。解决代码里已经加了set_reuse_addr(true)但如果你手动实现socket或者用了旧代码记得在监听前设置SO_REUSEADDR。还有一个相关参数是SO_REUSEPORT它允许端口共享但只在多进程模型下有意义Demo不需要。如果设置了reuse_addr还是报错用ss -tan state time-wait看看哪些连接处在TIME_WAIT确认是不是同一个端口被系统服务占用。5.6 消息体过大帧分片与内存占用现象客户端发送一个几十MB的消息服务端内存飙升甚至卡死。原因WebSocket本身不限制帧大小但websocketpp默认能处理大消息会把全部分片拼进内存。恶意客户端可以反复发送大帧直接耗尽服务端内存。解决合理的消息大小上限必须由业务层控制。websocketpp提供set_max_message_size接口m_server.set_max_message_size(1024 * 1024); // 限制单条消息不超过 1MB超过限制的帧会触发close行为并带着1009Message Too Big状态码。另一个隐藏问题是分片攻击FIN始终为0不断发分片让服务端一直缓存。这个要靠设置set_max_fragment_size让分片在协议层就被拆开并即时处理。这个参数在不需要大消息的默认场景建议设为64KB。6. 把Demo改成你要的形态TLS、二进制帧与压测脚本如果只是跑通Demo就收手收获有限。我建议你继续做三件事每一步都能把能运行升级成能上线。第一件事是加TLS变成wss。服务端把asio_no_tls.hpp换成asio.hppboost回调和客户端asio_client.hpp也要同步换。然后调用set_tls_init_handler加载证书和私钥。这个步骤最大的坑是证书链不完整——客户端会报TLS handshake failed用openssl s_client -connect 127.0.0.1:port能清晰看到服务端返回哪些证书。内网调试可以用自签证书但要记得打开set_verify_mode并添加豁免逻辑。第二件事是处理二进制帧。很多物联网协议走的是自定义二进制协议而不是JSON。websocketpp的message_ptr在二进制帧和文本帧上获得payload的方式完全相同但发送时必须显式指定opcode::binary否则服务端按文本帧解析对二进制内容做UTF-8合法性检查时会报错断开。建议在收发两端都按get_opcode()分发处理逻辑别只看payload。第三件事是压测。不用引入框架用Python的asyncio就能写一个并发连接脚本建立500个连接后同时ping统计pong回包时间和连接断线率。这些都是检验服务端线程模型和IO事件循环是否健壮的最小手段。500个连接能稳定跑10分钟百万连接先从网络层和文件描述符上限开始排查。最后说个习惯每改一次代码我会单独保存一个可编译基线的副本这个副本不变动专门用来对照是不是环境坏了还是代码坏了。这个习惯救过我很多次尤其是换电脑、换系统后又回头编译老项目时一个能编译的源头比任何文档都可靠。希望这个Demo也能成为你随手能用的基线让WebSocket这个技术点在你的工具箱里从此不再陌生。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑