拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WebSocket Echo Server 与客户端实战:cpp-httplib 双向消息通信指南

WebSocket Echo Server 与客户端实战:cpp-httplib 双向消息通信指南
  • 后端
  • 网络

【免费下载链接】cpp-httplib

A C++ header-only HTTP/HTTPS server and client library

项目地址:https://gitcode.com/GitHub_Trending/cp/cpp-httplib
点击查看免费下载

cpp-httplib 提供了基于 RFC 6455 的阻塞式 WebSocket 实现,服务端与客户端 API 齐备。本篇以最经典的echo(回显)场景为主线,演示如何用httplib::Server注册一个 WebSocket 处理器、用httplib::ws::WebSocketClient建立双向连接,并深入剖析ReadResult返回值、文本/二进制帧选择、线程模型与超时配置等关键细节。读完本篇,你将能独立搭建一对可收发文本与二进制消息的 WebSocket 服务端/客户端,并为并发连接合理配置线程池。

1. WebSocket 与 cpp-httplib 的对应关系

WebSocket 是一种在单个 TCP 连接上进行双向消息传递的协议:客户端与服务器建立连接后,双方可以随时向对方发送消息,无需像 HTTP 那样一问一答。cpp-httplib 在 httplib.h 中以httplib::ws命名空间提供了完整实现,服务端和客户端分属两个类:

  • 服务端:httplib::Server::WebSocket()注册处理器,处理器内拿到httplib::ws::WebSocket对象;
  • 客户端:httplib::ws::WebSocketClient,用ws://(明文)或wss://(TLS)URL 构造。

项目的 README-websocket.md 明确说明这是一个C++11、阻塞式 I/O、线程每连接(thread-per-connection)的实现,适用于小到中等规模的负载场景;若你的目标是数千并发连接的异步非阻塞场景,则不在本库的设计目标之内。另外,RFC 6455 定义的扩展(如permessage-deflate)未实现——即使客户端通过Sec-WebSocket-Extensions提出扩展,服务端也会静默拒绝,协商后的连接始终不带扩展。

在动手写代码前,你还需要知道三个宏默认值(完整清单见 README-websocket.md 的配置表):

宏默认值含义
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH16777216(16MB)单条消息的最大载荷
CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND300服务端读超时(秒),兜底用
CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND0客户端读超时(秒),0表示一直等待

2. 服务端:实现一个 echo 服务器

这是最简单也最典型的 WebSocket 服务端写法:

#include <httplib.h> int main() { httplib::Server svr; svr.WebSocket("/echo", [](const httplib::Request &req, httplib::ws::WebSocket &ws) { std::string msg; while (ws.is_open()) { auto result = ws.read(msg); if (result == httplib::ws::ReadResult::Fail) { break; } ws.send(msg); // echo back what we received } }); svr.listen("0.0.0.0", 8080); }

用svr.WebSocket()注册处理器即可。需要特别理解的一点是:当处理器开始执行时,WebSocket 握手(HTTP Upgrade)已经完成——也就是说,此刻双方已经处于可直接收发帧的 WebSocket 连接状态。这从 httplib.h 的实现可以印证:WebSocket()只是把pattern的匹配器、处理器和可选的子协议选择器存入websocket_handlers_向量,真正的握手与连接建立发生在请求处理流水线内部。

处理器内部就是一个循环:ws.read(msg)阻塞读取下一条消息,ws.send(msg)原样回显。当对端关闭或出错时read()返回Fail,循环退出,处理器返回。

仓库中 example/wsecho.cc 提供了一个更完整的可运行版本:它在/路径挂载了一个 HTML 页面,浏览器打开即可通过原生WebSocketAPI 连接/ws路径做交互测试,同时服务端在控制台打印连接、收到的消息与断开事件。你可以直接编译体验:

cd example && make wsecho && ./wsecho

然后浏览器访问http://localhost:8080(该演示页也展示了子协议协商,详见后文)。

2.1read()的返回值:ReadResult枚举

read()的返回值是一个ReadResult枚举,定义于 httplib.h:

enum ReadResult : int { Fail = 0, Text = 1, Binary = 2, Timeout = 3 };
返回值含义
ReadResult::Text收到一条文本消息
ReadResult::Binary收到一条二进制消息
ReadResult::Fail出错,或连接已关闭
ReadResult::Timeout你通过set_read_timeout()设置的读超时到期且没有任何消息到达;连接仍然打开

一个值得注意的设计点:由于Fail的值为0,ReadResult天然可以用于布尔上下文——while (ws.read(msg))会在连接关闭时自然退出,这正是 example/wsecho.cc 使用的写法。

而Timeout的情况比较特殊,README-websocket 对此有明确说明:

  • 只有你自己用set_read_timeout()设置的超时才以Timeout形式返回;超时发生时没有任何字节被消费,连接依然可用,你可以继续send()或再次read()。
  • 编译期默认超时(服务端 300 秒、客户端无限等待)是兜底机制而非控制请求:它到期时read()返回Fail并关闭连接,因此从不调用set_read_timeout()的代码可以放心使用while (ws.read(msg))模式。
  • Timeout返回时msg保持原样不变。因为Timeout非零,while (ws.read(msg))会继续循环——但msg里还是上一条旧消息。一旦设置了读超时,就应当显式检查返回值,而不是依赖布尔循环。

针对设置了读超时的正确循环模式,w06-websocket-timeouts.md 给出了推荐写法:

ws.set_read_timeout(std::chrono::milliseconds(100)); std::string msg; while (ws.is_open()) { auto r = ws.read(msg); if (r == httplib::ws::Timeout) { continue; } // nothing yet; send if you like if (r == httplib::ws::Fail) { break; } handle(msg); }

2.2 优雅关闭:close()与CloseStatus

服务端和客户端都有close(CloseStatus status = CloseStatus::Normal, const std::string &reason = "")。CloseStatus是对 RFC 6455 关闭码的封装(httplib.h),常用值包括Normal(1000)、GoingAway(1001)、PolicyViolation(1008)、MessageTooBig(1009)等。例如在处理器中校验失败时主动带状态关闭:

ws.close(httplib::ws::CloseStatus::PolicyViolation, "forbidden");

3. 客户端:与 echo 服务器对话

与服务端对称的客户端写法:

#include <httplib.h> int main() { httplib::ws::WebSocketClient cli("ws://localhost:8080/echo"); if (!cli.connect()) { std::cerr << "failed to connect" << std::endl; return 1; } cli.send("Hello, WebSocket!"); std::string msg; if (cli.read(msg) != httplib::ws::ReadResult::Fail) { std::cout << "received: " << msg << std::endl; } cli.close(); }

要点拆解:

  • URL 写法:使用ws://(明文)或wss://(TLS)URL,构造参数是scheme_host_port_path格式的完整地址。构造器还有第二个可选参数Headers,可用于携带自定义头(如认证信息),见 httplib.h。
  • connect():执行 HTTP Upgrade 握手。在 httplib.h 中,connect()返回一个Result对象:只有在握手完全成功时才为真值;失败时error()指明失败层(网络问题如Connection、TLS 问题如SSLServerVerification、升级被拒如WebSocketHandshake),status()/headers()携带服务端的升级响应(未收到响应时status()为-1)。
  • send()/read():与服务端完全相同的语义与签名。
  • close():发起关闭握手。

3.1 深入诊断连接失败

不要把connect()的结果当成布尔值一丢了事,失败信息对排查问题很有价值:

auto res = cli.connect(); if (!res) { std::cerr << "connect failed: " << httplib::to_string(res.error()) << std::endl; if (res.status() != -1) { // The server responded but refused the upgrade (e.g. 401, 404) std::cerr << "HTTP status: " << res.status() << std::endl; } }

这段代码直接来自 README-websocket.md 的示例:当服务端返回了101 Switching Protocols以外的响应时,status()和headers()就携带了那次拒绝升级的响应(比如 401 未授权、404 路径不存在),是定位握手失败的第一手证据。

3.2 带子协议的客户端

某些应用(如 GraphQL over WebSocket、MQTT over WebSocket)要求在握手中协商子协议。客户端通过Sec-WebSocket-Protocol头提出候选列表,服务端在注册WebSocket()时传入的SubProtocolSelector负责选定(返回空串表示全部拒绝),客户端再用subprotocol()查询协商结果。完整示例见 README-websocket.md,example/wsecho.cc 中服务端对echo、chat两个候选做了选择。

4. 文本帧与二进制帧:send()的两个重载

send()通过重载区分帧类型:

ws.send("Hello"); // text frame ws.send(binary_data, binary_data_size); // binary frame
  • std::string重载以文本帧发送;
  • const char*+ 长度重载以二进制帧发送。

这个区别有点微妙,但记住「string 是文本、指针+长度是二进制」就很好理解。如果你手里是一个std::string却想按二进制发送,显式传.data()和.size()即可:

std::string raw = build_binary_payload(); ws.send(raw.data(), raw.size()); // binary frame

接收侧的判断逻辑在 w04-websocket-binary.md 中有完整示例:用read()的返回值区分Text与Binary,二进制帧仍以std::string承载,但应把内容当作原始字节处理(用msg.data()/msg.size())。典型实践是同一连接上混合使用:JSON 走文本帧做控制消息,图片、protobuf 等原始字节走二进制帧,兼顾元数据可读性与载荷效率。

补充一点:二进制帧中的 Ping/Pong 属于控制帧,cpp-httplib 会自动处理(自动回复 Ping),应用代码无需介入。

5. 线程模型:为什么需要配置动态线程池

这是 WebSocket 与普通 HTTP 处理最大的差异点:

一个 WebSocket 处理器在其连接存续期间独占一个工作线程——一条连接占一个线程。

从源码结构看,这是线程每连接模型的直接推论:服务端处理器运行于任务队列派发的线程上,read()阻塞期间该线程无法服务其他请求。默认线程池具有动态伸缩能力(基础线程数CPPHTTPLIB_THREAD_POOL_COUNT,即 8 或hardware_concurrency() - 1取大者;负载时可自动扩展到最多 4 倍,CPPHTTPLIB_THREAD_POOL_MAX_COUNT;临时线程空闲 3 秒后退出,见 README-websocket.md)。

如果你预期大量并发 WebSocket 客户端,应当按需配置线程池:

svr.new_task_queue = [] { return new httplib::ThreadPool(8, 128); };

两个参数分别是基础线程数与最大线程数,选值时要同时考虑预期的 HTTP 负载与最大同时在线 WebSocket 连接数。更系统的讨论参见 s21-thread-pool.md。

5.1 多线程使用同一连接的边界

一个连接句柄可能同时被三个调用方接触:运行处理器(或持有客户端)的线程、心跳线程、以及(如果你自己这么写)在另一线程阻塞于read()时调用send()/close()的线程。官方明确支持:一个线程read()、另一个线程send()/close()(例如 UI 线程发送、读取循环线程接收);心跳线程的自动 Ping 走的是内部同一send()路径,因此与你的read()循环并发也是安全的。不支持的是两个线程同时对同一句柄调用read()——调用会被串行化而不至于破坏数据,但哪个线程收到哪条消息是不确定的,没有实际可用价值。

6. 在 HTTPS(wss://)下运行

原文档给出的关键提示:要在 HTTPS 上跑 WebSocket,把httplib::Server换成httplib::SSLServer即可——同一个WebSocket()处理器照常工作;客户端侧则改用wss://URL:

httplib::SSLServer svr; // instead of httplib::Server // svr.WebSocket("/echo", ...); // same handler, unchanged httplib::ws::WebSocketClient cli("wss://localhost:8080/echo"); // instead of ws://

客户端侧如需配置 CA 证书、启用服务端证书与主机名校验,可用set_ca_cert_path()、enable_server_certificate_verification()、enable_server_hostname_verification()(默认开启,传false可跳过身份校验),见 httplib.h。SSL 构建需要定义CPPHTTPLIB_OPENSSL_SUPPORT宏。CA 与客户端证书的完整配置参见 w05-websocket-tls.md。

7. 超时配置速览

对长时间存活的 WebSocket 连接,合理配置超时至关重要,相关文档为 w06-websocket-timeouts.md。WebSocketClient与普通Client一样有三种超时:

类型API默认值
连接set_connection_timeout300s
读set_read_timeout无(一直等待,由CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND决定)
写set_write_timeout5s

连接与写超时要在connect()之前设置;读超时可随时修改,对已打开连接设置的读超时在下次read()时生效。std::chrono重载同样可用:

using namespace std::chrono_literals; ws.set_connection_timeout(5s); ws.set_read_timeout(30s); ws.set_write_timeout(10s);

另外提醒:WebSocketClient没有Client的set_max_timeout()(它用来限定整个请求的总时长)——WebSocket 一旦连上,只要你还持续调用read(),连接就会一直保持。

8. 汇总:从 echo 到生产可用的关键清单

关注点结论与出处
处理器入口svr.WebSocket(pattern, handler),握手已完成,httplib.h
消息读写read(msg)阻塞读、send()两种重载发文本/二进制
循环写法无自定义读超时用while (ws.read(msg));设置了超时改用while (ws.is_open())+ 显式判断Timeout
连接失败诊断connect()返回Result,error()/status()/headers()分层定位,见 README-websocket.md
并发规模每条连接占一个线程,用svr.new_task_queue配置动态线程池,s21-thread-pool.md
保活与失效检测set_websocket_ping_interval()+set_websocket_max_missed_pongs(),详见 w02-websocket-ping.md
二进制消息帧类型判别与混合使用,详见 w04-websocket-binary.md
TLS服务端换SSLServer、客户端用wss://,详见 w05-websocket-tls.md
可运行示例example/wsecho.cc(含浏览器交互页),cd example && make wsecho && ./wsecho

从最小的 echo 服务器到带认证、二进制消息、心跳与 TLS 的完整方案,cpp-httplib 的 WebSocket API 保持了与 HTTP 处理一致的低门槛——理解ReadResult的语义和线程每连接模型这两个核心点,你就能在它的设计边界内构建可靠的实时双向通信应用。

  • 后端
  • 网络

【免费下载链接】cpp-httplib

A C++ header-only HTTP/HTTPS server and client library

项目地址:https://gitcode.com/GitHub_Trending/cp/cpp-httplib
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表