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 秒实验
- 算一下:47 字节的 GET 请求里,真正的内容占几个字节?
- 想一下:GET 语义上「不该有副作用」,为什么?
- 想一下:为什么有些系统故意把「无权访问」回成 404?
常见误区
- 以为「POST 比 GET 安全」。 取决于是否加密,明文 HTTP 上两者都能被抓包。
- 以为「HTTP 会记住你是谁」。 它无状态,记忆全靠 Cookie 和 Token。
- 以为「keep-alive 能解决慢」。 它只省握手,同一条连接上照样排队。
自测三问(点开对答案)
1. GET 和 POST 的关键差别是什么? —— **参数位置**:GET 在 URL 里,POST 在正文里,后者要多一个 Content-Length。 2. 状态码第一位有什么用? —— **让客户端不必认识每个具体码就知道该不该重试。** 3. 无状态带来什么好处和代价? —— **好处是扩展容易**(请求可落到任意机器);**代价是每个请求都要带凭证。**下一节看 Cookie 和 JavaScript 在这套体系里的位置。
- ☐ (本节完,下一节 8.5.6 JavaScript、CGI、Cookie)