简介:计算机网络课程设计《模拟Ethernet帧的发送过程》配套文档,适合计算机、软件工程专业学生完成以太网协议仿真类课设时参考。文档从网络协议与IEEE 802.3标准讲起,系统梳理CSMA/CD冲突检测机制、截断二进制指数退避算法,并以两个线程a、b模拟主机、双字变量Bus模拟共享总线,详细说明初始化、发送、冲突处理、输出报告等模块的编程思路,可帮助复现线程号与Bus“或”操作的数据发送流程。压缩包共1个文件,为663KB的doc文档,内含课程设计任务书、目录与完整报告框架,兼顾理论讲解和代码设计说明。已有164人学习下载,适合用于课设前期搭建方案、中期对照实现和后期整理报告,尤其能理清冲突窗口0.005、重试次数限制等易混参数,是一份可直接参考的计算机网络课设材料。
1. 模拟 Ethernet 帧的发送过程:看起来是拼字节,实际卡在链路层细节
“模拟Ethernet帧的发送过程”这类课设题,刚拿到时很容易误判难度:无非是把目的 MAC、源 MAC、类型和载荷拼成一段字节,用 socket 发出去。真的动手以后才发现,帧造对了发不出去,发出去抓包不认,抓到了 FCS 又是错的,每一步都在跟协议细节较劲。这个方向真正在练的是数据链路层的完整链路——从帧字段结构、最小帧长填充、CRC 计算,到发送通道选择和抓包验证,正好把计算机网络课里最抽象的那段“从报文到比特流”落到实。适合正在做课设的本科生、想补链路层细节的开发者,也适合要往报文底层追问题的运维或 DevOps 方向工程师。本文按我自己的课设做法展开:不依赖实验箱,一台装 Linux 的笔记本或虚拟机就能完整复现。
2. 以太帧结构先行:七段字段与最小帧长,少一个就发不出去
2.1 帧格式先立住:哪些字段由软件负责,哪些被网卡悄悄补上
做模拟发送的第一步,不是写代码,是把帧格式拆到“软件能控制”和“软件够不着”两个集合。按照 IEEE 802.3 和 DIX Ethernet II 的约定,完整以太网帧在线路上的形态是七段:前导码(Preamble)、帧起始定界符(SFD)、目的 MAC、源 MAC、长度/类型、载荷、FCS。谢希仁《计算机网络》里链路层那张结构图也是按这个顺序来的,但图里没告诉你的是,前导码和 SFD 在绝大多数网卡上由硬件自动生成,软件根本写不进去。
也就是说,你手里真正能拼的只有从目的 MAC 到 FCS 这 22 到 1500 多字节。抓包工具里显示的 Ethernet II 帧头也恰恰是这段。这里最容易混淆的两个数字:线上比特流最短是 72 字节(含前导码和 SFD),而协议定义的“帧”最短是 64 字节(不含这两段)。很多期末题和抓包分析里的“最短帧长 64 字节”指的是后者,做题和课设代码里都按 64 算就对了。
| 字段 | 长度 | 内容 | 软件是否可控 |
|---|---|---|---|
| Preamble | 7 字节 | 0x55 交替比特,用于时钟同步 | 不可控,网卡自动加 |
| SFD | 1 字节 | 0xD5,帧起始定界符 | 不可控,网卡自动加 |
| 目的 MAC | 6 字节 | 接收方地址,广播为 ff:ff:ff:ff:ff:ff | 可控 |
| 源 MAC | 6 字节 | 发送方地址 | 可控 |
| Length/Type | 2 字节 | 小于 0x05DC 表示长度,大于等于 0x0600 表示协议类型 | 可控 |
| Payload | 46-1500 字节 | 上层数据,IPv4 为 0x0800 类型 | 可控 |
| FCS | 4 字节 | CRC-32 校验值 | 可控,但计算方式有坑 |
2.2 用 Python 构造一个最小 Ethernet 帧:可直接抄的发送前代码
帧格式立住以后,构造代码本身很简单,难在把每个边界条件写对。我一般用 Python 写一个build_ethernet_frame函数,输入 MAC 和载荷,输出完整帧字节流。核心是三点:MAC 字符串转字节、Length/Type 字段按大端序打包、载荷不足 46 字节时填充。
def build_ethernet_frame(dst_mac, src_mac, eth_type, payload): # MAC 地址 "aa:bb:cc:dd:ee:ff" 转 6 字节 dst = bytes.fromhex(dst_mac.replace(':', '')) src = bytes.fromhex(src_mac.replace(':', '')) # 类型字段固定 2 字节,大端序 type_field = eth_type.to_bytes(2, 'big') # 最小帧长 64 字节 = 6 目的 MAC + 6 源 MAC + 2 类型 + 46 载荷 + 4 FCS if len(payload) < 46: payload = payload + b'\x00' * (46 - len(payload)) frame = dst + src + type_field + payload # FCS 先补 4 个零字节占位,标准算法在 4.3 给出 return frame + b'\x00' * 4这段代码有三个值得解释的决策。eth_type.to_bytes(2, 'big')是必须的,因为以太网所有多字节字段都是大端序,写反了在抓包里会看到类型字段完全错乱。payload不足 46 字节时用\x00填充,填到 46 字节是“帧最短 64 字节”的直接推论,这个填充逻辑如果漏掉,发出的帧会被接收端当成残帧丢弃。FCS 占位先写 4 个零字节,是因为标准 FCS 的计算范围覆盖从目的 MAC 到载荷末尾的全部字节,必须等整帧拼完才能算,这种“先占位后回填”的顺序在协议栈实现里很常见。
2.3 两个必调参数:Length/Type 取值规则与 46 字节填充
刚接触这个实验的人最容易在 Length/Type 字段上翻车。这个 2 字节字段在 802.3 和 Ethernet II 两套标准里含义不同:值小于 0x05DC(十进制的 1500)时,它表示载荷实际长度;值大于等于 0x0600 时,它表示上层协议类型。抓包工具靠这个字段判断帧的“方言”——填了一个既不像长度也不像类型的值,Wireshark 就会把它解析成奇怪的 802.3 LLC 格式,看起来就像发错了。
课设里最安全的取值是 0x0800,代表上层是 IPv4。如果你想避开“被识别成 IP 包”的歧义,可以用 0x88B5 到 0x88B7 这段本地实验用协议号,不会被误判为标准协议。另一个必调参数是 46 字节填充:如果模拟发一个 ARP 请求,ARP 报文只有 28 字节,必须补 18 字节零;如果模拟发一个 IPv4 的 ICMP 包,IP 头加 ICMP 头往往也不足 46 字节,同样要补。填充字节用什么值都行,接收端靠 Length/Type 字段或上层头部里的长度字段来判断真实数据边界,多余的零由上层协议忽略。
3. 发送通道怎么选:环回口、真实网卡与纯软件模拟的差异
3.1 三条发送路径对比:先摸清课设想要的是“帧”还是“过程”
帧构造好了,接下来的问题是谁来发。课设题目写的“模拟发送过程”,可以理解成纯软件模拟整个发送流程,也可以理解成把帧真的发到网卡上。我做过两种,差别很大:纯软件模拟不用碰系统权限,适合展示帧构造和接收解析的逻辑;真发网卡则要面对驱动、权限、抓包工具的配合,但验证感更强。
实际上还有第三条路:发到 Linux 的环回接口 lo。这条路不需要额外硬件,又能让 tcpdump 抓到以太网帧,是课设里性价比最高的验证方式。三种方式对比如下:
| 发送通道 | 实现难度 | 是否需要 root | 能被抓包工具看到 | 验证强度 |
|---|---|---|---|---|
| 纯软件模拟 | 低 | 否 | 否 | 只能验证帧格式自身 |
| 环回口 lo | 中 | 是 | 是 | 可验证帧格式与发送流程 |
| 真实网卡 | 中高 | 是 | 是,且外部可见 | 可验证端到端链路 |
还有一个背景值得知道:真实网卡内部有 PCS/PMA 或 SGMII 这类 MAC 层与物理层之间的接口,负责把 MAC 帧编码成线路信号并处理时钟恢复。这些工作对软件完全透明,所以“模拟发送”到不了这层,课设能触达的边界就是数据链路层的帧构造、发送调用和接收验证。
3.2 用 AF_PACKET 从环回口发帧:Linux 下的最小可行命令
Linux 下不借助第三方库发送原始以太网帧,标准做法是AF_PACKET套接字。它允许你在数据链路层指定接口,把已经构造好的帧字节直接交给驱动。下面是发到环回口的最小代码:
import socket def send_frame_via_lo(frame: bytes): # AF_PACKET 只在 Linux 上可用,SOCK_RAW 表示发送原始链路层帧 s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW) # 绑定到环回口,协议参数传 0 即可,发送时不影响 s.bind(('lo', 0)) s.send(frame) s.close()这个代码背后有两个重要的边界。第一,AF_PACKET+SOCK_RAW需要 root 权限,普通用户会直接报PermissionError,这是课设环境里最常见的第一个坎,解决办法是用sudo运行,或给 Python 解释器加cap_net_raw能力。第二,发到 lo 口的帧会在本机被“收到”一份,因此你可以在另一个终端用sudo tcpdump -i lo -e -XX抓包,-e显示链路层头,-XX同时输出十六进制和 ASCII,能直观看到目的 MAC、源 MAC、类型字段和填充字节。
3.3 用 PCAP 发到真实网卡:scapy 发送与权限、驱动边界
如果课设要求帧真的从物理网卡出去,我一般用 scapy 的sendp,它内部封装了 PCAP 的发送路径,比裸写AF_PACKET省心。发送时仍然要提供完整帧结构,scapy 的Ether对象会自动处理 MAC 和类型字段,Raw承载载荷。这里推荐加inter参数控制发包间隔,后面会解释它对应帧间隙。
from scapy.all import Ether, Raw, sendp payload = b'\x00\x01\x02\x03\x04\x05' * 8 # 48 字节测试载荷 frame = Ether( dst='ff:ff:ff:ff:ff:ff', # 广播地址 src='00:11:22:33:44:55', # 自定义源 MAC type=0x0800 # IPv4 协议类型 ) / Raw(load=payload) # inter=0.0001 表示每帧间隔 100 微秒,防止突发丢帧 sendp(frame, iface='eth0', inter=0.0001, verbose=False)这里有一个非常隐蔽的驱动层行为,课本上不会写:多数网卡驱动在发送路径上会自己计算并追加 FCS,也就是说软件在帧尾放的 4 字节会被硬件覆盖。这意味着如果你通过真实网卡发一个 FCS 算错的帧,对端收到的是网卡重算后的正确值,抓包工具不会报错——这给了你“我的 CRC 没问题”的错觉。真正验证 FCS 算法,要到环回口或用 tcpdump 在发送端看,或者用后面 4.3 节给出的标准算法自己比对。
4. 三个链路层约束:帧间隙、64 字节下限与 CRC 标准算法
4.1 帧间隙(IFG):发送节奏的物理约束,软件要自己模拟
以太网帧与帧之间必须有一段空闲时间,叫帧间隙(Inter-Frame Gap,IFG),标准值是 96 bit time。所谓 bit time 是指发送 1 比特所需的时间:10 Mbps 下是 100 纳秒,96 bit time 就是 9.6 微秒;100 Mbps 下是 96 纳秒;千兆下只有 9.6 纳秒。这个间隙存在的意义是给接收方的物理层留出复位和同步的时间,如果连续帧贴得太紧,接收端可能来不及处理前一个帧的尾部状态,导致丢帧。
软件模拟发送时,这个间隙不会自动出现。你在循环里连续调用send或sendp,系统调用本身的耗时可能正好掩盖问题,也可能不够,取决于速率和平台。课设里要做得严谨,应该把 IFG 转换成时间参数显式控制:10 Mbps 模拟速率下,两个帧之间至少隔96 / 10_000_000秒,也就是 9.6 微秒。代码里可以写一个简单的节流函数,把帧间隙作为发送循环的参数暴露出来,这样演示时就能解释“为什么发太快会丢帧”。
4.2 untagged 帧与 802.1Q 4 字节标签:最小载荷要重算
默认构造出来的以太网帧是没有 VLAN 标签的,也就是 untagged 帧,它遵循最原始的 802.3 格式:14 字节帧头加 46 到 1500 字节载荷。如果你想模拟的是带 VLAN 的发送过程,需要在源 MAC 之后插入一个 4 字节的 802.1Q 标签——2 字节 TPID(固定 0x8100)加 2 字节 TCI(优先级、CFI 和 VLAN ID)。插了标签的帧,帧头从 14 字节变成 18 字节,但“帧最短 64 字节”的线下约束不变。
这直接推导出一个容易算错的结论:untagged 帧的最小载荷是 46 字节,tagged 帧的最小载荷是 42 字节。很多课设在设计数据结构和填充逻辑时没留这 4 字节的余量,导致加标签后帧长反而低于 64,接收端直接丢弃。8.12 节还有一个连带坑:TPID 0x8100 大于 0x0600,所以抓包工具会把它识别成类型字段而不是长度字段,你需要在解析逻辑里显式判断是否携带 VLAN 标签,否则长度计算整体偏移 4 字节。
4.3 以太网 FCS 是标准 CRC-32:binascii.crc32 差在哪
这一节是血泪经验。很多人图省事,直接用 Python 的binascii.crc32算帧尾校验值,填进去以后 Wireshark 把 FCS 标红。原因不是校验值算错了,而是算错了“变体”。以太网 FCS 使用的是非反射的 CRC-32,多项式0x04C11DB7,初始值0xFFFFFFFF,结果再异或0xFFFFFFFF;而binascii.crc32实现的是反射变体(对应多项式0xEDB88320),两者对同一段数据算出的校验值完全不同。
标准做法是手动实现以太网 CRC:
def ethernet_fcs(data: bytes) -> bytes: crc = 0xFFFFFFFF for byte in data: # 逐字节处理,先与最高字节异或 crc ^= byte << 24 for _ in range(8): # 按位左移,最高位为 1 时与多项式异或 if crc & 0x80000000: crc = ((crc << 1) ^ 0x04C11DB7) & 0xFFFFFFFF else: crc = (crc << 1) & 0xFFFFFFFF return ((crc ^ 0xFFFFFFFF) & 0xFFFFFFFF).to_bytes(4, 'big')这段代码里最核心的是0x04C11DB7多项式和 MSB-first 的移位方向。处理顺序是从目的 MAC 的第一个字节开始,到载荷最后一个字节结束,FCS 本身不参与计算;前导码和 SFD 也不参与。写完以后建议自检:用build_ethernet_frame构造一帧,把 FCS 占位去掉,用这个函数算出 4 字节,填入帧尾;再把完整帧去掉 FCS 后重新算一次,结果必须与填入的 4 字节完全一致。这一致性验证通过,才算真正拿到了标准的帧尾。
5. 常见问题排查:帧没发出去、抓不到、抓到不认账怎么查
5.1 Wireshark 把帧识别成 802.3 LLC:type 字段取值失误
现象:帧从 lo 口发出去了,tcpdump 也抓到了,但 Wireshark 里显示的是 “802.3” 而不是 “Ethernet II”,载荷被解析成 LLC 头,看起来完全不对。原因:Length/Type 字段填了一个介于 0x05DC 和 0x0600 之间的值,或者填了 0x0000 这种无效值,抓包工具按长度字段去解析,帧头立刻错位。解决:统一用大于等于 0x0600 的协议号,IPv4 用 0x0800,测试用 0x88B5;自定义协议号时避开 0x05DC 到 0x05FF 这个边界区间,这个区间既是最大帧长又是类型字段的模糊地带,容易引起解析器误判。
5.2 FCS 被标红但网卡发送又正常:被硬件覆盖的错觉
现象:用binascii.crc32算完的帧在环回口抓包显示 FCS 错误,红底红字;但同一帧从物理网卡发到另一台机器,那边抓包却显示正常。原因:前面提到的驱动行为——真实网卡发送时会忽略软件填的 FCS,由硬件计算后覆盖,所以你的错误值根本不会被看到;环回口或某些虚拟接口没有这个硬件路径,软件填什么就留着什么,错误直接暴露。解决:验证 FCS 算法以环回口抓包为准,不拿物理链路当标准;计算一律换成 4.3 节的ethernet_fcs,并做二次计算一致性校验。如果课设报告需要展示“错的 FCS”,可以在环回口演示,结论写清楚是硬件覆盖导致物理链路不可见。
5.3 高速连发丢帧:帧间隙与突发间隔没控制好
现象:循环构造 1000 个帧连续sendp,接收端只收到 700 个左右,抓包看到帧与帧之间间隔几乎为 0。原因:发送循环没有遵守帧间隙约束,突发帧超出了接收端 FIFO 深度或协议栈的接收队列,缓冲区溢出后直接丢帧。解决:在发送循环里按模拟速率计算间隔,10 Mbps 下inter=0.0000096是最小间隔,实际用inter=0.0001留足余量;scapy 给sendp传inter参数即可,裸socket.send就在循环里time.sleep。另外注意Ether()对象在发送时会自动填充到 64 字节,如果手动填充过一遍,发送时就会变成 64 加多余填充,部分接收端会按帧长校验拒绝,建议两者只选其一。
5.4 Windows 上打不开 AF_PACKET:环境与权限的边界
现象:代码在 Windows 上跑,socket.socket(socket.AF_PACKET, ...)直接抛OSError: Address family not supported by protocol。原因:AF_PACKET是 Linux 内核特有的协议族,Windows 的 Winsock 不提供这个接口。解决:课设环境装 Linux 虚拟机,把 lo 口发送作为主力路径;如果必须在 Windows 上演示,安装 Npcap 驱动后可以使用 scapy 的sendp,它会走 Npcap 的 API,但同样需要管理员权限。补一句:macOS 上也没有AF_PACKET,需要改用 BPF 设备,所以统一建议是在 Linux 虚拟机上做这个实验,省掉大量环境适配时间。
6. 把单帧发送升级成介质访问模拟:CSMA/CD 退避的可复现写法
6.1 时隙与截断二进制指数退避:三行代码验证协议行为
帧发送做通以后,课设如果还想拿高分,我会把模拟往前推一步:既然题目叫“发送过程”,那就可以把“发送前检测介质是否空闲”也模拟出来,这就触到了以太网介质访问控制的核心。半双工以太网里,发送前要监听信道,信道忙就等待;如果两个站同时发送,就发生碰撞,碰撞后按截断二进制指数退避算法重传。
以太网把 512 bit time 定义为一个时隙,10 Mbps 下是 51.2 微秒。第 n 次碰撞后,退避时间从 0 到 2 的 k 次方减 1 个时隙里随机选一个,k 取碰撞次数和 10 的较小值;重传超过 16 次则放弃。这个算法的模拟代码只有十几行:
import random def backoff_time(collision_count: int, slot_time_us: float = 51.2) -> float: # 截断:k 最大取 10,对应 2^10 个时隙窗口 k = min(collision_count, 10) # 在 0 ~ 2^k-1 个时隙中随机选择 return random.randint(0, 2 ** k - 1) * slot_time_us # 模拟一次碰撞后的退避等待 for attempt in range(1, 17): wait = backoff_time(attempt) print(f"第 {attempt} 次重传,退避 {wait:.1f} 微秒") # 这里再调用 sendp 或 send_frame_via_lo 发送这段代码的价值在于它把协议参数变成了可观察的输出。第一次碰撞后退避只可能是 0 或 51.2 微秒,第二次在 0 到 153.6 微秒之间,到第十次碰撞窗口扩大到 2 的 10 次方个时隙。把wait打印出来,配合发送间隔统计,就能在报告里画出一条“碰撞次数越多,平均退避时间越大”的曲线,这比单纯发帧要有说服力得多。
我自己当年做这个课设时,是在 FCS 上翻的车——用binascii.crc32算了三遍没发现问题,最后对着标准查了一晚上才意识到是反射与非反射的差异。那之后我养成了一个习惯:凡是链路层的模拟实验,先造一个“构造帧 → 算 FCS → 二次校验 → 环回口抓包确认”的最小闭环,再往上加功能。这样每加一个字段、每调一个参数,都能立即通过抓包验证对错。这个习惯帮我省了后面无数次查错时间,希望帮到你。
本文还有配套的精品资源,点击获取