
本文详解如何在 C 语言编写的 WebSocket 服务器中正确处理 HTTP 升级握手、生成合法 Sec-WebSocket-Accept 响应,并对后续二进制帧进行合规解码与编码,避免浏览器报错 “Invalid frame header”。
本文详解如何在 c 语言编写的 websocket 服务器中正确处理 http 升级握手、生成合法 `sec-websocket-accept` 响应,并对后续二进制帧进行合规解码与编码,避免浏览器报错 “invalid frame header”。
WebSocket 协议并非在 HTTP 连接上“发送 JSON 字符串”即可运行,而是一套严格分阶段的协议:第一阶段为 HTTP Upgrade 握手(纯文本),第二阶段为 二进制帧通信(需按 RFC 6455 解析/构造)。常见错误——如仅完成握手却直接 send() 明文字符串——将导致浏览器收到非法 WebSocket 帧,触发 Invalid frame header 错误并重置连接(Wireshark 中可见 RST 标志)。
✅ 正确握手:服务端响应必须完整且格式精准
客户端发起的 GET / HTTP/1.1 请求含 Upgrade: websocket 和 Sec-WebSocket-Key。服务端必须:
- 返回状态行
HTTP/1.1 101 Switching Protocols - 包含三个必需头字段:
Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Accept: <base64-encoded-value></base64-encoded-value> -
不可遗漏换行与空行(
\r\n\r\n),否则视为 HTTP 响应不完整,握手失败。
Sec-WebSocket-Accept 的计算方式为(C 示例):
#include <openssl/sha.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <string.h>
#include <stdio.h>
char* compute_websocket_accept(const char* key) {
static char accept[30];
const char* guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
char concatenated[256];
SHA_CTX ctx;
unsigned char sha1_hash[SHA_DIGEST_LENGTH];
snprintf(concatenated, sizeof(concatenated), "%s%s", key, guid);
SHA1_Init(&ctx);
SHA1_Update(&ctx, concatenated, strlen(concatenated));
SHA1_Final(sha1_hash, &ctx);
// Base64 encode (简化示意,生产环境请用 OpenSSL EVP_EncodeBlock)
// ... 实际需调用 BIO系列函数或标准 base64 库
return accept; // 返回如 "57dCnQc8YJrJ51osdI/I71QHYi4="
}⚠️ 注意:Sec-WebSocket-Accept 是 key + guid 的 SHA-1 哈希再 Base64 编码,非 MD5 或自定义哈希;且必须原样返回,末尾无空格。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
⚠️ 关键陷阱:握手后必须切换到 WebSocket 帧协议
握手成功后,所有后续通信必须遵循 WebSocket 帧格式(RFC 6455 Section 5),而非继续发送原始字符串。浏览器会将未编码的 send("hello") 视为非法帧(缺少 FIN、opcode、mask、长度编码等),立即断连。
因此,服务端需实现:
-
接收端:
decode_incoming_request()—— 解析 WebSocket 帧头,提取 payload(需 XOR 解密客户端消息,因浏览器强制 mask) -
发送端:
code_outgoing_response()—— 构造合法帧:设置 FIN=1、opcode=1(text)、mask=0(服务端不 mask)、正确长度字段、payload 数据
一个最小可行的帧编码(服务端发文本)示例:
void ws_encode_frame(char* out, const char* payload, size_t len) {
uint8_t* p = (uint8_t*)out;
size_t offset = 0;
// FIN + Text opcode
p[offset++] = 0x81; // FIN=1, opcode=1 (text)
// Payload length (7-bit for <126, else extended)
if (len < 126) {
p[offset++] = (uint8_t)len;
} else if (len <= 0xFFFF) {
p[offset++] = 126;
p[offset++] = (len >> 8) & 0xFF;
p[offset++] = len & 0xFF;
}
// Copy payload
memcpy(p + offset, payload, len);
// Note: Server MUST NOT set mask bit (0x80) — browser expects unmasked frames from server
}✅ 完整流程与注意事项
-
阻塞/非阻塞 I/O 循环:使用
recv()检测数据到达后,必须先判断是否为握手请求(以"GET "开头),再决定走握手分支 or 帧解析分支; -
缓冲区安全:
recv()可能分片到达,需累积缓冲直至遇到\r\n\r\n(握手)或完整帧头(后续帧),不可假设单次recv()收全一帧; -
关闭处理:收到
opcode=8的关闭帧时,应发送对等关闭帧并close()socket; -
调试建议:
- 使用
curl -i -N -H "Upgrade: websocket" ...手动测试握手; - Wireshark 过滤
websocket或tcp.port == 8000,观察 Frame Type 字段; - 浏览器开发者工具 Network → WS → Frames 标签页查看收发帧详情。
- 使用
遵循以上规范,即可彻底解决 Invalid frame header 错误,建立稳定、标准的 WebSocket C 服务器。

















