📘 第六章 HTTP首部
📄 一句话概括

Upgrade 是「我想在这条连接上换个协议」的提议。 服务端同意就回 101,从此这条 TCP 连接上跑的不再是 HTTP。

📝 本节和前面几节的关系

上一节讲怎么传字节,这一节讲怎么不传 HTTP 了。WebSocket 的握手就是这个词的经典用法。

一、协商只有三步

第一步:客户端在请求里带 Connection: Upgrade 和 Upgrade: h2c(要换的协议名)。 注意两个都要带——Connection: Upgrade 是把 Upgrade 标记成逐跳字段,少了它, 中间的代理会把这个提议转给下一跳,那就问错人了。 第二步:服务端同意就回 101 Switching Protocols,并在自己的 Upgrade 里说明换成了哪个协议。 不同意就照常回 200,客户端得认命。 第三步:101 之后,这条连接上跑的字节就不再遵循 HTTP 格式了。 双方按新协议的规矩来,谁都不许再发 HTTP 报文。 这三步里最容易漏的是第一步的两个首部。 只发 Upgrade 不发 Connection: Upgrade, 中间有代理时提议会被转发到源服务器——而源服务器不在那一跳上,它没资格答应。

二、实测:发 hello 收 HELLO

Demo 里的服务端只认 h2c。不发 Upgrade 时它照常回 200; 发了 Upgrade: h2c 加 Connection: Upgrade,它回 101。 101 之后,客户端在管子里发一个自己定的假协议:一个字节的长度加内容。 服务端收到 hello,回一个长度字节加大写化的内容。客户端收到 HELLO。 这一段最能说明问题:101 之后双方交换的东西,HTTP 的解析器完全看不懂。它不再是 HTTP 了。

三、为什么今天改用 ALPN

Upgrade 只能在「连接已经建立、已经发过一个 HTTP 请求」之后才能协商。 也就是说明文 HTTP/2(h2c)必须先走一个 1.1 请求,多一个往返。 HTTPS 那边有更好的办法:TLS 握手时就能用 ALPN 扩展说清「我支持 h2 和 http/1.1」,服务端当场选定。 省掉了那次额外的往返。 所以现实是:加密的连接用 ALPN,明文的 h2c 基本没人用——中间的代理大多不支持它, 而且明文本来也不该用在公网上。 WebSocket 是另一条路:它确实靠 Upgrade: websocket 起手,但那是应用层协议自己的约定, 跟 HTTP/2 的协商不是一回事。

四、动手 Demo:从 200 到 101,再跑一段非 HTTP 的字节

起一个只认 h2c 的服务端。先不带 Upgrade 请求一次,看它回 200; 再带 Upgrade 请求一次,拿到 101,然后在管子里发一个自定义协议的字节,看服务端回什么。

if hd.get(u'upgrade', u'').lower() == u'h2c':
    self.request.sendall(
        b'HTTP/1.1 101 Switching Protocols\r\n'
        b'Upgrade: h2c\r\n'
        b'Connection: Upgrade\r\n\r\n')
    while True:
        lb = self.request.recv(1)
        n = lb[0]
        payload = read_n(self.request, n)
        self.request.sendall(b'\x01' + bytes([n]) + payload.upper())
else:
    self.request.sendall(b'HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nno')

实跑输出:

--------------------------------------------------------------------
Upgrade 与 101:把 HTTP 连接改成别的协议
--------------------------------------------------------------------
  先不发 Upgrade,看服务端怎么回:
    响应                     HTTP/1.1 200 OK
    服务端收到的 Upgrade     (空)

  再发 Upgrade: h2c,走协商:
    响应                     HTTP/1.1 101 Switching Protocols
    Upgrade                  
    Connection               

    101 之后,这条连接上不再是 HTTP 
  文本 \x01\x05HELLO
  十六 01 05 48 45 4c 4c 4f
    发出去的                 b'hello'
    收回来的                 b'HELLO'
    说明                     服务端在新协议里把内容转成了大写
  两次协商                   Service 收到 2 次 Upgrade 首部

  Upgrade 是「我想换协议」的提议,服务端同意就回 101,
  并用自己的 Upgrade 首部说明换成什么。101 之后,
  这条 TCP 连接上跑的就不再是 HTTP 了。
  历史上 WebSocket 就是这么起来的;今天 HTTP/2 主要走 TLS 上的 ALPN 协商,
  h2c(明文 HTTP/2)这条路实际很少用——中间的代理基本都不支持。

第一次不带 Upgrade:服务端回 HTTP/1.1 200 OK,服务端收到的 Upgrade 是空的。 协商没发起,就没得谈。 第二次带 Connection: Upgrade 和 Upgrade: h2c:服务端回 HTTP/1.1 101 Switching Protocols, 并带上 Upgrade 和 Connection 两个首部。 101 之后客户端发 hello,收回来的是 HELLO。这五个字节的往返跟 HTTP 一点关系都没有了。 服务端一共收到两次 Upgrade 首部——一次空,一次 h2c。这就是「协商」的全部:提议、接受、切换。

30 秒实验

  1. 只发 Upgrade: h2c 不发 Connection: Upgrade,看服务端还认不认。
  2. 把服务端改成只认 websocket,重跑,看它是不是照常回 200。
  3. 想一想:为什么 101 之后不能再发 HTTP 请求?协议上靠什么保证这一点?

常见误区

  1. 「Upgrade 是让服务端升级版本」——是换协议,不是升级 HTTP 版本号。
  2. 「101 之后还能发 HTTP」——不能,连接已被新协议接管。
  3. 「HTTP/2 都靠 Upgrade 协商」——加密的走 ALPN,明文的 h2c 基本没人用。
自测三问(点开对答案) 1. 发起协议切换要带哪两个首部? —— `Connection: Upgrade` 和 `Upgrade: 协议名`。两个都要带。 2. 服务端同意时回什么状态码? —— 101 Switching Protocols,并在自己的 Upgrade 里说明换成了什么。 3. 为什么今天 HTTP/2 不用 Upgrade 协商? —— 因为它要等连接建立后再发一个 1.1 请求,多一个往返;TLS 的 ALPN 能在握手时一次说清。

协议切换讲完了。下一个字段是代理链的脚印——第五章已经见过它一次。

相关笔记