Websocket中所遇到的粘包问题
问题描述
在websocket项目中,平时正常用网站与服务器进行少量的websocket数据传输没有出现问题。
而对服务端进行压测的时候,发现服务端收到了大量未知数据。

并且压测用客户端也在一定时间后奔溃:

初步判断
应该是网络传输的数据出现了问题。
所以利用gdb以及wireshark对程序进行调试。
对于服务端,对一处重要的地方进行了断点操作:
c++
res = jsonReader.parse(frame.payload_data, root);
if (!res) {
LOG_WARN << "parse json failed, data: " << frame.payload_data.substr(0, 100);
}
具体指令:
cmd
b websocket_conn.cc:433
condition 1 !res
对于客户端,通过bt观察最后断在哪里。
同时通过wireshark对传输的数据进行记录,毕竟内核的传输内容通过gdb无法看到:
cmd
sudo tcpdump -i lo -w chatroom_capture.pcap port 8080
并生成chatroom_capture.pcap。之后放在wireshark中查看。
现象观察
对于服务端,发现接受的数据打印如下:

对于websocket的一帧数据来说:
fin和opcode是一个字节:fin = 0, opcode = 1,说明是01110001,正好是'q'。
mask和payload是一个字节:mask = 0, payload_length = 114,说明是01110010,正好是'r'。
测试用的数据正好是26个字母!说明这里服务端把一个本不应该是websocket帧的数据作为了一个帧。
将chatroom_capture.pcap在wireshark gui中打开观察,发现tcp在发送的时候出现了粘包:

且最后明显将websocket帧进行了截断:

这是因为使用了本地回环,MTU达到了65535,65535 - 20(IP) - 20(TCP) - 12(TCP选项) = 65483,正好等于tcp目前的mss:

而使用以太网的话,应该就只有1460,所以目前的粘包问题更加严重了。
估算下来一次可以发送几百个websocket帧!
回到代码
c++
// ❌ 问题1:一次性取出所有数据,假设是完整的一帧或多帧
string request = buf->retrieveAllAsString();
LOG_DEBUG << "收到数据,长度: " << request.size();
// ❌ 问题2:直接解析,不检查数据是否完整
WebSocketFrame frame = parseWebSocketFrame_Original(request);
LOG_DEBUG << "解析帧: opcode=" << (int)frame.opcode << ", payload_len=" << frame.payload_length;
// ❌ 问题3:如果有多个帧粘在一起,只处理了第一个
// ❌ 问题4:如果帧不完整,可能解析出错误数据
// 业务逻辑处理(两个版本共用)
#endif
if (frame.opcode == 0x01) { // 文本帧
通过muduo一次性可以取出的字节非常多。

而parseWebSocketFrame_Original只截取了第一帧,后面的全部抛弃,导致可能后面的websocket的帧头也被抛弃掉了!出现的情况可能如下:
c++
═══════════════════════════════════════════════════════════
第 1 步:客户端快速发送 Frame1、Frame2、Frame3...
TCP 粘包,服务器一次收到 400 字节
═══════════════════════════════════════════════════════════
第 2 步:服务器 OnRead 被调用
buffer: [Frame1: 270字节][Frame2 前130字节]
↑ 共 400 字节
buf->retrieveAllAsString() 取走全部 400 字节!
parseWebSocketFrame_Original 解析 Frame1:
byte[0]=0x81, byte[1]=0x7E → 正常帧头
payload_data = Frame1的JSON + Frame2前130字节的垃圾
↑ 多出来的数据
JSON 解析器恰好可以解析到 }}\n 就停止 → 成功
═══════════════════════════════════════════════════════════
第 3 步:Frame2 剩余 140 字节到达
buffer: [Frame2 后140字节](从消息内容中间开始!)
OnRead 再次被调用
buf->retrieveAllAsString() 取走 140 字节
parseWebSocketFrame_Original 尝试解析:
byte[0] = 'q' (0x71) → fin=false, opcode=1 不是帧头!
byte[1] = 'r' (0x72) → mask=false, len=114 不是帧头!
payload_data = "stuvwxyzABCDEF..." 不是有效JSON
JSON 解析失败
═══════════════════════════════════════════════════════════
至于为啥客户端也会崩掉,我对opcode = 8的情况,也就是断联也打了断点,发现有些数据可能会被识别成断联:

对的,这里的XY被解读成断联了,所以触发了disconnect,而disconnect在写法上存在缺陷:
c++
void CWebSocketConn::sendCloseFrame(uint16_t code, const std::string reason) {
if (!tcp_conn_) return;
// 构造关闭帧
char frame[2 + reason.size()];
frame[0] = (code >> 8) & 0xFF; // 状态码高位
frame[1] = code & 0xFF; // 状态码低位
std::memcpy(frame + 2, reason.data(), reason.size());
// 发送关闭帧
tcp_conn_->send(frame, sizeof(frame));
}
光顾着包装fin和opcode,而没有注意mask和payload lens,故而使得客户端解析错误。
错误纠正
首先把disconnect的bug改过来:
c++
// 发送 WebSocket 关闭帧
void CWebSocketConn::sendCloseFrame(uint16_t code, const std::string reason) {
if (!tcp_conn_) return;
// 构造关闭帧的 payload:2字节状态码 + reason字符串
std::string payload;
payload.push_back((code >> 8) & 0xFF); // 状态码高位
payload.push_back(code & 0xFF); // 状态码低位
payload.append(reason);
// 用 buildWebSocketFrame 包装成完整的 WebSocket 帧(opcode=0x08 表示关闭帧)
std::string frame = buildWebSocketFrame(payload, 0x08);
tcp_conn_->send(frame);
}
然后在读取websocket帧的时候,用循环读取一个个帧:
c++
// websocket帧至少两个字节
while (buf->readableBytes() >= 2) {
const char* data = buf->peek();
size_t data_len = buf->readableBytes();
// 先检查帧是否完整
ssize_t frame_total_len = getWebSocketFrameLength(data, data_len);
if (frame_total_len <= 0) {
// 数据不足以判断帧长度,等待更多数据
LOG_DEBUG << "数据不足以判断帧长度,等待更多数据";
break;
}
// 防止超大帧长度导致挂起
// WebSocket 协议建议最大帧长度为 10MB(可根据需求调整)
const size_t MAX_FRAME_SIZE = 10 * 1024 * 1024; // 10MB
if (static_cast<size_t>(frame_total_len) > MAX_FRAME_SIZE) {
LOG_ERROR << "帧长度异常:" << frame_total_len << " 字节(超过 "
<< MAX_FRAME_SIZE << " 字节限制),可能是损坏数据,跳过";
buf->retrieve(1); // 跳过这个异常字节,尝试恢复
continue;
}
// 处理拆包 - 等待数据完整
if (static_cast<size_t>(frame_total_len) > data_len) {
// 帧不完整,等待更多数据(处理拆包)
LOG_DEBUG << "帧不完整,需要 " << frame_total_len << " 字节,当前 " << data_len << " 字节";
break;
}
// 解析一个完整的 WebSocket 帧
WebSocketFrame frame;
size_t frame_len = 0;
if (!parseWebSocketFrame(data, data_len, frame, frame_len)) {
LOG_ERROR << "解析 WebSocket 帧失败";
buf->retrieve(1); // 跳过一个字节,尝试恢复
continue;
}
// 从 buffer 中移除已解析的帧数据
buf->retrieve(frame_len);
LOG_DEBUG << "解析帧成功: opcode=" << (int)frame.opcode << ", payload_len=" << frame.payload_length;
getWebSocketFrameLength函数,其实就是看看包长度:
c++
ssize_t getWebSocketFrameLength(const char* data, size_t data_len) {
if (data_len < 2) {
return 0; // 数据不足
}
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(data);
size_t header_len = 2;
uint64_t payload_len = bytes[1] & 0x7F;
bool has_mask = (bytes[1] & 0x80) != 0;
if (payload_len == 126) {
header_len += 2;
if (data_len < header_len) return 0;
payload_len = (static_cast<uint64_t>(bytes[2]) << 8) | bytes[3];
} else if (payload_len == 127) {
header_len += 8;
if (data_len < header_len) return 0;
payload_len = (static_cast<uint64_t>(bytes[2]) << 56) |
(static_cast<uint64_t>(bytes[3]) << 48) |
(static_cast<uint64_t>(bytes[4]) << 40) |
(static_cast<uint64_t>(bytes[5]) << 32) |
(static_cast<uint64_t>(bytes[6]) << 24) |
(static_cast<uint64_t>(bytes[7]) << 16) |
(static_cast<uint64_t>(bytes[8]) << 8) |
static_cast<uint64_t>(bytes[9]);
}
if (has_mask) {
header_len += 4;
}
return static_cast<ssize_t>(header_len + payload_len);
}
正常压测
