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 秒实验
- 把载荷全改成
b'\x7e' * 8,看转义后长度是多少(每个都翻倍)。 - 算一下:如果字节完全随机,平均有多少比例的字节需要转义?
- 想一下:为什么以太网「用长度字段」就不需要转义?
常见误区
- 以为「转义只是加一个字节」。 原字节还要异或 0x20,两步都要做。
- 以为「只有数据部分要转义」。 帧头和 FCS 也要,只要落在规则里就得改。
- 以为「转义开销总是很小」。 加密或压缩数据接近随机,开销会明显上升。
自测三问(点开对答案)
1. PPP 用什么当帧标志?为什么需要转义? —— **0x7E。** 因为数据里也可能出现 0x7E,接收方会误以为帧结束了。 2. 转义的具体规则是什么? —— **小于 0x20 或等于 0x7D/0x7E 的字节,前面插 0x7D,原字节异或 0x20。** 3. 三种成帧办法各是什么? —— **看标志(PPP,要转义)、看长度(以太网)、看时钟/定长(ATM)。**下一节看 PPP 怎么被塞进以太网:PPPoE 的 8 个字节。
- ☐ (本节完,下一节 3.5.4 PPPoE)