- 网络
- 后端
- 通信
【免费下载链接】libhv
🔥 比libevent/libuv/asio更易用的网络库。A c/c++ network library for developing TCP/UDP/SSL/HTTP/WebSocket/MQTT/Redis client/server.
WebSocketServer 是 libhv 提供的高层 WebSocket 服务端封装,直接继承自HttpServer,因此天然复用 HTTP 服务的监听、多进程/多线程模型与 SSL 能力。本文以 docs/cn/WebSocketServer.md 为骨架,结合仓库源码与官方示例 examples/websocket_server_test.cpp,完整讲解 WebSocketService 回调体系、ping/pong 心跳机制、上下文(Context)生命周期管理、WSS(WebSocket over TLS)配置与浏览器端联调,让读者能独立搭建一个可用的 WebSocket 服务端。
一、WebSocketServer 类结构与设计思路
从 http/server/WebSocketServer.h 的源码可以看到,libhv 的 WebSocket 服务端封装得非常轻量,核心就是「继承 + 注册」两件事:
#define websocket_server_t http_server_t #define websocket_server_run http_server_run #define websocket_server_stop http_server_stop namespace hv { class WebSocketServer : public HttpServer { public: WebSocketServer(WebSocketService* service = NULL) : HttpServer() { this->ws = service; } ~WebSocketServer() { stop(); } void registerWebSocketService(WebSocketService* service) { this->ws = service; } }; }要点如下:
WebSocketServer继承自HttpServer,而HttpServer又继承自 C 结构体http_server_t(定义见 http/server/HttpServer.h),所以它天然具备 HTTP 服务的全部字段与能力:host、port、https_port、http_version、worker_processes、worker_threads、worker_connections等;- C API 层面通过宏直接复用
http_server_run/http_server_stop,即「WebSocket 服务端」本质上就是 HTTP 服务端在收到带Upgrade: websocket的握手请求后切换到 WebSocket 协议模式(底层实现在 http/server/HttpHandler.cpp 的HttpHandler::SwitchWebSocket()中); - 构造函数与
registerWebSocketService()都把业务服务指针挂到this->ws字段上,注册后即生效; - 析构函数自动调用
stop(),因此栈上创建的服务端对象退出作用域时会自动停止所有事件循环并回收资源。
一个端口同时提供 HTTP 与 WebSocket 服务
由于 WebSocketServer 与 HttpServer 共用同一套监听与握手流程,你可以在同一个服务端实例上同时注册 HTTP 路由和 WebSocket 业务。官方示例 examples/websocket_server_test.cpp 就演示了这一点:
HttpService http; http.GET("/ping", [](const HttpContextPtr& ctx) { return ctx->send("pong"); }); WebSocketService ws; // ... 设置 ws 的 onopen / onmessage / onclose ... WebSocketServer server; server.port = port; server.registerHttpService(&http); server.registerWebSocketService(&ws); server.start();浏览器访问http://ip:port/ping会得到 "pong",访问ws://ip:port/则完成 WebSocket 握手并进入onopen回调——HTTP 与 WebSocket 业务可以共存于同一端口,互不干扰。
二、WebSocketService 业务类:三个回调 + 心跳间隔
官方文档给出的核心结构是WebSocketService,源码实现在 http/server/WebSocketServer.h:
struct WebSocketService { std::function<void(const WebSocketChannelPtr&, const HttpRequestPtr&)> onopen; std::function<void(const WebSocketChannelPtr&, const std::string&)> onmessage; std::function<void(const WebSocketChannelPtr&)> onclose; int ping_interval; WebSocketService() : ping_interval(0) {} void setPingInterval(int ms) { ping_interval = ms; } };各成员的职责如下:
| 成员 | 类型 | 触发时机 | 典型用途 |
|---|---|---|---|
onopen | std::function<void(const WebSocketChannelPtr&, const HttpRequestPtr&)> | WebSocket 握手成功后 | 获取客户端请求信息(如req->Path()),初始化连接上下文、启动定时任务 |
onmessage | std::function<void(const WebSocketChannelPtr&, const std::string&)> | 收到 TEXT / BINARY 数据帧时 | 处理业务消息,向通道发送响应 |
onclose | std::function<void(const WebSocketChannelPtr&)> | 连接关闭(含对端断开、主动 close、心跳超时)时 | 清理定时器、释放上下文资源 |
ping_interval | int(毫秒) | 服务端周期性心跳 | 默认0(关闭);用setPingInterval(int ms)开启,实际生效值会被下限钳制为不小于 1000ms |
onopen的第二个参数是握手请求HttpRequestPtr,可用于识别客户端来源路径、解析查询参数、获取请求头,这让同一个服务端可以按路径区分不同的 WebSocket 端点。
ping_interval 的底层实现:基于 pong 超时判活的主动心跳
ping_interval并非简单的「定时发 ping」,libhv 在 http/server/HttpHandler.cpp 的SwitchWebSocket()中实现了一套完整的「发 ping 后校验 pong」的保活逻辑:
// NOTE: cancel keepalive timer, judge alive by heartbeat. ws_channel->setKeepaliveTimeout(0); if (ws_service && ws_service->ping_interval > 0) { int ping_interval = MAX(ws_service->ping_interval, 1000); ws_channel->setHeartbeat(ping_interval, [this](){ if (last_recv_pong_time < last_send_ping_time) { hlogw("[%s:%d] websocket no pong!", ip, port); ws_channel->close(); } else { ws_channel->sendPing(); last_send_ping_time = gethrtime_us(); } }); }从中可以提炼出几个关键结论:
- 切换到 WebSocket 协议后,会取消 HTTP keepalive 定时器(
setKeepaliveTimeout(0)),存活判断完全交给心跳机制; - 实际心跳周期是
MAX(ping_interval, 1000)毫秒,即配置值小于 1000ms 也会被提升到 1 秒,避免过频发包; - 每个心跳周期执行一次回调:若「上一次收到 pong 的时间」早于「上一次发送 ping 的时间」,说明对端未回应 pong,判定为死链并
ws_channel->close(),同时打印websocket no pong!警告日志;否则主动sendPing()并记录发送时间; sendPing/sendPong的具体帧构造见 http/WebSocketChannel.cpp,服务端发送的是最小 2 字节的未掩码控制帧(WS_SERVER_PING_FRAME,定义于 http/wsdef.h)。
因此,开启ping_interval后,服务端既能保持 NAT 中间设备上的长连接不被回收,又能在对端异常掉线时及时感知并回收连接资源。
三、WebSocketChannel:消息发送与连接生命周期
WebSocketChannel是服务端与客户端之间的一条逻辑通道,类型定义为std::shared_ptr<hv::WebSocketChannel>(即WebSocketChannelPtr)。其声明见 http/WebSocketChannel.h,核心能力包括:
class HV_EXPORT WebSocketChannel : public SocketChannel { public: ws_session_type type; // WS_CLIENT / WS_SERVER enum ws_opcode opcode; // 最近一帧消息的 opcode int send(const std::string& msg, enum ws_opcode opcode = WS_OPCODE_TEXT, bool fin = true); int send(const char* buf, int len, enum ws_opcode opcode = WS_OPCODE_BINARY, bool fin = true); // websocket fragment(大数据分片发送) int send(const char* buf, int len, int fragment, enum ws_opcode opcode = WS_OPCODE_BINARY); int sendPing(); int sendPong(); int close(); };send的重载设计非常实用:
- 传
std::string时默认按WS_OPCODE_TEXT(文本帧)发送; - 传
char* + len时默认按WS_OPCODE_BINARY(二进制帧)发送; - 当数据长度超过 65535 字节时,http/WebSocketChannel.cpp 会自动将其拆分为多个分片帧:首帧携带业务 opcode(
fin=false),中间帧与末帧使用WS_OPCODE_CONTINUE,从而规避单帧长度限制; - 发送全程通过
mutex_加锁,保证多线程(如定时器线程回调内send)并发安全; channel->opcode记录最近一次收到的消息类型,业务回调里可用它区分文本/二进制消息(如示例中channel->opcode == WS_OPCODE_TEXT)。
opcode 枚举定义在 http/wsdef.h:
enum ws_opcode { WS_OPCODE_CONTINUE = 0x0, // 分片续帧 WS_OPCODE_TEXT = 0x1, // 文本帧 WS_OPCODE_BINARY = 0x2, // 二进制帧 WS_OPCODE_CLOSE = 0x8, // 关闭帧 WS_OPCODE_PING = 0x9, // ping 帧 WS_OPCODE_PONG = 0xA, // pong 帧 };四、连接上下文(Context):在回调之间携带状态
WebSocket 是长连接,业务上常常需要在onopen时创建与连接绑定的状态(如用户信息、会话数据、定时器句柄),在onmessage/onclose中访问并清理。官方示例演示了通过channel->newContextPtr<T>()/getContextPtr<T>()实现的上下文机制:
ws.onopen = [](const WebSocketChannelPtr& channel, const HttpRequestPtr& req) { printf("onopen: GET %s\n", req->Path().c_str()); auto ctx = channel->newContextPtr<MyContext>(); // 创建连接私有上下文 ctx->timerID = setInterval(1000, channel { // 每秒推送一次服务器时间 if (channel->isConnected() && channel->isWriteComplete()) { char str[DATETIME_FMT_BUFLEN] = {0}; datetime_t dt = datetime_now(); datetime_fmt(&dt, str); channel->send(str); } }); }; ws.onmessage = [](const WebSocketChannelPtr& channel, const std::string& msg) { auto ctx = channel->getContextPtr<MyContext>(); // 取回本连接上下文 ctx->handleMessage(msg, channel->opcode); }; ws.onclose = [](const WebSocketChannelPtr& channel) { printf("onclose\n"); auto ctx = channel->getContextPtr<MyContext>(); if (ctx->timerID != INVALID_TIMER_ID) { killTimer(ctx->timerID); // 连接关闭时取消定时器 ctx->timerID = INVALID_TIMER_ID; } };这段代码同时演示了几个重要实践:
- 连接级状态隔离:每个连接调用一次
newContextPtr<MyContext>(),得到独立实例,避免多连接数据互相污染; - 定时推送:
setInterval(1000, ...)每秒触发一次,发送前检查channel->isConnected()与channel->isWriteComplete(),避免在写缓冲未完成时乱序发帧; - 资源回收:
onclose中killTimer取消定时器,防止连接销毁后定时器仍持有channel引用继续回调(示例中注释掉的deleteContextPtr()提示上下文生命周期默认由通道管理,通常无需手动删除)。
五、完整可运行的服务端示例
官方测试代码 examples/websocket_server_test.cpp 本身就是一份完整的最小服务端,其构建与运行方式如下:
# 1. 构建 make examples # 2. 启动服务端,监听 9999 端口 bin/websocket_server_test 9999 # 3. 使用官方客户端测试 bin/websocket_client_test ws://127.0.0.1:9999/服务端程序逻辑汇总(结合源码):
#include "WebSocketServer.h" #include "EventLoop.h" #include "htime.h" using namespace hv; int main(int argc, char** argv) { if (argc < 2) { printf("Usage: %s port\n", argv[0]); return -10; } int port = atoi(argv[1]); HttpService http; http.GET("/ping", [](const HttpContextPtr& ctx) { return ctx->send("pong"); }); WebSocketService ws; ws.setPingInterval(10000); // 可取消注释开启 10s 心跳 ws.onopen = ...; // 见上文 ws.onmessage = ...; ws.onclose = ...; WebSocketServer server; server.port = port; server.registerHttpService(&http); server.registerWebSocketService(&ws); server.start(); // 非阻塞启动,主线程继续执行 while (getchar() != '\n'); // 回车退出 return 0; }server.start()是非阻塞启动(内部调用run(ip_port, false)),主线程可以继续做其他事情;server.run()则是阻塞式(占用当前线程),两者定义见 http/server/HttpServer.h。示例程序通过「等待回车」保持进程存活。
六、WSS(WebSocket over TLS)配置
WebSocketServer 继承了 HttpServer 的 SSL 能力,开启方式与 HTTPS 完全一致。示例中通过宏TEST_WSS给出了完整配置:
#define TEST_WSS 1 server.port = port; // 例如 9999,普通 ws:// server.https_port = port + 1; // 例如 10000,wss:// hssl_ctx_opt_t param; memset(¶m, 0, sizeof(param)); param.crt_file = "cert/server.crt"; param.key_file = "cert/server.key"; param.endpoint = HSSL_SERVER; if (server.newSslCtx(¶m) != 0) { fprintf(stderr, "new SSL_CTX failed!\n"); return -20; }要点说明:
- 证书与私钥文件可使用仓库 cert 目录下的自签名
server.crt/server.key,或按 cert/gen.sh 自行生成; newSslCtx(hssl_ctx_opt_t*)内部调用hssl_ctx_new(opt)创建 SSL 上下文并挂到ssl_ctx字段,并在http_server_stop时自动释放(源码注释明确:NOTE: hssl_ctx_free in http_server_stop);- 需要以
./configure --with-openssl方式重新构建才能启用 SSL 支持(示例文件头注释说明了这一点); - 启用后同一进程同时监听两个端口:
ws://ip:9999/与wss://ip:10000/,客户端用bin/websocket_client_test wss://127.0.0.1:10000/即可验证。
七、浏览器端联调
仓库在 html/WebSocket.html 提供了可直接打开的浏览器 WebSocket 客户端页面。其核心逻辑是:
var ws = new WebSocket(url); ws.onopen = function() { alert("连接已建立"); ws.send("hello"); }; ws.onmessage = function(ev) { var received_msg = ev.data; console.log("received websocket message: " + received_msg); // 追加到页面 msg_list 列表 };将页面的 url 指向ws://127.0.0.1:9999/并打开,即可看到:页面onopen发送 "hello" 后,服务端每秒回推一次格式化时间字符串,页面onmessage将其逐条渲染到列表——这条端到端链路正好完整覆盖了前文讲解的onopen → setInterval 定时 send → onmessage → onclose全部回调路径。
八、从代码结构看服务端的协议切换机制
为了让读者对「WebSocketServer 为什么这么薄」有更深的认知,这里补充握手切换的底层路径。从源码结构看,整个流程是这样的:
- HTTP 层收到带
Connection: Upgrade与Upgrade: websocket头的请求; - http/server/HttpHandler.cpp 中的
SwitchWebSocket()将protocol置为WEBSOCKET,创建WebSocketParser与WebSocketChannel(WS_SERVER类型); onMessage回调内对收到的帧按 opcode 分发:CLOSE→ 回关闭帧并close();PING→ 自动回PONG;PONG→ 记录last_recv_pong_time;TEXT/BINARY→ 调用用户注册的ws_service->onmessage;- 握手成功后触发用户注册的
onopen,连接结束触发onclose。
这也解释了文档与示例中的几个行为:服务端收到 PING 会自动回 PONG(浏览器无需手动处理);TEXT/BINARY 之外的协议帧不会进入onmessage;心跳超时断链会打印websocket no pong!日志。
九、小结
综上,在 libhv 中搭建 WebSocket 服务端只需三步:定义一个WebSocketService并设置onopen/onmessage/onclose回调与心跳间隔、创建WebSocketServer并注册该服务、调用start()/run()。若需 HTTPS 级别的安全,追加newSslCtx()配置即可在同一进程提供ws://与wss://双端口服务。完整的可运行范例始终可以参考 examples/websocket_server_test.cpp,如需从零起步,可先按 getting_started.sh 完成编译环境的准备。
- 网络
- 后端
- 通信
【免费下载链接】libhv
🔥 比libevent/libuv/asio更易用的网络库。A c/c++ network library for developing TCP/UDP/SSL/HTTP/WebSocket/MQTT/Redis client/server.
相关推荐
TypeGraphQL 继承机制全指南:类型继承与 Resolver 继承实战解析
TypeGraphQL 继承机制全指南:类型继承与 Resolver 继承实战解析 TypeGraphQL 的核心设计理念是基于 TypeScript 类来构建
后端GraphQLAPI设计YimMenu Lua 脚本开发:button 按钮类完全指南(继承关系、回调机制与实战用法)
YimMenu Lua 脚本开发:button 按钮类完全指南(继承关系、回调机制与实战用法) 本文围绕 YimMenu 的 Lua API 文档中 butto
逆向工程游戏开发TypeGraphQL 类继承与 Resolver 继承实战指南
TypeGraphQL 类继承与 Resolver 继承实战指南 TypeGraphQL 的核心设计理念是"基于 TypeScript 类来创建 GraphQL
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考