📘 第二章 简单的HTTP协议
📄 一句话概括

请求和响应都是四段结构:首行 + 若干首部 + 一个空行 + 正文。 分界全靠 \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 秒实验

  1. 把 Content-Length 故意写成 10(实际 18),重跑, 看服务端会不会把后面 8 字节当成下一个请求的开头。
  2. 在请求里加一个空首部(X-Empty: 后面什么都不写),看解析结果。
  3. 把状态行改成 HTTP/1.1 201(去掉原因短语),看客户端认不认。

常见误区

  1. 「原因短语有用」——程序只认状态码,短语纯给人看。
  2. 「首部里可以有空行」——空行就是结束标志,正文里出现空行不违法, 但首部区里出现空行就等于提前结束。
  3. 「正文长度由服务端自己数」——必须显式声明,否则接收方不知道读到哪为止。
自测三问(点开对答案) 1. 请求和响应的结构差别在哪? —— 只在首行:请求是「方法 目标 版本」,响应是「版本 状态码 短语」。后面三段完全一样。 2. 为什么 `Content-Length` 算错会导致串包? —— 因为接收方按它决定正文读多长。算少了,剩下的字节就被当成下一个响应的开头。 3. HTTP/2 为什么不用文本了? —— 文本靠空行分段、靠人算长度,容易出错也难压缩; 二进制帧自带长度字段,还能做首部压缩和多路复用。

报文格式有了。下一节说这套「一问一答」的协议为什么不记人。

相关笔记