WebSocket 是「借 HTTP 进门、进门就换人」的协议。
握手的那个请求还是 HTTP:Upgrade: websocket 谈下来,服务端回 101,此后这条连接上跑的就是帧,
不再是 HTTP。
SPDY 改的是「怎么传 HTTP 报文」,WebSocket 改的是「干脆不用 HTTP 了」。 它要解决的是 9.1 那个实测里最刺眼的一条:服务端一个字都不会主动发。
一、全双工到底是什么意思
HTTP 是半双工:客户端说一句,服务端回一句,说完这一轮就结束。 服务端就算有新数据,也只能等下次被问。 WebSocket 建立之后,两边都可以随时发,不需要谁先问谁。 连接一直开着,两边各发各的帧,谁也不用等对方的许可。 这一个改动把一整类应用变简单了:聊天、行情、协同编辑、在线游戏、服务器日志实时看板。 以前这些都要靠轮询(9.1 里那个「沉默 350 毫秒」就是轮询的样子:问一次、没有、过一会儿再问)。
二、握手还是 HTTP 请求
第一步客户端发一个普通的 GET,多带四行首部:Upgrade: websocket、Connection: Upgrade、
Sec-WebSocket-Key(16 字节随机数的 base64)、Sec-WebSocket-Version: 13。
第二步服务端认了,回 101 Switching Protocols,加 Upgrade: websocket、Connection: Upgrade,
以及一个算出来的 Sec-WebSocket-Accept。
那个 Accept 不是随便写的:它是把客户端给的 Key 拼上一段全协议固定的 GUID,做 sha1 再 base64。
Demo 拿 RFC 6455 里的示例对拍了一遍,算出来的值和标准里写的一模一样。
这段设计的用意很实在:证明对面真的读懂了这个协议,而不是某个返回固定字符串的假服务器。
它不提供任何加密强度,只是「你懂规矩」的凭据。
三、地址怎么写,加密怎么办
WebSocket 有自己的 URI 方案:明文是 ws://,加密是 wss://,默认端口分别借用 80 和 443。
wss 就是 WebSocket 跑在 TLS 上,握手过程和 HTTPS 一样。
凡是对外的 WebSocket 都该用 wss——不然中间人能看到全部帧内容,而帧里往往是明文。
还有一条容易忽略:101 之后,Host、Cookie、Authorization 这些首部就再也没有了。
身份必须在握手那一次确认完,后面靠连接本身认人。
这也是为什么 8.5.2 的会话在 WebSocket 里常常要在握手时用 cookie 认一次,然后绑定到连接上。
四、动手 Demo:Accept 是算出来的:拿 RFC 示例对拍
先算一遍 RFC 6455 里给的那组 Key,看结果对不对得上标准答案; 再起一个本机服务端,用随机 Key 真握手一次,把请求和响应的首部原样打出来。
GUID = u'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
def accept_of(key):
return base64.b64encode(
hashlib.sha1((key + GUID).encode()).digest()).decode()
resp = (u'HTTP/1.1 101 Switching Protocols\r\n'
u'Upgrade: websocket\r\n'
u'Connection: Upgrade\r\n'
u'Sec-WebSocket-Accept: %s\r\n\r\n' % accept_of(key))
实跑输出:
--------------------------------------------------------------------
握手:Sec-WebSocket-Accept 是算出来的,不是乱写的
--------------------------------------------------------------------
RFC 6455 示例里的 Key dGhlIHNhbXBsZSBub25jZQ==
它加固定 GUID 再 sha1 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
base64 之后 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
RFC 里写的答案 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
对得上吗 对上
这个固定 GUID 是协议里写死的字符串,全世界都一样。
它的唯一作用是让 sha1 的输入不可预测,证明对方真的懂协议。
客户端发的 Key MopNVYWic1SkNoLMMhTktg==
服务端回的 Accept ZmHto/bFCX/KvmZzqtLowRWFACU=
请求整段 158 字节
响应整段 129 字节
> GET /chat HTTP/1.1
> Host: 127.0.0.1:60477
> Upgrade: websocket
> Connection: Upgrade
> Sec-WebSocket-Key: MopNVYWic1SkNoLMMhTktg==
> Sec-WebSocket-Version: 13
>
< HTTP/1.1 101 Switching Protocols
< Upgrade: websocket
< Connection: Upgrade
< Sec-WebSocket-Accept: ZmHto/bFCX/KvmZzqtLowRWFACU=
<
这就是 WebSocket 握手:用一次普通的 HTTP 请求,把「升级」谈下来。
101 之后,这条 TCP 连接上跑的东西就不再是 HTTP 了——没有方法、没有首部、
RFC 示例的 Key 是 dGhlIHNhbXBsZSBub25jZQ==,算出来是 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=,
和标准里写的完全一致。所以这个算法不是玄学,就是 sha1 加 base64。
真握手那一次,请求整段 158 字节、响应整段 129 字节。
响应比请求还短——因为它只需要确认三件事:升级、保持、Accept。
响应第一行是 HTTP/1.1 101 Switching Protocols。
101 这个码在 4.x 那批里没出现过,因为它是「切换协议」专用的,不是「请求成功」。
101 之后,这条连接上再没有 GET、没有首部、没有 Content-Length。
所以严格说它已经不是 HTTP 连接了,只是开局那一下用了 HTTP。
30 秒实验
- 把 Key 换成一个固定字符串,看服务端回的 Accept 是不是每次都一样。
- 把请求里的
Sec-WebSocket-Version改成 8,看服务端该怎么办。 - 想一想:为什么握手要用 sha1 而不是直接用 Key 本身做验证?
常见误区
- 「WebSocket 是一种 HTTP 请求」——只有握手那一个请求是,之后就不是了。
- 「握手用的 sha1 是加密」——它只是校验,不提供任何保密性,保密靠 wss。
- 「101 之后还能带 cookie」——不能,首部在 101 就结束了,身份要在握手时确认。
自测三问(点开对答案)
1. WebSocket 解决了 HTTP 的哪个根本限制? —— HTTP 只能客户端问、服务端答;WebSocket 让两边都能随时主动发。 2. Sec-WebSocket-Accept 是怎么算的? —— 客户端给的 Key 拼上固定 GUID,sha1 再 base64。 3. ws 和 wss 的区别是什么? —— wss 是 WebSocket 跑在 TLS 上,对应 https;对外的服务都该用 wss。握手谈成了,接下来这条连接上跑的就是帧。帧长什么样,下一节拆开看。
- ☐ (本节完,下一节 9.3.2 WebSocket协议)