简介:面向 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::beast,CMake 脚本里已经做了版本检查,这是最容易踩的坑之一。从性能角度看,Beast 是 header-only 的,但 SSL 支持依赖 OpenSSL。由于它是直接基于 Asio 的流式接口,和不经过中间层的原生 socket 性能差距很小。对于消息频率不高的控制面场景,用它完全够用;如果要高吞吐,也可以直接复用 Asio 的线程池模型。2.3 依赖与版本:Boost 1.75+,CMake 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 -> host=localhost, port=8080, 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::stream<beast::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_buffer,Beast 会自动扩容。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_stream<tcp::socket> ssl_stream(ioc, ctx); beast::websocket::stream<beast::ssl_stream<tcp::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_shared<websocket::stream<beast::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::stream<tcp::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,下面这个就是完整文件,可以直接用在大多数 C++17 工程里: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 编译器带来的额外配置。C++17 是必须的,Beast 内部用了std::string_view、std::optional等特性,如果编译时没加-std=c++17,会看到一堆从 boost 头文件里冒出来的语法错误。4.2 编译参数说明与常见链接错误构建命令:cmake -S . -B build -DCMAKE_BUILD_TYPE=Release 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 不对。比如服务端监听在/,但客户端传了/chat,Beast 握手会带上该 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_ptr,Lambda 按值捕获shared_ptr。这样异步操作持有了对象的引用计数,只要操作没完成,对象就不会析构。我自己的习惯是写一个websocket_session结构,把 stream、buffer、读写状态都放进去,用shared_ptr<websocket_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 -> 并发压测”这条验证路径。只要三步全通过,这块才算真正落地。希望帮到你。本文还有配套的精品资源,点击获取
简介:面向 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::beast,CMake 脚本里已经做了版本检查,这是最容易踩的坑之一。从性能角度看,Beast 是 header-only 的,但 SSL 支持依赖 OpenSSL。由于它是直接基于 Asio 的流式接口,和不经过中间层的原生 socket 性能差距很小。对于消息频率不高的控制面场景,用它完全够用;如果要高吞吐,也可以直接复用 Asio 的线程池模型。2.3 依赖与版本:Boost 1.75+,CMake 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 -> host=localhost, port=8080, 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::stream<beast::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_buffer,Beast 会自动扩容。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_stream<tcp::socket> ssl_stream(ioc, ctx); beast::websocket::stream<beast::ssl_stream<tcp::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_shared<websocket::stream<beast::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::stream<tcp::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,下面这个就是完整文件,可以直接用在大多数 C++17 工程里: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 编译器带来的额外配置。C++17 是必须的,Beast 内部用了std::string_view、std::optional等特性,如果编译时没加-std=c++17,会看到一堆从 boost 头文件里冒出来的语法错误。4.2 编译参数说明与常见链接错误构建命令:cmake -S . -B build -DCMAKE_BUILD_TYPE=Release 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 不对。比如服务端监听在/,但客户端传了/chat,Beast 握手会带上该 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_ptr,Lambda 按值捕获shared_ptr。这样异步操作持有了对象的引用计数,只要操作没完成,对象就不会析构。我自己的习惯是写一个websocket_session结构,把 stream、buffer、读写状态都放进去,用shared_ptr<websocket_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 -> 并发压测”这条验证路径。只要三步全通过,这块才算真正落地。希望帮到你。本文还有配套的精品资源,点击获取