news 2026/9/28 6:13:27

libhv WebSocketServer 开发指南:从 HttpServer 继承到回调、心跳与 WSS 全栈实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
libhv WebSocketServer 开发指南:从 HttpServer 继承到回调、心跳与 WSS 全栈实战
  • 网络
  • 后端
  • 通信

【免费下载链接】libhv

🔥 比libevent/libuv/asio更易用的网络库。A c/c++ network library for developing TCP/UDP/SSL/HTTP/WebSocket/MQTT/Redis client/server.

项目地址:https://gitcode.com/gh_mirrors/li/libhv
点击查看免费下载

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; } };

各成员的职责如下:

成员类型触发时机典型用途
onopenstd::function<void(const WebSocketChannelPtr&, const HttpRequestPtr&)>WebSocket 握手成功后获取客户端请求信息(如req->Path()),初始化连接上下文、启动定时任务
onmessagestd::function<void(const WebSocketChannelPtr&, const std::string&)>收到 TEXT / BINARY 数据帧时处理业务消息,向通道发送响应
onclosestd::function<void(const WebSocketChannelPtr&)>连接关闭(含对端断开、主动 close、心跳超时)时清理定时器、释放上下文资源
ping_intervalint(毫秒)服务端周期性心跳默认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(); } }); }

从中可以提炼出几个关键结论:

  1. 切换到 WebSocket 协议后,会取消 HTTP keepalive 定时器(setKeepaliveTimeout(0)),存活判断完全交给心跳机制;
  2. 实际心跳周期是MAX(ping_interval, 1000)毫秒,即配置值小于 1000ms 也会被提升到 1 秒,避免过频发包;
  3. 每个心跳周期执行一次回调:若「上一次收到 pong 的时间」早于「上一次发送 ping 的时间」,说明对端未回应 pong,判定为死链并ws_channel->close(),同时打印websocket no pong!警告日志;否则主动sendPing()并记录发送时间;
  4. 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; } };

这段代码同时演示了几个重要实践:

  1. 连接级状态隔离:每个连接调用一次newContextPtr<MyContext>(),得到独立实例,避免多连接数据互相污染;
  2. 定时推送:setInterval(1000, ...)每秒触发一次,发送前检查channel->isConnected()与channel->isWriteComplete(),避免在写缓冲未完成时乱序发帧;
  3. 资源回收: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(&param, 0, sizeof(param)); param.crt_file = "cert/server.crt"; param.key_file = "cert/server.key"; param.endpoint = HSSL_SERVER; if (server.newSslCtx(&param) != 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 为什么这么薄」有更深的认知,这里补充握手切换的底层路径。从源码结构看,整个流程是这样的:

  1. HTTP 层收到带Connection: Upgrade与Upgrade: websocket头的请求;
  2. http/server/HttpHandler.cpp 中的SwitchWebSocket()将protocol置为WEBSOCKET,创建WebSocketParser与WebSocketChannel(WS_SERVER类型);
  3. onMessage回调内对收到的帧按 opcode 分发:CLOSE→ 回关闭帧并close();PING→ 自动回PONG;PONG→ 记录last_recv_pong_time;TEXT/BINARY→ 调用用户注册的ws_service->onmessage;
  4. 握手成功后触发用户注册的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.

项目地址:https://gitcode.com/gh_mirrors/li/libhv
点击查看免费下载
上一篇:探索高效蓝牙低功耗连接:FlutterBluePlus 开源库详解
下一篇:Video2X 完整指南:用 4 类 AI 模型把视频放大到 4K、插帧到 60fps

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 6:13:14

上海搜索引擎关键词优化怎么选:3种方案报价拆解,避开这5个隐形坑

上海搜索引擎关键词优化怎么选:3种方案报价拆解,避开这5个隐形坑 网站上线三个月,后台流量曲线平得像心电图停止,你看着那些精心设计的页面、昂贵的服务器账单,心里只有一个念头: 网站做好了没人访问 。这时候,老板或客户问你:“SEO到底该怎么做?上海搜索引擎关键词优化服务 怎么选…

作者头像 李华
网站建设 2026/9/28 6:13:05

新手入门:3招提升网站粘性,告别模板丑站

新手入门:3招提升网站粘性,告别模板丑站 别再用那些千篇一律的模板网站了,真难看且根本留不住人。很多 新手入门 朋友一上来就买现成模板,结果上线后流量惨淡,用户进来三秒就关页,这比没有网站还糟糕。网站粘性不是玄学,它是你技术选型、交互设计和安全架构共同作用的结果。 威胁场景:为什么你的访客留不下来…

作者头像 李华
网站建设 2026/9/28 6:12:51

3年踩坑经验:厦门同安网站制作企业一文搞懂

3年踩坑经验:厦门同安网站制作企业一文搞懂 改个需求建站公司拖一周,这种憋屈事儿谁没经历过?很多老板找厦门同安网站制作企业,最后发现不仅钱花了,网站还慢得像蜗牛,SEO排名更是没影子。别急,今天咱不聊虚的,直接上干货。我混迹这行十年,见过太多因为不懂技术选型和服务器配置,导致项目烂尾的案例。这篇文章…

作者头像 李华
网站建设 2026/9/28 6:12:50

少走弯路:2026年AI论文写作软件接入TaoToken的config.toml配置盘点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 6:12:42

WordPress取消赞完整流程:不懂代码也能搞定

WordPress取消赞完整流程:不懂代码也能搞定 自己不会代码想做网站,最怕的就是卡在细节上。比如想给博客加个点赞功能,结果发现默认没有,或者装插件后想取消赞却找不到入口。别急,这事儿没那么复杂。今天就把WordPress取消赞的完整流程拆碎了讲,从底层逻辑到实操命令,全是干货。哪怕你是技术小白,…

作者头像 李华