📘 第一章 了解Web及网络基础
📄 一句话概括

分层不是把功能切成四块摆着,而是定了一条规矩:每层只用自己的头跟对端同层交流,不越界。

📝 本节和前面几节的关系

上一节列出了四层和成员。 这一节说清「层」到底是什么, 以及那条边界为什么不能随便破——破了的后果在第七章讲 TLS 时还会再看一次。

一、层的边界就是「头的边界」

每一层做的事,最后都体现为在数据前面加一段头。 TCP 头里有端口和序号,IP 头里有地址和 TTL,以太网头里有 MAC。 接收端判断「这属于哪一层」的唯一依据,就是当前在处理哪一段头。 剥完以太网头交给 IP,剥完 IP 头交给 TCP,剥完 TCP 头才是应用数据。这条链一旦错位,后面全乱。 所以「分层」在工程上等价于:别去读不属于你这一层的字节。

二、越界的两种典型后果

第一种是中间设备擅自改内容。 某些老代理会改写 Content-Length、重排首部顺序,表面上无害,实际上让签名、校验、缓存全部失效。 第二种是上层假设下层的细节。 应用代码里写死「一次 recv 能拿到完整响应」, 就是在假设 TCP 的边界等于 HTTP 的边界——而 TCP 是字节流,不保留边界。 这个坑在本课之后的 Demo 里会反复出现。 反过来,下层也不该管上层的语义。 IP 不知道这段字节是图片还是网页,所以它没法做「按内容优先转发」——这件事只能由上层自己做。

三、实测:换掉下面一层,上面一行不改

Demo 里同一段 64 字节的请求,分别用 IPv4 和 IPv6 解析地址,应用层代码一个字都没动, 服务端照旧回 200。 这就是分层的收益:地址族是可以换的,GET 是不用换的。 现实里从 IPv4 迁到 IPv6 花了几十年,难的不是改协议,是全世界每一台设备的配置都要跟着动。

四、动手 Demo:换下面一层,应用层代码一个字不改

同一段请求字节,先用 IPv4 解析、再用 IPv6 解析,各看能拿到几条候选; 然后原样发出去,看响应有没有区别。

for fam, name in ((socket.AF_INET, u'IPv4'),
                  (socket.AF_INET6, u'IPv6')):
    infos = socket.getaddrinfo(u'localhost', 80, fam,
                               socket.SOCK_STREAM)
    print(name, len(infos), infos[0][4][0])
with H.server() as port:
    raw = H.send(port, payload)   # payload 还是那 64 字节

实跑输出:

--------------------------------------------------------------------
换掉下面一层,上面一行代码都不用改
--------------------------------------------------------------------
  IPv4                       解析出 1 条候选,首条 127.0.0.1
  IPv6                       解析出 1 条候选,首条 ::1

  应用层代码                 没变过一行,仍然是 64 字节的同一个请求
  服务端回的                 HTTP/1.1 200 OK,正文 40 字节
  分层的意义就在这一屏:下面换协议、换网卡、换地址族,上面那句 GET 一直有效。
  反过来说——分层也有代价,每层都要加自己的头,越往下越看不见业务。

两行输出各 1 条候选:IPv4 是 127.0.0.1,IPv6 是 ::1。 名字一样,地址完全不同,而上面的请求字节一模一样。 应用层代码 没变过一行——这句话就是分层最直接的证据。 也顺便暴露了 D001 里那个坑:两个地址族都在,试错就要付超时的账。

30 秒实验

  1. 把 payload 里的 Host: 127.0.0.1 改成 Host: localhost,看服务端还认不认(它只记字节, 所以照收)。
  2. 用 socket.setsockopt 把 TCP_NODELAY 打开,再发一次,想想它改的是哪一层的行为。
  3. 写一行 s.recv(1) 只收一个字节,看会不会把响应切断——这就是「TCP 不保留应用层边界」的实证。

常见误区

  1. 「分层是理论模型」——它是实现里真实存在的代码层次,socket、TCP_* 选项、 IP_* 选项就是各层的开关。
  2. 「上层可以随便假设下层行为」——TCP 是字节流,一次 recv 拿到的字节数跟应用报文毫无关系。
  3. 「层越多越慢」——层多确实多几次内存拷贝,但现代内核用零拷贝把这件事压得很小。
自测三问(点开对答案) 1. 接收端怎么知道当前处理的是哪一层? —— 按剥头的顺序:每剥掉一段已知长度的头,剩下的就交给上一层。 2. 为什么「一次 recv 拿到完整响应」是错的假设? —— TCP 是字节流,不保留应用层报文边界;一次 recv 可能只拿到半个响应。 3. 分层最怕的越界是哪一种? —— 中间设备擅自改写不属于它这一层的字节,比如改 Content-Length 或重排首部。

层与层的规矩定了。下一节跟着一个字节走完全程,看它被包了几次、剥了几次。

相关笔记