§ 1.5 · Section

数据包 · Packet

The Atom of the Internet

互联网发送信息,从来不是一整块扔过去——而是先切成一小块一小块,每块叫一个"数据包"(Packet)。这些包独立走自己的路,到了目的地再拼回来。这个看似简单的设计,是互联网能稳定运转 50 年的核心秘密。

生活场景
📦 你搬家,但不会"一次性搬所有家具"

你不会叫一辆大卡车把所有家具一次搬走——你会把沙发拆成几块、把书装箱、把餐具分门别类包好,每一箱都贴上标签:"厨房"、"客厅"、"书房",到了新家再拼起来。
为什么要这么做?
① 卡车装不下整家当;
② 一箱丢了还能单独补,不用全部重搬;
③ 不同的箱子可以走不同的路(有的走高速、有的走船)——哪条快到哪。
网络发数据,逻辑完全一样。大包拆小包、每个包贴序号、到了目的地再拼起来——这就是为什么你的视频哪怕跨越半个地球,画面也是完整流畅的。

为什么必须分包:三个理由与一段历史

在讲包长什么样之前,先回答那个最根本的问题:为什么不能一整块发过去?这个问题的答案,恰好就是互联网战胜电话网络的原因。

1960 年代,全世界的通信网络都是电路交换(Circuit Switching)——就是电话的工作方式:你打电话,交换机在你和对方之间专门接通一条物理线路,这条线在整个通话期间被你独占,别人不能用,哪怕你俩沉默了三分钟一个字没说。

换成大白话,电路交换好比你去餐厅吃饭,餐厅把整个包间给你锁上——你在里面吃饭、聊天、发呆,甚至趴桌上睡了半小时,这个包间谁都不能进。分组交换好比大堂散台:你这桌吃完一道菜歇一会儿,服务员就趁这空当去照顾别桌。同样一间餐厅,散台能接的客人是包间的十几倍。

电路交换(电话网)分组交换(互联网)
建立方式先接通一整条专线,全程独占不预先建立,包各走各的路
沉默时的线路空占着,全浪费让给别人用
线路断了通话中断,得重新拨包自动绕路,用户可能毫无感觉
时延稳定、可预测(这是它的优点)抖动,不保证
利用率极低(语音通话中约一半时间是静音)极高,靠统计复用
扩容成本用户翻倍,线路要翻倍用户翻倍,带宽远不必翻倍

分组交换的想法在当时是异端邪说。它的两位独立发明人——美国的 Paul Baran(1964 年,为兰德公司研究"核战争后还能通信的网络")和英国的 Donald Davies(1965 年,他造了"packet"这个词)——都遭到了电话公司专家的强烈嘲笑:"你把一句话切成碎片乱扔出去,还指望对方能拼回来?荒谬。"六十年后的结果是:分组交换赢了,而且赢得如此彻底,以至于今天连打电话本身都跑在分组交换上(VoIP)——微信语音、5G 语音,全是包。

当年那句嘲笑现在读起来特别有意思:"你把一句话切成碎片乱扔出去,还指望对方能拼回来?"这就好比 1964 年有人说要把一整套家具拆成三十个纸箱、分几辆车走不同的路运走,搬家公司的老师傅肯定觉得荒谬。可六十年后的现实是:全世界搬家都是这么搬的,连当年嘲笑的那门生意(打电话)本身也改成这么干了。

那么分包具体带来了哪三个好处?(后面还有一节会用更具体的数字算给你看):① 统计复用——一条线路可以被成千上万个数据流交错使用,谁有数据谁就插进去发,利用率从电话网的百分之几拉到接近百分之百;② 失败成本极低——出错只需重传那一小块;③ 天生容错——一条链路断了,后面的包自动走另一条路,这正是 Baran 当年设计它的初衷(能在核打击下存活的网络)。

这三条好处翻译成人话就是搬家那三件事: 一辆卡车上能顺路捎别人家的箱子,不用为你一家空跑一趟(统计复用); 摔坏一箱只赔一箱,不用整套家具重买(失败成本低); 这条路封了,司机换条路照样到(天生容错)。

一个数据包长什么样

每个数据包大致分两块——头部(Header)载荷(Payload)

说白了,一个数据包就是一个贴了面单的快递纸箱头部是箱子外面那张面单(谁寄的、寄给谁、第几件、多重、易不易碎),载荷是箱子里真正装的东西。中间所有环节的人只看面单、从不拆箱——这条纪律是整个网络能跑得飞快的前提。

部分类比装的是什么
头部 · Header快递面单源地址、目标地址、序号、长度、协议类型、校验码
载荷 · Payload快递内容真正的数据——可能是网页代码的一小段、图片的一个分片、视频的一帧
Analogy · 快递包裹

一个快递包裹里——面单是"头部"(收件人、寄件人、单号、重量),里面的东西是"载荷"。
快递公司只看面单就能决定这包往哪送——根本不需要打开看里面是书还是袜子。
数据包也一样——路由器只读头部,不关心载荷里装的是什么字节。

光说"头部"还太笼统,我们把真正的 IPv4 头部逐字段摊开看一遍。它固定 20 字节(不带选项时),每一个字段都在解决一个具体问题:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|版本=4 |头长=5 |   服务类型TOS  |          总长度(字节)        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            标识 ID            |标志|      片偏移量            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     TTL       |   协议号      |         头部校验和            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        源 IP 地址(32位)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      目的 IP 地址(32位)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

关键字段的作用:
  总长度   最大 65535 字节 —— 这就是 IP 包的理论上限
  标识 ID  同一个原始包被分片后,各片共用这个 ID,用于重组
  片偏移   这一片在原包里的位置,重组时按它排序
  TTL      还能过几台路由器,每跳减 1,到 0 就丢弃
  协议号   6=TCP  17=UDP  1=ICMP  —— 告诉接收方"载荷该交给谁解析"
  校验和   只校验头部,不管载荷(载荷的正确性交给 TCP 负责)

这几个字段挨个翻译一遍就全懂了:总长度=面单上写的"本箱总重";标识 ID=同一套家具拆成五箱,五箱共用一个订单号;片偏移="本箱是第 3 箱,装的是第 21~30 页";TTL="这箱货最多再转 64 个中转站,超了就当无主件处理";协议号=面单上贴的"食品 / 危险品 / 普通件"标签,收件人靠它决定交给哪个部门拆;校验和=只核对面单有没有被涂改,箱子里的东西对不对那是收货人自己验的事。

协议号这个字段值得单独说,它是"分层"这件事在字节层面的体现。IP 层剥掉自己的头之后,剩下一堆字节它完全看不懂,但它靠这一个数字就知道该把这堆字节交给谁:6 交给 TCP 模块、17 交给 UDP 模块、1 交给 ICMP 模块。每一层的头部里,都必须有一个字段说明"我的载荷是什么类型",否则上层的解封装就无从下手。以太网帧头里的"类型"字段(0x0800=IPv4、0x86DD=IPv6、0x0806=ARP)也是同一个作用。

这件事打个比方就是仓库分拣:拆掉最外层大箱以后,里面那个盒子上必须贴着"生鲜""图书""数码",分拣员才知道往哪条传送带上放。要是里层盒子上啥标签都没贴,分拣员打开箱子只能傻站着。所以每一层的头部都必须有一个字段说清"我肚子里装的是哪一类东西"。

TCP 头部同样是 20 字节起,它管的是完全不同的事:

源端口(16位)           目的端口(16位)
序号 Sequence Number(32位)      ← 我这段数据是从第几个字节开始的
确认号 Acknowledgment(32位)      ← 我已经收到了对方的第几个字节
数据偏移 | 保留 | 标志位 URG ACK PSH RST SYN FIN | 窗口大小(16位)
校验和(16位)           紧急指针(16位)
[可选项:MSS、窗口缩放因子、时间戳、SACK 允许…]

六个标志位就是 TCP 的全部"表情":
  SYN  我要建连接        FIN  我说完了,要关了
  ACK  我确认收到了      RST  出错了,立刻断开(连挥手都省了)
  PSH  别缓存了赶紧交给应用   URG  紧急数据(几乎没人用)

窗口大小:接收方告诉发送方"我还能接多少字节,别发太快"
          —— 这就是流量控制的全部机制

把两个头部加起来你就得到一个重要的数字:TCP/IP 的最小头部开销是 20 + 20 = 40 字节,再加以太网帧头 14 字节和帧尾 CRC 4 字节,共 58 字节。这就是为什么上一节说"传几十字节的小包时,头部可能比数据还长"——发一个 20 字节的游戏操作指令,实际在线路上跑了 78 字节,开销占了 74%

这个比例换成生活话就非常刺眼了:相当于你寄一支口红,纸箱、气泡膜、面单加起来比口红本身重两倍多。寄一台冰箱时这点包装完全可以忽略,寄口红时就纯属浪费。这就是为什么聊天软件宁可把几条小消息攒一攒合成一个包发出去——凑够一箱再寄,比一支一支寄划算得多。

MTU 与分片:1500 这个数字的来历

数据包不是想多大就多大——受一个叫 MTU(Maximum Transmission Unit)的限制:以太网默认是 1500 字节。超过这个大小,就必须切片。

MTU 说白了就是快递公司规定"单个纸箱不能超过多大":以太网这家公司的规矩是一箱最多装 1500 字节。1500 字节是多少?大约 750 个汉字,也就是半页 A4 纸的量。超了怎么办?必须拆成两箱寄——这就是分片。

为什么偏偏是 1500?这是一个 1980 年代的工程折中,跟当时的硬件条件死死绑在一起:当年的以太网是共享总线(所有机器接在同一根同轴电缆上,一次只能一台说话),包太大意味着一台机器会长时间霸占线路,别人得干等;同时那个年代的网卡缓冲区只有几 KB,且线路误码率高——包越大,出一个错整包重传的代价越大。1500 字节在"头部开销比例"和"重传成本"之间取了一个平衡点,然后被冻结成了四十年的事实标准。

为什么定在 1500 而不是更大?打个比方,那年头的以太网好比一条只能单向通行的窄巷子,一次只许一辆车进去:车太大,一辆车堵在里面别人半天进不来;同时那年头路况差(误码率高),车越大,翻一次车损失越惨。1500 字节就是"车不能太大也不能太小"这两头拉扯之后定下的那个数,然后一直用到今天。

场景典型包大小备注
普通以太网1500 B最常见的 MTU
数据中心专用 Jumbo Frame9000 B减少头部开销,效率更高
语音 / 视频流几十到几百 B小包降低延迟
纯 ACK 确认包40-60 B只有头没有载荷
PPPoE 拨号(很多家宽)1492 BPPPoE 头占 8 字节,MTU 被削掉
VPN / IPsec 隧道1400 B 左右隧道头再吃掉几十字节
回环接口 lo65536 B不上物理线路,不受以太网限制

注意 MTU 和 MSS 的区别,这是个高频混淆点:MTU 是"链路层能承载的最大载荷",MSS 是"TCP 一次能装的最大数据量"。两者的关系是减法:

MTU  = 1500 字节(以太网标准)
     - 20 字节 IP 头
     - 20 字节 TCP 头
MSS  = 1460 字节  ← 这才是你的应用数据真正能塞的量

如果是 PPPoE 家宽:
MTU  = 1492 → MSS = 1452
如果走 IPsec VPN:
MTU  ≈ 1400 → MSS ≈ 1360

传 1 MB 数据需要多少个包?
1,048,576 ÷ 1460 ≈ 719 个包,另加 719 个 ACK 包
纯头部开销:719 × 58 ≈ 41 KB,约占 4%

MTU 和 MSS 的区别,用寄快递来说就一句话:MTU 是"纸箱外径不能超过多大",MSS 是"箱子里真正能塞多少东西"——中间还得扣掉气泡膜和面单的厚度(IP 头 20 + TCP 头 20)。所以标准是 1500,你实际能装的只有 1460。这也解释了"719 个包"这个数字:1 MB 的东西,得装 719 个纸箱寄出去。

当一个包超过路径上某段链路的 MTU 时,会发生分片(Fragmentation)。路由器把它切成几片,各片共用同一个"标识 ID",靠"片偏移"记住自己在原包里的位置,最后由目标主机负责重组(中间路由器不重组,因为各片可能走不同路径)。听起来很妥当,但分片其实是网络里公认的坏事,原因有三条:

分片这件事换成大白话就是:你的东西装不进一个纸箱,快递员当场帮你拆成三箱寄走,收件人得凑齐三箱才能拼回原样。听起来挺贴心,可实际上是件麻烦事——下面三条说的全是这个麻烦。

这三条用寄快递的话讲就是: 三箱里丢任意一箱,另外两箱也全成了废纸——拆成三箱,等于把丢件风险放大了三倍 收件人得腾出一块地方堆着等其余几箱,还得定个"等多久就不等了"的期限,有人专门利用这点寄一堆"永远缺一箱"的货把你仓库堆满; 只有第一箱的面单上写了完整信息,后面几箱面单是残缺的,门口的安检员看不出这几箱该不该放进来。

所以现代做法是宁可提前算小一点,也绝不分片。IPv4 头里有个 DF(Don't Fragment)标志,设上以后路由器遇到超限的包不切片,而是直接丢弃并回一个 ICMP 消息说"太大了,这条路只能过 1400"。发送方收到这个反馈就把包调小——这个自动探测机制叫 PMTUD(路径 MTU 发现)。IPv6 更彻底:路由器完全不许分片,一律由源主机负责。

所以现在的做法是宁可自己少装点,也绝不让别人替你拆箱DF 标志说白了就是在面单上写一句"禁止拆箱":中转站遇到装不下的,直接退回来并附一句"这条路只能过 1400,你自己重新装"。发货方收到反馈就换小箱子。这个来回试探的过程就是 PMTUD——相当于你第一次寄货先试探一下这条线路的箱子尺寸上限。

PMTUD 有个非常经典的故障模式,值得知道:如果路径中某台设备被管理员粗暴地"禁 ping"(屏蔽了所有 ICMP),那"太大了"这条反馈就传不回来,发送方既不知道要调小、包又过不去,结果是——小页面能打开,大文件传到一半就永久卡死。这个现象叫 PMTU 黑洞,是网络排障里最难查的问题之一。诊断方法是手动试探最大不分片包长:

PMTU 黑洞这个故障特别值得记住,因为它的症状极其古怪:小页面秒开,大文件传到一半永久卡死。原因说白了就是——中转站把"箱子太大"这句退货通知也一起屏蔽了。于是发货方傻等:货过不去,也没人告诉它为什么。小包裹本来就没超尺寸所以一路顺畅,大包裹卡在半路无声无息。这就是网络排障里最阴的一类问题。

# Windows:-f 表示不分片,-l 指定载荷大小
PS> ping -f -l 1472 baidu.com
正常回复 → 1472 + 8(ICMP头) + 20(IP头) = 1500,说明 MTU 至少 1500

PS> ping -f -l 1473 baidu.com
需要拆分数据包但已设置 DF。 → 说明 1500 就是上限

# Linux/macOS 对应写法
$ ping -M do -s 1472 baidu.com

# 直接看本机各网卡的 MTU
PS> netsh interface ipv4 show subinterfaces
$ ip link | grep mtu

为什么要拆小?三个原因

这三条用高速公路来理解一遍:公平共享=不允许一辆超长货车占满整条路半小时,大家分成小车穿插着走;失败成本低=一辆小车抛锚只影响那一车货,不用整个车队折返;绕路灵活=前面堵了就下高速走国道,反正大家不是一个车队非得走一块。

第二条可以用具体数字算一遍,效果会震撼得多。假设链路的比特错误率是万分之一(即每传 10000 个比特出错 1 个,这在无线环境下并不夸张),我们比较两种做法传完 1 MB 数据的期望代价:

【方案 A:不拆包,一整块 1 MB = 8,388,608 比特发出去】
整包成功的概率 = (1 - 0.0001)^8388608 ≈ 0 (无限接近于零)
结论:这个包永远传不完。每次都会出错,每次都要全部重发。
      ——不拆包,在有误码的链路上根本不可行。

【方案 B:拆成 719 个 1460 字节的包】
单包比特数 = 1460 × 8 = 11680
单包成功率 = (1 - 0.0001)^11680 ≈ 0.311
虽然单包成功率也不高,但失败只需重传这 1460 字节
期望总传输量 ≈ 1 MB ÷ 0.311 ≈ 3.2 MB
结论:多花 2 倍流量,但确定能传完。

这就是"拆小"最本质的价值:
把"几乎必然全盘失败"变成了"局部失败、局部重试"。

这一段算式的结论值得用大白话再说一遍:把 1 MB 当一整块发,成功率无限接近于零——不是"慢",是"永远传不完"。好比你要把一叠 100 页的手写稿一次抄完不许出一个错,几乎不可能;但拆成 100 张、每张抄错就重抄那一张,虽然总共可能抄了 320 页,但这活儿确定能干完。拆小的本质价值不是变快,是把"必然全盘失败"换成了"局部返工"

第一条"公平共享"也有个精确的名字,叫统计复用(Statistical Multiplexing)。它依赖一个统计学事实:虽然每个用户都有可能突发占满带宽,但一千个用户同时突发的概率极低。所以运营商敢卖出远超实际总带宽的套餐——一个小区有 500 户各签 500M,总和 250G,而小区实际上行只有 10G。这不是欺诈,而是统计复用的正常运作。它同时也解释了"晚上八点为什么变慢"——那正是统计假设失效的时刻。

统计复用打个比方,就是小区的电梯:一栋楼三百户,电梯只能装 13 个人,物业敢这么配,是因为三百户不会在同一分钟全按电梯。这不是偷工减料,是概率算出来的合理配置。但每天早上八点大家一起上班,这个假设就失效了——于是你在楼下等三趟。宽带晚上八点变慢,跟这一模一样。

封装与解封装:数据包的"穿衣脱衣"

上一节讲协议栈时说过"从上往下加壳、从下往上剥壳"。现在我们把这个过程精确到字节,看看你敲的一句话是怎么一层层膨胀起来的。

这个过程说白了就是寄国际快递时的层层套箱:礼物装进小盒 → 小盒放进带气泡膜的中盒 → 中盒装进写着国家地址的大纸箱 → 大纸箱扔上货车。收件那头则完全倒过来,一层层拆开。每加一层箱子就重一点,这就是下面那个"24 字节膨胀成 82 字节"的全部来历。

【封装 Encapsulation · 发送方,自上而下】

应用层  你的数据:"{"msg":"晚上吃啥"}"                        24 字节
           ↓ TCP 层:加 20 字节头(端口、序号、窗口、校验和)
传输层  [TCP头 20][数据 24]                                    44 字节
           ↓ IP 层:加 20 字节头(源/目的 IP、TTL、协议号)
网络层  [IP头 20][TCP头 20][数据 24]                           64 字节
           ↓ 链路层:加 14 字节帧头 + 4 字节尾部 CRC
链路层  [以太头 14][IP头 20][TCP头 20][数据 24][CRC 4]         82 字节
           ↓ 物理层:编码成电压/光/电磁波

24 字节的数据,出门时变成了 82 字节 —— 膨胀了 3.4 倍。
(这就是为什么聊天软件要把多条小消息合并成一个包发。)
(换算一下:24 个字节大约是 8 个汉字,也就是"晚上吃啥"这么一句话;
 为了把这 8 个字送出去,实际在网线上跑了 82 字节的包装——
 相当于寄一张便签纸,用了一个鞋盒。)

【解封装 Decapsulation · 接收方,自下而上】

网卡收到 82 字节 → 校验 CRC,对了 → 撕掉以太网头尾
  → 看类型字段 0x0800,交给 IP 模块
IP 模块 → 校验头部,确认目的 IP 是自己 → 撕掉 IP 头
  → 看协议号 6,交给 TCP 模块
TCP 模块 → 校验、按序号排队、发 ACK → 撕掉 TCP 头
  → 看目的端口 8080,交给监听这个端口的程序
应用程序 → 拿到那 24 字节:"{"msg":"晚上吃啥"}"

这里有一条严格的对称性:每一层加的头,只由对面的同一层撕掉。IP 层绝不会去碰 TCP 头,TCP 层也读不到以太网头。而中间的路由器只撕到 IP 层就停手——它读完 IP 头决定下一跳,然后换一个新的以太网头再发出去,TCP 头和载荷它一个字节都不看。这个"只看必要的那一层"的纪律,是路由器能做到每秒转发几亿个包的根本原因——如果每台路由器都要解析 HTTP 内容,互联网早就慢成蜗牛了。

这条纪律用大白话讲就是:快递中转站只看面单、绝不拆箱。它撕掉旧的运输标签、贴上新的、扔上下一辆车,从头到尾没打开过箱子。正因为它不拆箱,一个中转站一天才能过几百万件货——要是每一件都得开箱验货,堆到年底也发不完。

顺便说,这也是"深度包检测(DPI)"这个词的来历:某些设备故意违反这个纪律,往上多剥几层去看应用层内容——运营商用它识别 BT 流量、企业用它做内容审计、防火墙用它识别应用协议。它有效,但极其耗算力,也是加密协议(HTTPS、DoH、ECH)不断普及的最大推动力。

深度包检测(DPI)说白了就是中转站故意开箱查验:运营商靠它认出"这箱是 BT 下载",公司靠它查员工传了什么,防火墙靠它认出这是什么应用。管用,但极慢极贵——相当于给每一件快递都配一个安检员开箱翻看。这也正是为什么现在大家都拼命给箱子上锁(HTTPS):不是心里有鬼,是不想每一件都被人翻。

为什么"绕路"也能拼回去

这是新手最容易困惑的一点——包走的路线不同,到达时间不同,怎么保证最后拼出来的内容是正确的?答案是序号

答案朴素得让人失望,但它真的就这么简单:给每一箱都编上号。就像你搬家时在每个纸箱上写"3/10""7/10"——十个箱子哪怕分三辆车走、隔一天才到齐,收货那头按号一排,顺序就复原了。哪箱没到,报个号让人补寄那一箱就行。

生活场景
🧩 把 100 张纸装进 10 个信封寄出

你有一份 100 页的报告,但每个信封最多塞 10 页——你只能拆成 10 个信封,分别寄给朋友。
① 你给每个信封标注:"第 1 页到第 10 页"、"第 11 页到第 20 页"……
② 朋友收到所有信封后,按编号排好,从 1 拼到 100。
③ 如果第 5 个信封没到,朋友就告诉你:"请重发第 5 封。"——不用重发全部 10 封。
这就是 TCP 协议做的事——给每个字节一个序号,按序号排序、按序号确认、按序号重传。

数据包的"生命周期"

1. 诞生

你的应用程序(比如浏览器)构造一份数据,TCP 协议栈把它切成若干段,每段加序号、加校验和,封装成 TCP 包。

2. 加 IP 头

TCP 包再被 IP 层包裹——加上源 IP 和目标 IP。现在它能被路由器识别了。

3. 加 MAC 头

IP 包再被以太网层包裹——加上源 MAC 和下一跳路由器的 MAC。现在它能从你家网卡出去了。

4. 一跳一跳转发

包经过你家路由器 → 运营商骨干 → 骨干网 → 对方运营商 → 对方路由器——每跳都会看一眼目标 IP,决定下一跳去哪。

5. 抵达目标

对方机器收到包,反过来一层一层"剥皮"——MAC 头撕掉、IP 头撕掉、TCP 头撕掉——剩下的就是真正的数据。

6. 拼装回完整内容

所有包都到齐了,TCP 按序号排序、检查有没有缺——齐了,交给应用程序(浏览器)渲染。

这六步串起来就是一次完整的寄件: 分装打包、逐箱编号; 贴上写着城市的面单; 贴上"下一站送哪个网点"的标签; 一站一站往前转,每站都撕掉旧标签贴新的; 到了收件人手上层层拆开; 按编号拼回原样。你在浏览器里看到的每一张图片,都刚刚走完了这六步。

Analogy · 洋葱

一个完整的数据包像一颗洋葱——MAC 头在最外层,IP 头在中间,TCP 头在更里,数据在最心
每过一台路由器,路由器会撕掉外层(MAC 头),换上新的(自己的 MAC 头)继续往前送。最终目标机器一层层撕到最里,才看到真正的数据。

TTL:数据包的"寿命",防止它永远打转

互联网上有几十万台路由器,各自维护自己的路由表。如果某两台路由器的表因为配置错误或收敛延迟而互相指向对方,会发生什么?包就会在它们之间无限来回,永远到不了目的地,也永远不会消失。这种"路由环路"如果没有对策,几分钟就能把整条链路上的带宽全部塞满死循环的幽灵包。

路由环路说白了就是两个快递网点互相把一件货推给对方:"这不该我送,转给隔壁",隔壁说"这也不该我送,转回去"——这箱货就在两个网点之间来回坐车,永远到不了收件人手上,也永远不消失。要是没人管,一天下来两个网点之间的货车全在拉这种鬼包裹。

IP 协议的对策朴素得近乎粗暴:给每个包一个"还能走几跳"的计数器,每过一台路由器减 1,减到 0 就无条件丢弃。这个字段叫 TTL(Time To Live)——名字里虽然有 Time,但它实际上数的是跳数而不是时间。

TTL 的解法粗暴但极其有效:在面单上盖一个格子,每过一个中转站就划掉一格,划到没格了就当无主件销毁。好比一张只能转 64 手的票,转到第 65 次自动作废。名字里带 Time,但它数的其实是"转过几个手",不是"过了几分钟"——这是一个流传最广的误解。

操作系统默认发出的 TTL能推出的信息
Windows128收到 TTL=115,说明中间过了 13 跳
Linux / Android64收到 TTL=52,说明过了 12 跳
macOS / iOS64同上
部分网络设备255路由协议报文常用 255,表示"只许本地一跳"

这个字段有个非常巧妙的副产品:traceroute 完全是靠"故意让 TTL 耗尽"实现的。它的原理堪称网络工具里最漂亮的一个把戏——

traceroute 干的活儿,通俗地说就是故意寄一堆"只准转一次""只准转两次""只准转三次"的包裹:第一个包在第一个中转站就作废,中转站按规矩回你一张"作废通知单"——通知单上印着它自己的地址,你就知道第一站是谁了。用一堆注定送不到的包裹,反过来把整条路线上每一个站点的名字都问出来,这是网络工具里最漂亮的一个把戏。

traceroute 的工作原理:

发第 1 个包,TTL = 1
  → 第一台路由器把 TTL 减到 0,丢弃它,并回一个 ICMP 超时消息
  → 从这个回复的源 IP,我们就知道了第 1 跳是谁!

发第 2 个包,TTL = 2
  → 第二台路由器丢弃并回复 → 知道了第 2 跳是谁

发第 3 个包,TTL = 3 …… 依此类推,直到收到目标主机的正常回复。

# 实际使用
PS> tracert baidu.com                # Windows
  1    2 ms   192.168.1.1            <- 你家路由器
  2    8 ms   100.64.0.1             <- 运营商 CGNAT,注意这个网段
  3   12 ms   61.128.x.x             <- 城域网
  4    *      *      *   请求超时。   <- 这台设备屏蔽了 ICMP,不是故障
  5   28 ms   110.242.68.66          <- 到了

$ traceroute baidu.com               # Linux/macOS
$ mtr baidu.com                      # 更好用:持续跑并统计每跳丢包率

看 tracert 结果时有两个常见误读要避开。第一,出现 * * * 不代表网络断了——很多路由器出于安全策略不回 ICMP,跳过它继续往下看,如果后面几跳正常,那这一跳只是"沉默"而非"故障"。第二,中间某一跳延迟很高不代表它是瓶颈——路由器处理"给自己的 ICMP 回复"的优先级极低(它的正职是转发),所以它回得慢很正常,只要最终跳的延迟正常,中间的高延迟可以忽略。真正的问题信号是"从某一跳开始,之后所有跳的延迟都陡增或都开始丢包"。

那两个误读用大白话说清楚:第一,看到 * * * 别慌,那是这个中转站按规矩"不接受查询",不是它塌了——后面几站正常就说明货照样过得去;第二,中间某站显示延迟很高也别急着骂它,因为回复查询这件事在中转站的活儿里排最后一位,它正忙着分拣几百万件货,抽空回你一句已经算客气了。真正该紧张的是"从某一站往后,所有站全都变慢或全都开始丢件"。

丢包、重传与乱序:包在路上会遇到什么

互联网的底层承诺其实低得惊人。IP 协议提供的是"尽力而为"(Best Effort)的服务,它对上层的保证只有一句话:"我会努力送,但——可能丢、可能重复、可能乱序、可能损坏,我都不负责。"

"尽力而为"翻译成人话就是一家不签合同、不承诺时效、不保价的搬运队:"东西我肯定给你往那边搬,但丢了、碎了、重复送了、后寄的先到了——我都不赔。"听着不靠谱,可整个互联网就建在这个不靠谱的底座上;所有"靠谱"都是上面那一层(TCP)自己补出来的。

意外为什么会发生TCP 怎么应对UDP 怎么应对
丢包路由器队列满了主动丢;无线信号误码;链路中断超时或收到 3 个重复 ACK 后重传不管
乱序不同包走了不同路径,后发的先到按序号在缓冲区里排好再交给应用不管,应用自己处理
重复ACK 丢了导致发送方误以为没送到,重发了一次按序号识别并丢弃重复的不管
损坏电磁干扰导致某个比特翻转校验和不对 → 丢弃 → 触发重传校验和不对 → 直接丢弃
延迟暴增某段链路排队严重动态调整超时时间(RTO)并降速不管

这里最反直觉的一点是:路由器丢包不是故障,而是它的正常工作机制。路由器的队列(缓冲区)是有限的,当涌入的包超过出口带宽时,队列排满,新来的包就只能丢掉——这叫尾部丢弃(Tail Drop)丢包是网络在向发送方传递唯一的一个信号:慢点。TCP 的拥塞控制算法正是把"丢包"解读为"网络堵了",进而主动把发送速率砍半。整个互联网的流量调节机制,建立在"故意丢几个包"这个看起来很粗暴的信号上。

"路由器丢包不是故障"这句最反直觉,但用生活场景一比就通了:快递网点的仓库只有那么大,货堆满了,新来的车就只能被劝回去。这不是网点坏了,这是它唯一能表达"我这儿满了"的方式。丢包就是网络对你说的那句"慢点"——它不会打电话通知你,它只会默默扔掉几件,让你自己领会。

TCP 重传的两种触发方式值得分清楚,它们的代价差了几个数量级:

【快速重传 Fast Retransmit】—— 好情况
发送方发出 包1 包2 包3 包4 包5,其中包2 丢了
接收方收到 1、3、4、5 → 连续回三个"我要 2"的重复 ACK
发送方看到 3 个重复 ACK → 立刻重传包2,不等超时
耗时:约 1 个 RTT(几十毫秒)

【超时重传 RTO Retransmission Timeout】—— 坏情况
如果连重复 ACK 都收不到(比如整段全丢了),只能等超时
初始 RTO 通常 200ms~1s,且每次失败翻倍(指数退避)
1s → 2s → 4s → 8s …
耗时:可能好几秒 —— 这就是"网页卡住不动"的常见来源

结论:偶发单包丢失几乎无感;
      连续大段丢失会导致明显卡顿。

这两种重传的差别,换算成时间就特别有画面:快速重传大约几十毫秒,比你眨一次眼还快,你压根感觉不到超时重传可能等好几秒——而且每失败一次就翻倍地等(1 秒、2 秒、4 秒、8 秒)。你刷网页时那种"转圈转了三四秒突然就好了",八成就是撞上了超时重传。这好比你打电话没人接:第一次隔一分钟再打,第二次隔两分钟,第三次隔四分钟——越等越久。

再补一个 SACK(选择性确认)的价值:传统 ACK 只能说"我收到第 N 个字节之前的所有内容",如果丢的是中间某一段,发送方无法知道后面的到没到,只能从丢的地方全部重发。SACK 允许接收方精确说明"我收到了 1-1000 和 2000-3000,缺 1001-1999",发送方就只补那一段。这在丢包率高的移动网络上能带来数倍的性能差异,所以现代系统默认都开着它。

SACK 的价值用生活话讲就是:传统 ACK 只会说"我收到第 5 箱之前的了",第 6 箱丢了、第 7 到 10 箱其实早到了,发货方却只能从第 6 箱开始整批重寄。SACK 允许收货方说清楚"我缺的就是第 6 箱,其余都在",发货方补那一箱就完事。在信号飘忽的手机网络上,这一个改进能带来几倍的速度差别。

抓包入门:亲眼看见一个包

前面讲的全是抽象,但数据包是可以被亲眼看见的。这是学网络最值得动手做的一件事:当你第一次在屏幕上看到自己敲的那句话变成一行行十六进制、每个字段清清楚楚地摆在那里时,所有抽象概念都会瞬间变成实感。

抓包这件事说白了就是站在传送带旁边,把每一件经过的快递面单都拍下来看。前面讲的全是"面单上应该有哪些栏目",抓一次包,你就真的看见了那些栏目里填的字。这是学网络性价比最高的一次动手。

工具类型特点适合谁
Wireshark图形界面行业标准,能解析 3000+ 种协议,自动着色、追踪流所有人的第一选择
tcpdump命令行Linux/macOS 自带,能在没有图形界面的服务器上抓,存成文件再拿回来用 Wireshark 看运维、服务器排障
浏览器开发者工具图形界面按 F12 → Network 面板,看得到 HTTP 层,但看不到 TCP 层前端调试
Fiddler / Charles代理型能解密 HTTPS(靠安装自签证书做中间人)App 接口调试
# tcpdump 最常用的几条(Linux / macOS)
$ sudo tcpdump -i any -n port 80              # 抓 80 端口,不解析域名
$ sudo tcpdump -i eth0 host 110.242.68.66     # 只抓和某个 IP 的往来
$ sudo tcpdump -i any -c 20 icmp              # 抓 20 个 ping 包就停
$ sudo tcpdump -i any -w cap.pcap             # 存成文件,拿回本地用 Wireshark 打开
$ sudo tcpdump -i any -A port 80              # -A 直接以 ASCII 显示内容

# 典型输出解读
09:14:22.183 IP 192.168.1.20.51234 > 93.184.216.34.80: Flags [S], seq 1042
      └时间     └源IP:端口        └目的IP:端口     └SYN  └初始序号
09:14:22.221 IP 93.184.216.34.80 > 192.168.1.20.51234: Flags [S.], seq 88, ack 1043
                                                          └SYN+ACK
09:14:22.221 IP 192.168.1.20.51234 > 93.184.216.34.80: Flags [.], ack 89
                                                          └纯 ACK,三次握手完成

# Wireshark 里最有用的几个过滤器(注意语法和 tcpdump 不同)
http                        只看 HTTP
tcp.port == 443             只看 443
ip.addr == 110.242.68.66    只看和某 IP 的往来
tcp.flags.syn == 1          只看握手包
tcp.analysis.retransmission 只看重传包 ← 排查丢包神器
dns                         只看 DNS 查询

三条实践建议。第一,先用 ping 抓 ICMP 练手——它最简单,一问一答,正好能看清 IP 头的全部字段。第二,抓 HTTP(80 端口)而不是 HTTPS——443 的内容全是加密乱码,只能看到握手过程,学不到应用层。第三,善用过滤器——一台机器每秒产生几千个包,不过滤的话你会瞬间被淹没,什么也看不出来。最后一条合规提醒:只在自己的设备和自己有权限的网络上抓包,抓取他人的网络流量在很多地区是违法的。

补一句为什么建议先抓 ping它是全网最简单的一问一答,相当于只寄一张明信片,面单上每一栏你都能一眼看完。而 HTTPS 的内容是上了锁的箱子,你只能看到面单和"两人在门口对了半天口令"的过程,箱子里一个字也看不到——练手阶段学不到东西。

包丢了怎么办

两种模式的区别,一句生活话就够:TCP 好比寄挂号信,非要对方签收,没签就一直补寄;UDP 好比往楼下喊一嗓子,听见了就听见了,没听见你也不会再喊第二遍。视频通话为什么选后者?因为补喊一句三秒前的话,比丢掉它更让人难受——画面卡住重放,远不如糊一帧就过去。

包与流:从"一个个包"到"一条条连接"

最后一个概念上的跃迁。前面我们一直在讲"包",但你在实际工作里听到的更多是"连接"、"会话"、"流"、"会话保持"。它们是同一件事的两种视角,而这两种视角之间的落差,正是 TCP 存在的全部理由。

包和流的关系,打个比方就是"一箱箱货"和"一整套家具":搬运工眼里全是一箱箱互不相干的箱子,而你眼里只有"我那套沙发"。TCP 干的活儿就是站在中间,把一堆散箱子还原成你想要的那套完整家具——排序、点数、缺了催补,全是它在忙。

包的视角(IP 层)流的视角(TCP 层及以上)
基本单位一个个独立的、互不相干的包一条连续、有序、无边界的字节流
顺序不保证,谁先到算谁严格按发送顺序
完整性可能丢、可能重复保证不丢不重
状态无状态,每个包独立处理有状态,双方各自维护序号、窗口、缓冲区
边界包有明确的起止没有边界——你发两次,对方可能一次读到
谁在维护路由器、网卡只有两端主机的操作系统

那个"没有边界"是初学网络编程时最容易吃亏的地方,值得单独说清。TCP 是字节流协议,不是消息协议。你调用两次 send("你好")send("世界"),接收方可能一次 recv 就拿到"你好世界"(这叫粘包),也可能第一次拿到"你好世"、第二次拿到"界"(这叫半包)。

粘包半包这件事,说白了就是TCP 只保证"这一车沙子按顺序倒给你",它压根不记得你原本是分几袋装的。你分两袋倒进传送带,出来那头可能一次装走一堆、也可能第一次装走一袋半。所以要么你自己在沙子里插标记(分隔符),要么先说清"接下来这一段有多少斤"(长度前缀)——这就是下面三种办法的来历。

发送方:send("消息A")   send("消息B")   send("消息C")
                    ↓ TCP 只当成一串字节
线路上:消息A消息B消息C        (没有任何分隔标记)
                    ↓
接收方:recv() 可能拿到 "消息A消息",下一次拿到 "B消息C"

所以应用层必须自己定"消息边界",常见三种办法:
① 定长        每条消息固定 128 字节,不够就补空格
② 分隔符      每条消息以 \n 结尾(HTTP 头部、Redis 协议就这么干)
③ 长度前缀    先发 4 字节说明"接下来有 N 字节",再发内容
              ← 这是最常用、最稳的做法(gRPC、MySQL 协议都用它)

而 UDP 恰好相反:它保留边界。
你 sendto 一次 100 字节,对方 recvfrom 就是一次拿到完整 100 字节,
要么全到要么不到,绝不会拆开或粘连。

UDP 恰好相反这一点值得单独记住:它保留边界,好比寄整箱货——你寄一箱,对方就收一箱,绝不会把两箱货倒在一起,也不会给你半箱。要么整箱到,要么整箱没了。所以做自定义协议时,UDP 反而省掉了"划分消息边界"这桩麻烦事。

再引入一个运维和网络设备里的高频词:流(Flow)。它的定义就是我们在 § 1.3 见过的五元组——协议 + 源 IP + 源端口 + 目的 IP + 目的端口。所有五元组相同的包,属于同一条流。这个概念极其实用:

所以整条认知链是这样的:物理层看到比特 → 链路层看到帧 → 网络层看到包 → 传输层看到流 → 应用层看到消息。同一份数据,在五个层次上有五种截然不同的样貌。你排障时问的第一个问题应该是:"我现在是在哪一层看问题?"——因为"丢包"是包的视角,"连接超时"是流的视角,"接口报 500"是消息的视角,三者需要的工具和思路完全不同。

那五层视角用一句大白话串起来:物理层看到的是"电流的高低",链路层看到的是"一件件贴了本地标签的货",网络层看到的是"寄往某个城市的包裹",传输层看到的是"整套家具",应用层看到的是"沙发终于摆好了"。同一件事,五个人看到五幅完全不同的画面——所以排障时先问自己一句:"我这会儿是站在哪一层上看?"

你刚才看这篇文章,经过了多少数据包

粗略估算:本页 HTML + 样式 + 字体 + 图片 ≈ 300KB。
按每包 1500 字节算,大约需要 200 个数据包,从 Cloudflare 服务器发到你手机。这 200 个包跨越了十几个路由器,可能走了3-5 条不同路径,最终在 200 毫秒内全部到齐——你完全无感知。

把这些数字换算一下就更有画面了:300 KB 大约相当于 15 万个汉字,也就是一本中篇小说的量;它被拆成两百个纸箱,分三五条不同的路线运过来,全程二百毫秒——差不多是你眨两次眼的时间,一本小说就摆在你眼前拼装完毕了。而你唯一的感受是"这网站打开挺快"。

Recap · 收束

数据包是互联网的"原子"——小而独立、贴序号、走自己的路、到了再拼
它的对立面是电话网的电路交换(独占一条专线,沉默时也在浪费)。分组交换赢下这场六十年前的争论,靠的是三点:统计复用(利用率从百分之几到接近百分之百)、失败成本低(在万分之一误码率下,不拆包根本传不完一个 1MB 文件)、天生容错(断一条线,包自动绕路)。
每个包 = 头部 + 载荷。IP 头 20 字节、TCP 头 20 字节,加上以太网帧的 18 字节,最小开销 58 字节——所以传小数据时头部可能比内容还长。每层头里都必有一个"载荷类型"字段(以太网的 0x0800、IP 的协议号 6、TCP 的端口号),否则解封装无从下手。
MTU 1500 是 1980 年代的工程折中,减去 40 字节头部得到 MSS 1460。超限会触发分片——但分片是坏事(丢一片废全包),所以现代做法是靠 PMTUD 提前探测、宁小不切;而屏蔽 ICMP 会导致 PMTU 黑洞,表现为"小页面能开、大文件必卡"。
TTL 是包的寿命计数器(每跳减 1,到 0 丢弃),用来杀死路由环路里的幽灵包;traceroute 正是靠故意耗尽它来逐跳探路的。
IP 只提供"尽力而为"——可能丢、重复、乱序、损坏。所有可靠性都是 TCP 在两端主机上靠序号、ACK、快速重传、超时重传、SACK 补出来的。而且要记住:路由器丢包不是故障,是它在向你喊"慢点"——整个互联网的拥塞控制就建立在这个信号上。
最后是视角的跃迁:包是无状态的碎片,流是有序的字节河。TCP 保证顺序但不保留消息边界(粘包/半包),所以应用层必须自己用长度前缀或分隔符划界。下一次你看到"网络拥塞"、"丢包"、"MTU"、"粘包"这些词,脑海里就该浮现出"快递分箱"的画面——以及那 58 个字节的面单。

☰ 主页
Xue Hai Wu Ya · Network · § 1.5 · Packet