分层的操作定义只有一句话:把上一层的整包当成自己的数据,在前面加自己的头。 分层买到的不是性能,是「换一层不影响其他层」的自由。
1.4 讲完了标准由谁定,1.5 进入本书最核心的一张图:分层。 这一节先讲「分层到底是怎么做的」,1.5.2 用对话打比方,1.5.3 才给 OSI 七层。
一、不分层会怎样
假设只有一个协议管所有事:
| 问题 | 具体表现 |
|---|---|
| 改一处全崩 | 换个网卡,上层代码全要重写 |
| 没法复用 | 换个应用,底下全要重做 |
| 没法分工 | 一个人得懂编码、路由、电气 |
第一行是最致命的: 不分层的话,「换网卡」和「换浏览器」是同一件事——因为它们在同一段代码里。 分层之后,换网卡只动最底下那层,上面一行不改。 第三行说的是人的问题,不只是代码的问题:分层把「懂光纤的人」和「懂网页的人」隔开, 让他们能各自深入,不需要互相懂。 这是分层最被低估的收益。
二、分层的操作定义
不用记图,记住这一个动作就够了:
应用层 [ HTTP 报文 ]
传输层 [ TCP 头 ][ TCP 头后面的整包 ]
网络层 [ IP 头 ][ 上层整包 ]
链路层 [ 以太网头 ][ 上层整包 ]
用一条 27 字节的 HTTP 报文走一遍:
| 层 | 本层名字 | 本层加的头 | 累计长度 |
|---|---|---|---|
| 应用层 | HTTP 报文 | — | 27 |
| 传输层 | TCP 段 | 20 | 47 |
| 网络层 | IP 包 | 20 | 67 |
| 链路层 | 以太网帧 | 14 | 81 |
注意最后一列一直在变大、第二列一直在变:同一份数据,在每一层都有个新名字、新长度。 名字不是修辞,是「此刻这一层的视角」。 接收端反过来做:剥掉以太网头、剥掉 IP 头、剥掉 TCP 头,最后交上去的还是那 27 字节。 一个字节都没多、没少——这就是「封装」与「解封装」的严格对称。 头部是空的(全 0)不影响这个演示:这里只看长度,不看内容。 真实头部每个字段都有含义,第四章和第六章会逐个拆开。
三、分层的代价
分层不是白拿的,三处代价:
| 代价 | 来源 | 量级 |
|---|---|---|
| 多一次拷贝 | 每层都要组装/拆开 | 中 |
| 多一段头部 | 每层加自己的头 | 14+20+20=54 字节 |
| 多一次查表 | 每层各查各的表 | 小 |
第二行那个 54 字节是最容易被低估的: 一份 27 字节的请求,实际在线上要走 81 字节——头是正文的两倍。 这就是为什么小请求的传输效率天然很低。 第一行是高性能场景的老问题:一层套一层就意味着多次内存拷贝, 所以才会出现「零拷贝」「旁路」这些词——它们的本质都是「把中间几层省掉」。 但要注意:绕过分层是有代价的。 省掉的那几层,也就省掉了它们提供的检查与适配,所以这类方案只在自己完全可控的机房内网里用。
四、动手 Demo:分层就是一层套一层
手工构造四个头部,一层一层套上去,数累计长度。 这是本节那张表的代码版,数字完全对得上。
import _netsim as N
app = b'GET / HTTP/1.0\r\n\r\n'.ljust(27, b'x')[:27]
seg = N.tcp4h(40000, 80, flags=0x18) + app
pkt = N.ip4h('192.168.3.152', '93.184.216.34', seg, proto=6)
frm = N.eth('ff:ff:ff:ff:ff:ff', '9c:b6:d0:0d:d5:0f', pkt)
for name, buf, own in (('应用层 HTTP 报文', app, 0),
('传输层 TCP 段', seg, N.TCP_HDR),
('网络层 IP 包', pkt, N.IP_HDR),
('链路层 以太网帧', frm, N.ETH_HDR)):
print('%-16s 本层加 %-3d 累计 %-4d' % (name, own, len(buf)))
print('剥回去:%d' % len(frm[N.ETH_HDR + N.IP_HDR + N.TCP_HDR:]))
实跑输出:
--------------------------------------------------------------------
分层就是一层套一层
--------------------------------------------------------------------
**协议分层的操作定义很简单:每一层把上一层的整包当成自己的「数据」,在前面加自己的头。**
层 这一层的名字 本层加的头 本层总长
----------------------------------------------------
应用层 HTTP 报文 27 27
传输层 TCP 段 20 47
网络层 IP 包 20 67
链路层 以太网帧 14 81
**注意最后一列一直在变大,第二列一直在变**:**同一份数据,在每一层有个新名字、有个新长度。**
**接收端反过来做**:剥掉以太网头、剥掉 IP 头、剥掉 TCP 头,
**最后交上去的还是那 27 字节**——一个字节都没多、没少。
27、47、67、81 这四个数和书上的表完全一致:因为头部长度就是 20、20、14,一个不多一个不少。
最后一行剥回去还是 27 字节,这就验证了「封装与解封装严格对称」。
tcp4h 里算的校验和是真的按 RFC 1071 算的,所以这些头部不是随手填的 0,
换一下端口号,校验和就会跟着变。
30 秒实验
- 把
app改成 100 字节,看四层的累计长度各是多少。 - 把
eth的etype改成 0x86DD(IPv6),想想这时候 81 这个数会变成多少。 - 算一下:如果去掉分层、只用一个头,理论上最少要多少字节才能把「从哪来、到哪去、 给哪个程序」都说清楚。
常见误区
- 以为分层是为了「好看、好画图」。 它是为了「可替换」:换一层不影响其他层, 这才是分层的价值。
- 以为分层没有成本。 每层 14~20 字节的头是实打实的开销,小请求里头能占到一半以上。
- 以为「零拷贝」是免费的优化。 它是绕过分层,省掉的同时也丢掉了那几层的检查与适配, 只能在可控环境里用。
自测三问(点开对答案)
1. 用一句话说清「分层」到底做了什么? —— **每一层把上一层的整包当成自己的「数据」,在前面加自己的头。**接收端反着剥。 27→47→67→81 就是这么来的。 2. 分层的最大收益是什么? —— **可替换**:换网卡只动链路层,换浏览器只动应用层,中间的层一行不改。 此外还有「让人能分工深入」这个常被忽略的收益。 3. 一份 27 字节的 HTTP 请求,在以太网上实际要走多少字节? —— **81 字节**:加 TCP 头 20、IP 头 20、以太网头 14。 **头是正文的两倍**,这就是小请求效率低的根本原因。下一节换个说法讲分层:用「两个公司的部门之间传话」这个比方。
- ☐ (本节完,下一节 1.5.2 通过对话理解分层)