Appearance
封装成帧与透明传输
概念
封装成帧(framing)就是在网络层交下来的数据报前后加上首部和尾部,拼成一个"帧"(frame),使接收方能从连续的比特流里认出"哪一段是一个完整的数据单位"。
一句话说清它是什么:帧 = 数据链路层的"包裹";封装成帧 = 打包 —— 贴上面单(首部)、标上封口(尾部),让对面知道"从哪开始、到哪结束"。
为什么必须成帧? 物理层只保证"比特流照着送",它不保证"边界"——对面收到的是一长串 0 和 1,如果不知道哪里是一个单位的开头,收到的数据就没法交给上层。 成帧就是给比特流"分段"。
由此引出两个必须同时解决的小问题:
| 问题 | 要回答什么 | 对应手段 |
|---|---|---|
| 帧定界(framing / 帧同步) | 接收方怎么知道"这一帧到哪结束" | 帧定界符 + 填充 / 计数 / 违规编码 |
| 透明传输(transparent transmission) | 数据里恰好出现了"和定界符一样的比特"怎么办 | 字节填充 / 零比特填充 |
⚠️ "透明"是本课最关键的一个词:"透明"不是"看不见",而是"不管数据里出现什么内容,链路层都能原样把它送过去"——即"数据对链路层是透明(无害)的"。 如果数据里出现定界符就会把帧提前截断,那就是"不透明"。 这两个问题是同一枚硬币的两面:定界符是必需的,而它一旦可能与数据撞车,就必须有填充(转义)机制。
与上一章(net/10-physical.md)的衔接:上一章讲"比特怎么变成信号";本章讲"这些比特怎么被切成一个个有边界的数据单位"——物理层给了"管道",链路层开始"分件包装"。
原理
一、帧的构成:首部 + 数据 + 尾部
任何一帧都是三段:
text
┌──────────────┬──────────────────────────┬──────────────┐
│ 首部 │ 数据部分 │ 尾部 │
│ (帧头) │ (来自网络层的分组/数据报) │ (含校验码) │
└──────────────┴──────────────────────────┴──────────────┘
目的/源地址 就是"要送的东西" FCS 校验码
类型/控制/长度| 段 | 作用 | 以太网的具体值 |
|---|---|---|
| 首部 | 放定界信息、地址、类型、长度等控制信息 | 14 B(目的 MAC 6 + 源 MAC 6 + 类型 2) |
| 数据 | 上层交下来的载荷 | 46 ~ 1500 B |
| 尾部 | 放校验码(帧检验序列 FCS) | FCS 4 B |
⚠️ "数据链路层既加首部又加尾部"是它与别的层的显著差别——网络层、传输层只加首部(因为它们的数据是"有序流",前面有长度字段就够);链路层必须在尾部放 FCS,因为它的差错是"传输过程中逐位产生"的,校验码要读完整帧才能算完。 这个"头部 + 尾部"的形状,正是"帧"与"分组""报文段"在外观上最好认的区别。
不同链路的帧长:
| 链路层协议 | 首部 | 数据上限 | 尾部 | 帧长范围 |
|---|---|---|---|---|
| 以太网(Ethernet V2) | 14 B | 46 ~ 1500 B | FCS 4 B | 64 ~ 1518 B |
| PPP | 5 B(含标志) | 1500 B | 3 B(含标志、FCS 2 B) | 随协商 |
| HDLC | 4 B | — | 4 B(含标志、FCS 2 B) | 随协商 |
记忆锚点:"以太网帧 64 ~ 1518 B"这个数就是本章要算出来的——下限 64 B 来自"CSMA/CD 的争用期"(见 net/23-csma.md),上限 1518 B 来自"MTU 1500 + 14 + 4"(见 net/01-architecture.md)。
二、帧定界:四种方法
帧定界的任务只有一句话:让接收方能从比特流里切出边界。 教材给出四种(以太网用的是"第五种",见后):
| 方法 | 怎么做 | 弱点 |
|---|---|---|
| ① 字符计数法 | 帧首第一个字段是"本帧的字符数"(含计数字段本身) | 计数字段一旦出错,后面所有帧的边界全部错位且无法恢复("一错到底") |
| ② 字符填充法(字节填充) | 用特殊字符 SOH 开头、EOT 结尾;数据里出现这些字符就转义 | 填充开销随数据内容波动;且"字符"这个概念与编码有关,对任意比特不透明 |
| ③ 零比特填充法(比特填充) | 用固定的比特模式(HDLC 的标志 01111110)定界;数据里连续 5 个 1 后插 0 | 需要比特级处理,实现比字节级复杂 |
| ④ 违规编码法 | 用物理层编码中"不该出现"的码型作定界(如曼彻斯特编码的"高-高""低-低") | 依赖物理层编码本身有冗余码型;若编码没有冗余(如 NRZ),此法不可用 |
⚠️ 四种方法的分水岭在"出错后能不能自恢复":
- 字符计数法:不能自恢复(数错一个字符,之后全乱)——实考中常问"为什么计数法不实用",答案就是这条。
- 字符填充 / 零比特填充:能自恢复(每帧都有独立的定界符,错一帧不影响下一帧)。
- 违规编码法:能自恢复,且不占数据带宽(定界信息藏在"码型"里,不占比特位)。
以太网的做法其实更简单:帧首有 7 B 前导码 + 1 B 帧开始定界符 SFD,帧与帧之间要求有最小间隔(帧间间隔 IFG)——"定界"靠"前导码同步 + 固定最小时延",而不是靠"数据里不可能出现的字符"。 它也属于"违约定界"的思路:以太网规定接收方在帧间间隔期间不找帧,因此不需要在数据里做任何填充。
为什么以太网不需要转义? 因为以太网的帧长有上限(1518 B)且帧间隔有下限(12 B 时间,即 9.6 μs)——接收方靠"前导码重新同步"就能找到帧头,不需要在数据里塞定界符,所以以太网的数据部分是"完全透明"的(可传任意 46 ~ 1500 B 的字节序列,不做任何填充)。
三、透明传输:字节填充与零比特填充
(1)字节填充法(字符填充,character stuffing)
规则(发送方):数据中只要出现与定界符/转义符相同的字节,就在它前面插入一个转义字节 ESC;接收方收到 ESC 就把它丢掉、把后一个字节当普通数据处理。
定界符约定(本章锚点):SOH = 0x01(帧首)、EOT = 0x04(帧尾)、ESC = 0x1B(转义)。
text
发送方: 数据里遇到 01 / 04 / 1B 三个字节, 一律在前面插一个 1B
数据: 41 01 42 1B 43 04 (6 B)
| | | |
填充: 41 1B 01 42 1B 1B 43 1B 04 (9 B) ★ 插了 3 个 1B
线上帧: SOH | 41 1B 01 42 1B 1B 43 1B 04 | EOT (2 + 9 = 11 B)
接收方: 看到 1B 就"吃掉下一个字节的转义语义"
→ 收到 SOH 说明帧开始 → 收到 EOT 说明帧结束(它前面没有 1B)⚠️ "ESC 自己出现时也要转义"是必考细节:数据里本来就有
0x1B,发送方要把它写成1B 1B;接收方看到连续两个1B就还原成一个1B。 少了这一条,接收方无法区分"这个0x1B是转义符还是真数据"。
(2)零比特填充法(比特填充,bit stuffing)
HDLC 用 01111110(0x7E)作为帧的标志字段(标志既作帧首又作帧尾)。 为了防止数据里出现同样的 8 位模式,规则是:
★ 发送方:数据比特流中只要出现连续 5 个
1,就立即插入一个0。★ 接收方:数据比特流中只要看到连续 5 个1,就把紧随其后的那个0删掉。
"为什么是 5 个而不是 6 个?" 因为标志是 6 个连续 1(01111110 中间是 6 个 1)——只要保证"数据里绝不出现 6 个连续 1",标志就不会与数据撞车。 发送方在 5 个 1 之后就插 0,正好把"6 个 1"这个可能压掉。
text
标志: 0 1 1 1 1 1 1 0
数据: 111111111111 (12 个 1)
↑ 第 5 个 1 后插 0, 第 10 个 1 后插 0
发送: 11111011111011 (14 位, 插了 2 个 0)
└───┬───┘
5 个 1 后跟一个 0
数据: 0111111011111101 (16 位)
发送: 011111010111110101 (18 位, 插了 2 个 0)
└┘ └┘
这两个 0 是插进去的
接收方: 见 5 个 1 就删下一个 0 → 精确还原| 数据比特 | 原长 | 填充后 | 膨胀率 | 插了几个 0 |
|---|---|---|---|---|
111111111111 | 12 | 11111011111011 | 16.67% | 2 |
0111111011111101 | 16 | 011111010111110101 | 12.50% | 2 |
101010101010 | 12 | 101010101010 | 0.00% | 0 |
0111111011111010 | 16 | 011111010111110010 | 12.50% | 2 |
⚠️ 零比特填充的"最坏情况"必须会算:数据全是 1 时,每 5 位就要插 1 位,膨胀率上界是
。 本例 111111111111只插了 2 位(16.67%),因为没有出现第 11 个 1 之后的第 5 个 1——12 位里前 10 位触发两次,剩下 2 位不够触发。
(3)两种填充的对照
| 对比项 | 字节填充 | 零比特填充 |
|---|---|---|
| 处理单位 | 字节(8 位) | 比特 |
| 触发条件 | 遇到 SOH / EOT / ESC 三种字节 | 遇到连续 5 个 1 |
| 膨胀率 | 取决于数据内容,最坏可达 100%(数据全是需转义字节时) | 有上界 20%(数据全 1 时) |
| 透明性 | 对 8 位字节透明,对"任意比特"不透明 | 对任意比特流透明 |
| 典型协议 | PPP(异步链路)、BSC | HDLC、PPP(同步链路) |
锚点(字节填充最坏情况):数据 01 01 01(3 B)全是需转义字节 → 填充后 1B 01 1B 01 1B 01(6 B),膨胀率整整 100.00%。
四、帧长:MTU、最小帧与最大帧
帧长有两个边界,各有各的来历:
| 边界 | 值 | 来历 |
|---|---|---|
| 最大帧长 | 1518 B | = MTU(1500 B,IP 层载荷上限)+ 14 B 首部 + 4 B FCS——限制它是为了让"一个帧的发送时间不至于太长",避免一个站长期占住共享信道 |
| 最小帧长 | 64 B | = 争用期 × 数据率 = 51.2 μs × 10 Mbps = 512 bit——限制它是为了让"发送方在还没发完时就能检测到冲突"(见 net/23-csma.md) |
由 64 B 反推最小数据长度:64 − 14(首部)− 4(FCS)= 46 B——数据不足 46 B 时必须填充(padding)到 46 B,这就是"以太网最小数据 46 B"的由来。
帧效率 = 数据长度 ÷ 帧长:
| 数据长度 | 帧长(+18 B) | 帧效率 |
|---|---|---|
| 46 B(最小) | 64 B | 71.88% |
| 100 B | 118 B | 84.75% |
| 512 B | 530 B | 96.60% |
| 1024 B | 1042 B | 98.27% |
| 1500 B(最大) | 1518 B | 98.81% |
⚠️ 这张表说明"帧越大越划算":18 B 固定开销在 46 B 数据上占 28.12%,在 1500 B 数据上只占 1.19%——所以协议都希望"尽量装满"。 但帧又不能无限大:帧越长,一个站占用共享信道的时间越久(时延变大),且"一位出错就要重传整帧"的代价越大——1518 B 这个上限就是这两种考虑的折中。
⚠️ 算"帧效率"时最常见的错:忘了 FCS 也算帧长。"以太网帧长 = 数据 + 14"是错的,必须是"数据 + 14 + 4"。 而且前导码 7 B 与 SFD 1 B 都"不计入"帧长(它们是物理层的同步开销、不属于链路层帧)——这也是常考的"送分陷阱"。
五、帧定界失败的后果
| 失败方式 | 现象 | 能否恢复 |
|---|---|---|
| 定界符被数据撞车(无填充) | 帧被提前截断,后半段被当成新帧的帧头 | 不能(后面全乱) |
| 计数字段出错(字符计数法) | 从这一帧起所有边界错位 | 不能(必须重新同步) |
| 帧长超上限 / 不足下限 | 接收方丢弃该帧 | 能(丢一帧而已) |
| FCS 校验错 | 接收方丢弃该帧 | 能 |
⚠️ 一句话总结"透明传输为什么重要":定界符是"约定的暗号",数据是"任意的内容",两者必然会有撞车的一天——填充机制就是"把撞车的位置改写成不会撞车的写法",这是"任意数据 + 固定定界"能共存的前提。
示例
例 1:C 实现——字节填充与零比特填充
参数:字节填充数据
41 01 42 1B 43 04;定界符SOH = 0x01、EOT = 0x04、ESC = 0x1B;零比特填充的四个比特串见下;帧效率按"数据 + 18 B"计算。
#include <stdio.h>
/* 帧定界符(沿用 PPP / HDLC 风格) */
#define SOH 0x01
#define EOT 0x04
#define ESC 0x1B
/* ① 字节填充: 数据里出现 SOH/EOT/ESC 时, 前面插一个 ESC */
int byte_stuff(const unsigned char *in, int n, unsigned char *out) {
int i, k = 0;
for (i = 0; i < n; i++) {
if (in[i] == SOH || in[i] == EOT || in[i] == ESC) out[k++] = ESC;
out[k++] = in[i];
}
return k; /* 返回填充后的长度 */
}
/* ② 零比特填充: 连续 5 个 1 之后插一个 0 (比特用字符 '0'/'1' 表示) */
int bit_stuff(const char *in, int n, char *out) {
int i, k = 0, run = 0; /* run = 当前连续 1 的个数 */
for (i = 0; i < n; i++) {
out[k++] = in[i];
if (in[i] == '1') {
if (++run == 5) { out[k++] = '0'; run = 0; }
} else {
run = 0; /* 遇到 0, 连续计数清零 */
}
}
return k;
}
int main(void) {
unsigned char data[6] = {0x41, 0x01, 0x42, 0x1B, 0x43, 0x04};
unsigned char buf[64];
const char *bits[4] = {"111111111111", "0111111011111101",
"101010101010", "0111111011111010"};
int lens[4] = {12, 16, 12, 16};
int dl[5] = {46, 100, 512, 1024, 1500};
char ob[80];
int i, m;
printf("① 字节填充 (SOH=01 EOT=04 ESC=1B)\n");
m = byte_stuff(data, 6, buf);
printf(" 原始 %d B:", 6);
for (i = 0; i < 6; i++) printf(" %02X", data[i]);
printf("\n 填充 %d B:", m);
for (i = 0; i < m; i++) printf(" %02X", buf[i]);
printf("\n 线上帧 = SOH + 填充数据 + EOT = %d B; 膨胀率 %.2f%%\n",
m + 2, (m - 6) * 100.0 / 6);
printf("\n② 零比特填充 (HDLC 标志 01111110)\n");
for (i = 0; i < 4; i++) {
m = bit_stuff(bits[i], lens[i], ob);
ob[m] = '\0';
printf(" %s (%2d 位) -> %s (%2d 位) 膨胀率 %5.2f%%\n",
bits[i], lens[i], ob, m, (m - lens[i]) * 100.0 / lens[i]);
}
printf("\n③ 帧效率 = 数据 / (数据 + 14 首部 + 4 FCS)\n");
for (i = 0; i < 5; i++)
printf(" 数据 %4d B -> 帧 %4d B, 效率 %5.2f%%\n",
dl[i], dl[i] + 18, dl[i] * 100.0 / (dl[i] + 18));
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
① 字节填充 (SOH=01 EOT=04 ESC=1B)
原始 6 B: 41 01 42 1B 43 04
填充 9 B: 41 1B 01 42 1B 1B 43 1B 04
线上帧 = SOH + 填充数据 + EOT = 11 B; 膨胀率 50.00%
② 零比特填充 (HDLC 标志 01111110)
111111111111 (12 位) -> 11111011111011 (14 位) 膨胀率 16.67%
0111111011111101 (16 位) -> 011111010111110101 (18 位) 膨胀率 12.50%
101010101010 (12 位) -> 101010101010 (12 位) 膨胀率 0.00%
0111111011111010 (16 位) -> 011111010111110010 (18 位) 膨胀率 12.50%
③ 帧效率 = 数据 / (数据 + 14 首部 + 4 FCS)
数据 46 B -> 帧 64 B, 效率 71.88%
数据 100 B -> 帧 118 B, 效率 84.75%
数据 512 B -> 帧 530 B, 效率 96.60%
数据 1024 B -> 帧 1042 B, 效率 98.27%
数据 1500 B -> 帧 1518 B, 效率 98.81%⚠️ 三点说明:
- 本机无 C 编译器:此段代码逐行人工审查,并用等价的 Python 实现实跑核对,输出 17 行逐字一致(含
%02X的大写十六进制与两位补零、%.2f%%的百分号转义)。 %02X里的02不能省:省了的话0x04会打印成4而不是04——十六进制序列一旦宽度不齐,"同一帧的字节位置"就看不清了。- 比特用字符
'0'/'1'表示而不是真比特位:C 里没有"比特数组"这个类型,教学代码用字符是最直观的写法(真实实现用位操作 + 移位,见net/21-error.md的 CRC 部分)。
例 2:Python——填充开销与帧效率全表
def pad(s, w):
"""按"显示宽度"补空格: 中文算 2 列, 否则终端里对不齐"""
return s + ' ' * max(0, w - sum(2 if ord(c) > 0x2000 else 1 for c in s))
def stuff_bit(bits):
"""零比特填充: 连续 5 个 1 之后插一个 0"""
out, run = [], 0
for b in bits:
out.append(b)
if b == '1':
run += 1
if run == 5:
out.append('0')
run = 0
else:
run = 0
return ''.join(out)
def destuff_bit(bits):
"""接收方: 连续 5 个 1 之后删掉紧跟的那个 0"""
out, run, i = [], 0, 0
while i < len(bits):
out.append(bits[i])
if bits[i] == '1':
run += 1
if run == 5:
i += 1 # 跳过插入的 0
run = 0
else:
run = 0
i += 1
return ''.join(out)
print('=== ① 四种帧定界法 ===')
print(' ' + pad('方法', 22) + pad('怎么做', 40) + '弱点')
rows = [('字符计数法', '帧首一个字段记"本帧多少字符"', '一错到底, 无法自恢复'),
('字符填充法', 'SOH 开头 EOT 结尾, 特殊字符前插 ESC', '开销随内容波动, 最坏 100%'),
('零比特填充法', '标志 01111110; 连续 5 个 1 后插 0', '要比特级处理, 开销上界 20%'),
('违规编码法', '用曼彻斯特的"高-高/低-低"码型定界', '依赖物理层编码有冗余码型'),
('以太网的做法', '前导码 + SFD 同步, 帧间留最小间隔 IFG', '数据部分完全透明, 不做填充')]
for r in rows:
print(' ' + pad(r[0], 22) + pad(r[1], 40) + r[2])
print(' ★ 分水岭: 出错后能不能自恢复 —— 只有字符计数法不能')
print()
print('=== ② 字节填充 (SOH=01 EOT=04 ESC=1B) ===')
SOH, EOT, ESC = 0x01, 0x04, 0x1B
print(' ' + pad('原始数据', 26) + pad('填充后', 32) + '膨胀率')
for d in [(0x41, 0x01, 0x42, 0x1B, 0x43, 0x04), (0x48, 0x49), (0x01, 0x01, 0x01)]:
f = []
for b in d:
if b in (SOH, EOT, ESC):
f.append(ESC)
f.append(b)
print(' ' + pad(' '.join('%02X' % x for x in d), 26)
+ pad(' '.join('%02X' % x for x in f), 32)
+ '%7.2f%%' % ((len(f) - len(d)) * 100.0 / len(d)))
print(' ★ 全是需转义字节时最坏: 3 B -> 6 B, 膨胀 100.00%')
print()
print('=== ③ 零比特填充 (HDLC 标志 01111110) ===')
print(' ' + pad('原始比特', 20) + pad('填充后', 22) + pad('原长', 8) + pad('新长', 8) + '膨胀率')
for b in ['111111111111', '0111111011111101', '101010101010',
'0111111011111010', '1' * 20]:
s = stuff_bit(b)
back = destuff_bit(s)
print(' ' + pad(b, 20) + pad(s, 22) + pad(str(len(b)), 8) + pad(str(len(s)), 8)
+ '%7.2f%% %s' % ((len(s) - len(b)) * 100.0 / len(b),
'还原 OK' if back == b else '还原失败'))
print(' ★ 上界: 数据全是 1 时每 5 位插 1 位 -> 20 位插 4 位 = 20.00%')
print()
print('=== ④ 帧长三兄弟: MTU / 最小帧 / 最大帧 ===')
print(' MTU 1500 = IP 层载荷上限; + 14 首部 + 4 FCS -> 最大帧 1518 B')
print(' 争用期 51.2 us x 10 Mbps = 512 bit = 64 B -> 最小帧 64 B')
print(' 64 - 14 - 4 = 46 B -> 最小数据 46 B')
print(' ' + pad('数据', 10) + pad('帧长', 10) + pad('开销占比', 12) + '帧效率')
for n in [46, 100, 512, 1024, 1500]:
print(' ' + pad('%d B' % n, 10) + pad('%d B' % (n + 18), 10)
+ pad('%.2f%%' % (18 * 100.0 / (n + 18)), 12)
+ '%.2f%%' % (n * 100.0 / (n + 18)))
print(' ★ 前导码 7 B + SFD 1 B 不计入帧长 (属物理层同步开销)')
print()
print('=== ⑤ 透明传输的两个"反例" ===')
print(' 若不做填充: 数据里的 0x04 会被当成帧尾 EOT -> 帧被提前截断')
print(' 若不做填充: 数据里出现 01111110 -> 接收方误以为帧结束')
print(' 修法: 字节填充插 1B / 比特填充插 0 —— 代价是带宽, 换来的是"任意数据都能过"')
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照(真实运行结果):
=== ① 四种帧定界法 ===
方法 怎么做 弱点
字符计数法 帧首一个字段记"本帧多少字符" 一错到底, 无法自恢复
字符填充法 SOH 开头 EOT 结尾, 特殊字符前插 ESC 开销随内容波动, 最坏 100%
零比特填充法 标志 01111110; 连续 5 个 1 后插 0 要比特级处理, 开销上界 20%
违规编码法 用曼彻斯特的"高-高/低-低"码型定界 依赖物理层编码有冗余码型
以太网的做法 前导码 + SFD 同步, 帧间留最小间隔 IFG 数据部分完全透明, 不做填充
★ 分水岭: 出错后能不能自恢复 —— 只有字符计数法不能
=== ② 字节填充 (SOH=01 EOT=04 ESC=1B) ===
原始数据 填充后 膨胀率
41 01 42 1B 43 04 41 1B 01 42 1B 1B 43 1B 04 50.00%
48 49 48 49 0.00%
01 01 01 1B 01 1B 01 1B 01 100.00%
★ 全是需转义字节时最坏: 3 B -> 6 B, 膨胀 100.00%
=== ③ 零比特填充 (HDLC 标志 01111110) ===
原始比特 填充后 原长 新长 膨胀率
111111111111 11111011111011 12 14 16.67% 还原 OK
0111111011111101 011111010111110101 16 18 12.50% 还原 OK
101010101010 101010101010 12 12 0.00% 还原 OK
0111111011111010 011111010111110010 16 18 12.50% 还原 OK
1111111111111111111111111011111011111011111020 24 20.00% 还原 OK
★ 上界: 数据全是 1 时每 5 位插 1 位 -> 20 位插 4 位 = 20.00%
=== ④ 帧长三兄弟: MTU / 最小帧 / 最大帧 ===
MTU 1500 = IP 层载荷上限; + 14 首部 + 4 FCS -> 最大帧 1518 B
争用期 51.2 us x 10 Mbps = 512 bit = 64 B -> 最小帧 64 B
64 - 14 - 4 = 46 B -> 最小数据 46 B
数据 帧长 开销占比 帧效率
46 B 64 B 28.12% 71.88%
100 B 118 B 15.25% 84.75%
512 B 530 B 3.40% 96.60%
1024 B 1042 B 1.73% 98.27%
1500 B 1518 B 1.19% 98.81%
★ 前导码 7 B + SFD 1 B 不计入帧长 (属物理层同步开销)
=== ⑤ 透明传输的两个"反例" ===
若不做填充: 数据里的 0x04 会被当成帧尾 EOT -> 帧被提前截断
若不做填充: 数据里出现 01111110 -> 接收方误以为帧结束
修法: 字节填充插 1B / 比特填充插 0 —— 代价是带宽, 换来的是"任意数据都能过"五条结论:
- 封装成帧要同时解决"定界"与"透明"两件事:定界靠定界符/计数/违规编码,透明靠填充——只做定界不做填充,数据里的定界符就会把帧截断。
- 四种定界法的分水岭是"能不能自恢复":字符计数法一错到底,其余三种都能——所以以太网、PPP、HDLC 都不用计数法。
- 字节填充的开销"看内容":锚点数据从 6 B 涨到 9 B(50.00%);最坏情况(数据全是需转义字节)膨胀 100.00%——这就是"字节填充不适合高吞吐链路"的原因。
- 零比特填充的开销"有上界":12 个连续 1 只插 2 个 0(16.67%),上界是 20.00%——因为触发条件是"5 个连续 1",与字节边界无关,所以它对任意比特流都透明。
- 帧效率只由"数据长度"决定(固定开销 18 B):46 B → 71.88%,1500 B → 98.81%——"帧越大越划算,但上限被 MTU 卡在 1518 B"。
考点
考点
1. 必背结论
- 封装成帧 = 给网络层的分组加上首部和尾部,形成"帧";帧 = 首部 + 数据 + 尾部。
- 两个必须同时解决的问题:帧定界(边界在哪)+ 透明传输(数据里出现定界符怎么办)。
- "透明"的含义:不管数据里出现什么比特组合,都能原样送过去——不是"看不见",是"不受影响"。
- 四种帧定界法:字符计数法 / 字符填充法 / 零比特填充法 / 违规编码法。
- ★ 四种方法的分水岭:能否在出错后自恢复——只有字符计数法不能(一错到底)。
- 字节填充三符(本章锚点):SOH =
0x01、EOT =0x04、ESC =0x1B;三个都要转义,ESC 本身也要转义。 - ★ 零比特填充规则:发送方"连续 5 个 1 后插一个 0";接收方"连续 5 个 1 后删掉紧跟的 0"。
- 为什么是 5 个:标志字段
01111110中间是 6 个连续 1;压住"第 6 个 1"就压住了撞车。 - 两种填充的开销上界:字节填充最坏 100.00%(数据全是需转义字节);零比特填充上界 20.00%(数据全是 1)。
- ★ 锚点(字节填充):
41 01 42 1B 43 04(6 B)→41 1B 01 42 1B 1B 43 1B 04(9 B),膨胀 50.00%;线上帧 = SOH + 9 B + EOT = 11 B。 - ★ 锚点(零比特填充):12 个 1 →
11111011111011(14 位),膨胀 16.67%;0111111011111101(16 位)→011111010111110101(18 位),膨胀 12.50%。 - 以太网帧长范围:64 ~ 1518 B;数据 46 ~ 1500 B;前导码 7 B + SFD 1 B 不计入帧长。
- 最大帧 1518 的来历:MTU 1500 + 首部 14 + FCS 4。
- 最小帧 64 的来历:争用期 51.2 μs × 10 Mbps = 512 bit = 64 B(见
net/23-csma.md)。 - 帧效率锚点(固定开销 18 B):46 B → 64 B、71.88%;100 B → 118 B、84.75%;1500 B → 1518 B、98.81%。
- 以太网为什么不用转义:靠"前导码同步 + 帧间最小间隔 IFG"定界,数据部分天然透明,不做任何填充。
2. 高频陷阱
- 把"透明传输"理解成"数据加密/看不见":错。透明 = 数据内容不受链路层影响、能原样通过——与加密无关。
- 认为"定了界就不需要填充":错。定界符本身就可能出现在数据里——有定界符就必须有填充(转义),除非定界机制与数据内容完全无关(如以太网的前导码方案)。
- 零比特填充写成"连续 6 个 1 后插 0":错。是"连续 5 个 1 后插 0"——写成 6 就与标志
01111110撞车,等于没填。 - 零比特填充的插入位写成"1":错。插入的是
0;接收方"见 5 个 1 删掉下一个 0"。 - 算帧长时漏掉 FCS:错。帧长 = 数据 + 14 + 4;"数据 + 14"算出的是"不含校验码的长度"。
- 把前导码 7 B 与 SFD 1 B 算进帧长:错。它们是物理层的同步开销,不计入 64 ~ 1518 B——算进去会得出"最小帧 72 B"这种错答案。
- 认为"字节填充的开销是固定的":错。它随数据内容变化,最坏 100%(原始数据里每个字节都要转义)。
- 认为"零比特填充的开销也是随机的":错(半对)。它随数据内容变化,但有上界 20.00%(因为触发条件是固定的 5 位,不是"某几个字节值")。
- 把"违规编码法"说成"用特殊字符定界":错。它用的是"物理层编码中不会出现的码型"(曼彻斯特编码的
高-高/低-低),与字符无关。 - 认为"以太网帧的最小数据 64 B":错。最小帧 64 B(含 14 + 4 开销)→ 最小数据 46 B。
- 把"帧定界失败"与"FCS 校验失败"混为一谈:错。定界失败会导致"后续帧全部错位"(无法自恢复);FCS 校验失败只丢一帧,下一帧照收。
3. 解题模板("成帧与透明传输")
① 认清"定界手段"是哪一种:
计数法 -> 一错到底, 不可靠
字符填充 -> 定界符 SOH/EOT + 转义 ESC
零比特填充 -> 标志 01111110; 连续 5 个 1 后插 0
违规编码 -> 用编码里不出现的码型
以太网 -> 前导码 + SFD + 帧间间隔 IFG (数据不做填充)
② 字节填充题: "数据字节流里凡出现 01/04/1B 就在前面插 1B"
膨胀率 = (填充后长度 - 原长) / 原长
③ 零比特填充题: 从左往右扫, 每数到 5 个 1 就写一个 0, 计数清零
上界 = 1/5 = 20%; 还原时"见 5 个 1 删下一个 0"
④ 帧长题: 帧长 = 数据 + 14(首部) + 4(FCS)
最大 1518 B (MTU 1500); 最小 64 B (争用期 512 bit); 最小数据 46 B
前导码 7 + SFD 1 不计入帧长
⑤ 帧效率题: 效率 = 数据长度 / 帧长
46 B -> 71.88%; 1500 B -> 98.81%; 开销固定 18 B4. 与相邻章节的接口
net/10-physical.md(物理层):"违规编码法"直接用上一章的曼彻斯特编码——曼彻斯特每个比特中间必有一次跳变,所以"高-高""低-低"这两种码型根本不会出现,正好拿来当定界符;"NRZ 没有冗余码型,所以不能用违规编码法"也是上一章的结论。net/21-error.md(差错控制):本章帧尾那 4 B FCS 就是下一章 CRC 的落点——"帧为什么要加尾部"的答案在下一章展开。net/23-csma.md(介质访问控制):"最小帧 64 B = 争用期 × 数据率"整条推导在那一章——本章只用结论,不讲过程。net/24-ethernet.md(以太网):"以太网帧 64 ~ 1518 B、前导码 7 + SFD 1"在那里给出完整的字段图;本章的"帧三段式"到那一章变成"6 个具体字段"。net/01-architecture.md(体系结构):"每层加首部"在本层多了一个尾部——上一章说"逐层封装",本章说"链路层这一层的封装长什么样、为什么需要填充"。net/30-ip.md(IP 数据报):"MTU 1500 = IP 层载荷上限"是两章共用的锚点——IP 分片就是"载荷超过 MTU 时怎么切"(见那一章的 4000 B → 3 片算例)。os/32-io.md(I/O 管理):"缓冲 + 分帧 + 校验"与"设备驱动的分组收发"是同一套思路——都是"把不定长的数据流切成定长的处理单位"。
小结
- 封装成帧 = 给分组加首部和尾部,形成"帧";帧 = 首部 + 数据 + 尾部。
- 必须同时解决两件事:帧定界(边界在哪)+ 透明传输(数据撞上定界符怎么办)。
- "透明"= 数据内容不受链路层影响、原样通过。
- 四种定界法:字符计数(一错到底,不可用)/ 字符填充 / 零比特填充 / 违规编码;以太网另用"前导码 + SFD + 帧间间隔"。
- ★ 字节填充锚点:
41 01 42 1B 43 04→41 1B 01 42 1B 1B 43 1B 04,6 B → 9 B、膨胀 50.00%;最坏 100.00%。 - ★ 零比特填充锚点:12 个 1 →
11111011111011,膨胀 16.67%;上界 20.00%。 - ★ 帧长:以太网 64 ~ 1518 B;数据 46 ~ 1500 B;最大 1518 = MTU 1500 + 14 + 4;最小 64 = 争用期 51.2 μs × 10 Mbps = 512 bit。
- 前导码 7 B + SFD 1 B 不计入帧长。
- 帧效率锚点:46 B → 71.88%;100 B → 84.75%;512 B → 96.60%;1024 B → 98.27%;1500 B → 98.81%。
下一篇:差错控制:检错与纠错编码
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。