协议落到机器上,只剩三件事:字节怎么排、每个字段什么意思、谁先谁后。 一条能用的 HTTP 请求最短 7 个字节——但 7 字节的请求干不了今天的事。
1.3.3 用的是比方,这一节全部换成字节。 这一节的三分法(语法/语义/时序)是后面所有协议的总纲,值得记住。
一、协议的三件事
任何协议都能拆成三块,一块都不能少:
| 部分 | 管什么 | HTTP 里的例子 |
|---|---|---|
| 语法 | 字节怎么排、哪些字段必填 | 方法 路径 版本 三段用空格隔开 |
| 语义 | 每个字段什么意思 | GET 表示「取」而不是「改」 |
| 时序 | 谁先谁后、什么算完 | 先建连接、再发请求、最后关 |
第一行「语法」是最容易被当成全部的那块:很多人以为协议就是格式说明,其实格式只占三分之一。
格式对了但语义错的报文,照样是坏报文——比如把 POST 写成 GET,格式合法,语义完全变了。
第三行「时序」是最容易被忽略的那块:同样三个字节,发早了叫「抢跑」,发晚了叫「超时」。
很多「时好时坏」的故障,根子都在时序上,不在格式上。
二、一条请求最少几个字节
从最短的写法开始,一档一档往上加:
| 写法 | 字节数 | 多了什么 |
|---|---|---|
| HTTP/0.9 极简 | 7 | 只有 GET / 加一个 CRLF |
| 加版本 | 18 | HTTP/1.0 与第二个 CRLF |
| 加 Host | 27 | 虚拟主机必需的那一行 |
| 加 UA 与关闭 | 61 | 身份说明与 Connection: close |
7 个字节就能让 HTTP/0.9 的服务端把首页吐出来:
因为它只认「GET 加路径」,没有版本、没有头部、没有状态码,
应答直接把 HTML 从头灌到尾,靠关闭连接表示结束。
第二行加版本不是为了好看:版本号一进去,服务端才知道该用哪套规则答,
应答也就从「裸正文」变成了「状态行 + 头部 + 正文」。
第四行那个 Connection: close 是给 1.1 用的:
1.1 默认保持连接,不写这一行,服务端答完不会主动关,
客户端就得靠 Content-Length 判断正文到哪结束。
三、Host 为什么是强制的
因为一个 IP 上可能挂着几百个网站:
| 情形 | 没有 Host | 有 Host |
|---|---|---|
| 一台服务器一个站 | 勉强能跑 | 正常 |
| 一台服务器多个站 | 只能给默认那个 | 按名字分发 |
| 反向代理后面一堆站 | 没法分发 | 按名字转发 |
第二行就是「虚拟主机」:同一个 80 端口,靠 Host 头区分你要哪个域名。 这也是 HTTP/1.1 把 Host 列为唯一强制头部的原因—— 没有它,一台机器只能伺候一个网站,互联网的服务器数量要翻几十倍。 第三行解释了为什么现代架构里 Host 更重要:负载均衡器、网关、CDN 全靠这个字段决定往哪转。 Host 写错,请求会被转到完全不相干的后端, 而且返回的可能还是一个正常的 200——这种故障最难查。
四、字节数不只是数字
每加一个头部,代价不只是那几个字节:
| 代价 | 表现 | 量级 |
|---|---|---|
| 带宽 | 多传几个字节 | 小 |
| 解析 | 多切一次、多查一次表 | 中 |
| 出错面 | 多一个可能写错的字段 | 大 |
第三行才是重点:头部越多,能被写错的地方越多,而且很多错不会报错。 比如时间戳格式不对、编码名写错,服务端往往照收不误,然后在某个下游环节莫名其妙地炸。 所以协议设计一直在「加头部」与「减头部」之间拉扯: HTTP/2 用 HPACK 压缩头部,就是嫌头部太占地方;而各种自定义头又在不断往回加。
五、动手 Demo:一条能用的 HTTP 请求,最少要几个字节
手工拼四档请求,从 7 字节一路加到 61 字节。 真正的目的是看清「每加一行,多买到了什么能力」。
import socket
cases = [
('HTTP/0.9 极简', b'GET /\r\n'),
('加版本', b'GET / HTTP/1.0\r\n\r\n'),
('加 Host', b'GET / HTTP/1.0\r\nHost: example.com\r\n\r\n'),
('加 UA 与关闭', b'GET / HTTP/1.0\r\nHost: example.com\r\n'
b'User-Agent: demo\r\nConnection: close\r\n\r\n'),
]
for tag, raw in cases:
print('%-14s %3d 字节 CRLF %d 个'
% (tag, len(raw), raw.count(b'\r\n')))
c = socket.create_connection(('example.com', 80), timeout=5)
c.sendall(cases[1][1])
buf = c.recv(200)
c.close()
print('18 字节请求拿到的头一行:%s'
% buf.split(b'\r\n')[0].decode('latin-1'))
实跑输出:
--------------------------------------------------------------------
一条能用的 HTTP 请求,最少要几个字节
--------------------------------------------------------------------
**「计算机中的协议」讲到底就是「字节怎么排」。**下面从最短的开始,一档一档加内容。
写法 字节数 CRLF 个数
---------------------------------------
HTTP/0.9 极简 7 1
加版本 18 2
加 Host 27 3
加 UA 与关闭 61 5
**最短的 7 个字节就能让一个 HTTP/0.9 服务端把首页吐出来。**
**但它没有版本号、没有 Host,也就没法虚拟主机。**
**HTTP/1.1 强制要求 Host 头**,原因很实际:
**同一个 IP 上可能挂着几百个网站,服务端靠 Host 才知道你要哪个。**
四档分别是 7、18、27、61 字节,CRLF 个数 1、2、3、5。 CRLF 的数量和字节数是一起涨的——因为每个头行都得用 CRLF 收尾。 7 字节那档用的是 HTTP/0.9 语法:没有版本号那一格,服务端只能按最老的规则理解, 所以它回的也是不带状态行的裸正文。 18 字节那档现在已经极少有服务端接受: HTTP/1.1 强制 Host,主流服务器收到不带 Host 的 1.1 请求会回 400。 所以「最短」和「能用」在今天的互联网上是两回事。
30 秒实验
- 把
Host那行的域名改成www.baidu.com再发到 example.com 的 IP 上, 看返回的是哪个站点的页面——这就是虚拟主机的直接演示。 - 把
Connection: close去掉,看recv会不会一直等不到结束。 - 数一下你自己浏览器发一个首页请求时,头部有多少行、 共多少字节(开发者工具的「网络」面板能看到)。
常见误区
- 以为协议就是格式说明。 格式只占三分之一,语义与时序同样是协议的一部分。 格式对的报文也可能完全错。
- 以为字节越少越好。 少到一定程度就丢掉能力:没有 Host 就没法虚拟主机, 没有版本号就没法协商规则。
- 以为 Host 只是个可选的说明。 它是 HTTP/1.1 唯一的强制头部,写错会被转到别的后端, 还可能返回一个看似正常的 200。
自测三问(点开对答案)
1. 协议的三个组成部分是什么? —— **语法(字节怎么排)、语义(字段什么意思)、时序(谁先谁后)。**三者缺一不可——格式对、 语义错,报文照样是坏的。 2. 为什么 HTTP/1.1 强制要求 Host 头? —— **因为一个 IP 上可能挂几百个网站**,服务端只能靠 Host 决定给哪个站点的内容。 没有它,一台机器只能伺候一个站点。 3. 7 字节的 HTTP/0.9 请求为什么能工作,今天却不能用? —— **0.9 只认「GET 加路径」,应答是裸正文、靠关闭连接结束。**今天的主流服务端强制 Host, 收到不带 Host 的请求直接回 400。下一节讲「分组交换」——数据为什么要切成小包,切小了又要付什么代价。
- ☐ (本节完,下一节 1.3.5 分组交换协议)