57、WebSocket握手:WebSocket协议、HTTP Upgrade握手、Sec-WebSocket-Key
WebSocket 这玩意儿,说白了就是给 HTTP 这辆「老马车」装上了涡轮增压。你想想看,传统的 HTTP 请求-响应模式,就像你给朋友写信——寄出去,等回信,再寄,再等。实时性?不存在的。而 WebSocket 一旦握手成功,就像拉了一条专线电话,两边随时可以说话。
我在做即时通讯项目时,第一次接触 WebSocket。当时客户端和服务端怎么都连不上,抓包一看,握手阶段就卡住了。嗯,今天我们就来彻底搞懂这个握手过程。
WebSocket 协议的本质
WebSocket 不是 HTTP,但它「借道」HTTP 完成握手。为什么这么设计?因为大部分防火墙和代理只放行 80 和 443 端口。如果 WebSocket 另起炉灶用新端口,很多内网环境根本连不上。
所以,WebSocket 的策略是:先用 HTTP 协议敲门,告诉服务器「我想升级成 WebSocket」,服务器同意后,双方再切换到 WebSocket 协议。这个敲门动作,就是 HTTP Upgrade 握手。
HTTP Upgrade 握手流程
握手过程其实就两步:客户端发请求,服务器回响应。但细节里全是坑。
客户端请求头
客户端发起的 HTTP 请求,长这样:
GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
注意几个关键点:
- Upgrade: websocket —— 明确告诉服务器我要升级
- Connection: Upgrade —— 表示这是一个升级请求
- Sec-WebSocket-Key —— 一个 Base64 编码的 16 字节随机值
- Sec-WebSocket-Version: 13 —— 协议版本,目前主流就是 13
我个人习惯在生成 Sec-WebSocket-Key 时,用 /dev/urandom 读取 16 字节,然后 Base64 编码。千万别用固定值,否则容易被攻击。
服务器响应头
服务器如果同意升级,会返回 101 状态码:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
这里最关键的是 Sec-WebSocket-Accept。它不是随便生成的,而是根据客户端发来的 Sec-WebSocket-Key 计算出来的。
Sec-WebSocket-Key 与 Sec-WebSocket-Accept
为什么要有这个 Key/Accept 机制?说白了就是为了防止缓存代理把旧的 WebSocket 响应错误地返回给新客户端。
计算规则很简单:
- 把客户端发来的 Sec-WebSocket-Key 拼接上一个固定的 GUID 字符串
258EAFA5-E914-47DA-95CA-C5AB0DC85B11 - 对拼接后的字符串做 SHA-1 哈希
- 对哈希结果做 Base64 编码
用 C 语言实现的话:
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>
void compute_accept(const char *key, char *output) {
const char *guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
char buffer[256];
unsigned char hash[SHA_DIGEST_LENGTH];
// 拼接 key 和 GUID
snprintf(buffer, sizeof(buffer), "%s%s", key, guid);
// SHA-1 哈希
SHA1((unsigned char*)buffer, strlen(buffer), hash);
// Base64 编码
// 这里省略 Base64 实现,实际项目中用现成库
// 输出结果存入 output
}
我在项目中遇到过一个问题:OpenSSL 的 SHA1 函数在旧版本中返回的是 unsigned char *,但有些开发者误用了 char *,导致签名结果不对。嗯,类型转换一定要小心。
核心要点:Sec-WebSocket-Accept 是服务器对客户端身份的「回执」。客户端通过验证这个回执,可以确认服务器确实理解 WebSocket 协议,而不是某个瞎回应的中间件。
握手失败时的处理
如果服务器不支持 WebSocket,或者 Key 验证失败,会返回 400 或 426 状态码。客户端收到非 101 响应时,应该关闭连接并报错。
我曾经调试过一个诡异的问题:客户端能连上,但几秒后自动断开。抓包发现服务器返回了 200 OK 而不是 101。原来是一个反向代理把 Upgrade 头给过滤掉了。从那以后,我写代码都会先检查响应状态码是不是 101。
注意:握手完成后,双方必须立即切换到 WebSocket 协议。如果服务器在 101 响应后继续发送 HTTP 数据,客户端会直接断开。同样,客户端也不能在握手后发送 HTTP 请求。
握手过程的完整流程图
下面这张图展示了从 HTTP 请求到 WebSocket 连接建立的全过程:
实际开发中的注意事项
写 WebSocket 握手代码时,有几个地方容易踩坑:
- 大小写敏感:HTTP 头字段名不区分大小写,但 Sec-WebSocket-Key 的值是大小写敏感的。我曾经因为 Base64 编码时多了一个换行符,导致 Key 值多了一个字符,握手失败。
- 超时处理:客户端发送握手请求后,如果 5 秒内没收到 101 响应,应该主动断开。我习惯用
select()或poll()设置超时。 - 代理问题:有些代理会修改 Connection 头。如果发现握手总是失败,可以试试用 HTTPS 加密通道。
小技巧:调试握手问题时,用 Wireshark 抓包最直接。过滤条件设为 http.upgrade == "websocket",一眼就能看出握手是否成功。
总结
WebSocket 握手,说白了就是一次「协议切换」的协商过程。客户端带着 Key 去敲门,服务器算好 Accept 来开门。门开了,两边就切换到 WebSocket 协议,开始全双工通信。
我在实际项目中,通常把握手逻辑封装成一个独立的函数,这样不管是聊天、推送还是游戏,直接调用就行。嗯,代码复用才是王道。
公众号:蓝海资料掘金营,微信deep3321