跳到主要内容WebSocket 通信原理及 C++ 实现 | 极客日志C++
WebSocket 通信原理及 C++ 实现
深入解析 WebSocket 协议原理,对比其与 HTTP 的差异,详述握手流程、数据帧结构及关闭机制。基于 libwebsockets 库提供 C++ 服务端与客户端完整代码,涵盖连接管理、回声响应、心跳保活及断线重连。同时介绍 WSS 安全配置、Nginx 反向代理及常见工程问题排查方案。
女王3 浏览 WebSocket 是 HTML5 规范定义的基于 TCP 的全双工、双向、持久化应用层通信协议(RFC 6455),核心解决了 HTTP 协议'请求 - 响应'半双工模型无法满足实时通信需求的痛点。
一、WebSocket 核心定位:突破 HTTP 的实时性瓶颈
1.1 HTTP 协议的实时性缺陷
HTTP 协议自设计之初就围绕'客户端请求、服务端响应'的单向模型,在实时通信场景(如聊天、行情推送、物联网数据上报)中存在致命问题:
- 半双工通信:服务端无法主动向客户端推送数据,只能被动响应请求;
- 短连接特性:即使 HTTP/1.1 引入 Keep-Alive 实现长连接,本质仍是'请求 - 响应'周期的延长,连接会因超时被销毁;
- 轮询/长轮询的弊端:轮询(定时发送 HTTP 请求)会产生大量无效带宽消耗,长轮询(挂起请求直到有数据)仍有连接建立/销毁开销,且延迟无法低于轮询间隔。
1.2 WebSocket 的核心优势
- 全双工通信:连接建立后,客户端和服务端可随时双向发送数据,无需等待对方请求;
- 持久化连接:一次 TCP 握手后,连接持续至主动关闭,避免频繁建连/断连的开销;
- 轻量级协议:数据帧仅包含 2~14 字节的头部(HTTP 头部通常数百字节),大幅降低传输开销;
- 兼容性强:基于 HTTP 升级机制实现,可穿透大部分防火墙和代理服务器;
- 多数据类型支持:原生支持文本(UTF-8)和二进制数据传输,无需额外封装。
1.3 WebSocket vs HTTP 核心特性对比
| 特性 | HTTP | WebSocket |
|---|
| 通信方向 | 客户端主动请求,服务端被动响应 | 全双工,双向主动通信 |
| 连接状态 | 短连接(Keep-Alive 仅延长) | 持久连接(主动关闭前一直存活) |
| 头部开销 | 大(包含 Cookie、User-Agent 等) | 极小(最小 2 字节帧头) |
| 主动推送 | 不支持 | 原生支持 |
| 数据格式 | 需封装 HTTP 头,仅文本/二进制 | 帧化数据(文本/二进制/控制帧) |
| 关闭方式 | 响应完成后自动关闭 | 协商式关闭(Close 帧) |
二、WebSocket 协议底层原理
2.1 握手流程:基于 HTTP 的协议升级
WebSocket 连接的建立依赖 HTTP 101(Switching Protocols)升级机制,全程基于 TCP 连接(默认端口 80,WSS 为 443),分为'客户端请求'和'服务端验证响应'两步:
(1)客户端发起升级请求
客户端向服务端发送 HTTP GET 请求,核心头字段决定了升级能否成功:
GET /chat HTTP/1.1
Host: example.com:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Extensions: permessage-deflate
(2)服务端验证并响应升级
服务端必须完成 Sec-WebSocket-Key 的验证,否则客户端会拒绝建立连接:
- 将
Sec-WebSocket-Key 与固定 UUID 字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
- 对拼接结果做 SHA-1 哈希计算,再将哈希值进行 Base64 编码,得到
Sec-WebSocket-Accept;
- 返回 HTTP 101 响应,确认协议升级:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
握手成功后,TCP 连接从 HTTP 协议切换为 WebSocket 协议,后续所有通信均使用 WebSocket 帧格式。
2.2 数据帧格式:WebSocket 的通信最小单位
WebSocket 所有数据(文本、二进制、控制指令)均封装为'帧(Frame)'传输,帧格式是协议的核心,每个字段的含义和规则必须严格遵守:
| 字段 | 长度(位) | 核心含义与规则 |
|---|
| FIN | 1 | 帧结束标记:1=当前帧是消息最后一帧;0=消息分片,后续还有帧 |
| RSV1/RSV2/RSV3 | 1*3 | 保留位,仅启用扩展时非 0(如 permessage-deflate 用 RSV1),未启用时必须为 0(否则关闭连接) |
| Opcode | 4 | 帧类型: |
| 0=继续帧(分片消息的后续帧) | | |
| 1=文本帧(UTF-8 编码) | | |
| 2=二进制帧 | | |
| 8=关闭帧 | | |
| 9=Ping 帧(心跳) | | |
| 10=Pong 帧(心跳响应) | | |
| Mask | 1 | 掩码标记:客户端发的帧必须为 1(需掩码加密),服务端发的帧必须为 0(无需掩码) |
| Payload len | 7/7+16/7+64 | 负载长度: |
| 0~125=直接表示长度; | | |
| 126=后续 2 字节(16 位无符号整数)表示长度; | | |
| 127=后续 8 字节(64 位无符号整数)表示长度 | | |
| Masking-key | 0/32 | 掩码密钥:仅 Mask=1 时存在(4 字节),客户端用于加密负载数据 |
| Payload data | 可变 | 实际传输的数据(文本/二进制/控制指令),Mask=1 时需用 Masking-key 解密 |
关键规则:掩码计算
客户端发送的所有数据帧必须用 Masking-key 加密,解密公式为:
decoded_byte = encoded_byte ^ masking_key[i % 4]
其中 i 是负载数据的字节索引,%4 表示掩码密钥 4 字节循环使用。示例:
- 加密前字节:
0x41(字符'A')
- Masking-key 第 1 字节:
0x1F
- 加密后字节:
0x41 ^ 0x1F = 0x5E(字符'^')
服务端接收后需反向解密,而服务端发送的帧无需掩码,客户端可直接解析。
分片传输规则
- 首帧:FIN=0,Opcode=1(文本)/2(二进制);
- 中间帧:FIN=0,Opcode=0(继续帧);
- 最后一帧:FIN=1,Opcode=0;
- 控制帧(Ping/Pong/Close)不允许分片,必须是单帧(FIN=1)。
2.3 连接关闭机制:协商式关闭
WebSocket 禁止直接断开 TCP 连接,必须通过'Close 帧'完成协商式关闭,避免数据丢失:
- 发起方发送 Opcode=8 的 Close 帧,负载可携带:
- 2 字节无符号整数状态码(如 1000=正常关闭);
- 可选的 UTF-8 编码原因文本;
- 接收方收到 Close 帧后,必须立即回复相同的 Close 帧;
- 双方完成 Close 帧交互后,关闭 TCP 连接。
常见关闭状态码
| 状态码 | 含义 | 适用场景 |
|---|
| 1000 | 正常关闭 | 业务完成后主动关闭 |
| 1001 | 端点离开 | 客户端关闭浏览器/服务端停机 |
| 1002 | 协议错误 | 帧格式非法/Opcode 不支持 |
| 1003 | 不支持的数据类型 | 接收非 UTF8 的文本帧 |
| 1006 | 连接异常关闭 | TCP 连接被强制断开(非协商) |
| 1011 | 服务端内部错误 | 服务端处理消息时崩溃 |
2.4 心跳保活机制:避免'假死'连接
由于 NAT 超时、防火墙清理、网络波动等原因,WebSocket 连接可能出现'假死'(TCP 连接存在但无法通信),需通过 Ping/Pong 帧实现心跳:
- 发起方(通常是服务端)定时发送 Opcode=9 的 Ping 帧(可携带少量负载);
- 接收方必须立即回复 Opcode=10 的 Pong 帧,且负载需与 Ping 帧一致;
- 若发起方超时(如 30 秒)未收到 Pong 帧,判定连接失效,主动发送 Close 帧关闭连接。
三、C++ 实现 WebSocket
C++ 无原生 WebSocket 库,主流选择有两个:
- libwebsockets:轻量、跨平台、无 Boost 依赖,适合高性能场景;
- websocketpp:基于 Boost.Asio,面向对象设计,开发效率高。
本文以libwebsockets为例(工业界更常用),提供完整的服务端和客户端实现。
3.1 libwebsockets 环境搭建
(1)Linux/macOS 环境
sudo apt install libssl-dev libz-dev libpthread-stubs0-dev
brew install openssl zlib
git clone https://github.com/warmcat/libwebsockets.git
cd libwebsockets && mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr/local -DLWS_WITH_SSL=ON ..
make -j4 && sudo make install
(2)Windows 环境
- 下载 CMake 和 Visual Studio 2022;
- 编译 OpenSSL 并配置环境变量;
- 通过 CMake-GUI 生成 VS 工程,编译安装 libwebsockets。
3.2 C++ WebSocket 服务端实现(核心功能:回声 + 心跳 + 客户端管理)
以下代码实现了一个完整的 WebSocket 服务端,支持客户端连接管理、消息回声、心跳保活、协商式关闭:
#include <libwebsockets.h>
#include <string.h>
#include <unistd.h>
#include <vector>
#include <mutex>
std::vector<struct lws*> g_clients;
std::mutex g_client_mutex;
#define HEARTBEAT_INTERVAL 30
#define HEARTBEAT_TIMEOUT 10
struct PerClientData {
time_t last_pong_time;
};
static int ws_callback(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len) {
PerClientData *client_data = (PerClientData*)user;
switch(reason) {
case LWS_CALLBACK_ESTABLISHED: {
std::lock_guard<std::mutex> lock(g_client_mutex);
g_clients.push_back(wsi);
client_data->last_pong_time = time(NULL);
lwsl_notice("Client connected: %p, total clients: %zu\n", wsi, g_clients.size());
break;
}
case LWS_CALLBACK_RECEIVE: {
char buf[LWS_PRE + 4096] = {0};
memcpy(buf + LWS_PRE, in, len);
lwsl_notice("Received from client %p: %s (len: %zu)\n", wsi, buf + LWS_PRE, len);
int ret = lws_write(wsi, (unsigned char*)buf + LWS_PRE, len, LWS_WRITE_TEXT);
if(ret < 0) {
lwsl_err("Failed to write to client %p\n", wsi);
}
break;
}
case LWS_CALLBACK_SERVER_PING: {
client_data->last_pong_time = time(NULL);
lwsl_notice("Received Ping from client %p\n", wsi);
break;
}
case LWS_CALLBACK_SERVER_HEARTBEAT: {
std::lock_guard<std::mutex> lock(g_client_mutex);
time_t now = time(NULL);
for(auto it = g_clients.begin(); it != g_clients.end();) {
struct lws* client_wsi = *it;
PerClientData *data = (PerClientData*)lws_wsi_user(client_wsi);
if(now - data->last_pong_time > HEARTBEAT_INTERVAL + HEARTBEAT_TIMEOUT) {
lwsl_notice("Client %p heartbeat timeout, closing\n", client_wsi);
lws_close_reason(client_wsi, LWS_CLOSE_STATUS_NORMAL, (unsigned char*)"timeout", 7);
it = g_clients.erase(it);
} else {
lws_callback_on_writable(client_wsi);
++it;
}
}
break;
}
case LWS_CALLBACK_SERVER_WRITEABLE: {
lws_write(wsi, NULL, 0, LWS_WRITE_PING);
break;
}
case LWS_CALLBACK_CLOSED: {
std::lock_guard<std::mutex> lock(g_client_mutex);
for(auto it = g_clients.begin(); it != g_clients.end(); ++it) {
if(*it == wsi) {
g_clients.erase(it);
lwsl_notice("Client disconnected: %p, total clients: %zu\n", wsi, g_clients.size());
break;
}
}
break;
}
default:
break;
}
return 0;
}
static struct lws_protocols ws_protocols[] = {
{"ws-echo-protocol",
ws_callback,
sizeof(PerClientData),
4096,
0,
NULL,
0},
{NULL, NULL, 0, 0}
};
int main(int argc, char** argv) {
lws_set_log_level(LLL_NOTICE | LLL_ERR | LLL_WARN, NULL);
struct lws_context_creation_info ctx_info;
memset(&ctx_info, 0, sizeof(ctx_info));
ctx_info.port = 8080;
ctx_info.protocols = ws_protocols;
ctx_info.gid = -1;
ctx_info.uid = -1;
ctx_info.options = LWS_SERVER_OPTION_VALIDATE_UTF8 |
LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
struct lws_context* ctx = lws_create_context(&ctx_info);
if(!ctx) {
lwsl_err("Failed to create lws context\n");
return -1;
}
lwsl_notice("WebSocket server started on ws://localhost:8080\n");
lwsl_notice("Heartbeat interval: %d seconds, timeout: %d seconds\n", HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT);
while(1) {
lws_service(ctx, 50);
usleep(10000);
}
lws_context_destroy(ctx);
return 0;
}
核心代码解释
- PerClientData:每个客户端的自定义数据,存储最后一次收到 Pong 的时间,用于心跳检测;
- ws_callback:事件回调函数,处理连接建立、数据接收、心跳、连接关闭等核心事件;
- lws_service:事件循环函数,负责处理客户端的 IO 事件和定时任务(如心跳检查);
- 客户端管理:通过全局向量 + 互斥锁实现线程安全的客户端连接管理,避免多线程竞争。
3.3 C++ WebSocket 客户端实现
以下代码实现了 WebSocket 客户端,支持连接服务端、发送消息、接收响应、心跳处理:
#include <libwebsockets.h>
#include <string.h>
#include <unistd.h>
struct PerClientData {
char send_buf[LWS_PRE + 4096];
int send_len;
};
static int ws_client_callback(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len) {
PerClientData *client_data = (PerClientData*)user;
switch(reason) {
case LWS_CALLBACK_CLIENT_ESTABLISHED: {
lwsl_notice("Connected to WebSocket server\n");
const char* msg = "Hello WebSocket Server (C++)";
client_data->send_len = strlen(msg);
memcpy(client_data->send_buf + LWS_PRE, msg, client_data->send_len);
lws_callback_on_writable(wsi);
break;
}
case LWS_CALLBACK_CLIENT_WRITEABLE: {
if(client_data->send_len > 0) {
int ret = lws_write(wsi, (unsigned char*)client_data->send_buf + LWS_PRE, client_data->send_len, LWS_WRITE_TEXT);
if(ret > 0) {
lwsl_notice("Sent to server: %s\n", client_data->send_buf + LWS_PRE);
client_data->send_len = 0;
}
}
break;
}
case LWS_CALLBACK_CLIENT_RECEIVE: {
char buf[4096] = {0};
memcpy(buf, in, len);
lwsl_notice("Received from server: %s (len: %zu)\n", buf, len);
break;
}
case LWS_CALLBACK_CLIENT_PING: {
lwsl_notice("Received Ping from server\n");
break;
}
case LWS_CALLBACK_CLIENT_CLOSED: {
lwsl_notice("Disconnected from server\n");
break;
}
default:
break;
}
return 0;
}
static struct lws_protocols ws_client_protocols[] = {
{"ws-echo-protocol", ws_client_callback, sizeof(PerClientData), 4096, 0, NULL, 0},
{NULL, NULL, 0, 0}
};
int main(int argc, char** argv) {
lws_set_log_level(LLL_NOTICE | LLL_ERR | LLL_WARN, NULL);
struct lws_context_creation_info ctx_info;
memset(&ctx_info, 0, sizeof(ctx_info));
ctx_info.protocols = ws_client_protocols;
ctx_info.gid = -1;
ctx_info.uid = -1;
struct lws_context* ctx = lws_create_context(&ctx_info);
if(!ctx) {
lwsl_err("Failed to create client context\n");
return -1;
}
struct lws_client_connect_info conn_info;
memset(&conn_info, 0, sizeof(conn_info));
conn_info.context = ctx;
conn_info.address = "localhost";
conn_info.port = 8080;
conn_info.path = "/";
conn_info.host = conn_info.address;
conn_info.origin = conn_info.address;
conn_info.protocol = "ws-echo-protocol";
struct lws* wsi = lws_client_connect_via_info(&conn_info);
if(!wsi) {
lwsl_err("Failed to connect to server\n");
lws_context_destroy(ctx);
return -1;
}
while(1) {
lws_service(ctx, 50);
usleep(10000);
}
lws_context_destroy(ctx);
return 0;
}
3.4 编译与运行
(1)编译服务端
g++ -o ws_server ws_server.cpp -lwebsockets -lpthread -lssl -lcrypto -lz
(2)编译客户端
g++ -o ws_client ws_client.cpp -lwebsockets -lpthread -lssl -lcrypto -lz
(3)运行
运行后客户端会向服务端发送消息,服务端回声响应,同时服务端会定时发送 Ping 帧检测客户端心跳。
四、WebSocket 高级特性与工程实践
4.1 安全加固:WSS(WebSocket Secure)配置
生产环境必须使用 WSS(基于 TLS/SSL 加密),避免数据明文传输。libwebsockets 配置 WSS 只需修改上下文配置:
ctx_info.ssl_cert_filepath = "/path/to/server.crt";
ctx_info.ssl_private_key_filepath = "/path/to/server.key";
ctx_info.port = 443;
生产环境推荐:Nginx 反向代理 WSS
直接在应用层配置 SSL 易出问题,推荐通过 Nginx 反向代理实现 WSS:
server {
listen 443 ssl;
server_name example.com;
# SSL 证书配置
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# 反向代理 WebSocket
location /ws {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400; // 延长超时时间(避免心跳中断)
}
}
4.2 断线重连机制(工程必备)
网络波动会导致连接断开,需实现断线重连逻辑(指数退避策略,避免频繁重试):
int reconnect_count = 0;
const int MAX_RECONNECT = 10;
const int BASE_RECONNECT_INTERVAL = 1;
while(reconnect_count < MAX_RECONNECT) {
struct lws* wsi = lws_client_connect_via_info(&conn_info);
if(wsi) {
reconnect_count = 0;
break;
}
int interval = BASE_RECONNECT_INTERVAL * (1 << reconnect_count);
interval = std::min(interval, 512);
lwsl_notice("Reconnect failed, retry in %d seconds (count: %d)\n", interval, reconnect_count);
sleep(interval);
reconnect_count++;
}
4.3 性能优化策略
- 分片传输大数据:将超过 125 字节的消息拆分为多个帧,避免单次传输阻塞;
- 连接池限制:服务端限制最大连接数(如 10000),避免资源耗尽;
- 异步 IO 优化:结合 epoll/kqueue(Linux/macOS)实现高并发,libwebsockets 已内置异步 IO,无需手动实现。
启用压缩扩展:配置 permessage-deflate 扩展,压缩文本数据(减少带宽):
ctx_info.extensions = lws_get_internal_extensions();
4.4 常见问题与排错
| 问题 | 根因 | 解决方案 |
|---|
| 握手失败(400 错误) | Sec-WebSocket-Version≠13 或 Key 验证失败 | 确保客户端使用版本 13,服务端正确计算 Accept |
| 连接立即关闭 | 子协议不匹配 | 客户端和服务端 Sec-WebSocket-Protocol 一致 |
| 数据乱码 | 文本帧非 UTF8 编码 | 强制使用 UTF8 编码,验证数据格式 |
| 心跳超时 | 防火墙拦截 Ping/Pong 帧 | 调整心跳间隔,通过 Nginx 转发时延长超时时间 |
| 高并发下连接不稳定 | 文件描述符耗尽 | 调整系统最大文件描述符(ulimit -n 65535) |
- 协议本质:WebSocket 是基于 HTTP 升级的全双工持久化协议,通过轻量级帧格式实现高效双向通信,核心解决 HTTP 实时性差的问题;
- 核心规则:握手需验证 Sec-WebSocket-Key、客户端帧必须掩码、控制帧(Ping/Pong/Close)需遵守单帧规则、连接需协商式关闭;
- C++ 实现:优先选择 libwebsockets 库,核心是事件回调函数 + 上下文事件循环,需实现客户端管理、心跳保活、断线重连,生产环境必须配置 WSS;
- 工程要点:心跳保活避免连接假死、指数退避实现重连、Nginx 反向代理优化 WSS 配置、限制连接数避免性能瓶颈。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online