📚 第三章 数据链路
📄 一句话概括

PPP 用 0x7E 当帧标志,所以数据里出现 0x7E 就必须转义。 转义规则:前面插 0x7D、原字节异或 0x20;最坏情况下长度翻倍。

📝 本节和前面几节的关系

3.5.2 讲的是「怎么建链」,这一节讲「建好之后帧长什么样」。 核心是「成帧」这个通用问题:怎么让接收方知道边界在哪。

一、七个字段

PPP 的帧比以太网简单,但多了「转义」这件事:

字段 字节 说明
标志 7E 1 帧开始
地址 FF 1 固定值(PPP 没有地址,这是历史遗留)
控制 03 1 固定值,表示无编号帧
协议 2 0xC021 是 LCP、0x0021 是 IPv4
载荷 可变 最长 1500(可协商)
FCS 2 16 位校验,不是以太网的 32 位
标志 7E 1 帧结束

第二、三行那两个固定值很没道理,但有历史原因: PPP 脱胎于 HDLC,HDLC 是有地址的(用于多点链路)。 PPP 只用点对点,地址就没用了,但字段留着。 所以它固定是 FF 03,永远不变。 第六行那个「16 位校验」比以太网弱:因为点对点链路没有共享介质的冲突,出错概率低, 16 位够用,还能省 2 个字节。 这个取舍在拨号时代很实际——每一帧省 2 字节,一晚上能省不少。

二、为什么必须转义

因为 7E 既是「标志」也可能出现在数据里:

情况 不转义的后果
数据里有 0x7E 接收方以为帧在这里结束了
数据里有 0x7D 接收方以为「下一个是转义字符」
数据里有 0x00~0x1F 某些设备会当作控制字符

第一行是根本问题:帧边界靠「特殊字节」标记,那数据里就不能出现这个字节。 所以要么转义(改数据),要么用长度前缀(以太网的做法),要么定长(ATM 的做法)。 三种答案,各有代价。 转义规则很简单: 凡是小于 0x20、或者等于 0x7D、0x7E 的字节,前面插一个 0x7D,原字节异或 0x20。 接收端反过来做一遍就行。 代价是「长度会涨」,最坏情况每个字节都要转义,长度翻倍。 最坏情况不是理论问题:传压缩包、加密数据时,字节分布接近随机, 大约有 (32+2)/256 约 13% 的字节需要转义。 而在拨号时代,链路本来就慢,这 13% 很疼。 所以后来有了「ACFC/PFC」这些选项——能省掉固定字段和部分协议字段的压缩。

三、三种成帧办法

这是本节最该记住的对比:

做法 代表 好处 代价
看标志 PPP 实现简单 要转义、长度会涨
看长度 以太网 不用转义 要多一个长度字段
看时钟(定长) ATM 硬件极快 浪费带宽

第一行和第二行的差别很实际: PPP 用「特殊字节」,所以数据要改;以太网用「长度字段」,数据不用改。 以太网的做法更「不侵入数据」, 所以它更适合「什么数据都可能出现」的场景——这也是它赢的原因之一。 第三行「定长」是最快的:因为接收方不需要「找边界」,只要按 53 字节切就行, 硬件实现极其简单。 代价是小数据浪费严重(上一节 ATM 算过,1 字节要占 53)。 所以「成帧」这个问题没有最优解:只有「在你的场景里,哪种代价可以接受」。 PPP 选了「改数据」,因为拨号线路上「简单可靠」比「省带宽」重要。

四、动手 Demo:PPP 帧里为什么要转义

手工造一个 PPP 帧,载荷里故意放一个 0x7E 和一个 0x7D,然后做转义。 看原始十六进制和转义后的十六进制差在哪。

flag, fcs = 0x7E, 0xB5C3
payload = bytes([0x01, 0x01, 0x00, 0x08, 0x7E, 0x7D, 0x05, 0x02])
body = bytes([0xFF, 0x03, 0xC0, 0x21]) + payload
raw = bytes([flag]) + body + bytes([fcs >> 8, fcs & 0xFF]) + bytes([flag])
print('原始', raw.hex())

out = bytearray()
for b in body + bytes([fcs >> 8, fcs & 0xFF]):
    if b < 0x20 or b == 0x7D or b == 0x7E:
        out.append(0x7D)
        out.append(b ^ 0x20)
    else:
        out.append(b)
print('转义', (bytes([flag]) + bytes(out) + bytes([flag])).hex())
print('中间从 %d 字节变成 %d 字节' % (len(body) + 2, len(out)))

实跑输出:

--------------------------------------------------------------------
PPP 帧里为什么要转义
--------------------------------------------------------------------
**PPP 用 0x7E 当帧的开始和结束标志。**
**可数据里完全可能出现 0x7E**——那接收方就会以为「帧到这儿结束了」。
**解决办法是把数据里的 0x7E 换掉,这就是转义。**

  字段          字节     说明
  ------------------------------------------------------------
  标志 7E       1        帧开始
  地址 FF       1        固定值(PPP 没有地址,这是历史遗留)
  控制 03       1        固定值,表示无编号帧
  协议 C021     2        C021 表示这是 LCP 报文
  载荷          8        含 0x7E 与 0x7D 各一个,用来演示转义
  FCS           2        16 位校验(不是以太网的 32 位)
  标志 7E       1        帧结束

**原始帧长 16 字节,十六进制**:

  7eff03c021010100087e7d0502b5c37e

**转义规则:凡是小于 0x20、或者等于 0x7D、0x7E 的字节,前面插一个 0x7D,原字节异或 0x20。**
**这一帧里被转义的字节一共 9 个**,
十六进制是 03 01 01 00 08 7e 7d 05 02——**注意控制字段的 0x03 也在这批里**(它小于 0x20),
**所以连「帧头」本身都要转义,不只是用户数据。**

**转义后**(连同首尾两个 7E 一起打印):
  7eff7d23c0217d217d217d207d287d5e7d5d7d257d22b5c37e

**中间的字节数从 14 变成 23,多了 9 个。**
**最坏情况是「每个字节都要转义」,长度翻倍**:
**所以 PPP 的帧长上限 1500 字节,转义后最坏要占 3000 字节的线路时间。**
**这不是理论问题**——**传压缩包、加密数据时,字节分布接近随机,转义会明显变多。**
**三种常见做法各有代价**:
  做法                   好处                 代价
  ---------------------------------------------------------------------
  **转义**(PPP)        简单,接收端好解     **长度会涨,最坏翻倍**
  长度前缀(以太网)     长度固定             **需要一个「长度」字段**
  定长帧(ATM)          硬件实现简单         **浪费带宽**

**这就是「成帧」这个问题的全貌**:
**在一条连续的比特流里,怎么让接收方知道「从哪开始、到哪结束」。**
**三种答案:看标志(要转义)、看长度(要有字段)、看时钟(要定长)。**
**每一种都行,每一种都要付代价。**

原始帧是 16 字节,转义后 25 字节:9 个字节被转义,每个多一个 0x7D,所以多了 9 个。 注意被转义的字节里包括控制字段的 0x03—— 它小于 0x20,所以连「帧头」本身都要转义,不只是用户数据。 转义后的字节里能看到 7d 5e 和 7d 5d: 7d 5e 是 0x7E 异或 0x20 的结果、7d 5d 是 0x7D 异或 0x20。 这就是「为什么挑 0x7D 当转义字符」: 因为 0x7D 本身也要转义,但异或 0x20 之后是 0x5D,不会再撞。 这套规则保证「转义后的数据里绝对不会出现裸的 7E」。 「9 个字节里 8 个是控制字符」这件事说明了一件事:真实数据里「小于 0x20 的字节」其实很少, 所以正常情况的转义开销远小于「翻倍」。 但加密或压缩之后就不一样了——字节分布接近随机,转义开销就上来了。

30 秒实验

  1. 把载荷全改成 b'\x7e' * 8,看转义后长度是多少(每个都翻倍)。
  2. 算一下:如果字节完全随机,平均有多少比例的字节需要转义?
  3. 想一下:为什么以太网「用长度字段」就不需要转义?

常见误区

  1. 以为「转义只是加一个字节」。 原字节还要异或 0x20,两步都要做。
  2. 以为「只有数据部分要转义」。 帧头和 FCS 也要,只要落在规则里就得改。
  3. 以为「转义开销总是很小」。 加密或压缩数据接近随机,开销会明显上升。
自测三问(点开对答案) 1. PPP 用什么当帧标志?为什么需要转义? —— **0x7E。** 因为数据里也可能出现 0x7E,接收方会误以为帧结束了。 2. 转义的具体规则是什么? —— **小于 0x20 或等于 0x7D/0x7E 的字节,前面插 0x7D,原字节异或 0x20。** 3. 三种成帧办法各是什么? —— **看标志(PPP,要转义)、看长度(以太网)、看时钟/定长(ATM)。**

下一节看 PPP 怎么被塞进以太网:PPPoE 的 8 个字节。

相关笔记