📘 第九章 基于HTTP的功能追加协议
📄 一句话概括

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 秒实验

  1. 把 Key 换成一个固定字符串,看服务端回的 Accept 是不是每次都一样。
  2. 把请求里的 Sec-WebSocket-Version 改成 8,看服务端该怎么办。
  3. 想一想:为什么握手要用 sha1 而不是直接用 Key 本身做验证?

常见误区

  1. 「WebSocket 是一种 HTTP 请求」——只有握手那一个请求是,之后就不是了。
  2. 「握手用的 sha1 是加密」——它只是校验,不提供任何保密性,保密靠 wss。
  3. 「101 之后还能带 cookie」——不能,首部在 101 就结束了,身份要在握手时确认。
自测三问(点开对答案) 1. WebSocket 解决了 HTTP 的哪个根本限制? —— HTTP 只能客户端问、服务端答;WebSocket 让两边都能随时主动发。 2. Sec-WebSocket-Accept 是怎么算的? —— 客户端给的 Key 拼上固定 GUID,sha1 再 base64。 3. ws 和 wss 的区别是什么? —— wss 是 WebSocket 跑在 TLS 上,对应 https;对外的服务都该用 wss。

握手谈成了,接下来这条连接上跑的就是帧。帧长什么样,下一节拆开看。

相关笔记