请求和响应都是四段结构:首行 + 若干首部 + 一个空行 + 正文。
分界全靠 \r\n,少一个字节就串包。
上一节分了角色,这一节看它们说的话。第三章会把每一段再拆到字段,这里先立骨架。
一、请求的四段
① 请求行:方法、请求目标、版本,三段用空格分开。
② 首部:一行一个「名字: 值」,大小写不敏感。③ 空行:只有一个 CRLF,标志首部结束。
④ 正文:可以没有;有的话长度必须由 Content-Length 或 Transfer-Encoding 说明。
Demo 里那个 POST 请求是 129 字节:请求行 26、三个首部 76 左右、空行 2、正文 18。
小请求里首部的量往往超过正文。
二、响应的四段,只差首行
响应把请求行换成状态行:版本、状态码、原因短语。
HTTP/1.1 201 Created —— 201 是码,Created 只是给人看的说明。
原因短语没有任何程序意义。服务端可以写任意文字甚至留空,客户端只认那个三位数字。
这一点在 HTTP/2 里被彻底去掉——状态行只留数字,短语删了。
响应首部在 Demo 里有 4 行:Content-Type、Location、Content-Length、Connection。
其中 Location 是 201/3XX 才有的,告诉客户端「东西建在哪」或「该跳去哪」。
三、分界符是这套协议唯一的「标点」
HTTP/1.1 是纯文本,靠空行分段。
这意味着正文里不能出现没有被声明的空行边界——一旦 Content-Length 算错,
接收方就会把正文的一部分当成下一个响应的开头。
这就是「串包」。线上排查时看到「响应里混进了别的内容」,九成是长度算错,或者两层代理各改了一次。
HTTP/2 换成了二进制帧,每个帧自带长度字段,从根上消灭了这个问题——代价是人再也读不懂抓包内容了。
四、动手 Demo:拆一份真请求与真响应
发一个带正文的 POST,再用同一个解析函数把请求和响应各拆成四段,逐段打印长度。
body = u'name=宜昌&age=30'.encode()
req = _req(u'POST', u'/submit',
extra=[(u'Content-Type',
u'application/x-www-form-urlencoded')],
body=body)
first, hd, bd = H.parse_headers(req)
print(u'① 请求行', first)
print(u'④ 正文', len(bd))
实跑输出:
--------------------------------------------------------------------
一份请求长什么样:四段结构
--------------------------------------------------------------------
① 请求行 POST /submit HTTP/1.1
② 首部行数 3 行
host 127.0.0.1
content-type application/x-www-form-urlencoded
content-length 18
③ 空行 2 字节 CRLF,紧跟首部之后
④ 正文 18 字节「name=宜昌&age=30」
整个请求 129 字节
响应状态行 HTTP/1.1 201 Created
响应首部 4 行
响应正文 9 字节
四段的分界全靠 `\r\n`:行内用 **两个字节** 分隔,首部与正文之间 **空一行**。
少一个 CR 就变成半个分隔符,接收方会一直等下去——这是最典型的「串包」。
请求有请求行、响应有状态行,位置一样、格式不同,这是两者唯一的结构差别。
整个请求 129 字节,④ 正文 18 字节——正文只占 14%。这个比例在 POST 表单里很典型。
响应首部 4 行、响应正文 9 字节。
Location: /result/1 是 201 的标配,它让客户端知道新资源在哪,不用再猜。
注意 ② 首部行数 3 行 里键都被转成小写了——这是 parse_headers 的处理,也提醒一句:
首部名大小写不敏感,值则通常敏感(除了少数枚举型字段)。
30 秒实验
- 把
Content-Length故意写成 10(实际 18),重跑, 看服务端会不会把后面 8 字节当成下一个请求的开头。 - 在请求里加一个空首部(
X-Empty:后面什么都不写),看解析结果。 - 把状态行改成
HTTP/1.1 201(去掉原因短语),看客户端认不认。
常见误区
- 「原因短语有用」——程序只认状态码,短语纯给人看。
- 「首部里可以有空行」——空行就是结束标志,正文里出现空行不违法, 但首部区里出现空行就等于提前结束。
- 「正文长度由服务端自己数」——必须显式声明,否则接收方不知道读到哪为止。
自测三问(点开对答案)
1. 请求和响应的结构差别在哪? —— 只在首行:请求是「方法 目标 版本」,响应是「版本 状态码 短语」。后面三段完全一样。 2. 为什么 `Content-Length` 算错会导致串包? —— 因为接收方按它决定正文读多长。算少了,剩下的字节就被当成下一个响应的开头。 3. HTTP/2 为什么不用文本了? —— 文本靠空行分段、靠人算长度,容易出错也难压缩; 二进制帧自带长度字段,还能做首部压缩和多路复用。报文格式有了。下一节说这套「一问一答」的协议为什么不记人。
- ☐ (本节完,下一节 2.3 HTTP是不保存状态的协议)