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

协议的对话模型只有两种角色:问的和答的。 真报文的结构与对话是镜像的——看懂一边,另一边照镜子就懂。

📝 本节和前面几节的关系

1.3.2 讲了没有协议会怎样,这一节换个更好懂的说法。 比方好用,但也有边界,第三节会把边界划清楚。

一、问一句、答一句

书里把通信比成人对话,是因为两者的骨架完全一样:

对话要素 网络里的对应 没了会怎样
谁先说 客户端发起连接 两边同时发,撞车
说什么 请求行:方法 路径 版本 对端不知道你要干什么
听懂没有 状态码 + 版本号 只能猜
说完没有 空行(两个 CRLF) 对端一直等下去

第一行「谁先说」是整套模型的地基:客户端与服务端的角色是固定的,不许互换。 所以「服务端主动推消息」才需要另想办法(长轮询、WebSocket), 因为原始模型里没有「服务端先开口」这个位置。 第四行那个「说完没有」看着琐碎,其实最容易出事: 没有结束标记,接收方就分不清「这条消息完了」还是「网慢了」。 分不清就只能等,等到超时为止。

二、四段式是镜像的

一条请求和一条应答,段落是一一对应的:

| 段 | 请求 | 应答 | |---| | 第一行 | 请求行:方法 路径 版本 | 状态行:版本 状态码 原因 | | 头部 | 名字: 值 | 名字: 值 | | 空行 | 1 个 CRLF | 1 个 CRLF | | 正文 | GET 一般没有 | 有(就是页面内容) |

这个对称不是巧合,是省事的做法: 解析器写一份,请求和应答都能用;排障时看懂一边,另一边自然就懂。 第三行那个 CRLF 是两个字节,不是「一个换行符」:回车(\r,0x0D)一个字节、换行(\n, 0x0A)一个字节。协议里明确规定必须成对出现,只发一个 \n 有些服务端会直接拒。

三、比方到哪里为止

人对话能含糊,协议不能,差别集中在三处:

能力 人 协议
改口 说错了能补一句 发出去的字节收不回
语气 语调能表达疑问 只能靠状态码
猜测 上下文能补全 缺一个字节就是错

第一行是两者最根本的分歧: 人说话是「流」,说错了可以回溯;网络是「包」,发出去就已经在对端缓冲区里了。 所以要「撤销」只能再发一个包去纠正——这就是各种确认、重传、事务回滚的由来。 第二行决定了协议必须把「情绪」翻译成数字: 人不高兴会用语气表达,服务端不高兴只能回一个 4xx 或 5xx。 看懂状态码,等于听懂了对端的语气。 所以比方是用来入门的,不能用来推理: 凡是涉及「字节数」「超时」「重传」的地方,必须回到协议文档。

四、动手 Demo:一条请求和一条应答,结构是镜像的

各发一条请求、收一条应答,数清两边的字节与段落。 重点是看「段」的对应关系,不是看数值。

import socket

req = (b'GET / HTTP/1.0\r\n'
       b'Host: example.com\r\n'
       b'User-Agent: demo\r\n'
       b'\r\n')
c = socket.create_connection(('www.example.com', 80), timeout=5)
c.sendall(req)
buf = b''
while True:
    d = c.recv(4096)
    if not d:
        break
    buf += d
c.close()

head, _, body = buf.partition(b'\r\n\r\n')
print('请求 %d 字节、应答 %d 字节' % (len(req), len(buf)))
print('应答头部 %d 字节、正文 %d 字节' % (len(head), len(body)))
print('应答第一行:%s' % head.split(b'\r\n')[0].decode('latin-1'))

实跑输出:

--------------------------------------------------------------------
一条请求和一条应答,结构是镜像的
--------------------------------------------------------------------
**书里用「人与人的对话」打比方:你问一句、我答一句。**
**真报文也是这个结构——请求行对状态行,头部对头部。**

请求 67 字节、应答 69 字节。

  段         请求                       应答
  ---------------------------------------------------------------
  第一行     请求行:方法 路径 版本     状态行:版本 状态码 原因
  头部       名字: 值                   名字: 值
  空行       1 个 CRLF                  1 个 CRLF
  正文       GET 一般没有               有(这里 5 字节)

**两边都是「四段式」,这就是协议设计里的对称性。**
**看懂一边,另一边照镜子就懂了。**

**注意 CRLF 是两个字节**(回车 + 换行),不是一行的结束符号那么简单:
**协议里明确规定必须是 CRLF,只写 LF 有些服务端会拒。**

请求 67 字节、应答 69 字节,量级差不多——因为两边的骨架一样长。 应答比请求多出来的那点,主要是状态行里的原因短语(200 OK 那段英文)。 正文是单独一段,用两个 CRLF 与头部隔开:这个空行不是排版,是协议规定的分隔符。 少了它,正文会被当成头部的一部分继续解析。 注意这条应答里 GET 没有正文,而应答有正文——表里第四行写的就是这个不对称。

30 秒实验

  1. 把请求里的 \r\n 全换成 \n 再发一次,看服务端还认不认——这就是「CRLF 必须成对」的实测。
  2. 数一下应答头部的行数,再算平均每行多少字节,感受一下「头部也是流量」。
  3. 发一条 HEAD / HTTP/1.0 的请求,对比它和 GET 的应答差在哪。

常见误区

  1. 以为「对话」意味着可以随时插话。 HTTP 是严格的一问一答,同一条连接上不排队就发第二条, 应答会对不上号。
  2. 以为空行可有可无。 两个 CRLF 是头和正文的分界,少了它整条报文都会解析错。
  3. 以为换行就是一个字符。 协议里的 CRLF 是两个字节,回车与换行是分开的两个控制字符。
自测三问(点开对答案) 1. 为什么 HTTP 里没有「服务端先开口」这个位置? —— **因为对话模型固定了角色:客户端问、服务端答。**服务端要主动推消息,只能另建机制(长轮询、 WebSocket)。 2. 请求与应答的「四段式」分别是哪四段? —— **第一行(请求行/状态行)、头部、空行、正文。**两边一一对应,所以解析器可以复用。 3. 「人说话能改口、网络不能」的底层原因是什么? —— **人说话是流,可以回溯;网络是包,发出去就在对端缓冲区里了。**要纠正只能再发一个包, 这就是确认与重传的由来。

下一节把比方收起来,直接看字节:一条能用的请求,最少要几个字节。

相关笔记