资讯动态

基于Boost.Beast的C++ WebSocket客户端Demo:编译、避坑与实战

发布时间:2026/10/20 22:48:12 来源:尧图企业网站定制
简介面向 C 开发者的一份可直接编译的 WebSocket 通信示例 Demo整合 websocketpp、Boost 与 jsoncpp 三套库帮助读者快速掌握 WebSocket 客户端/服务器全双工通信的搭建流程。压缩包内共 14553 个文件主体为 1.4 万余个 hpp/ipp 头文件及 Boost 相关标准头文件另包含少量 cpp 示例、CMake 配置和 Git 仓库信息整个包体积约 154.62MB属于带完整依赖的工程模板。目前已有 1077 人学习适合正在学习 C 网络编程或需要为项目接入 WebSocket 功能的开发者参考。通过这份代码可以直接在本地用 CMake 生成构建文件并编译运行省去繁琐的环境配置代码中能看到 websocketpp 处理握手、收发消息、事件回调的典型写法以及 jsoncpp 做 JSON 序列化与解析的配合方式。对于想了解 WebSocket 协议在 C 中如何落地、或希望直接改造为业务原型的读者是一个实用的起点。1. 先搞清楚这是个能直接编译的 C WebSocket Demo别去手写握手了很多刚接触 C WebSocket 的人第一反应是自己构造 HTTP Upgrade 请求手写 base64 编解码和帧掩码。实际上 WebSocket 的握手、帧解析和 TLS 细节远比想象的多。这个 Demo 是一个基于 Boost.Beast 的 C WebSocket 客户端工程带 CMake 构建脚本拉到本地后按顺序敲两三条命令就能编译出可执行文件并完成一次完整的 ws/wss 通信。它能解决两类问题一是给刚入门的开发者一个“能跑、能改”的起点把协议握手、帧收发、SSL/TLS 这些黑匣子变成可读代码二是给已经在用 Boost 的团队一份可复用的异步客户端骨架省去从零拼装的麻烦。适合对 C 有一定基础、想快速拿到可用 WebSocket 客户端源码的人阅读。2. WebSocket 与 Boost.Beast 的选型为什么这个 Demo 值得直接编译2.1 WebSocket 协议的核心机制WebSocket 协议分两个阶段握手阶段和数据帧阶段。握手阶段复用 HTTP/1.1客户端发一个Upgrade: websocket请求服务端返回 101 状态码之后连接切换为全双工的消息通道。数据帧阶段有一个简单的帧格式FIN 位、opcode、payload length、mask 和 payload。客户端发送数据必须加掩码服务端不需要。我拆这个 Demo 时最直观的感受是握手本身不难但要想在异步 I/O 下正确处理帧的分片、关闭握手Close Handshake和 ping/pong代码量会迅速膨胀。Beast 把这些协议细节全部封装好了你只需要配置一个stream设置回调剩下的都由库处理。握手请求里有一个Sec-WebSocket-Key是随机生成的 base64 字符串服务端拼上固定 GUID 做 SHA1 后返回Sec-WebSocket-Accept。这个校验过程如果自己实现涉及 SHA1 和 base64是第一个容易翻车的点。Beast 的handshake()函数内部全部处理完而且会校验服务端返回值不接受就报upgrade_error。2.2 为什么用 Boost.Beast而不是自己手写或套 libwebsockets先说结论Boost.Beast 是基于 Boost.Asio 的高层库和 Asio 的异步模型自然融合而且是 Boost 官方维护的。自己手写帧解析前期可能很快但遇到二进制消息、多帧分片、SSL 连接复用这些场景工作量和验收成本都不是 Demo 阶段能承受的。而 libwebsockets 功能也全但它的线程模型与 Asio 绑定相对较深如果你想与现有 Boost.Asio TCP 服务共用事件循环Beast 是更平滑的选择。有人担心 Beast 在 Boost 1.75 之前命名空间不同。这个 Demo 明确针对boost::beast命名空间也就是使用 Boost 1.75 及以上版本。在更早版本里可能叫beast而不是boost::beastCMake 脚本里已经做了版本检查这是最容易踩的坑之一。从性能角度看Beast 是 header-only 的但 SSL 支持依赖 OpenSSL。由于它是直接基于 Asio 的流式接口和不经过中间层的原生 socket 性能差距很小。对于消息频率不高的控制面场景用它完全够用如果要高吞吐也可以直接复用 Asio 的线程池模型。2.3 依赖与版本Boost 1.75CMake 3.16 以上编译这个 Demo 所需依赖很简单需要 Boost 1.75 或更高版本支持boost::beast与boost::asio::ssl以及 CMake 3.16 以上。网络库本身 header-only但因为我们开了 SSL/TLS 支持需要链接系统 OpenSSL。在常见的 Linux 发行版上一般安装libssl-dev在命令行环境使用预编译的 Boost 二进制也可以。环境自查命令如下# 检查 Boost 版本 grep BOOST_LIB_VERSION /usr/include/boost/version.hpp | head -n1 # 检查 CMake 版本 cmake --version | head -n1 # 检查 OpenSSL openssl version这里在编译前先跑一遍可以省去很多“编译到一半发现缺头文件”的麻烦。特别需要注意 Boost 版本1.75 之后 Beast 才被归到boost::beast命名空间如果用 1.74 及更早的版本代码里的命名空间要改这属于另一个坑后面会细说。还有一个容易忽视的点Beast 的异步操作默认需要链接 Boost.System。以前有些项目只在 CMake 里写了Boost::headers运行时会出现system_category相关的链接错误。这个 Demo 的 CMakeLists 里已经带上了Boost::system但如果你把它拆到别的工程里很容易漏掉。3. Demo 的工程结构与代码拆解一个可编译的 TCP TLS 客户端3.1 工程文件清单这个 Demo 采用 CMake 组织工程目录结构如下websocket-demo/ ├── CMakeLists.txt ├── src/ │ ├── ws_client.cpp │ └── ws_server.cpp └── README.mdCMakeLists.txt 负责自动查找 Boost 和 OpenSSL生成可执行文件ws_client.cpp 是 WebSocket 客户端实现支持 ws:// 和 wss:// 两种地址ws_server.cpp 是一个极简单的回显服务器用于本地联调不走真实业务逻辑。README 里有编译命令和参数说明我一般会先看它再动手避免在 CMake 参数上瞎猜。3.2 客户端主流程从解析 URL 到收发消息ws_client.cpp 的核心流程可以拆成四步解析 URL、创建 IO 上下文、连接并握手、循环收发。下面这段代码是握手与首次发送的核心片段#include boost/beast/core.hpp #include boost/beast/websocket.hpp #include boost/asio/connect.hpp #include boost/asio/ip/tcp.hpp namespace beast boost::beast; namespace websocket beast::websocket; namespace net boost::asio; using tcp net::ip::tcp; // 解析主机名、端口和路径 // 例如 ws://localhost:8080/chat - hostlocalhost, port8080, path/chat void parse_url(const std::string url, std::string host, std::string port, std::string path); int main(int argc, char** argv) { // 1. 解析命令行参数得到 URL std::string url argv[1]; // 例如 ws://127.0.0.1:8080/ws // 2. 创建 IO 上下文用于驱动所有异步操作 net::io_context ioc; // 3. 解析域名并建立 TCP 连接 tcp::resolver resolver(ioc); auto results resolver.resolve(host, port); beast::tcp_stream stream(ioc); stream.connect(results); // 4. 在 TCP 连接上创建 WebSocket 流并握手 websocket::streambeast::tcp_stream ws(std::move(stream)); ws.handshake(host, path); // 5. 发送一条文本消息 ws.write(net::buffer(std::string(hello from demo))); // 6. 读取回显消息 beast::flat_buffer buffer; ws.read(buffer); std::cerr reply: beast::buffers_to_string(buffer.data()) \n; // 7. 关闭连接先发 close 帧再等对端响应 ws.close(websocket::close_code::normal); return 0; }这段代码用的是同步接口只是为了让你一眼看清流程。实际工程里更推荐async_read/async_write但作为 Demo同步版本更容易断点和观察。注意第 4 行的handshake函数会拼接 HTTP Upgrade 请求Beast 会处理Sec-WebSocket-Key和Sec-WebSocket-Accept校验这就是我前面说的“不用自己写握手”的原因。参数说明ws.write的net::buffer可以传字符串、字节数组或自定义结构体read需要一块flat_bufferBeast 会自动扩容。handshake的 host 用于服务端校验 Host 头path 默认为/如果服务端有路径路由必须传对否则返回 404。如果要支持 wss:// 地址还需要先建立 SSL 流// wss:// 地址需要先建立 SSL 流 net::ssl::context ctx(net::ssl::context::tlsv12_client); ctx.set_verify_mode(net::ssl::verify_peer); beast::ssl_streamtcp::socket ssl_stream(ioc, ctx); beast::websocket::streambeast::ssl_streamtcp::socket ws(std::move(ssl_stream)); // 先 SSL 握手再 WebSocket 握手 ws.next_layer().handshake(net::ssl::stream_base::client); ws.handshake(host, path);注意这里next_layer()返回底层 SSL 流先做 TLS 握手之后handshake才是 WebSocket 协议层的握手。很多第一次写 wss 的人会漏掉这层导致连接直接失败。3.3 关键类与回调如何管理异步生命周期如果我们要改成异步版本核心是websocket::stream的async_handshake、async_read、async_write三个成员函数。它们的回调签名都是(error_code, bytes_transferred)其中bytes_transferred表示本次操作的数据长度。异步版本最需要注意的是 stream 对象的生命周期在异步操作完成之前stream 和 buffer 都必须保持有效。常见做法是把它放进shared_ptr或者绑定在 lambda 的捕获列表里。auto ws std::make_sharedwebsocket::streambeast::tcp_stream(std::move(stream)); ws-async_handshake(host, path, [ws](beast::error_code ec) { if (ec) { std::cerr handshake failed: ec.message() \n; return; } ws-async_write(net::buffer(std::string(hello)), [ws](beast::error_code ec, std::size_t n) { // 处理发送完成后的后续逻辑 if (ec) { std::cerr write failed: ec.message() \n; } }); });这里有个容易被忽略的坑回调捕获的 ws 必须是shared_ptr否则当异步操作尚未完成时外层作用域退出导致 ws 析构底层 socket 被关闭回调收到operation_aborted。这个现象我在调试时花了不少时间才发现后面避坑章节会再提。buffer 的生命周期也要注意。异步读取用的flat_buffer必须在回调触发前一直存活否则数据还没拷贝出来buffer 就被析构了。常见做法是把这个 buffer 也放进同一个shared_ptr或者作为 stream 的成员变量。3.4 服务器与回显测试一个最小 ws_server为了能在本地不依赖公网服务测试客户端Demo 里附带了一个最小的 WebSocket 回显服务器。它本质上就是 Beast 的websocket::stream套在一个 TCP acceptor 上每收到一条消息就把原文发回去。#include boost/beast/websocket.hpp #include boost/asio/ip/tcp.hpp // 省略 io_context 与 acceptor 创建监听 8080 端口 tcp::acceptor acceptor(ioc, tcp::endpoint(tcp::v4(), 8080)); for (;;) { tcp::socket socket acceptor.accept(); websocket::streamtcp::socket ws(std::move(socket)); ws.accept(); // 完成服务端握手 for (;;) { beast::flat_buffer buffer; ws.read(buffer); ws.write(buffer.data()); // 回显 } }这里的ws.accept()在服务端角色中代替了handshake它自动校验客户端发来的 Upgrade 请求。read返回后flat_buffer中的数据就是客户端发来的原始消息直接写回去就是最简单的回显服务。注意当前版本是阻塞式单连接只用于本地快速验证真要压测并发需要换成异步 acceptor并在每连接里起独立协程或线程。这个回显服务器没有业务逻辑但它能把“客户端能不能通、帧格式对不对、二进制消息是否完整”这类问题快速暴露出来。我通常先用它把 Demo 跑通再对接到真实业务服务。4. 编译与运行从 boost 编译参数到实际联调4.1 使用 CMake 构建避免手写 makefile工程里已经配好 CMakeLists.txt下面这个就是完整文件可以直接用在大多数 C17 工程里cmake_minimum_required(VERSION 3.16) project(websocket_demo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Boost 1.75 COMPONENTS system REQUIRED) find_package(OpenSSL REQUIRED) add_executable(ws_client src/ws_client.cpp) target_link_libraries(ws_client PRIVATE Boost::system OpenSSL::SSL OpenSSL::Crypto Threads::Threads) add_executable(ws_server src/ws_server.cpp) target_link_libraries(ws_server PRIVATE Boost::system Threads::Threads)这里要留意find_package(Boost 1.75)中的版本号如果你的 Boost 是 1.74 或更低Beast 的命名空间是beast而不是boost::beast这个 CMakeLists 会直接报错找不到包或者编译期报 namespace 错误。所以我在 README 里特意强调了“确认boost/version.hpp中BOOST_LIB_VERSION不小于 1.75”。project(websocket_demo CXX)这里显式声明只使用 C避免 CMake 去探测 C 编译器带来的额外配置。C17 是必须的Beast 内部用了std::string_view、std::optional等特性如果编译时没加-stdc17会看到一堆从 boost 头文件里冒出来的语法错误。4.2 编译参数说明与常见链接错误构建命令cmake -S . -B build -DCMAKE_BUILD_TYPERelease cmake --build build -j$(nproc)在 Windows 环境中-DCMAKE_BUILD_TYPE可能无效需要用--config Release。这里-S指定源码目录-B指定构建目录-j表示并行编译。如果 Boost 不是安装到系统默认路径需要额外传cmake -S . -B build -DBOOST_ROOT/path/to/boost -DOPENSSL_ROOT_DIR/path/to/openssl最常遇到的链接错误是undefined reference to SSL_library_init或者boost::system::system_category()。前者是因为 OpenSSL 库没链上检查 CMake 里有没有OpenSSL::SSL后者是 Boost.System 没链上通常发生在只链接了Boost::headers、没有链接Boost::system的情况。这个工程里已经显式链接了Boost::system但如果读者是从别处混用需要注意。还有一类编译错误是fatal error: boost/beast/core.hpp: No such file or directory。这通常意味着 CMake 找到的 Boost 头文件路径不对。可以在 CMake 配置时加-DCMAKE_CXX_FLAGS-I/path/to/boost/include手动指定或者直接设置BOOST_ROOT。4.3 运行测试连接公网 wss 还是本地 echo运行客户端有两种方式# 连接本地回显服务器 ./build/ws_server ./build/ws_client ws://127.0.0.1:8080/echo # 连接公网 wss 服务测试环境可用 ./build/ws_client wss://echo.websocket.events我一般先用本地 echo 跑通再换 wss 验证 SSL 链路。如果本地连接返回 EOF检查 ws_server 是否真的在监听在另一个终端用lsof -i:8080查看端口占用。连接公网 wss 经常失败是因为系统时间不正确SSL 证书验证失败现象是ssl handshake error。这个坑在避坑章节会列。还有一种情况客户端连接成功后发送第一条消息服务器却没有任何输出。常见原因是 URL 里的 path 不对。比如服务端监听在/但客户端传了/chatBeast 握手会带上该 path服务端如果没在websocket::stream::accept之前做路径路由判断就会默默接受但应用层路由不匹配消息可能被丢进回收站。5. 避坑Boost.Beast 编译与运行中最常见的五个坑5.1 命名空间版本差异boost::beast和beast不通用现象编译时报错beast is not a namespace-name或者找不到boost::beast。原因Beast 在 Boost 1.75 之前被放在beast命名空间1.75 之后统一搬迁到boost::beast。如果你使用旧版 Boost代码里的boost::beast全部失效。解决先用grep BOOST_LIB_VERSION /usr/include/boost/version.hpp确认版本。如果低于 1.75要么升级 Boost要么把代码里的boost::beast全部替换为beast同时注意boost::beast::websocket要变成beast::websocket。我建议直接升级 Boost因为旧版本的 Beast 接口和行为都不如新版稳定。5.2 异步回调中 stream 生命周期失效现象程序运行一段时间后弹出operation_aborted或者回调里访问ws时直接段错误。原因异步操作如async_read发起后外层作用域退出ws对象被析构底层 socket 关闭。Asio 检测到 socket 关闭就会以operation_aborted取消回调。解决把ws和相关的 buffer 都放进shared_ptrLambda 按值捕获shared_ptr。这样异步操作持有了对象的引用计数只要操作没完成对象就不会析构。我自己的习惯是写一个websocket_session结构把 stream、buffer、读写状态都放进去用shared_ptrwebsocket_session管理这样生命周期一目了然。5.3 wss 连接失败证书验证和系统时间现象连接wss://地址时握手阶段报certificate verify failed或ssl handshake error。原因SSL 上下文默认的verify_peer会校验服务端证书链但系统里没有安装根证书或者系统时间错误导致证书有效期校验失败。解决生产环境必须正确加载根证书路径比如ctx.load_verify_file(/etc/ssl/certs/ca-certificates.crt);如果只是本地测试可以临时把验证模式降为verify_none但务必在注释里写清楚这是测试代码不能直接用于生产。同时检查系统时间date命令看到的时间如果偏差超过几分钟证书校验基本必挂。5.4 发完消息立刻 close服务端收到不完整数据现象服务端先收到 FIN 分片再收到部分业务数据或者客户端自己报sent_fin_incomplete。原因客户端调用ws.close()时Beast 会先发送一个 Close 控制帧但如果之前还有数据帧没有完全发出比如async_write还在进行中直接 close 会导致发送队列里残留数据被截断。解决在发送完成后等待对端响应再调用ws.close()。如果是同步模式先ws.read(buffer)收到回复再ws.close()。异步模式可以在async_write的回调里再发起async_close保证没有未完成的写操作。5.5 二进制消息被截断或乱序现象发送大 payload 时对端收到的数据不完整或者出现连续的丢帧。原因WebSocket 帧可以分片Beast 的read默认一次只读一个帧。如果发送方把一个消息拆成多个帧比如 128KB 以上的大数据接收方需要循环读取直到收到 FIN 标志。另外read_message_max默认值很小超过限制会直接报message_too_big。解决在 stream 创建后设置ws.read_message_max(1 20); // 限制单个消息最大 1MB如果需要完整读取所有分片循环调用read直到ws.got_binary()或ws.got_text()返回 false或者简单一点直接用ws.async_read每次读取一块缓冲区并拼接直到收到 FIN。6. 从 Demo 到生产心跳、重连与并发验证Demo 能跑通只是第一步真正接到业务里还有三件事必须做心跳、断线重连、并发验证。心跳防止中间网络设备因为空闲把连接回收。Beast 提供了async_ping我一般用一个net::steady_timer每隔 30 秒发一次 ping超过 45 秒没收到 pong 就手动断开并触发重连。定时器和 ping 的回调都捕获同一个连接对象的 shared_ptr避免生命周期问题。timer.expires_after(std::chrono::seconds(30)); timer.async_wait([ws, timer](beast::error_code ec) { if (ec) return; ws-async_ping({}, [ws](beast::error_code ping_ec) { if (ping_ec) { // 主动关闭并触发重连逻辑 } }); timer.expires_after(std::chrono::seconds(30)); timer.async_wait(...); });重连逻辑也不复杂在回调里捕获error_code如果是connection_reset或broken_pipe就重新执行“解析 URL - 建立连接 - 握手”这段流程并加一个随机退避避免服务端故障时客户端疯狂重试。我习惯设置最小重连间隔 1 秒、最大 60 秒每次失败翻倍。最后是并发验证。这个 Demo 是单连接的真实场景往往需要同时开几百个客户端。简单的方式是在客户端程序里循环fork每个子进程跑一个单独的io_context如果是一个进程内多连接就要把每个连接放进独立线程或者用异步模式跑在同一个io_context里。我验证并发时最常遇到的就是 socket 描述符不够用这通常不是代码问题而是系统ulimit限制需要先用ulimit -n 65535调整。我从这次拆 Demo 的教训里养成一个习惯任何 WebSocket 代码不管是自己写的还是拿来的都会强制走一遍“本地 echo - 公网 wss - 并发压测”这条验证路径。只要三步全通过这块才算真正落地。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑