简介:这份PPT面向正在学习计算机网络原理的高校学生与备考人员,聚焦数据链路层这一核心章节,帮助读者通过历年真题巩固HDLC、PPP、CRC校验、流量控制与差错控制编码等高频考点。资源包内含1个PPT文件,整体约1.32MB,以幻灯片形式集中呈现试题与参考答案,便于课堂复习或考前突击。内容按考试年份编排,收录2008年至2009年多次考试的选择题、填空题与计算题,并附有详细解答过程,例如CRC生成多项式校验、调制解调器传输时间计算、停等协议与滑动窗口机制分析、HDLC帧格式绘制等典型题型。已有37人学习,适合需要系统梳理第四章知识点、熟悉命题风格并查漏补缺的读者使用。
1. 从一份“计算机网络原理第四章试题汇总.ppt”说起:链路层到底在考什么
如果你手头正好有一份“计算机网络原理第四章试题汇总.ppt”,大概率是数据链路层那一章。翻开来反复出现的无非这几个词:HDLC、PPP、CRC、Go-back-N。很多人复习时把它们当四块孤立的知识点背,结果一到算 CRC 校验码、数滑动窗口序号就翻车。其实这一章的主线很清楚:链路层要解决的是“相邻两个节点之间,怎么把一串比特可靠地交过去”。HDLC 和 PPP 是两种成帧协议,CRC 是差错检测手段,Go-back-N 是出错之后的重传策略。把这四样串成一条线,试题里八成以上的计算和简答都能顺下来。这篇笔记就按这条线走,先讲清每个点为什么这么设计,再落到能动手复现的步骤和参数,最后把常见的坑摆出来。适合正在啃这一章的学生,也适合需要快速把链路层知识捡回来的从业者。
2. HDLC 与 PPP 的成帧差异:从比特填充到字节填充
2.1 为什么链路层一定要“成帧”
物理层送上来的是连续比特流,接收方根本不知道哪一段是一个完整的数据单元。成帧就是给数据加上明确的起止标记,让接收方能从比特流里切出一个个帧。HDLC 和 PPP 都干这件事,但标记方式和适用场景不同,这也是试题里最爱对比的地方。
HDLC 面向比特,用标志字段01111110作为帧的起止。问题是数据里如果恰好出现连续的 1,就可能和标志混淆。解决办法是比特填充:发送方在数据中每遇到 5 个连续 1,就插入一个 0;接收方收到 5 个连续 1 后跟一个 0,就把这个 0 删掉。这样数据里永远不会出现 6 个连续 1,标志字段就唯一了。
PPP 面向字节,常用0x7E作为帧定界符。如果数据里出现0x7E或转义字符0x7D,就用字节填充:把该字节异或0x20后再前面加一个0x7D。比如0x7E变成0x7D 0x5E,0x7D变成0x7D 0x5D。PPP 还支持异步和同步两种链路,异步用字节填充,同步用比特填充,这一点试题里经常拿来设陷阱。
2.2 用一段 Python 复现比特填充与字节填充
光看规则容易记混,直接写几行代码跑一遍,印象会深很多。下面这段脚本同时实现 HDLC 的比特填充和 PPP 的字节填充,你可以拿它验证试题里的填充结果。
def bit_stuff(data_bits): """HDLC 比特填充:每 5 个连续 1 后插入一个 0""" result = [] count = 0 for bit in data_bits: result.append(bit) if bit == '1': count += 1 if count == 5: result.append('0') # 插入 0 count = 0 else: count = 0 return ''.join(result) def byte_stuff(data_bytes): """PPP 字节填充:转义 0x7E 和 0x7D""" result = bytearray() for b in data_bytes: if b == 0x7E: result.extend([0x7D, 0x5E]) # 0x7E ^ 0x20 = 0x5E elif b == 0x7D: result.extend([0x7D, 0x5D]) # 0x7D ^ 0x20 = 0x5D else: result.append(b) return bytes(result) # 测试 print(bit_stuff('11111011111')) # 观察插入 0 的位置 print(byte_stuff(b'\x7e\x7d\x01').hex()) # 观察转义结果逻辑说明:bit_stuff维护一个连续 1 的计数器,达到 5 就补 0 并清零,保证数据段不会出现 6 个连续 1。byte_stuff只对0x7E和0x7D两个特殊字节做异或0x20并加转义前缀,其他字节原样输出。参数上,比特填充的阈值固定是 5,这是 HDLC 标准规定的;字节填充的转义字符是0x7D,异或值是0x20,这两个值在 PPP 的 RFC 里有明确定义,试题里如果改了这两个值,那一定是错的。
2.3 HDLC 和 PPP 的帧结构对比
试题里常考帧格式字段的含义,下面这张表把两者放在一起,方便对照记忆。
| 字段 | HDLC | PPP |
|---|---|---|
| 标志 | 01111110 | 0x7E |
| 地址 | 8 位,通常全 1 | 0xFF(广播) |
| 控制 | 8 或 16 位,含序号 | 0x03(无序号帧) |
| 协议 | 无独立协议字段 | 2 字节,标识上层协议 |
| 信息 | 可变长 | 可变长,默认最大 1500 字节 |
| FCS | CRC-16 或 CRC-32 | CRC-16 |
| 填充方式 | 比特填充 | 字节填充(异步) |
从表里能看出,PPP 比 HDLC 多了协议字段,这是为了支持多种网络层协议复用同一条链路。HDLC 的控制字段承载了序号和确认,所以它本身就能做可靠传输;PPP 默认不提供可靠传输,靠上层保证。这个差异直接决定了后面 Go-back-N 和 CRC 在两种协议里的角色不同。
3. CRC 校验码计算:从生成多项式到余数
3.1 CRC 的数学直觉:为什么模 2 除法能检错
CRC 的核心是把数据看成多项式,用一个约定的生成多项式去除,余数就是校验码。发送方把余数附在数据后面,接收方用同一个生成多项式去除整个码字,余数为 0 就认为没出错。这里的除法是模 2 除法,加减都是异或,不产生进位借位,所以硬件实现特别简单,一个移位寄存器加几个异或门就能跑。
生成多项式的最高位和最低位必须是 1,这是为了保证检错能力。热搜里那句“不能作为 CRC 生成多项式”说的就是这种情况:如果最低位是 0,那么任何奇数个比特错误都可能检不出来,因为余数永远是偶数。试题里经常给几个多项式让你判断哪个不能选,记住“首尾必须为 1”就能秒杀。
3.2 手算 CRC 校验码的完整步骤
假设生成多项式是G(x) = x^4 + x + 1,对应二进制10011,数据是110101。步骤如下:
- 在数据后面补 4 个 0,因为生成多项式最高次是 4。得到
1101010000。 - 用模 2 除法除以
10011,每次对齐最高位做异或。 - 除到最后剩下的 4 位余数就是 CRC 校验码。
- 把余数附在原始数据后面,形成发送码字。
下面用 Python 把这个过程写出来,你可以直接改数据和多项式验证。
def crc_remainder(data_bits, poly_bits): """模 2 除法求余数,data_bits 已补零""" data = list(data_bits) poly = list(poly_bits) poly_len = len(poly) for i in range(len(data) - poly_len + 1): if data[i] == '1': for j in range(poly_len): # 异或操作,模 2 加法 data[i + j] = str(int(data[i + j]) ^ int(poly[j])) return ''.join(data[-(poly_len - 1):]) def crc_encode(data_bits, poly_bits): """生成 CRC 校验码并拼接""" appended = data_bits + '0' * (len(poly_bits) - 1) remainder = crc_remainder(appended, poly_bits) return data_bits + remainder # 测试:数据 110101,生成多项式 10011 print(crc_encode('110101', '10011'))逻辑说明:crc_remainder从高位到低位扫描,只要当前位是 1 就和生成多项式对齐异或,模拟手算的模 2 除法。crc_encode先补零再求余,最后把余数拼回数据后面。参数上,生成多项式的位数决定了补零个数和余数长度,比如10011是 5 位,补 4 个零,余数 4 位。试题里如果给的是x^16 + x^15 + x^2 + 1,那就是 CRC-16,补 16 个零,余数 16 位。
3.3 接收端怎么验证
接收端收到码字后,用同一个生成多项式去除整个码字。如果余数为 0,就认为传输无误;余数不为 0,说明出错了。注意 CRC 只能检错,不能纠错,纠错要靠重传。这也是为什么 CRC 总是和 Go-back-N 这类重传机制一起出现。
4. Go-back-N 的窗口与重传:序号到底怎么数
4.1 滑动窗口的基本规则
Go-back-N 是滑动窗口协议的一种。发送方可以连续发送多个帧而不等确认,但窗口大小有限。接收方只按序接收,如果收到失序的帧就直接丢弃,并重复确认最后一个按序收到的帧。发送方一旦超时,就从那个未被确认的帧开始,把所有后续帧全部重传。这就是“回退 N”的含义。
窗口大小和序号位数的关系是试题里的高频计算点。如果序号用 n 位表示,序号空间是 2^n。Go-back-N 要求发送窗口大小 W ≤ 2^n - 1。为什么?因为如果 W = 2^n,接收方就无法区分“新帧”和“重传帧”,会出现二义性。这个结论一定要记牢,选择题和计算题都爱考。
4.2 用代码模拟一次 Go-back-N 的发送过程
下面这段模拟展示了窗口滑动、超时重传和累计确认的过程,你可以调整窗口大小和丢包位置观察行为。
def go_back_n(total_frames, window_size, lost_frames): """模拟 Go-back-N 发送过程""" base = 0 # 窗口起始序号 next_seq = 0 # 下一个待发序号 acked = -1 # 最后一个被确认的帧 steps = [] while base < total_frames: # 发送窗口内的帧 while next_seq < base + window_size and next_seq < total_frames: if next_seq not in lost_frames: steps.append(f"发送帧 {next_seq}") else: steps.append(f"帧 {next_seq} 丢失") next_seq += 1 # 假设接收方按序确认,遇到丢失就停止确认 for seq in range(base, next_seq): if seq in lost_frames: break acked = seq if acked >= base: base = acked + 1 steps.append(f"确认到帧 {acked},窗口滑动到 {base}") else: steps.append(f"超时,从帧 {base} 开始重传") next_seq = base # 回退重传 return steps # 测试:共 6 帧,窗口 3,帧 2 丢失 for s in go_back_n(6, 3, {2}): print(s)逻辑说明:base是窗口左边界,next_seq是下一个要发的序号。发送时只发窗口内的帧,遇到丢失帧就记录。确认阶段模拟接收方按序确认,一旦遇到丢失帧就停止,所以acked停在丢失帧前一个。如果acked没有前进,就触发超时,把next_seq拉回base重传。参数上,window_size不能超过2^n - 1,lost_frames用来模拟链路丢包。你可以把窗口改成 4、序号位数设成 3 位,观察什么时候会出现二义性。
4.3 累计确认和捎带确认的区别
Go-back-N 用的是累计确认:接收方确认序号 n,表示 n 及之前的所有帧都收到了。这和选择重传不同,选择重传会单独确认每个收到的帧。试题里经常把两者混在一起考,记住 Go-back-N 的接收窗口是 1,只能按序收,所以它只能累计确认。捎带确认是双向通信时把确认信息搭在数据帧里发回去,和窗口机制是两回事,不要混淆。
5. 避坑与排查:CRC 和滑动窗口里最容易翻车的地方
5.1 现象:CRC 余数算出来位数不对
原因:补零个数搞错了。补零个数等于生成多项式的最高次,也就是多项式二进制位数减一。比如10011是 5 位,最高次是 4,补 4 个零。很多人直接补成 5 个零,余数就多一位。
解决:先数生成多项式的位数,减一就是补零个数,也是余数位数。算完检查余数长度是否等于位数减一。
5.2 现象:Go-back-N 窗口大小设成 2^n 后,接收方分不清新旧帧
原因:序号空间是 2^n,如果窗口大小也是 2^n,发送方重传的帧序号会和下一轮新帧序号完全重叠。接收方收到同一个序号,无法判断这是重传还是新帧。
解决:发送窗口最大只能取 2^n - 1。如果试题给的是选择重传,接收窗口最大是 2^(n-1),这个边界也要分清。
5.3 现象:PPP 字节填充时把普通字节也转义了
原因:转义规则只针对0x7E和0x7D两个字节,其他字节一律原样发送。有人误以为所有控制字符都要转义,结果把0x03也转了,接收方解析就乱了。
解决:严格按 RFC 定义,只转义帧定界符和转义字符本身。异步 PPP 里,小于0x20的字符是否转义取决于具体实现,但试题通常只考0x7E和0x7D。
5.4 现象:HDLC 比特填充后,接收方删 0 删多了
原因:接收方看到 5 个连续 1 就删后面的 0,但如果这个 0 本来就是数据里的,就会误删。实际上发送方保证数据里不会出现 5 个连续 1 后跟 0 的情况,因为它在第 5 个 1 后面插了 0。接收方删 0 的前提是确认这 5 个 1 是数据的一部分,而不是标志字段。
解决:接收方要先识别标志字段01111110,在标志之外的区域才做删 0 操作。标志字段本身不参与填充和删除。
5.5 现象:CRC 校验通过但数据仍然错了
原因:CRC 不是万能的,它只能检测一定范围内的错误。如果错误模式恰好是生成多项式的倍数,余数就会是 0,校验通过但数据错了。热搜里“无法保证检出全部奇数个比特错误”说的就是生成多项式选得不好时的情况。
解决:选择标准的生成多项式,比如 CRC-16 用x^16 + x^15 + x^2 + 1,CRC-32 用 IEEE 802.3 规定的多项式。这些多项式经过验证,检错能力有保证。不要自己随便造一个。
6. 把四个考点串成一条线:用一道综合题验证掌握程度
6.1 综合题:从成帧到重传的完整链路
假设一条链路用 HDLC 成帧,生成多项式为x^4 + x + 1,发送窗口大小为 3,序号用 3 位表示。现在要发送数据110101,请回答:成帧后的比特流是什么?CRC 校验码是多少?如果第 2 帧丢失,Go-back-N 会重传哪些帧?
这道题把 HDLC 比特填充、CRC 计算和 Go-back-N 重传全串起来了。你可以先自己算,再用前面的代码验证。比特填充时注意标志字段不参与填充,CRC 计算时注意补零个数,Go-back-N 重传时注意窗口大小和序号空间的关系。
6.2 用代码一次性验证三个环节
下面这段脚本把三个环节串起来,输入数据和参数就能看到完整结果。
def full_pipeline(data_bits, poly_bits, window_size, total_frames, lost_frame): # 第一步:CRC 编码 encoded = crc_encode(data_bits, poly_bits) print(f"CRC 编码结果: {encoded}") # 第二步:HDLC 比特填充 stuffed = bit_stuff(encoded) print(f"比特填充后: {stuffed}") # 第三步:Go-back-N 重传 steps = go_back_n(total_frames, window_size, {lost_frame}) print("Go-back-N 过程:") for s in steps: print(" " + s) # 运行综合示例 full_pipeline('110101', '10011', 3, 6, 2)逻辑说明:full_pipeline先做 CRC 编码,把余数拼到数据后面;然后对比特流做 HDLC 填充;最后模拟 Go-back-N 发送过程。参数上,data_bits是原始数据,poly_bits是生成多项式,window_size是发送窗口,total_frames是总帧数,lost_frame是丢失的帧序号。你可以改这些参数,观察每一步输出的变化。
6.3 我踩过的坑和现在的习惯
早年复习这一章时,我总把 CRC 和 Go-back-N 分开背,结果一到综合题就卡壳。后来强迫自己每做一道题就把四个考点过一遍:先看成帧方式,再算 CRC,再画滑动窗口,最后写重传序列。这个习惯让我在考试里再也没漏过步骤。另外,生成多项式我从来不死记,而是先写二进制再数位数,补零个数和余数长度自然就出来了。窗口大小我习惯先算 2^n,再减一,这样不会把 Go-back-N 和选择重传搞混。如果你也在啃这一章,建议把上面的代码跑一遍,改几个参数看看输出怎么变,比单纯看书快得多。希望帮到你。
本文还有配套的精品资源,点击获取