WebSocket 从一次普通的 HTTP 请求开始,升级之后在同一条 TCP 连接上变成全双工的二进制帧。这篇不借助 WebSocket 客户端库,用套接字手工完成握手、拼帧、读帧,逐字节验证掩码、长度编码、分片、控制帧和关闭握手。
客户端发出:
GET /chat HTTP/1.1
Host: 127.0.0.1:9400
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: v4G9Mo9wRNCMNExBg0pZ4w==
Sec-WebSocket-Version: 13
服务器回复:
HTTP/1.1 101 Switching Protocols
Date: Thu, 15 Oct 2026 10:46:02 GMT
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: UVpqiCGPdLKx3UxuR7NIgKcZLTw=
Server: Python/3.14 websockets/17.2
Accept 的计算:base64( SHA-1( Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ) ) = UVpqiCGPdLKx3UxuR7NIgKcZLTw=;服务器给的 = UVpqiCGPdLKx3UxuR7NIgKcZLTw=;一致
101 之后同一条 TCP 连接不再说 HTTP,双方改用 WebSocket 帧;握手后服务器多发的字节 0 个HTTP/1.1 GET + Upgrade;Accept = base64(SHA-1(Key + 固定 GUID)),只证明服务器懂 WebSocket。
客户端→服务器 帧(11 字节):81 85 37 fa 21 3d 7f 9f 4d 51 58
= 81(FIN=1,操作码 1 文本)| 85(掩码位 1,长度 5)| 37 fa 21 3d(4 字节掩码密钥)| 7f 9f 4d 51 58(= "Hello" 逐字节与密钥循环异或)
验证:7f^37=48 9f^fa=65 4d^21=6c 51^3d=6c 58^37=6f → b'Hello'
服务器→客户端 帧(7 字节):81 05 48 65 6c 6c 6f = 81 05 + "Hello"(没有掩码位,没有密钥,头部只有 2 字节)
所以 5 字节的消息,客户端发的开销是 6 字节(2 + 4 掩码),服务器发的开销是 2 字节FIN + 操作码 + 掩码位 + 长度;客户端帧必须掩码(4 字节密钥异或),服务器帧不掩码。
载荷 125 字节:客户端帧头 6 字节(2 + 0 长度扩展 + 4 掩码),服务器帧头 2 字节(长度字段 125);回显正确 True 载荷 126 字节:客户端帧头 8 字节(2 + 2 长度扩展 + 4 掩码),服务器帧头 4 字节(长度字段 126);回显正确 True 载荷 65535 字节:客户端帧头 8 字节(2 + 2 长度扩展 + 4 掩码),服务器帧头 4 字节(长度字段 126);回显正确 True 载荷 65536 字节:客户端帧头 14 字节(2 + 8 长度扩展 + 4 掩码),服务器帧头 10 字节(长度字段 127);回显正确 True
≤125 用 7 位,126~65535 用 126+2 字节,更大用 127+8 字节。
服务器回来的帧:[('pong', 1, b'PING1'), ('文本', 1, b'frag-ment-end')]
分片之间可以夹控制帧(ping / pong / close),但不能夹别的数据消息;服务器先回 pong,分片消息收齐之后才回显消息可拆成多帧,控制帧可以夹在分片之间。
服务器回 关闭 帧,关闭码 1002,原因 'incorrect masking'
客户端发未掩码帧,服务器回 Close 码 1002(incorrect masking)。
客户端→服务器 Close 帧:88 85 37 fa 21 3d 34 12 43 44 52(88 = FIN + 操作码 8;载荷 = 码 1000 + "bye") 服务器→客户端:关闭 帧,载荷 03 e8 62 79 65(码 1000);随后读到的是 连接关闭
Close 帧(码 1000)一来一回,然后 TCP 才关闭。