标准化统一的是「行为」,不是「写法」——同一件事可以有好几种合规的实现。 标准文档里的 MUST/SHOULD/MAY 三档,决定了「不合规」到底有多严重。
1.4.1 讲了谁在定标准,这一节讲「标准定下来之后管什么、不管什么」。 这一节会用到 HTTP/1.1 与 HTTP/2 的对比,1.5 之后会从分层角度再讲一次。
一、标准统一了什么
标准管的是「对端能看到的行为」,不管「内部怎么实现」:
| 对象 | 标准管不管 | 例子 |
|---|---|---|
| 报文格式 | 管 | 请求行三段、CRLF 结尾 |
| 状态码语义 | 管 | 404 就是「找不到」 |
| 内部数据结构 | 不管 | 用哈希表还是红黑树随便 |
| 编程语言 | 不管 | C 写的和 Go 写的能互通 |
第三、四行是标准化的分寸感所在:只要对外行为一致,里面怎么折腾都行。 正因为不管内部,各家才能在自己的场景里优化—— nginx 和 Apache 对同一个请求回一样的状态码,但内部结构完全不同。 第一行那个「CRLF 结尾」是典型的「必须精确」的例子:差一个字节就可能被拒。 而「状态码语义」是「意思要对」的例子:你可以加自己的原因短语,但 404 的意思不能改。
二、三档要求
RFC 里的关键词是有法律效力级别的(RFC 2119 定义):
| 英文 | 中文 | 违反的后果 |
|---|---|---|
| MUST | 必须 | 就是不合规,别人可以不兼容你 |
| SHOULD | 应当 | 有正当理由可以不做 |
| MAY | 可以 | 做不做都合规 |
第一行解释了为什么很多「小毛病」会导致连不上:比如 HTTP/1.1 的 Host 是 MUST, 不发就是不合规,服务端回 400 完全正当。 第二行是最容易被忽略的一档:「应当」意味着「不做要能说出理由」, 所以有些老实现为了兼容,会故意违反 SHOULD——这不是 bug,是权衡,但得写进文档。 读协议文档时先找这三个词,它们标出了「硬边界」与「软建议」的分界, 比通读全文效率高得多。
三、标准会被换掉
同一组语义,可以有两种完全不同的写法:
| 版本 | 头部编码 | 四个头的字节数 | |---|---| | HTTP/1.1 | 纯文本,一行一个 | 65 | | HTTP/2 | 二进制 + 静态表索引 | 4 |
从 65 字节压到 4 字节,靠的是「双方都认得同一张表」:
GET、https、/、example.com 这些最常见的内容,在静态表里有固定编号,直接发编号就行。
注意这里「语义一个字没改」:请求还是请求,头还是头,404 还是 404。
改的只是编码方式——这正是标准化的好处:换编码不用换应用。
所以升到 HTTP/2 不需要改业务代码,这也是它能推得动的原因。
反过来,任何要求「应用层跟着大改」的协议升级,基本都推不动。
四、动手 Demo:标准化之后,同一件事有两种写法
把同一组 HTTP 头部,分别按 1.1 的纯文本和 2 的静态表索引编码。 数一下字节差多少。
hdrs = [(':method', 'GET'), (':scheme', 'https'),
(':path', '/'), (':authority', 'example.com')]
text = ''.join('%s: %s\r\n' % h for h in hdrs)
print('HTTP/1.1 纯文本 %d 字节' % len(text.encode('latin-1')))
# RFC 7541 附录 A 的静态表里,这四项的索引是固定的
STATIC = {(':authority', ''): 1, (':method', 'GET'): 2,
(':path', '/'): 4, (':scheme', 'https'): 7}
tot = 0
for k, v in hdrs:
i = STATIC.get((k, v), 0)
n = 1 if i else len(('%s: %s' % (k, v)).encode()) + 1
tot += n
print('%-12s 索引 %-3d 编码 %d 字节' % (k, i, n))
print('HTTP/2 编码后共 %d 字节' % tot)
实跑输出:
--------------------------------------------------------------------
标准化之后,同一件事有两种写法
--------------------------------------------------------------------
**HTTP/1.1 的头部是纯文本,HTTP/2 改成了二进制加索引表。**同一组头部,两种写法的字节数差得很远。
HTTP/1.1 写法(纯文本,一行一个头)共 65 字节:
:method: GET
:scheme: https
:path: /
:authority: example.com
HTTP/2 用 HPACK 静态表,前四项都有固定索引:
头部 静态表索引 编码后字节
--------------------------------------------
:authority 1 1
:method GET 2 1
:path / 4 1
:scheme https 7 1
**四个头从 65 字节压到 4 字节**(每项一个索引字节,最高位为 1 表示「全索引」)。
**这就是「标准化」的另一面**:**标准不只统一了写法,还留了「换一种更省的写法」的余地。**
**HTTP/1.1 到 HTTP/2 换了编码方式,但语义一个字没改——请求还是请求,头还是头。**
65 字节对 4 字节,差了十六倍。
这不是压缩算法的功劳,是「预先约好的表」的功劳——
双方都存着同一张 61 项的静态表,编号就代表内容。
注意这四项的索引是 1、2、4、7,不是 1、2、3、4:静态表的顺序是 RFC 定死的,
中间几项被别的头占着。所以「索引号」本身也是标准的一部分,不能自己排。
第一行那个纯文本长度 65 是在没有 Host 域名的前提下算的:
真实请求里光一个 User-Agent 就可能上百字节,所以头部压缩在移动网络上收益更明显。
30 秒实验
- 把
:authority的域名换成www.baidu.com,看 1.1 的字节数变多少、 2 的变多少——想想为什么 2 不变。 - 查 RFC 7541 附录 A 的静态表,数出一共多少项。
- 想一个「语义不变、写法可以大改」的其他例子(提示:编码、序列化格式都算)。
常见误区
- 以为标准化就是「只有一种正确实现」。 标准约束的是对外行为,内部结构、语言、 性能优化全都不管。
- 以为 MUST 和 SHOULD 只是措辞差别。 它们是合规性级别:违反 MUST 就是不合规, 别人有权不兼容你。
- 以为协议升级必然要改应用代码。 HTTP/2 换了编码没换语义, 业务代码一行不用动——这也正是它能推得动的原因。
自测三问(点开对答案)
1. 标准管什么、不管什么? —— **管对外行为:报文格式、状态码语义、时序。** **不管内部:数据结构、编程语言、性能优化。**正因为不管内部,各家实现才能共存。 2. MUST、SHOULD、MAY 的区别是什么? —— **MUST 是硬要求,违反即不合规;SHOULD 是「有理由可以不做」;MAY 是完全可选。** 读协议时先找这三个词,效率最高。 3. HTTP/2 把四个头部从 65 字节压到 4 字节,靠的是什么? —— **靠双方预先约好的静态表**:常见头在表里有固定索引,直接发编号。 **语义没变,只换了编码**,所以应用代码不用改。下一节进入 1.5:分层——讲清楚为什么要分层、分层怎么让「换一层不影响其他层」成立。
- ☐ (本节完,下一节 1.5.1 协议的分层)