发送就是「加四次头」:应用数据 → TCP 段 → IP 包 → 以太网帧。 1 个字节的数据在线上要占 55 字节——放大 55 倍,这是小包的代价。
2.5.1 讲的是首部结构,这一节讲这些首部怎么被加上去。 这一节的核心数字是「放大倍数」,它解释了为什么小包不可取。
一、加四次头
每一层加自己的头,长度逐层变大:
| 步骤 | 动作 | 累计(1 字节数据) |
|---|---|---|
| 应用层 | 生成数据 | 1 |
| 传输层 | 加 TCP 头 20 | 21 |
| 网络层 | 加 IP 头 20 | 41 |
| 链路层 | 加以太网头 14 | 55 |
每一步都是「把上一层的整包当成自己的数据」:TCP 不关心那 1 个字节是 HTTP 还是别的, IP 也不关心那 21 个字节里哪部分是头。 这就是封装的严格定义。 注意第四行那个 14 字节里没有帧尾:以太网帧尾还有一个 4 字节的 FCS(校验), 所以完整帧是 59 字节。 但 FCS 是网卡硬件加的,软件通常看不到——所以很多时候算「帧长」只算到 14 字节头。
二、放大倍数
数据越小,放大倍数越离谱:
| 应用数据 | 线上总字节 | 数据占比 |
|---|---|---|
| 1 | 55 | 1.8% |
| 10 | 64 | 15.6% |
| 100 | 154 | 64.9% |
| 1400 | 1454 | 96.3% |
第一行那个 1.8% 是最刺眼的:发 1 个字节,线上要跑 55 个字节,其中 54 个是头。 放大 55 倍。 第四行 1400 字节时数据占 96.3%:同样是发数据,效率从 1.8% 到 96.3%,差五十多倍。 所以协议设计里有一条不成文的规矩:别用大量的小包传数据。 注意第二行那个 64 不是 55 加 9:10 字节数据加 54 字节头是 64 字节,正好等于最小帧长, 所以这一档不需要填充。 而第一行那个 55 字节的帧,还要填 9 字节才够 64——实际占用是 64,不是 55。
三、小包的代价
三个层面都在惩罚小包:
| 层面 | 代价 | 表现 |
|---|---|---|
| 带宽 | 头占比高 | 有效吞吐低 |
| 设备 | PPS 打满 | 帧数多、查表多 |
| 填充 | 不足 64 要填 | 小包被放大到 64 |
第二行那个 PPS 是设备侧的限制:每个帧都要查表、判 CRC、决定往哪送,成本固定。 所以小包多的时候,设备先满而链路还没满。 这就是「用小包打 DDoS」的原理。 第三行那个填充最容易被忽略:1 字节数据的帧只有 55 字节,要填到 64, 所以实际占用是 64 字节,放大 64 倍。 这一层惩罚在「带宽」之外,是「帧长」的硬规定。 所以工程上的对策是「合并」:SSH 会把键盘输入攒一小会儿再发,数据库驱动会合并小查询, HTTP/2 有多路复用与头部压缩。 这些优化针对的不是「带宽不够」,而是「小包太多」。
四、动手 Demo:1 个字节的数据,在线上占多少
拿 1、10、100、1400 字节各算一遍线上总字节与数据占比。 第一行那个 1.8% 最能说明问题。
import _netsim as N
for pay in (1, 10, 100, 1400):
wire = N.ETH_HDR + N.IP_HDR + N.TCP_HDR + pay
print('数据 %-5d 线上 %-5d 占比 %.1f%%'
% (pay, wire, pay * 100.0 / wire))
实跑输出:
--------------------------------------------------------------------
1 个字节的数据,在线上占多少
--------------------------------------------------------------------
**「头部开销」这件事,用极端例子最容易看清。**下面拿 1 字节和 1400 字节各算一遍。
应用数据 线上总字节 数据占比
-------------------------------------
1 55 1.8%
10 64 15.6%
100 154 64.9%
1400 1454 96.3%
**第一行那个 1 字节,线上要占 55 字节**:**以太网头 14 + IP 头 20 + TCP 头 20 + 数据 1 = 55。**
**数据只占 1.8%,放大 55 倍。**
**第四行 1400 字节时,数据占 96.3%**——**同样是发数据,效率从 1.8% 到 96.4%,差五十多倍。**
**所以协议设计里有一条不成文的规矩**:**别用大量的小包传数据。**
**telnet 那种「一个按键发一个包」的用法在今天是灾难**,
**这也是 SSH 要做「输入缓冲 + 合并发送」的原因。**
**顺带算一下「最小帧 64 字节」的影响**:**1 字节数据的帧只有 55 字节,还要填 9 字节才够 64**,
**所以实际在线上占 64 字节**——**放大 64 倍。**
1 字节要 55 字节、10 字节要 64 字节: 注意 10 那一档是 64,不是 64 以外的数——因为 10 加 54 正好等于 64,刚好够最小帧长。 1400 字节时占比 96.3%,不是 96.4%:1454 分之 1400 等于 96.28%,四舍五入到 96.3%。 这种小数位不要凭印象写,算出来是多少就写多少。 这里只算了三层头(TCP + IP + 以太网),没算帧尾 FCS: 加上 4 字节 FCS,1400 那一档是 1458,占比 96.0%。 所以「线上一共多少字节」这个数,取决于你算不算 FCS 与前导码—— 写这类数时必须说明口径,否则两边对不上。
30 秒实验
- 把 1400 改成 1460(MTU 满打满算),算占比。
- 加上 4 字节 FCS 再算一遍,看 1 字节那一档会变成多少。
- 想一下:为什么 telnet 那种「一个按键一个包」在今天不可接受?
常见误区
- 以为「头发开销」可以忽略。 小包里它能占 98% 以上,这才是效率问题的根源。
- 以为「加头」只是「多几个字节」。 它还要多一次查表与校验,设备侧成本按「帧数」算。
- 以为「一个按键发一个包」没问题。 它会被填充到 64 字节,放大几十倍, 所以 SSH 要合并发送。
自测三问(点开对答案)
1. 发送时一共加几次头?各多少字节? —— **三次主要的:TCP 20、IP 20、以太网 14,共 54 字节。**以太网还有 4 字节帧尾 FCS, 通常由硬件加。 2. 1 个字节的数据在线上占多少? —— **55 字节(不含 FCS)**,数据只占 1.8%,放大 55 倍。算上最小帧填充,实际占 64 字节。 3. 为什么小包对设备的压力更大? —— **每个帧的处理成本固定**(查表、CRC、入队),所以帧数多时 PPS 先满,而链路带宽还没满。下一节看这些包是怎么「过链路」的。
- ☐ (本节完,下一节 2.5.3 经过数据链路的包)