协议的对话模型只有两种角色:问的和答的。 真报文的结构与对话是镜像的——看懂一边,另一边照镜子就懂。
1.3.2 讲了没有协议会怎样,这一节换个更好懂的说法。 比方好用,但也有边界,第三节会把边界划清楚。
一、问一句、答一句
书里把通信比成人对话,是因为两者的骨架完全一样:
| 对话要素 | 网络里的对应 | 没了会怎样 |
|---|---|---|
| 谁先说 | 客户端发起连接 | 两边同时发,撞车 |
| 说什么 | 请求行:方法 路径 版本 | 对端不知道你要干什么 |
| 听懂没有 | 状态码 + 版本号 | 只能猜 |
| 说完没有 | 空行(两个 CRLF) | 对端一直等下去 |
第一行「谁先说」是整套模型的地基:客户端与服务端的角色是固定的,不许互换。 所以「服务端主动推消息」才需要另想办法(长轮询、WebSocket), 因为原始模型里没有「服务端先开口」这个位置。 第四行那个「说完没有」看着琐碎,其实最容易出事: 没有结束标记,接收方就分不清「这条消息完了」还是「网慢了」。 分不清就只能等,等到超时为止。
二、四段式是镜像的
一条请求和一条应答,段落是一一对应的:
| 段 | 请求 | 应答 | |---| | 第一行 | 请求行:方法 路径 版本 | 状态行:版本 状态码 原因 | | 头部 | 名字: 值 | 名字: 值 | | 空行 | 1 个 CRLF | 1 个 CRLF | | 正文 | GET 一般没有 | 有(就是页面内容) |
这个对称不是巧合,是省事的做法:
解析器写一份,请求和应答都能用;排障时看懂一边,另一边自然就懂。
第三行那个 CRLF 是两个字节,不是「一个换行符」:回车(\r,0x0D)一个字节、换行(\n,
0x0A)一个字节。协议里明确规定必须成对出现,只发一个 \n 有些服务端会直接拒。
三、比方到哪里为止
人对话能含糊,协议不能,差别集中在三处:
| 能力 | 人 | 协议 |
|---|---|---|
| 改口 | 说错了能补一句 | 发出去的字节收不回 |
| 语气 | 语调能表达疑问 | 只能靠状态码 |
| 猜测 | 上下文能补全 | 缺一个字节就是错 |
第一行是两者最根本的分歧: 人说话是「流」,说错了可以回溯;网络是「包」,发出去就已经在对端缓冲区里了。 所以要「撤销」只能再发一个包去纠正——这就是各种确认、重传、事务回滚的由来。 第二行决定了协议必须把「情绪」翻译成数字: 人不高兴会用语气表达,服务端不高兴只能回一个 4xx 或 5xx。 看懂状态码,等于听懂了对端的语气。 所以比方是用来入门的,不能用来推理: 凡是涉及「字节数」「超时」「重传」的地方,必须回到协议文档。
四、动手 Demo:一条请求和一条应答,结构是镜像的
各发一条请求、收一条应答,数清两边的字节与段落。 重点是看「段」的对应关系,不是看数值。
import socket
req = (b'GET / HTTP/1.0\r\n'
b'Host: example.com\r\n'
b'User-Agent: demo\r\n'
b'\r\n')
c = socket.create_connection(('www.example.com', 80), timeout=5)
c.sendall(req)
buf = b''
while True:
d = c.recv(4096)
if not d:
break
buf += d
c.close()
head, _, body = buf.partition(b'\r\n\r\n')
print('请求 %d 字节、应答 %d 字节' % (len(req), len(buf)))
print('应答头部 %d 字节、正文 %d 字节' % (len(head), len(body)))
print('应答第一行:%s' % head.split(b'\r\n')[0].decode('latin-1'))
实跑输出:
--------------------------------------------------------------------
一条请求和一条应答,结构是镜像的
--------------------------------------------------------------------
**书里用「人与人的对话」打比方:你问一句、我答一句。**
**真报文也是这个结构——请求行对状态行,头部对头部。**
请求 67 字节、应答 69 字节。
段 请求 应答
---------------------------------------------------------------
第一行 请求行:方法 路径 版本 状态行:版本 状态码 原因
头部 名字: 值 名字: 值
空行 1 个 CRLF 1 个 CRLF
正文 GET 一般没有 有(这里 5 字节)
**两边都是「四段式」,这就是协议设计里的对称性。**
**看懂一边,另一边照镜子就懂了。**
**注意 CRLF 是两个字节**(回车 + 换行),不是一行的结束符号那么简单:
**协议里明确规定必须是 CRLF,只写 LF 有些服务端会拒。**
请求 67 字节、应答 69 字节,量级差不多——因为两边的骨架一样长。
应答比请求多出来的那点,主要是状态行里的原因短语(200 OK 那段英文)。
正文是单独一段,用两个 CRLF 与头部隔开:这个空行不是排版,是协议规定的分隔符。
少了它,正文会被当成头部的一部分继续解析。
注意这条应答里 GET 没有正文,而应答有正文——表里第四行写的就是这个不对称。
30 秒实验
- 把请求里的
\r\n全换成\n再发一次,看服务端还认不认——这就是「CRLF 必须成对」的实测。 - 数一下应答头部的行数,再算平均每行多少字节,感受一下「头部也是流量」。
- 发一条
HEAD / HTTP/1.0的请求,对比它和 GET 的应答差在哪。
常见误区
- 以为「对话」意味着可以随时插话。 HTTP 是严格的一问一答,同一条连接上不排队就发第二条, 应答会对不上号。
- 以为空行可有可无。 两个 CRLF 是头和正文的分界,少了它整条报文都会解析错。
- 以为换行就是一个字符。 协议里的 CRLF 是两个字节,回车与换行是分开的两个控制字符。
自测三问(点开对答案)
1. 为什么 HTTP 里没有「服务端先开口」这个位置? —— **因为对话模型固定了角色:客户端问、服务端答。**服务端要主动推消息,只能另建机制(长轮询、 WebSocket)。 2. 请求与应答的「四段式」分别是哪四段? —— **第一行(请求行/状态行)、头部、空行、正文。**两边一一对应,所以解析器可以复用。 3. 「人说话能改口、网络不能」的底层原因是什么? —— **人说话是流,可以回溯;网络是包,发出去就在对端缓冲区里了。**要纠正只能再发一个包, 这就是确认与重传的由来。下一节把比方收起来,直接看字节:一条能用的请求,最少要几个字节。
- ☐ (本节完,下一节 1.3.4 计算机中的协议)