简介:基于WebSocket++构建的客户端与服务端示例代码包,面向需要在C++项目中快速实现实时双向通信的开发者,尤其适合结合Boost在Windows环境下搭建WebSocket应用的初学者。压缩包共2000个文件,大小约320.48MB,内容以hpp头文件、cpp示例代码、Boost相关依赖库(so/a/lib)及编译配置文件(pro/bat/pri)为主,结构清晰,便于按需检索。已有947人学习下载。示例覆盖了服务端与客户端的完整流程,包括创建server/client实例、注册on_open、on_message等回调、发起连接、发送数据、运行事件循环等关键步骤;同时涉及标准库与Boost.Asio异步I/O的配合使用,可帮助开发者理解非阻塞通信模型的实现方式。若正在为在线游戏、实时聊天、股票交易等低延迟场景寻找可直接参考的C++ WebSocket方案,这份demo能提供从代码到运行环境配置的实用支持。1. websocket++ demo:先有一个能跑的通信对端,再去改业务把 HTTP 轮询换成 WebSocket,最难受的不是写业务,而是自己跟协议细节较劲。这份基于 websocket++ 的客户端&服务端 demo,把两端各做成了一个最小可跑工程:服务端负责监听、握手、原样回写,客户端负责连接、发消息、收响应,协议层全交给 websocket++ 这个 header-only 的 C++ 库兜住。它不给你讲长篇原理,但能让你在一开始就拥有一对真实连通的对端,再往里面填自己的逻辑。适合刚接手实时通信功能、需要快速打通链路的 C++ 开发者,也适合想拿 websocket++ 做二次开发但不想从零搭工程的人。2. 项目全景与选型拆解:websocket++ 的架构定位拿到这个 demo,我第一件事是看它的依赖和目录结构。搞清楚 websocket++ 到底以什么形态存在,后面改起来才不慌。2.1 header-only 的库形态与双端点模型websocket++ 是一个纯头文件的模板库,核心是两个模板类:websocketpp::server 和 websocketpp::client。两个端点共用同一套基于 Asio 的传输层,只是握手和消息帧的处理方向不同。对工程来说,header-only 带来的最大好处是不用单独编译、不用管理 .so 版本,把头文件目录加进 include path 就能用。模板库的另外一面是编译错误信息极长。不小心把 server 端的配置类型传给 client 端点,模板展开能刷出几百行报错。所以拆项目时我会先确认它用的是哪个 config 头文件,这个 demo 用的是 asio_no_tls 系列,也就是不带 TLS 的纯 Asio 版本,这也是大多数内网通信场景的默认选择。如果后面要做 wss,需要换成 asio_tls 并在初始化时注册证书上下文,代价会大不少。2.2 工程目录结构与职责划分整个工程的骨架不长,拆分得很干净:websocket_demo/ ├── CMakeLists.txt ├── server/ │ └── server.cpp # websocket 服务端:监听、握手、echo ├── client/ │ └── client.cpp # websocket 客户端:连接、发送、接收 └── third_party/ └── websocketpp/ # websocket++ 头文件目录 ├── websocketpp.hpp ├── config/ └── ...路径职责关键文件server/server.cpp服务端入口,包含连接管理与消息回调config/asio_no_tls.hppclient/client.cpp客户端入口,包含连接发起与消息处理config/asio_no_tls_client.hppthird_party/websocketpp第三方库,只被 include,不参与编译websocketpp.hppCMakeLists.txt依赖查找与两端目标构建find_package / find_path服务端和客户端是独立可执行程序,中间通过 WebSocket 协议通信。这种分离写法很适合做联调:先起服务端,再用客户端连上去,出了问题能直接定位到是哪一端。third_party 单独列目录而不是用系统安装的 websocketpp,是为了让工程自带完整依赖,换机器不愁环境缺失。2.3 依赖选型:Boost.Asio 与独立 Asio 两种跑法websocket++ 的传输层基于 Asio,而 Asio 又分两个派系:一个走 Boost.Asio,通过 find_package(Boost) 引入;另一个是独立 Asio,定义 ASIO_STANDALONE 宏后直接用 asio 头文件。我见过不少人在这一步翻车,把两个派系混着用,编译期报一堆命名空间冲突。这个 demo 的默认配置走的是 Boost.Asio 路线,CMake 里需要找到 Boost 的 system、random 组件。如果你觉得 Boost 太笨重,也可以切到独立 Asio:给编译目标加ASIO_STANDALONE宏,然后 include 独立 asio 的头文件路径。两种配置都能跑,区别在于 Boost 路线更省心、踩坑资料多;独立 Asio 路线依赖更干净,但得自己保证 asio 头文件版本和 websocket++ 匹配。我对这个 demo 的改动建议是:本地开发按 Boost 路线跑通一次,确认链路没问题之后,再决定要不要切独立 Asio 去瘦身。一上来就折腾依赖选型,容易连 demo 都跑不起来。3. 服务端核心拆解:从 listen 到广播的完整链路服务端是 websocket++ 最典型的应用场景。这个 demo 的服务端代码虽然短,但把 websocket++ 服务端的骨架完整勾勒出来了:初始化、回调注册、事件循环、消息回写。我拆着看一遍。3.1 服务端初始化:从 init_asio 到 run 的事件循环先看服务端的主流程代码:#include <websocketpp/config/asio_no_tls.hpp> #include <websocketpp/server.hpp> #include <iostream> #include <functional> typedef websocketpp::server<websocketpp::config::asio> WsServer; int main() { WsServer server; // 日志级别:关掉帧内容打印,保留连接日志 server.set_access_channels(websocketpp::log::alevel::all); server.clear_access_channels(websocketpp::log::alevel::frame_payload); // 初始化 Asio 传输层 server.init_asio(); // 端口复用,避免 TIME_WAIT 状态导致重启失败 server.set_reuse_addr(true); // 注册回调:连接建立、连接关闭、收到消息 server.set_open_handler(std::bind(&on_open, &server, std::placeholders::_1)); server.set_close_handler(std::bind(&on_close, &server, std::placeholders::_1)); server.set_message_handler(std::bind(&on_message, &server, std::placeholders::_1, std::placeholders::_2)); // 监听 9002 端口并开始接收新连接 server.listen(9002); server.start_accept(); // 阻塞进入事件循环 server.run(); return 0; }这段代码里init_asio()是关键分水岭:在它之前 websocket++ 只是一个配置壳子,之后才拥有真正的 socket 事件循环。listen(9002)省略了地址参数,websocket++ 会默认绑定在本机双栈地址上,局域网内其他机器也能通过 IP 连进来。如果只想本机访问,可以把监听改成listen(websocketpp::lib::asio::ip::tcp::v4(), 9002)。日志配置也需要留意。set_access_channels(alevel::all)会把每一条 WebSocket 帧的收发都打出来,调试时信息量很大;但frame_payload会把消息内容明文打到控制台,生产或联调时一旦消息量大,刷屏非常严重。所以标准做法是像上面这样 clear 掉 frame_payload,只留连接级日志。3.2 连接生命周期回调与 echo 消息处理服务端真正干活的地方在三个回调函数里。这个 demo 的 on_message 是标准的 echo 逻辑,回写原消息:std::map<websocketpp::connection_hdl, std::string, std::owner_less<websocketpp::connection_hdl>> conns; void on_open(WsServer* srv, websocketpp::connection_hdl hdl) { std::cout << "connection opened, total: " << conns.size() + 1 << std::endl; conns[hdl] = "anonymous"; } void on_close(WsServer* srv, websocketpp::connection_hdl hdl) { conns.erase(hdl); std::cout << "connection closed, left: " << conns.size() << std::endl; } void on_message(WsServer* srv, websocketpp::connection_hdl hdl, WsServer::message_ptr msg) { std::string payload = msg->get_payload(); std::cout << "received: " << payload << std::endl; websocketpp::lib::error_code ec; srv->send(hdl, payload, msg->get_opcode(), ec); if (ec) { std::cout << "send failed: " << ec.message() << std::endl; } }connection_hdl是一个弱引用句柄,它不等于连接对象本身,而是用来在回调里定位连接的钥匙。因为它是 weak_ptr,没法直接比较大小,所以上面 map 的 key 特意用了std::owner_less作为比较器。这是 websocket++ 里非常容易踩的细节:如果直接用默认的std::less编译,map 会报错;即使编译过了,运行期也可能因为内部 weak_ptr 过期而出现诡异的查找失败。send的第四个参数是可选的 error_code 引用,demo 里带上它是好习惯。WebSocket 连接随时可能被对端关闭,如果没有这个 ec 参数,send 在连接失效时会抛异常;带上之后错误就被收纳进 ec,程序继续往下走,不会因为一个已断开的连接把整个服务端打崩。3.3 连接管理:广播、剔除失效连接从 echo 到广播只差一步。既然 conns 里已经维护了所有在线连接的 hdl,遍历它就能实现群发:void broadcast(WsServer* srv, const std::string& data) { websocketpp::lib::error_code ec; for (auto& item : conns) { srv->send(item.first, data, websocketpp::frame::opcode::text, ec); if (ec) { std::cout << "broadcast to one connection failed: " << ec.message() << std::endl; ec.clear(); } } }这段代码要注意的是错误处理方式:广播过程中某一条连接断开是很常见的事,绝不能因为一个失败就中断整轮广播。所以每发送一个连接都检查 ec,失败后clear()重置,继续发下一个。另一个细节是显式传了frame::opcode::text,因为广播的消息往往是序列化后的文本数据,显式指定 opcode 能避免二进制和文本语义混用。真正生产环境里,conns 的增删会发生在不同回调里,比如 on_close 在 erase 的同时广播逻辑正在遍历,这种情况需要考虑加锁或改用标记失效后统一清理的策略。demo 没做并发保护,是因为 websocket++ 默认的单线程事件循环让回调串行执行,只要不在 send 的回调里强行做耗时操作,单线程模型在大多数场景下够用。4. 客户端核心拆解:连接生命周期与重连策略服务端跑通之后,客户端的价值才真正体现。很多开发者只写了服务端就用浏览器调试页面去连,但浏览器调试工具看不到握手过程、也控制不了重连节奏。一个独立的 C++ 客户端,才能完整复现连接生命周期里那些边界场景。4.1 客户端初始化与 URI 连接请求客户端的初始化路径跟服务端几乎对称,但有几个关键差异:#include <websocketpp/config/asio_no_tls_client.hpp> #include <websocketpp/client.hpp> #include <iostream> typedef websocketpp::client<websocketpp::config::asio_client> WsClient; int main() { WsClient client; client.set_access_channels(websocketpp::log::alevel::none); client.init_asio(); // 连接建立后立刻发一条文本消息 client.set_open_handler([&client](websocketpp::connection_hdl hdl) { std::cout << "connected, sending hello..." << std::endl; websocketpp::lib::error_code ec; client.send(hdl, "hello from client", websocketpp::frame::opcode::text, ec); if (ec) { std::cout << "send failed: " << ec.message() << std::endl; } }); // 收到服务端回包时打印 client.set_message_handler([](websocketpp::connection_hdl hdl, WsClient::message_ptr msg) { std::cout << "reply: " << 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::cout << "connection resolve failed: " << ec.message() << std::endl; return -1; } client.connect(con); client.run(); return 0; }客户端的get_connection负责解析 URI 并创建连接对象,注意它只是创建,还没发请求。真正的握手发生在client.connect(con)之后,由run()事件循环驱动。这个分层很容易被误解:connect 不是阻塞函数,它把连接请求投递到 Asio 队列就返回了,之后再被事件循环异步执行。这里用 lambda 而不是 std::bind 是我改 demo 时的习惯。lambda 捕获外部 client 引用,代码读起来更直观,也不会出现 bind 占位符和参数个数不匹配的编译问题。需要注意 lambda 里捕获的是&client,生命周期必须覆盖 client.run() 执行的整个阶段,所以在 main 函数里把它们放在同一作用域就是安全的。4.2 消息回调、发送回包与 ping 保活客户端发消息有三种常见姿势:在 open 回调里发、在 message 回调里回包、或者由外部业务线程主动发。demo 覆盖了前两种,第三种要额外小心线程安全。先看一个客户端主动保活的写法:#include <websocketpp/common/chrono.hpp> #include <websocketpp/common/thread.hpp> int main() { WsClient client; client.init_asio(); client.set_open_handler([&client](websocketpp::connection_hdl hdl) { // 打开连接后,周期性发送 ping 帧 websocketpp::lib::thread::id tid = websocketpp::lib::thread::id(); std::cout << "ping thread start" << std::endl; }); websocketpp::lib::error_code ec; auto con = client.get_connection("ws://127.0.0.1:9002", ec); client.connect(con); // 另起线程做保活,避免阻塞主事件循环 websocketpp::lib::thread t([&client]() { while (true) { websocketpp::lib::error_code ec; client.send(websocketpp::connection_hdl(), "", websocketpp::frame::opcode::ping, ec); std::this_thread::sleep_for(std::chrono::seconds(30)); } }); client.run(); return 0; }这段 ping 代码其实是示意性质,不能直接照抄进 demo 跑。原因有两个:websocketpp::connection_hdl()默认构造出来的句柄不指向任何有效连接,send 会失败;跨线程调用client.send没有经过 strand 串行化,存在数据竞争。我之所以把它放出来,是想说明保活是一个需要单独设计的模块,你在网上看到的不少 websocket++ 心跳示例都有类似问题。正确的做法是把 ping 发送逻辑绑定到某个具体 hdl,并且在 send 之前用con->get_underlying_transport()之类的接口确认连接仍然存活。更多时候,服务端会主动 ping 客户端,客户端只需要在 message 回调里识别 ping/pong 帧并回应,又把复杂度推回服务端。这个 demo 的 echo 逻辑天然能透传消息,你可以把 ping 帧作为普通消息发到服务端验证链路活性。4.3 断线重连:从直连到带退避的工程化写法demo 里没有重连逻辑,但客户端一旦引入真实业务,断线重连马上就变成刚需。最朴素的写法是在 close 回调里重新get_connection和connect:std::atomic<bool> stopped{false}; int retry_count = 0; void start_reconnect(WsClient& client, const std::string& uri) { websocketpp::lib::error_code ec; auto con = client.get_connection(uri, ec); if (ec) { std::cout << "reconnect get_connection failed: " << ec.message() << std::endl; return; } client.set_open_handler([&client, uri](websocketpp::connection_hdl hdl) { retry_count = 0; std::cout << "reconnected" << std::endl; }); client.connect(con); }这个写法的坑在于:如果直接在 close 回调里循环重连,而服务端恰好是正常的,那没问题;但如果服务端宕机,get_connection的 URI 解析会失败,重连逻辑立即退出,永远不会回来。所以真实工程里重连必须配合定时器和指数退避——第一次 1 秒、第二次 2 秒、第三次 4 秒,封顶 30 秒,避免服务端恢复时被几十个客户端同时涌入打挂。websocket++ 本身不提供重连组件,常见做法是用websocketpp::lib::asio::steady_timer替代 sleep,在回调里异步等待,这样不会阻塞事件循环。我改这类 demo 时习惯把重连策略单独抽成一个类,暴露 start/stop 两个接口,而不是把退避逻辑散落在回调里。5. 编译运行与避坑排查:把 demo 真正跑起来代码拆得再细,跑不起来等于零。这一章从零开始编译、连接、验证,再把我拆过这个 demo 后积攒的常见坑一次性倒出来。5.1 CMake 配置与依赖安装websocket++ 没有 INSTALL 步骤,CMake 只需要找到头文件目录和 Boost 组件:cmake_minimum_required(VERSION 3.10) project(websocket_demo CXX) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 若使用独立 Asio,开启下面这行 # add_definitions(-DASIO_STANDALONE) find_package(Boost REQUIRED COMPONENTS system random) # websocketpp 头文件目录 find_path(WEBSOCKETPP_INCLUDE_DIR NAMES websocketpp/websocketpp.hpp PATHS ${CMAKE_SOURCE_DIR}/third_party ) add_executable(server server/server.cpp) target_include_directories(server PRIVATE ${WEBSOCKETPP_INCLUDE_DIR}) target_link_libraries(server PRIVATE Boost::system Boost::random pthread) add_executable(client client/client.cpp) target_include_directories(client PRIVATE ${WEBSOCKETPP_INCLUDE_DIR}) target_link_libraries(client PRIVATE Boost::system Boost::random pthread)Boost 的 random 组件是 websocket++ 生成 masking key 时需要的。如果你系统里没装 Boost,也可以切独立 Asio 并去掉所有 Boost::* 链接项,代价是需要额外准备 asio 头文件。对新手我更推荐直接安装 Boost,包管理器里搜 libboost-system-dev / libboost-random-dev 即可。编译阶段最容易出现的问题是find_path找不到头文件。把 third_party 放对位置很关键,cmake 会基于CMAKE_SOURCE_DIR去拼接路径,如果工程目录层级不对,find_path静默返回 NOTFOUND,后续 include 阶段报一堆 “No such file or directory”。排查时可以打开 CMakeCache 看WEBSOCKETPP_INCLUDE_DIR的实际值。5.2 编译运行与连通性验证按下面的顺序执行,就能得到一条可观测的完整链路:# 1. 配置并编译 mkdir build && cd build cmake .. make -j$(nproc) # 2. 终端 A:启动服务端 ./server # 3. 终端 B:启动客户端 ./client服务端启动后应打印监听日志,客户端启动后应打印 connected 和 reply 两行输出。服务端因为开着 alevel::all 日志,会打印完整的 TCP 握手和 WebSocket 握手过程。我验证连通性时一般盯三个节点:一是服务端出现Connection from 127.0.0.1,二是服务端打印received: hello from client,三是客户端打印reply: hello from client。三段全齐,链路就是通的。如果手里没有现成客户端,还可以在浏览器控制台里用原生 WebSocket 去连ws://127.0.0.1:9002做验证——这是最快的外部探针。但这只能验证服务端,验证不了客户端代码本身,所以要真正调试客户端逻辑,还是得靠编译出的 client 可执行文件。5.3 websocket++ 常见问题与避坑清单以下五条是我实际拆这个 demo 时踩过的坑,按现象、原因、解决整理:坑一:客户端 connect 后一直不握手,卡在 “Connection” 状态现象:客户端没有任何日志输出,服务端也没有收到新连接。 原因:调了client.connect(con)但没调client.run(),事件循环没启动,所有异步操作都排队等待。 解决:确保connect之后主线程调用了run(),并且run()没有被其他阻塞代码挡住。坑二:服务端向已断开连接 send,直接抛异常现象:客户端强杀进程后,服务端在广播消息时崩溃,栈上显示 weak_ptr 转换失败。 原因:connection_hdl是弱引用,连接断开后对象被销毁,send 拿到过期句柄。 解决:send 时接收错误码 ec 而不是让它抛异常;连接管理容器里及时清理 on_close 的 hdl。坑三:map 用 connection_hdl 做 key 编译报错现象:using std::less<connection_hdl>作为 map 比较器时编译不通过。 原因:connection_hdl 是 weak_ptr,不满足std::less对原始指针的要求。 解决:map 的第三个模板参数换成std::owner_less<websocketpp::connection_hdl>。坑四:Boost 版本不匹配导致模板实例化失败现象:编译 websocket++ 头文件时报 Boost.Asio 内部类型找不到,或 error_code 相关符号重定义。 原因:websocket++ 发布较早,与较新版本的 Boost.Asio 存在兼容间隙。 解决:优先统一 Boost 版本;如果项目不允许动 Boost,切换ASIO_STANDALONE走独立 Asio 通常能绕开这类问题。坑五:多线程里直接调 send,偶发崩溃现象:业务线程向 websocket 连接发数据,运行几个小时崩溃一次,堆栈指向 socket write。 原因:websocket++ 默认没有对 send 做串行化保护,多线程并发写同一个 socket 是数据竞争。 解决:用websocketpp::lib::asio::strand包住发送操作,或把发送动作丢回事件循环线程执行。6. 二次开发实战:把 echo demo 改成带鉴权的消息路由demo 跑通后,改造的第一站通常是把 echo 改成真正的业务消息分发。我的做法是先在 on_message 里加一层路由逻辑,再给握手加上鉴权。先看服务端 on_message 的路由改造。假设消息体是 JSON,里面有个action字段,根据 action 决定是转给所有连接还是转给特定连接:void on_message(WsServer* srv, websocketpp::connection_hdl hdl, WsServer::message_ptr msg) { std::string payload = msg->get_payload(); // 伪代码:解析 JSON 并分发,实际用 jsoncpp / nlohmann 解析 auto cmd = parseJson(payload).action; if (cmd == "broadcast") { broadcast(srv, payload); } else if (cmd == "peer") { std::string target = parseJson(payload).target; websocketpp::lib::error_code ec; srv->send(target_hdl_map[target], payload, websocketpp::frame::opcode::text, ec); } else { srv->send(hdl, "unsupported action", websocketpp::frame::opcode::text, ec); } }路由改造的核心是维护好target_hdl_map这个业务映射表。注意它和 conns 的区别:conns 是 websocket++ 层面的连接池,target_hdl_map 是业务层面的用户索引,需要在 on_open 里根据用户标识填充、在 on_close 里删除。两套映射要同步更新,漏删一边就会出现“消息发给已下线用户”的问题。鉴权加在握手之前。websocket++ 提供set_validate_handler,在 HTTP 握手阶段做检查:server.set_validate_handler([](websocketpp::connection_hdl hdl) -> bool { auto con = server.get_con_from_hdl(hdl); std::string resource = con->get_resource(); // 从 query string 或 header 里取 token 并校验 if (resource.find("token=secret") == std::string::npos) { std::cout << "reject connection without valid token" << std::endl; return false; } return true; });这个回调返回 false 时,服务端会直接拒绝 WebSocket 握手,客户端会收到握手失败。改造后建议按三步验证:先不带 token 连接,确认被拒;再带 token 连接,确认握手成功;最后跑一次广播,确认路由逻辑没破坏原 demo 的链路。最后补一句我拆项目的习惯:拿到任何 websocket++ demo,第一件事都是把客户端和服务端同时跑起来、确认日志完整闭环,然后才动代码。那次以后我每次改完通信代码都会强制走一遍两端的握手日志验证,再小的改动也不例外。希望帮到你。本文还有配套的精品资源,点击获取
简介:基于WebSocket++构建的客户端与服务端示例代码包,面向需要在C++项目中快速实现实时双向通信的开发者,尤其适合结合Boost在Windows环境下搭建WebSocket应用的初学者。压缩包共2000个文件,大小约320.48MB,内容以hpp头文件、cpp示例代码、Boost相关依赖库(so/a/lib)及编译配置文件(pro/bat/pri)为主,结构清晰,便于按需检索。已有947人学习下载。示例覆盖了服务端与客户端的完整流程,包括创建server/client实例、注册on_open、on_message等回调、发起连接、发送数据、运行事件循环等关键步骤;同时涉及标准库与Boost.Asio异步I/O的配合使用,可帮助开发者理解非阻塞通信模型的实现方式。若正在为在线游戏、实时聊天、股票交易等低延迟场景寻找可直接参考的C++ WebSocket方案,这份demo能提供从代码到运行环境配置的实用支持。1. websocket++ demo:先有一个能跑的通信对端,再去改业务把 HTTP 轮询换成 WebSocket,最难受的不是写业务,而是自己跟协议细节较劲。这份基于 websocket++ 的客户端&服务端 demo,把两端各做成了一个最小可跑工程:服务端负责监听、握手、原样回写,客户端负责连接、发消息、收响应,协议层全交给 websocket++ 这个 header-only 的 C++ 库兜住。它不给你讲长篇原理,但能让你在一开始就拥有一对真实连通的对端,再往里面填自己的逻辑。适合刚接手实时通信功能、需要快速打通链路的 C++ 开发者,也适合想拿 websocket++ 做二次开发但不想从零搭工程的人。2. 项目全景与选型拆解:websocket++ 的架构定位拿到这个 demo,我第一件事是看它的依赖和目录结构。搞清楚 websocket++ 到底以什么形态存在,后面改起来才不慌。2.1 header-only 的库形态与双端点模型websocket++ 是一个纯头文件的模板库,核心是两个模板类:websocketpp::server 和 websocketpp::client。两个端点共用同一套基于 Asio 的传输层,只是握手和消息帧的处理方向不同。对工程来说,header-only 带来的最大好处是不用单独编译、不用管理 .so 版本,把头文件目录加进 include path 就能用。模板库的另外一面是编译错误信息极长。不小心把 server 端的配置类型传给 client 端点,模板展开能刷出几百行报错。所以拆项目时我会先确认它用的是哪个 config 头文件,这个 demo 用的是 asio_no_tls 系列,也就是不带 TLS 的纯 Asio 版本,这也是大多数内网通信场景的默认选择。如果后面要做 wss,需要换成 asio_tls 并在初始化时注册证书上下文,代价会大不少。2.2 工程目录结构与职责划分整个工程的骨架不长,拆分得很干净:websocket_demo/ ├── CMakeLists.txt ├── server/ │ └── server.cpp # websocket 服务端:监听、握手、echo ├── client/ │ └── client.cpp # websocket 客户端:连接、发送、接收 └── third_party/ └── websocketpp/ # websocket++ 头文件目录 ├── websocketpp.hpp ├── config/ └── ...路径职责关键文件server/server.cpp服务端入口,包含连接管理与消息回调config/asio_no_tls.hppclient/client.cpp客户端入口,包含连接发起与消息处理config/asio_no_tls_client.hppthird_party/websocketpp第三方库,只被 include,不参与编译websocketpp.hppCMakeLists.txt依赖查找与两端目标构建find_package / find_path服务端和客户端是独立可执行程序,中间通过 WebSocket 协议通信。这种分离写法很适合做联调:先起服务端,再用客户端连上去,出了问题能直接定位到是哪一端。third_party 单独列目录而不是用系统安装的 websocketpp,是为了让工程自带完整依赖,换机器不愁环境缺失。2.3 依赖选型:Boost.Asio 与独立 Asio 两种跑法websocket++ 的传输层基于 Asio,而 Asio 又分两个派系:一个走 Boost.Asio,通过 find_package(Boost) 引入;另一个是独立 Asio,定义 ASIO_STANDALONE 宏后直接用 asio 头文件。我见过不少人在这一步翻车,把两个派系混着用,编译期报一堆命名空间冲突。这个 demo 的默认配置走的是 Boost.Asio 路线,CMake 里需要找到 Boost 的 system、random 组件。如果你觉得 Boost 太笨重,也可以切到独立 Asio:给编译目标加ASIO_STANDALONE宏,然后 include 独立 asio 的头文件路径。两种配置都能跑,区别在于 Boost 路线更省心、踩坑资料多;独立 Asio 路线依赖更干净,但得自己保证 asio 头文件版本和 websocket++ 匹配。我对这个 demo 的改动建议是:本地开发按 Boost 路线跑通一次,确认链路没问题之后,再决定要不要切独立 Asio 去瘦身。一上来就折腾依赖选型,容易连 demo 都跑不起来。3. 服务端核心拆解:从 listen 到广播的完整链路服务端是 websocket++ 最典型的应用场景。这个 demo 的服务端代码虽然短,但把 websocket++ 服务端的骨架完整勾勒出来了:初始化、回调注册、事件循环、消息回写。我拆着看一遍。3.1 服务端初始化:从 init_asio 到 run 的事件循环先看服务端的主流程代码:#include <websocketpp/config/asio_no_tls.hpp> #include <websocketpp/server.hpp> #include <iostream> #include <functional> typedef websocketpp::server<websocketpp::config::asio> WsServer; int main() { WsServer server; // 日志级别:关掉帧内容打印,保留连接日志 server.set_access_channels(websocketpp::log::alevel::all); server.clear_access_channels(websocketpp::log::alevel::frame_payload); // 初始化 Asio 传输层 server.init_asio(); // 端口复用,避免 TIME_WAIT 状态导致重启失败 server.set_reuse_addr(true); // 注册回调:连接建立、连接关闭、收到消息 server.set_open_handler(std::bind(&on_open, &server, std::placeholders::_1)); server.set_close_handler(std::bind(&on_close, &server, std::placeholders::_1)); server.set_message_handler(std::bind(&on_message, &server, std::placeholders::_1, std::placeholders::_2)); // 监听 9002 端口并开始接收新连接 server.listen(9002); server.start_accept(); // 阻塞进入事件循环 server.run(); return 0; }这段代码里init_asio()是关键分水岭:在它之前 websocket++ 只是一个配置壳子,之后才拥有真正的 socket 事件循环。listen(9002)省略了地址参数,websocket++ 会默认绑定在本机双栈地址上,局域网内其他机器也能通过 IP 连进来。如果只想本机访问,可以把监听改成listen(websocketpp::lib::asio::ip::tcp::v4(), 9002)。日志配置也需要留意。set_access_channels(alevel::all)会把每一条 WebSocket 帧的收发都打出来,调试时信息量很大;但frame_payload会把消息内容明文打到控制台,生产或联调时一旦消息量大,刷屏非常严重。所以标准做法是像上面这样 clear 掉 frame_payload,只留连接级日志。3.2 连接生命周期回调与 echo 消息处理服务端真正干活的地方在三个回调函数里。这个 demo 的 on_message 是标准的 echo 逻辑,回写原消息:std::map<websocketpp::connection_hdl, std::string, std::owner_less<websocketpp::connection_hdl>> conns; void on_open(WsServer* srv, websocketpp::connection_hdl hdl) { std::cout << "connection opened, total: " << conns.size() + 1 << std::endl; conns[hdl] = "anonymous"; } void on_close(WsServer* srv, websocketpp::connection_hdl hdl) { conns.erase(hdl); std::cout << "connection closed, left: " << conns.size() << std::endl; } void on_message(WsServer* srv, websocketpp::connection_hdl hdl, WsServer::message_ptr msg) { std::string payload = msg->get_payload(); std::cout << "received: " << payload << std::endl; websocketpp::lib::error_code ec; srv->send(hdl, payload, msg->get_opcode(), ec); if (ec) { std::cout << "send failed: " << ec.message() << std::endl; } }connection_hdl是一个弱引用句柄,它不等于连接对象本身,而是用来在回调里定位连接的钥匙。因为它是 weak_ptr,没法直接比较大小,所以上面 map 的 key 特意用了std::owner_less作为比较器。这是 websocket++ 里非常容易踩的细节:如果直接用默认的std::less编译,map 会报错;即使编译过了,运行期也可能因为内部 weak_ptr 过期而出现诡异的查找失败。send的第四个参数是可选的 error_code 引用,demo 里带上它是好习惯。WebSocket 连接随时可能被对端关闭,如果没有这个 ec 参数,send 在连接失效时会抛异常;带上之后错误就被收纳进 ec,程序继续往下走,不会因为一个已断开的连接把整个服务端打崩。3.3 连接管理:广播、剔除失效连接从 echo 到广播只差一步。既然 conns 里已经维护了所有在线连接的 hdl,遍历它就能实现群发:void broadcast(WsServer* srv, const std::string& data) { websocketpp::lib::error_code ec; for (auto& item : conns) { srv->send(item.first, data, websocketpp::frame::opcode::text, ec); if (ec) { std::cout << "broadcast to one connection failed: " << ec.message() << std::endl; ec.clear(); } } }这段代码要注意的是错误处理方式:广播过程中某一条连接断开是很常见的事,绝不能因为一个失败就中断整轮广播。所以每发送一个连接都检查 ec,失败后clear()重置,继续发下一个。另一个细节是显式传了frame::opcode::text,因为广播的消息往往是序列化后的文本数据,显式指定 opcode 能避免二进制和文本语义混用。真正生产环境里,conns 的增删会发生在不同回调里,比如 on_close 在 erase 的同时广播逻辑正在遍历,这种情况需要考虑加锁或改用标记失效后统一清理的策略。demo 没做并发保护,是因为 websocket++ 默认的单线程事件循环让回调串行执行,只要不在 send 的回调里强行做耗时操作,单线程模型在大多数场景下够用。4. 客户端核心拆解:连接生命周期与重连策略服务端跑通之后,客户端的价值才真正体现。很多开发者只写了服务端就用浏览器调试页面去连,但浏览器调试工具看不到握手过程、也控制不了重连节奏。一个独立的 C++ 客户端,才能完整复现连接生命周期里那些边界场景。4.1 客户端初始化与 URI 连接请求客户端的初始化路径跟服务端几乎对称,但有几个关键差异:#include <websocketpp/config/asio_no_tls_client.hpp> #include <websocketpp/client.hpp> #include <iostream> typedef websocketpp::client<websocketpp::config::asio_client> WsClient; int main() { WsClient client; client.set_access_channels(websocketpp::log::alevel::none); client.init_asio(); // 连接建立后立刻发一条文本消息 client.set_open_handler([&client](websocketpp::connection_hdl hdl) { std::cout << "connected, sending hello..." << std::endl; websocketpp::lib::error_code ec; client.send(hdl, "hello from client", websocketpp::frame::opcode::text, ec); if (ec) { std::cout << "send failed: " << ec.message() << std::endl; } }); // 收到服务端回包时打印 client.set_message_handler([](websocketpp::connection_hdl hdl, WsClient::message_ptr msg) { std::cout << "reply: " << 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::cout << "connection resolve failed: " << ec.message() << std::endl; return -1; } client.connect(con); client.run(); return 0; }客户端的get_connection负责解析 URI 并创建连接对象,注意它只是创建,还没发请求。真正的握手发生在client.connect(con)之后,由run()事件循环驱动。这个分层很容易被误解:connect 不是阻塞函数,它把连接请求投递到 Asio 队列就返回了,之后再被事件循环异步执行。这里用 lambda 而不是 std::bind 是我改 demo 时的习惯。lambda 捕获外部 client 引用,代码读起来更直观,也不会出现 bind 占位符和参数个数不匹配的编译问题。需要注意 lambda 里捕获的是&client,生命周期必须覆盖 client.run() 执行的整个阶段,所以在 main 函数里把它们放在同一作用域就是安全的。4.2 消息回调、发送回包与 ping 保活客户端发消息有三种常见姿势:在 open 回调里发、在 message 回调里回包、或者由外部业务线程主动发。demo 覆盖了前两种,第三种要额外小心线程安全。先看一个客户端主动保活的写法:#include <websocketpp/common/chrono.hpp> #include <websocketpp/common/thread.hpp> int main() { WsClient client; client.init_asio(); client.set_open_handler([&client](websocketpp::connection_hdl hdl) { // 打开连接后,周期性发送 ping 帧 websocketpp::lib::thread::id tid = websocketpp::lib::thread::id(); std::cout << "ping thread start" << std::endl; }); websocketpp::lib::error_code ec; auto con = client.get_connection("ws://127.0.0.1:9002", ec); client.connect(con); // 另起线程做保活,避免阻塞主事件循环 websocketpp::lib::thread t([&client]() { while (true) { websocketpp::lib::error_code ec; client.send(websocketpp::connection_hdl(), "", websocketpp::frame::opcode::ping, ec); std::this_thread::sleep_for(std::chrono::seconds(30)); } }); client.run(); return 0; }这段 ping 代码其实是示意性质,不能直接照抄进 demo 跑。原因有两个:websocketpp::connection_hdl()默认构造出来的句柄不指向任何有效连接,send 会失败;跨线程调用client.send没有经过 strand 串行化,存在数据竞争。我之所以把它放出来,是想说明保活是一个需要单独设计的模块,你在网上看到的不少 websocket++ 心跳示例都有类似问题。正确的做法是把 ping 发送逻辑绑定到某个具体 hdl,并且在 send 之前用con->get_underlying_transport()之类的接口确认连接仍然存活。更多时候,服务端会主动 ping 客户端,客户端只需要在 message 回调里识别 ping/pong 帧并回应,又把复杂度推回服务端。这个 demo 的 echo 逻辑天然能透传消息,你可以把 ping 帧作为普通消息发到服务端验证链路活性。4.3 断线重连:从直连到带退避的工程化写法demo 里没有重连逻辑,但客户端一旦引入真实业务,断线重连马上就变成刚需。最朴素的写法是在 close 回调里重新get_connection和connect:std::atomic<bool> stopped{false}; int retry_count = 0; void start_reconnect(WsClient& client, const std::string& uri) { websocketpp::lib::error_code ec; auto con = client.get_connection(uri, ec); if (ec) { std::cout << "reconnect get_connection failed: " << ec.message() << std::endl; return; } client.set_open_handler([&client, uri](websocketpp::connection_hdl hdl) { retry_count = 0; std::cout << "reconnected" << std::endl; }); client.connect(con); }这个写法的坑在于:如果直接在 close 回调里循环重连,而服务端恰好是正常的,那没问题;但如果服务端宕机,get_connection的 URI 解析会失败,重连逻辑立即退出,永远不会回来。所以真实工程里重连必须配合定时器和指数退避——第一次 1 秒、第二次 2 秒、第三次 4 秒,封顶 30 秒,避免服务端恢复时被几十个客户端同时涌入打挂。websocket++ 本身不提供重连组件,常见做法是用websocketpp::lib::asio::steady_timer替代 sleep,在回调里异步等待,这样不会阻塞事件循环。我改这类 demo 时习惯把重连策略单独抽成一个类,暴露 start/stop 两个接口,而不是把退避逻辑散落在回调里。5. 编译运行与避坑排查:把 demo 真正跑起来代码拆得再细,跑不起来等于零。这一章从零开始编译、连接、验证,再把我拆过这个 demo 后积攒的常见坑一次性倒出来。5.1 CMake 配置与依赖安装websocket++ 没有 INSTALL 步骤,CMake 只需要找到头文件目录和 Boost 组件:cmake_minimum_required(VERSION 3.10) project(websocket_demo CXX) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 若使用独立 Asio,开启下面这行 # add_definitions(-DASIO_STANDALONE) find_package(Boost REQUIRED COMPONENTS system random) # websocketpp 头文件目录 find_path(WEBSOCKETPP_INCLUDE_DIR NAMES websocketpp/websocketpp.hpp PATHS ${CMAKE_SOURCE_DIR}/third_party ) add_executable(server server/server.cpp) target_include_directories(server PRIVATE ${WEBSOCKETPP_INCLUDE_DIR}) target_link_libraries(server PRIVATE Boost::system Boost::random pthread) add_executable(client client/client.cpp) target_include_directories(client PRIVATE ${WEBSOCKETPP_INCLUDE_DIR}) target_link_libraries(client PRIVATE Boost::system Boost::random pthread)Boost 的 random 组件是 websocket++ 生成 masking key 时需要的。如果你系统里没装 Boost,也可以切独立 Asio 并去掉所有 Boost::* 链接项,代价是需要额外准备 asio 头文件。对新手我更推荐直接安装 Boost,包管理器里搜 libboost-system-dev / libboost-random-dev 即可。编译阶段最容易出现的问题是find_path找不到头文件。把 third_party 放对位置很关键,cmake 会基于CMAKE_SOURCE_DIR去拼接路径,如果工程目录层级不对,find_path静默返回 NOTFOUND,后续 include 阶段报一堆 “No such file or directory”。排查时可以打开 CMakeCache 看WEBSOCKETPP_INCLUDE_DIR的实际值。5.2 编译运行与连通性验证按下面的顺序执行,就能得到一条可观测的完整链路:# 1. 配置并编译 mkdir build && cd build cmake .. make -j$(nproc) # 2. 终端 A:启动服务端 ./server # 3. 终端 B:启动客户端 ./client服务端启动后应打印监听日志,客户端启动后应打印 connected 和 reply 两行输出。服务端因为开着 alevel::all 日志,会打印完整的 TCP 握手和 WebSocket 握手过程。我验证连通性时一般盯三个节点:一是服务端出现Connection from 127.0.0.1,二是服务端打印received: hello from client,三是客户端打印reply: hello from client。三段全齐,链路就是通的。如果手里没有现成客户端,还可以在浏览器控制台里用原生 WebSocket 去连ws://127.0.0.1:9002做验证——这是最快的外部探针。但这只能验证服务端,验证不了客户端代码本身,所以要真正调试客户端逻辑,还是得靠编译出的 client 可执行文件。5.3 websocket++ 常见问题与避坑清单以下五条是我实际拆这个 demo 时踩过的坑,按现象、原因、解决整理:坑一:客户端 connect 后一直不握手,卡在 “Connection” 状态现象:客户端没有任何日志输出,服务端也没有收到新连接。 原因:调了client.connect(con)但没调client.run(),事件循环没启动,所有异步操作都排队等待。 解决:确保connect之后主线程调用了run(),并且run()没有被其他阻塞代码挡住。坑二:服务端向已断开连接 send,直接抛异常现象:客户端强杀进程后,服务端在广播消息时崩溃,栈上显示 weak_ptr 转换失败。 原因:connection_hdl是弱引用,连接断开后对象被销毁,send 拿到过期句柄。 解决:send 时接收错误码 ec 而不是让它抛异常;连接管理容器里及时清理 on_close 的 hdl。坑三:map 用 connection_hdl 做 key 编译报错现象:using std::less<connection_hdl>作为 map 比较器时编译不通过。 原因:connection_hdl 是 weak_ptr,不满足std::less对原始指针的要求。 解决:map 的第三个模板参数换成std::owner_less<websocketpp::connection_hdl>。坑四:Boost 版本不匹配导致模板实例化失败现象:编译 websocket++ 头文件时报 Boost.Asio 内部类型找不到,或 error_code 相关符号重定义。 原因:websocket++ 发布较早,与较新版本的 Boost.Asio 存在兼容间隙。 解决:优先统一 Boost 版本;如果项目不允许动 Boost,切换ASIO_STANDALONE走独立 Asio 通常能绕开这类问题。坑五:多线程里直接调 send,偶发崩溃现象:业务线程向 websocket 连接发数据,运行几个小时崩溃一次,堆栈指向 socket write。 原因:websocket++ 默认没有对 send 做串行化保护,多线程并发写同一个 socket 是数据竞争。 解决:用websocketpp::lib::asio::strand包住发送操作,或把发送动作丢回事件循环线程执行。6. 二次开发实战:把 echo demo 改成带鉴权的消息路由demo 跑通后,改造的第一站通常是把 echo 改成真正的业务消息分发。我的做法是先在 on_message 里加一层路由逻辑,再给握手加上鉴权。先看服务端 on_message 的路由改造。假设消息体是 JSON,里面有个action字段,根据 action 决定是转给所有连接还是转给特定连接:void on_message(WsServer* srv, websocketpp::connection_hdl hdl, WsServer::message_ptr msg) { std::string payload = msg->get_payload(); // 伪代码:解析 JSON 并分发,实际用 jsoncpp / nlohmann 解析 auto cmd = parseJson(payload).action; if (cmd == "broadcast") { broadcast(srv, payload); } else if (cmd == "peer") { std::string target = parseJson(payload).target; websocketpp::lib::error_code ec; srv->send(target_hdl_map[target], payload, websocketpp::frame::opcode::text, ec); } else { srv->send(hdl, "unsupported action", websocketpp::frame::opcode::text, ec); } }路由改造的核心是维护好target_hdl_map这个业务映射表。注意它和 conns 的区别:conns 是 websocket++ 层面的连接池,target_hdl_map 是业务层面的用户索引,需要在 on_open 里根据用户标识填充、在 on_close 里删除。两套映射要同步更新,漏删一边就会出现“消息发给已下线用户”的问题。鉴权加在握手之前。websocket++ 提供set_validate_handler,在 HTTP 握手阶段做检查:server.set_validate_handler([](websocketpp::connection_hdl hdl) -> bool { auto con = server.get_con_from_hdl(hdl); std::string resource = con->get_resource(); // 从 query string 或 header 里取 token 并校验 if (resource.find("token=secret") == std::string::npos) { std::cout << "reject connection without valid token" << std::endl; return false; } return true; });这个回调返回 false 时,服务端会直接拒绝 WebSocket 握手,客户端会收到握手失败。改造后建议按三步验证:先不带 token 连接,确认被拒;再带 token 连接,确认握手成功;最后跑一次广播,确认路由逻辑没破坏原 demo 的链路。最后补一句我拆项目的习惯:拿到任何 websocket++ demo,第一件事都是把客户端和服务端同时跑起来、确认日志完整闭环,然后才动代码。那次以后我每次改完通信代码都会强制走一遍两端的握手日志验证,再小的改动也不例外。希望帮到你。本文还有配套的精品资源,点击获取