📚 第一章 网络基础知识
📄 一句话概括

协议落到机器上,只剩三件事:字节怎么排、每个字段什么意思、谁先谁后。 一条能用的 HTTP 请求最短 7 个字节——但 7 字节的请求干不了今天的事。

📝 本节和前面几节的关系

1.3.3 用的是比方,这一节全部换成字节。 这一节的三分法(语法/语义/时序)是后面所有协议的总纲,值得记住。

一、协议的三件事

任何协议都能拆成三块,一块都不能少:

部分 管什么 HTTP 里的例子
语法 字节怎么排、哪些字段必填 方法 路径 版本 三段用空格隔开
语义 每个字段什么意思 GET 表示「取」而不是「改」
时序 谁先谁后、什么算完 先建连接、再发请求、最后关

第一行「语法」是最容易被当成全部的那块:很多人以为协议就是格式说明,其实格式只占三分之一。 格式对了但语义错的报文,照样是坏报文——比如把 POST 写成 GET,格式合法,语义完全变了。 第三行「时序」是最容易被忽略的那块:同样三个字节,发早了叫「抢跑」,发晚了叫「超时」。 很多「时好时坏」的故障,根子都在时序上,不在格式上。

二、一条请求最少几个字节

从最短的写法开始,一档一档往上加:

写法 字节数 多了什么
HTTP/0.9 极简 7 只有 GET / 加一个 CRLF
加版本 18 HTTP/1.0 与第二个 CRLF
加 Host 27 虚拟主机必需的那一行
加 UA 与关闭 61 身份说明与 Connection: close

7 个字节就能让 HTTP/0.9 的服务端把首页吐出来: 因为它只认「GET 加路径」,没有版本、没有头部、没有状态码, 应答直接把 HTML 从头灌到尾,靠关闭连接表示结束。 第二行加版本不是为了好看:版本号一进去,服务端才知道该用哪套规则答, 应答也就从「裸正文」变成了「状态行 + 头部 + 正文」。 第四行那个 Connection: close 是给 1.1 用的: 1.1 默认保持连接,不写这一行,服务端答完不会主动关, 客户端就得靠 Content-Length 判断正文到哪结束。

三、Host 为什么是强制的

因为一个 IP 上可能挂着几百个网站:

情形 没有 Host 有 Host
一台服务器一个站 勉强能跑 正常
一台服务器多个站 只能给默认那个 按名字分发
反向代理后面一堆站 没法分发 按名字转发

第二行就是「虚拟主机」:同一个 80 端口,靠 Host 头区分你要哪个域名。 这也是 HTTP/1.1 把 Host 列为唯一强制头部的原因—— 没有它,一台机器只能伺候一个网站,互联网的服务器数量要翻几十倍。 第三行解释了为什么现代架构里 Host 更重要:负载均衡器、网关、CDN 全靠这个字段决定往哪转。 Host 写错,请求会被转到完全不相干的后端, 而且返回的可能还是一个正常的 200——这种故障最难查。

四、字节数不只是数字

每加一个头部,代价不只是那几个字节:

代价 表现 量级
带宽 多传几个字节 小
解析 多切一次、多查一次表 中
出错面 多一个可能写错的字段 大

第三行才是重点:头部越多,能被写错的地方越多,而且很多错不会报错。 比如时间戳格式不对、编码名写错,服务端往往照收不误,然后在某个下游环节莫名其妙地炸。 所以协议设计一直在「加头部」与「减头部」之间拉扯: HTTP/2 用 HPACK 压缩头部,就是嫌头部太占地方;而各种自定义头又在不断往回加。

五、动手 Demo:一条能用的 HTTP 请求,最少要几个字节

手工拼四档请求,从 7 字节一路加到 61 字节。 真正的目的是看清「每加一行,多买到了什么能力」。

import socket

cases = [
    ('HTTP/0.9 极简', b'GET /\r\n'),
    ('加版本', b'GET / HTTP/1.0\r\n\r\n'),
    ('加 Host', b'GET / HTTP/1.0\r\nHost: example.com\r\n\r\n'),
    ('加 UA 与关闭', b'GET / HTTP/1.0\r\nHost: example.com\r\n'
                    b'User-Agent: demo\r\nConnection: close\r\n\r\n'),
]
for tag, raw in cases:
    print('%-14s %3d 字节  CRLF %d 个'
          % (tag, len(raw), raw.count(b'\r\n')))

c = socket.create_connection(('example.com', 80), timeout=5)
c.sendall(cases[1][1])
buf = c.recv(200)
c.close()
print('18 字节请求拿到的头一行:%s'
      % buf.split(b'\r\n')[0].decode('latin-1'))

实跑输出:

--------------------------------------------------------------------
一条能用的 HTTP 请求,最少要几个字节
--------------------------------------------------------------------
**「计算机中的协议」讲到底就是「字节怎么排」。**下面从最短的开始,一档一档加内容。

  写法              字节数     CRLF 个数
  ---------------------------------------
  HTTP/0.9 极简     7          1
  加版本            18         2
  加 Host           27         3
  加 UA 与关闭      61         5

**最短的 7 个字节就能让一个 HTTP/0.9 服务端把首页吐出来。**
**但它没有版本号、没有 Host,也就没法虚拟主机。**

**HTTP/1.1 强制要求 Host 头**,原因很实际:
**同一个 IP 上可能挂着几百个网站,服务端靠 Host 才知道你要哪个。**

四档分别是 7、18、27、61 字节,CRLF 个数 1、2、3、5。 CRLF 的数量和字节数是一起涨的——因为每个头行都得用 CRLF 收尾。 7 字节那档用的是 HTTP/0.9 语法:没有版本号那一格,服务端只能按最老的规则理解, 所以它回的也是不带状态行的裸正文。 18 字节那档现在已经极少有服务端接受: HTTP/1.1 强制 Host,主流服务器收到不带 Host 的 1.1 请求会回 400。 所以「最短」和「能用」在今天的互联网上是两回事。

30 秒实验

  1. 把 Host 那行的域名改成 www.baidu.com 再发到 example.com 的 IP 上, 看返回的是哪个站点的页面——这就是虚拟主机的直接演示。
  2. 把 Connection: close 去掉,看 recv 会不会一直等不到结束。
  3. 数一下你自己浏览器发一个首页请求时,头部有多少行、 共多少字节(开发者工具的「网络」面板能看到)。

常见误区

  1. 以为协议就是格式说明。 格式只占三分之一,语义与时序同样是协议的一部分。 格式对的报文也可能完全错。
  2. 以为字节越少越好。 少到一定程度就丢掉能力:没有 Host 就没法虚拟主机, 没有版本号就没法协商规则。
  3. 以为 Host 只是个可选的说明。 它是 HTTP/1.1 唯一的强制头部,写错会被转到别的后端, 还可能返回一个看似正常的 200。
自测三问(点开对答案) 1. 协议的三个组成部分是什么? —— **语法(字节怎么排)、语义(字段什么意思)、时序(谁先谁后)。**三者缺一不可——格式对、 语义错,报文照样是坏的。 2. 为什么 HTTP/1.1 强制要求 Host 头? —— **因为一个 IP 上可能挂几百个网站**,服务端只能靠 Host 决定给哪个站点的内容。 没有它,一台机器只能伺候一个站点。 3. 7 字节的 HTTP/0.9 请求为什么能工作,今天却不能用? —— **0.9 只认「GET 加路径」,应答是裸正文、靠关闭连接结束。**今天的主流服务端强制 Host, 收到不带 Host 的请求直接回 400。

下一节讲「分组交换」——数据为什么要切成小包,切小了又要付什么代价。

相关笔记