📚 第八章 应用协议
📄 一句话概括

HTTP 是「请求—响应」的文本协议:请求行、头、空行、正文。 「无状态」是理解它的全部关键:每个请求都要重新自报家门。

📝 本节和前面几节的关系

这一节真发 GET 和 POST、真收 404、真复用连接,把请求和响应的字节数都量出来。 最后说清「无状态」带来了什么后果。

一、四个方法,一条请求行

请求行是三段:方法、路径、协议版本。 方法是动词:GET 取、POST 提交、PUT 覆盖、DELETE 删。 GET 和 POST 最重要的差别在「参数放哪儿」:GET 的参数在 URL 里,POST 的参数在正文里。 所以 POST 必须多一个 Content-Length,否则服务端不知道读到哪儿算完。 这个头缺失是「请求卡住」的经典原因——服务端一直在等正文,客户端以为已经发完了。 还有一条容易被忽略:GET 在语义上是「安全」的,也就是「不该有副作用」。 所以浏览器会预取 GET、缓存 GET、刷新时重发 GET。 如果你的 GET 会改数据(比如 ?action=delete),那预取和刷新都会造成事故。 这是「语义约定」被违反之后的典型后果。

二、三个数字的状态码

响应首行是「版本、状态码、原因短语」三段。 状态码第一位是分类:1xx 信息、2xx 成功、3xx 重定向、4xx 客户端错、5xx 服务端错。 这个分类的用处是「让客户端不必认识每一个具体码」: 见到 4xx 就知道「我这边有问题,重试也没用」;见到 5xx 就知道「对方有问题,等会儿可以再试」。 这和第 8.4.4 节 SMTP 的 4xx 与 5xx 分法是同一个思路, 因为「重试与否」是所有协议都要回答的问题。 404 和 403 常被混:404 是「没有这个东西」,403 是「有,但不给你看」。 有些系统故意把「无权访问」也回 404, 目的是不让攻击者通过「403 和 404 的差别」探测出哪些资源存在。 这是「安全优先于语义准确」的一个取舍。

三、无状态:理解 HTTP 的全部关键

「无状态」意味着服务端不记得上一个请求是谁发的。 所以每个请求都要重新自报家门。 Cookie、Token、Basic 认证,全都是为了给无状态协议补上「记忆」。 这也是为什么「退出登录」在 HTTP 里要靠客户端删掉凭证,而不是「通知服务端忘掉我」—— 服务端本来就没记住你。 无状态带来的好处是「扩展容易」:任何一个请求都可以落到任何一台服务器上, 不需要「会话粘滞」。 这就是为什么「加一台机器就能扛更多流量」在 Web 上成立,而在「有状态」的系统里要复杂得多。 代价是「每个请求都要带凭证」,所以头会变大,这也是 Cookie 和 Token 要尽量小的原因。

四、连接复用省的是握手

HTTP/1.1 默认 keep-alive:一条 TCP 连接上可以连着发多个请求。 省掉的是「每次都要三次握手」这件事。 在局域网里是零点几毫秒,在跨洋链路上是几百毫秒。 所以「连接复用」对远距离访问的改善特别明显。 但要注意它「不解决排队」:同一条连接上的请求还是串行的,前面的没回来,后面的就得等。 这一点在第 8.5.1 节的实测里能直接看出来——单连接复用的耗时几乎等于串行。 所以「keep-alive 等于变快」是个错觉:它只是「不再重复付握手成本」, 真正要解决排队,得靠多连接或者多路复用。

五、动手 Demo:HTTP 的真面目——两个方法、两种状态、一条连接

用裸 socket 发 GET 和 POST,再看 404 长什么样。 最后在一条连接上连发 3 个请求,验证复用。

import socket

for method, path in (('GET', '/a'), ('POST', '/a'), ('GET', '/missing')):
    s = socket.create_connection(('127.0.0.1', port), timeout=5)
    if method == 'POST':
        payload = b'x=1&y=2'
        req = ('POST %s HTTP/1.1\r\nHost: x\r\nContent-Length: %d\r\n'
               'Connection: close\r\n\r\n' % (path, len(payload))).encode()
        s.sendall(req + payload)
        req_len = len(req) + len(payload)
    else:
        req = ('%s %s HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n'
               % (method, path)).encode()
        s.sendall(req)
        req_len = len(req)
    buf = b''
    while True:
        c = s.recv(65536)
        if not c:
            break
        buf += c
    s.close()
    print(method, path, buf.split(b'\r\n')[0].decode(), req_len, len(buf))

实跑输出:

--------------------------------------------------------------------
HTTP 的真面目——两个方法、两种状态、一条连接
--------------------------------------------------------------------
**HTTP 是「请求—响应」的文本协议**:**一行请求行、一堆头、一个空行、然后是正文。**
**下面把 GET、POST、404 各跑一遍,再试连接复用。**

  方法     路径           响应首行                   请求字节     响应字节
  -------------------------------------------------------------------------
  GET      `/a`           HTTP/1.1 200 OK            47           209
  POST     `/a`           HTTP/1.1 200 OK            74           210
  GET      `/missing`     HTTP/1.1 404 Not Found     53           154

**看第四列:POST 的请求比 GET 多 27 字节**——**多出来的是 `Content-Length` 那一行和正文本身。**
**GET 的参数在 URL 里,POST 的参数在正文里。**
**所以 POST 必须多一个 `Content-Length`,否则服务端不知道读到哪儿算完。**
**这个头缺失是「请求卡住」的经典原因。**

**第三行是 404**:**注意它的响应体只有 9 个字节,头却有一百多。**
**「错误页比错误信息长」是 HTTP 的常态。**

**再看连接复用**:

**在一条 TCP 连接上连发 3 个请求,全部拿到响应:3 个。**
**如果每次都要新建连接,就要多付 3 次三次握手**——
**在局域网里是零点几毫秒,在跨洋链路上是几百毫秒。**

  特点             具体表现                 带来的后果
  --------------------------------------------------------------------------------------------
  **无状态**       服务端不记你是谁         **每个请求要自带全部信息(Cookie 就是干这个的)**
  **文本**         头和正文都是可读文本     **好调试,但比二进制费字节**
  **可扩展**       头能随便加               **新功能不用改协议版本**
  **请求响应**     一定是客户端先开口       **服务端不能主动推**

**第一行是理解 HTTP 的全部关键**:**「无状态」意味着服务端不记得上一个请求是谁发的。**
**所以每个请求都要重新自报家门**——
**Cookie、Token、Basic 认证,全都是为了给无状态协议补上「记忆」。**
**这也是为什么「退出登录」在 HTTP 里要靠客户端删掉凭证,而不是「通知服务端忘掉我」。**

GET 请求 47 字节,POST 74 字节,多出 27 字节—— 多出来的是 Content-Length 那一行和正文本身。 这就是「参数放正文」的代价:要多一行说明。 响应字节看第五列:200 是 209 字节,404 是 154 字节。 404 的正文只有 9 个字节,头却占了一百四十多。 「错误页比错误信息长」是 HTTP 的常态。 注意 404 也会带 Set-Cookie 和 X-Demo:因为那几个头是「每个响应都加」的中间件行为, 和状态码无关。 这一点在排错时很有用:如果连 404 都缺某个头,问题就在更前面(网关、代理),不在应用里。

30 秒实验

  1. 算一下:47 字节的 GET 请求里,真正的内容占几个字节?
  2. 想一下:GET 语义上「不该有副作用」,为什么?
  3. 想一下:为什么有些系统故意把「无权访问」回成 404?

常见误区

  1. 以为「POST 比 GET 安全」。 取决于是否加密,明文 HTTP 上两者都能被抓包。
  2. 以为「HTTP 会记住你是谁」。 它无状态,记忆全靠 Cookie 和 Token。
  3. 以为「keep-alive 能解决慢」。 它只省握手,同一条连接上照样排队。
自测三问(点开对答案) 1. GET 和 POST 的关键差别是什么? —— **参数位置**:GET 在 URL 里,POST 在正文里,后者要多一个 Content-Length。 2. 状态码第一位有什么用? —— **让客户端不必认识每个具体码就知道该不该重试。** 3. 无状态带来什么好处和代价? —— **好处是扩展容易**(请求可落到任意机器);**代价是每个请求都要带凭证。**

下一节看 Cookie 和 JavaScript 在这套体系里的位置。

相关笔记