
📚 系列:C++ 工程实践 第 07 篇 ⏱ 阅读约 10 分钟
📌 一句话总结
本文用「同端口双协议 → 握手 → 六步骨架 → 浏览器验证」四步讲透 WebSocket,读完后你能:① 说清 101 握手是怎么来的;② 用 websocketpp 跑通一个 8888 端口的 echo 服务;③ 不写 C++ 客户端,用浏览器 30 行 JS 验证双向通信。
文章目录
-
- [1. WebSocket 和 HTTP 到底差在哪:同一个 8888 端口,两种协议](#1. WebSocket 和 HTTP 到底差在哪:同一个 8888 端口,两种协议)
- [2. 握手:服务端怎么知道你要「升级」](#2. 握手:服务端怎么知道你要「升级」)
- [3. 环境与 makefile:header-only 也要装 Boost](#3. 环境与 makefile:header-only 也要装 Boost)
- [4. server.cc 逐段:两行 typedef 与六步 main](#4. server.cc 逐段:两行 typedef 与六步 main)
- [5. 回调生命周期:hdl 是 weak_ptr,别拿它当连接 ID](#5. 回调生命周期:hdl 是 weak_ptr,别拿它当连接 ID)
- [6. 消息与帧:get_payload() 拿到什么,send() 怎么回](#6. 消息与帧:get_payload() 拿到什么,send() 怎么回)
- [7. 跑起来:浏览器验证 echo,一张表排掉 8 个坑](#7. 跑起来:浏览器验证 echo,一张表排掉 8 个坑)
- 附录:完整代码整合
1. WebSocket 和 HTTP 到底差在哪:同一个 8888 端口,两种协议
上一篇我们用 cpp-httplib 做了个一问一答的 HTTP 服务:浏览器请求一次,服务端答一次,连接就断了------它没法「服务端主动推给你」。WebSocket 补的就是这段:一次 HTTP 请求把连接「升级」成 ws://,之后双方可以随时互发消息;而且同一条 TCP 连接在升级前是 HTTP、升级后是 WebSocket。
| 对比项 | HTTP | 长轮询 | WebSocket |
|---|---|---|---|
| 连接状态 | 请求-响应后关闭 | 反复建连 | 一条长连接 |
| 谁能先发 | 客户端 | 客户端(假推送) | 双方都可以 |
| 服务端主动推 | 不支持 | 靠挂起请求绕 | 原生支持 |
| 单条消息开销 | 完整头部 | 完整头部 | 2~14 字节帧头 |
本文的 server.cc 最能说明问题:同一个 8888 端口,既回 HTTP 欢迎页,也接 ws:// 长连接 。set_http_handler 处理的是「没走升级的普通 HTTP 请求」,set_open_handler 处理的是「升级成功后的 WebSocket 连接」------两个入口,一个端口。
ℹ️ 为什么一定要有 HTTP 入口
WebSocket 的握手本身就是一次 HTTP 请求。服务端要能解析这个请求、区分「普通 GET」和「Upgrade 请求」,所以 websocketpp 默认带 HTTP 处理能力;不注册 set_http_handler 也能跑,但浏览器直接访问 8888 时会看到空白响应,排查起来很别扭。
2. 握手:服务端怎么知道你要「升级」
客户端发的是一次带 Connection: Upgrade / Upgrade: websocket / Sec-WebSocket-Key 的 HTTP GET,服务端回 101 Switching Protocols + 由 key 算出的 Sec-WebSocket-Accept。 这之后同一条连接就只说 WebSocket 帧,不再说 HTTP。
websocketpp 浏览器 websocketpp 浏览器 #mermaid-svg-D7BfE0bMbvqZCdhx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-D7BfE0bMbvqZCdhx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-D7BfE0bMbvqZCdhx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-D7BfE0bMbvqZCdhx .error-icon{fill:#552222;}#mermaid-svg-D7BfE0bMbvqZCdhx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-D7BfE0bMbvqZCdhx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-D7BfE0bMbvqZCdhx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-D7BfE0bMbvqZCdhx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-D7BfE0bMbvqZCdhx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-D7BfE0bMbvqZCdhx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-D7BfE0bMbvqZCdhx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-D7BfE0bMbvqZCdhx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-D7BfE0bMbvqZCdhx .marker.cross{stroke:#333333;}#mermaid-svg-D7BfE0bMbvqZCdhx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-D7BfE0bMbvqZCdhx p{margin:0;}#mermaid-svg-D7BfE0bMbvqZCdhx .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-D7BfE0bMbvqZCdhx text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-D7BfE0bMbvqZCdhx .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-D7BfE0bMbvqZCdhx .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-D7BfE0bMbvqZCdhx .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-D7BfE0bMbvqZCdhx .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-D7BfE0bMbvqZCdhx #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-D7BfE0bMbvqZCdhx .sequenceNumber{fill:white;}#mermaid-svg-D7BfE0bMbvqZCdhx #sequencenumber{fill:#333;}#mermaid-svg-D7BfE0bMbvqZCdhx #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-D7BfE0bMbvqZCdhx .messageText{fill:#333;stroke:none;}#mermaid-svg-D7BfE0bMbvqZCdhx .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-D7BfE0bMbvqZCdhx .labelText,#mermaid-svg-D7BfE0bMbvqZCdhx .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-D7BfE0bMbvqZCdhx .loopText,#mermaid-svg-D7BfE0bMbvqZCdhx .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-D7BfE0bMbvqZCdhx .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-D7BfE0bMbvqZCdhx .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-D7BfE0bMbvqZCdhx .noteText,#mermaid-svg-D7BfE0bMbvqZCdhx .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-D7BfE0bMbvqZCdhx .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-D7BfE0bMbvqZCdhx .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-D7BfE0bMbvqZCdhx .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-D7BfE0bMbvqZCdhx .actorPopupMenu{position:absolute;}#mermaid-svg-D7BfE0bMbvqZCdhx .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-D7BfE0bMbvqZCdhx .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-D7BfE0bMbvqZCdhx .actor-man circle,#mermaid-svg-D7BfE0bMbvqZCdhx line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-D7BfE0bMbvqZCdhx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET Upgrade 请求 101 Switching Protocols text 帧 hello text 帧 hello
关键请求头长这样:
http
GET / HTTP/1.1
Host: 127.0.0.1:8888
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Accept 的算法一行就能说清:把 Sec-WebSocket-Key 拼上固定 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,做 SHA1,再 base64。websocketpp 会自动算,读者不需要手写,但知道它怎么来,看到 101 就不会心虚。
⚠️ curl 测不了 WebSocket
curl http://127.0.0.1:8888 只能拿到欢迎页(HTTP 200),它不会完成升级。要看握手和帧,用浏览器 DevTools 的 Network → WS 面板,或者第 7 节的 JS 测试页。
3. 环境与 makefile:header-only 也要装 Boost
websocketpp 是纯头文件库,但底层用 Boost.Asio,真正要装的是 Boost;makefile 只要 -std=c++17、-lpthread -lboost_system。
| 环境 | 版本 |
|---|---|
| 系统 | Ubuntu 22.04 |
| 编译器 | g++ 11(C++17) |
| websocketpp | 0.8.2(header-only) |
| Boost | 1.74(libboost-system-dev) |
安装与构建:
bash
sudo apt install libwebsocketpp-dev libboost-system-dev
# 或者把 websocketpp clone 到 third_party/ 下
参考代码的 makefile:
makefile
server : server.cc
g++ -std=c++17 $^ -o $@ -lpthread -lboost_system
clean:
rm -rf server
两个细节:#include <websocketpp/...> 用的是尖括号,装到系统路径不用加 -I,clone 到工程目录就必须加 -Ithird_party/websocketpp;rm -rf 在 Windows/MinGW 下不可用,换 WSL 或改成 del server.exe。
4. server.cc 逐段:两行 typedef 与六步 main
server.cc 只有两块------前半段是让回调能被注册的准备,后半段 main 是六步:实例化 → init_asio → 注册回调 → listen(8888) → start_accept → run。
先看准备部分:
cpp
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
typedef websocketpp::server<websocketpp::config::asio> websocketsvr;
typedef websocketsvr::message_ptr message_ptr;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;
注意一个反直觉的点:头文件叫 asio_no_tls,类型名却写 websocketpp::config::asio 。写成 config::asio_no_tls 编译不过;要上 TLS,才换成 config/asio.hpp + config::asio_tls 并链接 -lssl -lcrypto,本文不展开。
_1、_2 是 bind 的参数钩子,必须走 websocketpp::lib:: 这一层别名,它让代码在 Boost.Asio 和独立 Asio 两种构建下都能编;混用 std::placeholders 会换来一屏模板报错。
再看 main 的六步:
cpp
int main() {
websocketsvr server; // ① 实例化
server.set_access_channels(websocketpp::log::alevel::none); // ② 关日志
server.init_asio(); // ③ 初始化 asio
server.set_http_handler(bind(&OnHttp, &server, ::_1));
server.set_open_handler(bind(&OnOpen, &server, ::_1));
server.set_close_handler(bind(&OnClose, &server, ::_1));
server.set_message_handler(bind(&OnMessage, &server, ::_1, ::_2));
server.listen(8888); // ④ 占端口
server.start_accept(); // ⑤ 开始接收连接
server.run(); // ⑥ 事件循环,阻塞
return 0;
}
每一步删掉会发生什么:
| 操作 | 漏写的后果 |
|---|---|
init_asio() |
后续注册/监听没有事件循环,直接异常或无效 |
listen(8888) |
没有监听端口,start_accept 无处可挂 |
start_accept() |
端口在监听,但谁都连不上(全篇最高频的坑) |
| 不注册 handler | 连接能建立,但收不到消息、看不到关闭 |
run() |
main 直接结束,服务瞬间退出 |
🚫 最容易漏的一行
start_accept() 漏写时,程序不报错、ss -lntp 也能看到 8888 在监听,但浏览器和客户端全部连接失败。排查时先确认这一行在不在。
5. 回调生命周期:hdl 是 weak_ptr,别拿它当连接 ID
一条连接的生命是 OnOpen → OnMessage × N → OnClose 或 OnFail;五个回调里的 connection_hdl 只是个弱引用,要拿连接对象必须 get_con_from_hdl(hdl)。
#mermaid-svg-oQ4kMKAc8NDsiNNu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oQ4kMKAc8NDsiNNu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oQ4kMKAc8NDsiNNu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oQ4kMKAc8NDsiNNu .error-icon{fill:#552222;}#mermaid-svg-oQ4kMKAc8NDsiNNu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oQ4kMKAc8NDsiNNu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oQ4kMKAc8NDsiNNu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oQ4kMKAc8NDsiNNu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oQ4kMKAc8NDsiNNu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oQ4kMKAc8NDsiNNu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oQ4kMKAc8NDsiNNu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oQ4kMKAc8NDsiNNu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oQ4kMKAc8NDsiNNu .marker.cross{stroke:#333333;}#mermaid-svg-oQ4kMKAc8NDsiNNu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oQ4kMKAc8NDsiNNu p{margin:0;}#mermaid-svg-oQ4kMKAc8NDsiNNu defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-oQ4kMKAc8NDsiNNu g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-oQ4kMKAc8NDsiNNu g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-oQ4kMKAc8NDsiNNu g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-oQ4kMKAc8NDsiNNu g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-oQ4kMKAc8NDsiNNu g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-oQ4kMKAc8NDsiNNu .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-oQ4kMKAc8NDsiNNu .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-oQ4kMKAc8NDsiNNu .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-oQ4kMKAc8NDsiNNu .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-oQ4kMKAc8NDsiNNu .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-oQ4kMKAc8NDsiNNu .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-oQ4kMKAc8NDsiNNu .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-oQ4kMKAc8NDsiNNu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oQ4kMKAc8NDsiNNu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oQ4kMKAc8NDsiNNu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oQ4kMKAc8NDsiNNu .edgeLabel .label text{fill:#333;}#mermaid-svg-oQ4kMKAc8NDsiNNu .label div .edgeLabel{color:#333;}#mermaid-svg-oQ4kMKAc8NDsiNNu .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-oQ4kMKAc8NDsiNNu .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-oQ4kMKAc8NDsiNNu .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-oQ4kMKAc8NDsiNNu .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-oQ4kMKAc8NDsiNNu .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-oQ4kMKAc8NDsiNNu .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oQ4kMKAc8NDsiNNu .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oQ4kMKAc8NDsiNNu #statediagram-barbEnd{fill:#333333;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oQ4kMKAc8NDsiNNu .cluster-label,#mermaid-svg-oQ4kMKAc8NDsiNNu .nodeLabel{color:#131300;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-oQ4kMKAc8NDsiNNu .note-edge{stroke-dasharray:5;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-note text{fill:black;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram-note .nodeLabel{color:black;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagram .edgeLabel{color:red;}#mermaid-svg-oQ4kMKAc8NDsiNNu #dependencyStart,#mermaid-svg-oQ4kMKAc8NDsiNNu #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-oQ4kMKAc8NDsiNNu .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oQ4kMKAc8NDsiNNu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 握手成功
收到消息
继续收发
正常关闭
异常断开
Open
Message
Close
Fail
| 回调 | 触发时机 | 常见坑 |
|---|---|---|
OnOpen |
握手成功、连接建立 | 可以在这里主动 send 欢迎消息 |
OnMessage |
收到文本/二进制帧 | 核心逻辑,见第 6 节 |
OnClose |
客户端正常关闭 | 只打「连接关闭」排不了错,要取关闭码 |
OnFail |
传输层异常 | 参考代码定义了但没注册,是死代码 |
OnHttp |
普通 HTTP 请求 | 缺 Content-Type 时浏览器可能乱码 |
两个可以直接抄的修法:
cpp
// ① 把 OnFail 真正注册上,否则「连接异常」永远不会打印
server.set_fail_handler(bind(&OnFail, &server, ::_1));
// ② 关闭时打印远端关闭码与原因,排查才有信息量
void OnClose(websocketsvr *server, websocketpp::connection_hdl hdl) {
auto con = server->get_con_from_hdl(hdl);
std::cout << "连接关闭 code=" << con->get_remote_close_code()
<< " reason=" << con->get_remote_close_reason() << std::endl;
}
hdl 是 weak_ptr,不能直接当 map 的 key 去存业务状态。要按连接存数据,用 std::map<websocketpp::connection_hdl, State, std::owner_less<websocketpp::connection_hdl>>,或者先 get_con_from_hdl(hdl) 拿到 connection_ptr(具体容器写法随版本有差异,这一条标待实测)。
6. 消息与帧:get_payload() 拿到什么,send() 怎么回
OnMessage 里 msg->get_payload() 就是一段 std::string,第二个参数 websocketpp::frame::opcode::text 决定你回的是文本帧;echo 就两行------原样 send 回去。
cpp
void OnMessage(websocketsvr *server, websocketpp::connection_hdl hdl,
message_ptr msg) {
std::cout << "收到消息: " << msg->get_payload() << std::endl;
server->send(hdl, msg->get_payload(), websocketpp::frame::opcode::text);
}
参考代码里 cout << "收到消息" << msg->get_payload() 中间没有分隔符,实际输出是「收到消息hello」;上面加了冒号和空格,贴日志时别被格式差异误导。
WebSocket 帧头只有几个字段,理解它们能解释很多怪问题:
| 字段 | 作用 | 规则 |
|---|---|---|
| FIN | 是否消息最后一帧 | 分片消息靠它判断结束 |
| opcode | 帧类型 | 1 文本 / 2 二进制 / 8 关闭 / 9 ping / 10 pong |
| MASK | 是否掩码 | 客户端发必须掩码,服务端发必须不掩码 |
| payload len | 负载长度 | 7 / 16 / 64 位三档 |
✅ 业务里只看 text 和 binary
ping/pong 由 websocketpp 自动应答,close 由连接生命周期处理;业务代码通常只判断 msg->get_opcode() 是 text 还是 binary,其余交给库。
7. 跑起来:浏览器验证 echo,一张表排掉 8 个坑
验证 echo 不需要 C++ 客户端 ------浏览器里 new WebSocket("ws://127.0.0.1:8888") 就行。编译并启动:
bash
make
./server
把测试页塞进 OnHttp 的欢迎页里,打开 http://127.0.0.1:8888 就能测:
html
<!doctype html>
<html><head><meta charset="utf-8"><title>websocketpp echo</title></head>
<body>
<h1>websocketpp echo 测试页</h1>
<p id="log">连接中...</p>
<script>
const ws = new WebSocket("ws://127.0.0.1:8888");
const log = (s) => { document.getElementById("log").textContent = s; console.log(s); };
ws.onopen = () => { log("已连接"); ws.send("hello websocket"); };
ws.onmessage = (e) => log("收到回显: " + e.data);
ws.onclose = (e) => log("连接关闭 code=" + e.code);
ws.onerror = () => log("连接失败:检查 ws:// 地址与端口");
</script>
</body></html>
配套的 C++ 侧只需改两行(顺手补上 Content-Type):
cpp
con->append_header("Content-Type", "text/html; charset=utf-8");
con->set_body(html);
con->set_status(websocketpp::http::status_code::ok);
服务端控制台的预期输出:
text
处理http请求
连接成功
收到消息: hello websocket
连接关闭 code=1000 reason=
没有浏览器或想在服务器上测,可以用 Python 或 wscat:
python
# pip install websockets(10/11/12 的 API 有差异,以实测为准)
import asyncio, websockets
async def main():
async with websockets.connect("ws://127.0.0.1:8888") as ws:
await ws.send("hello websocket")
print(await asyncio.wait_for(ws.recv(), timeout=3))
asyncio.run(main())
bash
npx wscat -c ws://127.0.0.1:8888
# > hello websocket
# < hello websocket
常见问题一张表:
| 坑 | 现象 | 修法 |
|---|---|---|
漏写 start_accept() |
端口在监听,但连不上 | 补上这一步 |
类型写成 config::asio_no_tls |
编译报错 | 类型名是 config::asio |
找不到 boost_system |
链接或头文件报错 | 装 libboost-system-dev;clone 的要加 -I |
用 http:// 连 WS |
浏览器报错 | 地址必须是 ws:// |
localhost 解析到 ::1 |
连接被拒 | 先试 127.0.0.1 |
| 8888 被 Jupyter 占用 | 启动/连接失败 | 查端口占用,换端口并同步改 JS |
关掉 alevel::none 后没日志 |
看不到任何输出 | 调回 alevel::all 排查 |
回调里 send 抛异常 |
进程直接退出 | try/catch 或先判连接状态 |
📝 小结
① 同端口双协议:HTTP 请求走 `set_http_handler`,升级请求走 `set_open_handler`。
② 六步骨架:实例化 → `init_asio` → 注册回调 → `listen` → `start_accept` → `run`,一步都不能少。
③ 回调顺序:Open → Message×N → Close 或 Fail;`hdl` 是弱引用,用 `get_con_from_hdl` 取连接。
📦 项目源码
本文示例所在的 C++ 微服务脚手架(点播系统):https://github.com/syagain-zsx/cpp-microservice-scaffold-vod-system
🔜 下一篇
《Websocket 使用实践:把回声服务改成业务消息通道》------从单文件 demo 到项目里能用的连接管理。关注专栏不迷路。
🔗 参考
1 websocketpp GitHub:https://github.com/zaphoyd/websocketpp
2 RFC 6455(WebSocket 协议):https://datatracker.ietf.org/doc/html/rfc6455
3 Boost.Asio 文档:https://www.boost.org/doc/libs/release/doc/html/boost_asio.html
附录:完整代码整合
前面七节把每段代码拆开讲过,这一段只做一件事:把零散的片段合成一份复制即可编译运行的最小工程 。三个文件放进同一目录,make && ./server 就能跑;环境与第 3 节一致(Ubuntu 22.04 + g++ 11 + websocketpp 0.8.2 + Boost 1.74),前提是依赖已装好。
A.1 目录结构
text
websocketpp-demo/
├── server.cc # 完整服务端:HTTP 欢迎页 + WebSocket echo
├── makefile # 编译 + clean,命令行用真 tab
└── webroot/
└── index.html # 浏览器测试页,与 OnHttp 内嵌的是同一份
server.cc 和 makefile 放在同一层,webroot/ 只装页面;服务端不读它,原因见 A.4。
A.2 server.cc(完整版)
在参考代码上只动了三处:OnClose 打印远端关闭码、OnHttp 补 Content-Type 并内嵌测试页、注册 set_fail_handler;其余 typedef、回调和 main 的六步保持原样。
📄 server.cc · 完整版,可直接 make
cpp
#include <iostream>
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
using namespace std;
typedef websocketpp::server<websocketpp::config::asio> websocketsvr;
typedef websocketsvr::message_ptr message_ptr;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;
// 浏览器测试页:直接内嵌,省掉静态文件服务
static const char *kIndexHtml = R"html(<!doctype html>
<html>
<head><meta charset="utf-8"><title>websocketpp echo</title></head>
<body>
<h1>websocketpp echo 测试页</h1>
<p id="log">连接中...</p>
<script>
const ws = new WebSocket("ws://127.0.0.1:8888");
const log = (s) => { document.getElementById("log").textContent = s; console.log(s); };
ws.onopen = () => { log("已连接"); ws.send("hello websocket"); };
ws.onmessage = (e) => log("收到回显: " + e.data);
ws.onclose = (e) => log("连接关闭 code=" + e.code);
ws.onerror = () => log("连接失败:检查 ws:// 地址与端口");
</script>
</body>
</html>
)html";
// websocket连接成功的回调函数
void OnOpen(websocketsvr *server, websocketpp::connection_hdl hdl) {
cout << "连接成功" << endl;
}
// 连接关闭:打印远端关闭码,1000 表示正常关闭
void OnClose(websocketsvr *server, websocketpp::connection_hdl hdl) {
websocketsvr::connection_ptr con = server->get_con_from_hdl(hdl);
cout << "连接关闭 code=" << static_cast<int>(con->get_remote_close_code())
<< " reason=" << con->get_remote_close_reason() << endl;
}
// websocket连接收到消息的回调函数
void OnMessage(websocketsvr *server, websocketpp::connection_hdl hdl, message_ptr msg) {
cout << "收到消息: " << msg->get_payload() << endl;
// 收到消息将相同的消息发回给websocket客户端
server->send(hdl, msg->get_payload(), websocketpp::frame::opcode::text);
}
// websocket连接异常的回调函数
void OnFail(websocketsvr *server, websocketpp::connection_hdl hdl) {
cout << "连接异常" << endl;
}
// 处理http请求的回调函数:返回带 UTF-8 声明的测试页
void OnHttp(websocketsvr *server, websocketpp::connection_hdl hdl) {
cout << "处理http请求" << endl;
websocketsvr::connection_ptr con = server->get_con_from_hdl(hdl);
con->append_header("Content-Type", "text/html; charset=utf-8");
con->set_body(kIndexHtml);
con->set_status(websocketpp::http::status_code::ok);
}
int main() {
// 使用websocketpp库创建服务器
websocketsvr server;
// 设置websocketpp库的日志级别:all 打印全部,none 什么都不打印
server.set_access_channels(websocketpp::log::alevel::none);
/*初始化asio*/
server.init_asio();
// 注册http请求的处理函数
server.set_http_handler(bind(&OnHttp, &server, ::_1));
// 注册websocket请求的处理函数
server.set_open_handler(bind(&OnOpen, &server, ::_1));
server.set_close_handler(bind(&OnClose, &server, ::_1));
// 注册异常处理函数:参考代码定义了 OnFail 却没注册,这里补上
server.set_fail_handler(bind(&OnFail, &server, ::_1));
server.set_message_handler(bind(&OnMessage, &server, ::_1, ::_2));
// 监听8888端口
server.listen(8888);
// 开始接收tcp连接
server.start_accept();
// 开始运行服务器(阻塞)
server.run();
return 0;
}
ℹ️ 三处补全对应正文哪一节
OnClose 取关闭码的写法见第 5 节;OnHttp 的 Content-Type 是第 5 节表格里那条坑;set_fail_handler 也在第 5 节------参考代码定义了 OnFail 却没注册,补上这行「连接异常」才会真的打印。测试页用 C++17 的原始字符串 R"html(...)html" 包起来,省掉逐行 ss << 和引号转义;set_body 接收 std::string,传 const char * 会自动转换。
A.3 makefile
比参考 makefile 多了 -I.、all 和 .PHONY,编译器与参数抽成变量,换工具链只改变量。
📄 makefile · 缩进必须是真 tab,空格会报 missing separator
makefile
# websocketpp echo 服务:make 编译,make clean 清理
CXX ?= g++
# 头文件在系统路径时 -I. 够用;clone 到 third_party/ 时换成 -Ithird_party/websocketpp
CXXFLAGS ?= -std=c++17 -O2 -Wall -I.
LDFLAGS ?= -lpthread -lboost_system
all: server
server: server.cc
$(CXX) $(CXXFLAGS) $^ -o $@ $(LDFLAGS)
clean:
rm -rf server
.PHONY: all clean
⚠️ 两个跨平台细节
-I 指到哪:系统装了 libwebsocketpp-dev 时,#include <websocketpp/...> 不用 -I;把仓库 clone 到 third_party/websocketpp 后要写成 -Ithird_party/websocketpp(该目录下才是 websocketpp/ 头文件夹)。另外 clean 里的 rm -rf 在 Windows/MinGW 下没有,换 WSL 或改成 del server.exe。
A.4 webroot/index.html
内容和 A.2 里的 kIndexHtml 一字不差,服务端并不读这个文件 ------它存在是为了两个场景:不用启动服务端,直接用 file:// 打开验证 ws://127.0.0.1:8888 通不通;以及以后想把页面改成真正的静态资源时,有现成文件可用。
📄 webroot/index.html · 与内嵌版本同一份页面
html
<!doctype html>
<html>
<head><meta charset="utf-8"><title>websocketpp echo</title></head>
<body>
<h1>websocketpp echo 测试页</h1>
<p id="log">连接中...</p>
<script>
const ws = new WebSocket("ws://127.0.0.1:8888");
const log = (s) => { document.getElementById("log").textContent = s; console.log(s); };
ws.onopen = () => { log("已连接"); ws.send("hello websocket"); };
ws.onmessage = (e) => log("收到回显: " + e.data);
ws.onclose = (e) => log("连接关闭 code=" + e.code);
ws.onerror = () => log("连接失败:检查 ws:// 地址与端口");
</script>
</body>
</html>
这个文件还有个排查用途:file:// 打开能连上、http://127.0.0.1:8888 打开却连不上,问题就在 OnHttp 返回的内容,不在 WebSocket 服务本身。
A.5 编译与启动
两条命令,编译完在前台跑,日志直接打在终端。
bash
make clean && make # 从干净状态编译,首次十几秒(模板 + Boost.Asio 的代价)
./server # 前台启动,Ctrl+C 退出
A.6 验证步骤与预期输出
三件事逐条确认:响应头对不对、页面有没有回显、关闭码有没有打出来。
- 另开一个终端执行
curl -i http://127.0.0.1:8888 | head -5,响应头里应有Content-Type: text/html; charset=utf-8,缺了它中文可能乱码。 - 浏览器打开
http://127.0.0.1:8888,页面从「连接中...」变成「收到回显: hello websocket」;F12 打开 DevTools,Network → WS 里能看到101 Switching Protocols和两条 text 帧。 - 关掉标签页,终端应打印
连接关闭 code=1000 reason=,1000就是正常关闭。 Ctrl+C结束服务端。
终端预期输出(与第 7 节一致):
text
$ make clean && make
rm -rf server
g++ -std=c++17 -O2 -Wall -I. server.cc -o server -lpthread -lboost_system
$ ./server
处理http请求
连接成功
收到消息: hello websocket
连接关闭 code=1000 reason=
看到两次 处理http请求 是浏览器顺带请求了 /favicon.ico,属正常现象,不是回调注册错了。
A.7 常见错误对照
| 报错 / 现象 | 原因 | 修法 |
|---|---|---|
websocketpp/server.hpp: No such file or directory |
头文件没装,或 clone 了却没加 -I |
装 libwebsocketpp-dev,或改 -Ithird_party/websocketpp |
asio.hpp: No such file or directory |
只装了 websocketpp,没装 Boost | sudo apt install libboost-system-dev |
cannot find -lboost_system |
Boost 库没装或版本路径不同 | 装 libboost-system-dev;Boost ≥ 1.69 可先去掉 -lboost_system 试 |
makefile: *** missing separator |
命令行用空格缩进 | 换成真 tab |
| 端口在监听但浏览器连不上 | 漏了 start_accept() |
补上,第 4 节最容易漏的一行 |
| 页面中文乱码 | 响应缺 Content-Type 或 charset |
补 append_header("Content-Type", "text/html; charset=utf-8") |
与正文示例的差异就三处 :OnClose 从「只打一行字」改成打印远端关闭码;OnHttp 加了 Content-Type 并把测试页内嵌进来(正文第 7 节只给了片段和两行 C++);main 补注册了 set_fail_handler。其余代码与正文里的参考代码完全一致,可以对照着看。