简介:计算机网络课程设计资料《模拟Ethernet帧的发送过程.doc》面向计算机相关专业学生,聚焦以太网CSMA/CD协议,提供一份完整可参考的课设实现方案。内容从知识背景讲起,涵盖网络协议、以太网、CSMA/CD机制与截断二进制指数退避算法,并详细讲解用两个线程模拟两台主机、以双字变量Bus模拟总线、通过或操作发送数据及冲突检测与随机退避的完整流程,同时按初始化、发送、冲突处理、输出和结束判断划分功能模块,便于用C/C++/VC/VB/JAVA等语言实现。文档还包含课程设计任务书、目录结构与报告撰写框架,并给出时间安排和输出报告示例,可帮助读者快速理解模拟逻辑并对照完成自己的课程设计。压缩包仅含1个doc文件,大小663KB,已有164人学习,适合计算机网络原理、数据链路层实验及CSMA/CD仿真课设参考。
1. 一份课设文档,怎么把Ethernet帧从纸面送到线上
计算机网络课设里,模拟Ethernet帧的发送过程是出现频率最高的题目之一。大多数人的卡点不在“知不知道帧格式”——前导码、MAC、类型、FCS都能背——而在“怎么把一堆字段变成一串能发出去的字节”。这份文档本质上就是干这个的:从帧结构拆解、CRC32校验计算,到CSMA/CD发送时序和帧间隙的处理,把发送端该做的事按步骤写全了。适合三类人:被课设卡住的学生、准备网络方向面试想补实操的人、以及想拿一份完整代码改造进自己项目里的从业者。我拆完这份文档后验证了一件事:按它的流程走,Wireshark里确实能看到自己构造的帧,而且CRC是绿色的Good状态。
2. Ethernet帧结构:前导码到FCS,每个字段的字节序都不能错
2.1 帧头字段逐一拆解
Ethernet II帧(这个课设模拟的就是它,也就是不含VLAN Tag的untagged帧)从上到下依次是:7字节前导码(Preamble)、1字节帧起始定界符(SFD)、6字节目的MAC、6字节源MAC、2字节类型/长度、数据、4字节FCS。
先纠正一个高频误解:前导码和SFD在物理层就被消费掉了,抓包工具默认不显示这部分。但从“发送过程模拟”的角度,你必须把它们拼进字节流里再送出去,因为接收方是靠前导码来同步时钟的。前导码是7个0xAA,整个比特流是10101010交替排列;SFD是0xAB,它的特殊之处在于最后两位是11,打破了交替模式,接收方一看到这个就认定“帧头在这”。
| 字段 | 长度 | 取值/说明 |
|---|---|---|
| 前导码 | 7字节 | 0xAA×7,比特流10101010交替,用于时钟同步 |
| SFD | 1字节 | 0xAB,最后两位11,标识帧起始 |
| 目的MAC | 6字节 | 单播/广播/组播地址 |
| 源MAC | 6字节 | 网卡地址,从左到右逐字节发送 |
| 类型/长度 | 2字节 | ≥0x0600是类型(0x0800=IPv4),≤0x05DC是长度 |
| 数据 | 46~1500字节 | 不足46字节补Padding |
| FCS | 4字节 | CRC32,范围从目的MAC到数据末尾 |
这里最容易被忽略的是“类型/长度”字段的双重身份。早期802.3标准用这个字段表示长度,Ethernet II用这个字段表示上层协议类型,后来用0x0600作为分界线来区分两者。课设里只需要记住:填0x0800表示后面是IP包,填0x0806表示ARP,这个字段决定接收方把payload交给上层的哪个协议栈。如果你填了一个小于等于1500的数,对端会认为你发的是802.3帧,整个解析顺序就乱了。
2.2 最小帧长与Padding规则
为什么最小帧长是64字节,而且是从目的MAC算到FCS末尾,前导码和SFD不算?这是CSMA/CD机制逼出来的:发送站在发送过程中要能检测到冲突,就必须保证最远的两个站之间,一帧还在线上没发完。64字节在10Mbps、最长电缆距离的约束下,正好满足“发完之前能听到冲突”这个条件。这个约束已经从历史问题变成了工程规范,所以模拟发送时必须遵守:数据少于46字节就补0,凑到46字节,加上14字节帧头(DA+SA+Type)和4字节FCS,正好64。
最大帧长1518字节对应数据1500字节,这就是MTU(最大传输单元)的由来。超过1518就是巨型帧(Jumbo Frame),课设阶段不用处理,但你要知道为什么“1500”这个数字在网络配置里到处出现——它就是从Ethernet帧结构里带出来的。后面做IP分片、TCP MSS调整时,计算基数是1500而不是1518,就是因为帧头14字节和FCS 4字节不算有效载荷。
2.3 CRC32的算法选择与计算范围
FCS用的是CRC32,更准确地说是CRC-32/ISO-HDLC这个变体,多项式0x04C11DB7,初值全1,结果再取反。Python里直接用zlib.crc32就行,它用的就是这个变体。算出来的值按小端字节序(little-endian)写进帧尾,Wireshark就会报Good CRC。
容易踩坑的是计算范围:从目的MAC的第一个字节开始,到数据(含Padding)的最后一个字节结束。前导码和SFD不参与计算,FCS自己也不参与计算。我见过不少同学把前导码也一起丢进CRC计算,结果对端永远报Bad CRC。这个问题不细看很难发现,因为打印出来的帧长度看起来是对的,但接收端的校验结果是错的。具体怎么定位这个坑,第4章展开讲。
3. 模拟发送流程:从上层数据到线上字节流的四步落地
3.1 发送端的状态机:空闲、等待IFG、发送、冲突退避
把“发送”拆成状态机,是这份课设文档里的核心思路,也直接对应了真实网卡的链路层行为。发送端不是“拿一帧直接扔出去”,而是一个循环状态机:先监听信道,介质忙就继续等;介质空闲后,还得等一个帧间隙(IFG,Inter-Frame Gap,96 bit time),这是给接收方处理上一帧的时间;然后才开始发前导码+SFD+帧体;发送过程中如果检测到冲突,立即停止发送,执行截断二进制指数退避(Truncated Binary Exponential Backoff),随机等待0到2^n-1个时隙再重发。这里n是重传次数,n超过10之后上限固定为10,重传16次仍冲突就丢弃这一帧。
没有实操经验的人容易把IFG忽略掉。在纸上模拟没感觉,一旦接到真实链路,两台机器配置成相同速率直连,连续快速发两帧,第二帧大概率被丢弃,原因就是没等IFG。帧间隙的单位是bit time,10Mbps下是9.6微秒,100Mbps下是0.96微秒,1000Mbps下是96纳秒。所以它不是一个固定sleep值,必须跟链路速率挂钩换算。文档里用的是“96 × bit_time”的写法,而不是写死一个微秒数,这个设计在答辩时是一个加分点。
3.2 用Python构造一帧:字段拼接与CRC计算
下面这段代码是文档里最核心的部分,我整理成可直接运行的版本。它把一个IP数据包封装成完整的Ethernet II帧,包含前导码和SFD:
import zlib import struct import time import random PREAMBLE = b'\xaa' * 7 # 前导码:10101010交替,用于接收方时钟同步 SFD = b'\xab' # 帧起始定界符:最后两位是11,打破交替模式 MIN_FRAME = 64 # 最小帧长:从目的MAC到FCS末尾,不含前导码和SFD def build_frame(dst_mac: bytes, src_mac: bytes, ethertype: int, payload: bytes) -> bytes: """构造一帧Ethernet II帧,返回包含前导码+SFD的完整字节流""" # 1. 数据不足46字节时补0,保证帧长>=64 if len(payload) < 46: payload = payload + b'\x00' * (46 - len(payload)) # 2. 拼接14字节帧头:目的MAC + 源MAC + 类型/长度 header = dst_mac + src_mac + struct.pack('!H', ethertype) frame_body = header + payload # 3. CRC32计算范围:帧头+数据,结果按小端写入4字节FCS fcs = struct.pack('<I', zlib.crc32(frame_body)) # 4. 完整帧 = 前导码 + SFD + 帧体 + FCS return PREAMBLE + SFD + frame_body + fcs这段代码的逻辑可以拆成四步看。第一步的Padding最容易漏,漏了帧长小于64字节,接收方或交换机会直接丢弃。第二步的类型字段用struct.pack('!H')按网络字节序(大端)打包,注意MAC地址是逐个字节从左到右发送,所以直接拿bytes传递,不要转成整数再做字节序转换。第三步是关键:zlib.crc32只接受bytes,返回的是uint32整数,用“<”号(小端)pack成4字节,才是线上FCS的实际字节顺序。如果你用“!H”这种方式按大端写入,Wireshark一定报Bad CRC。第四步把前导码放在最前面,模拟物理层发送时第一个字节就是0xAA。
3.3 模拟发送循环:IFG等待与冲突退避
只有构造函数还不够,发送过程本身要模拟信道竞争。常见做法是封装一个EthernetSender类,把IFG和退避逻辑都收进去。我在文档基础上补全了参数处理:
class EthernetSender: def __init__(self, src_mac: bytes, speed: int = 100_000_000): self.src_mac = src_mac self.bit_time = 1.0 / speed # 一个比特的传输时间,单位秒 self.retry = 0 def wait_ifg(self): ifg_bits = 96 # 帧间隙固定96 bit time time.sleep(ifg_bits * self.bit_time) def backoff(self): n = min(self.retry, 10) # 重传次数超过10后上限固定为10 slots = random.randint(0, 2**n - 1) # 随机等待0~2^n-1个时隙 time.sleep(slots * 512 * self.bit_time) # 1个时隙=512 bit time self.retry += 1 def send(self, frame: bytes): self.wait_ifg() # 信道空闲后先等帧间隙 # 真实场景在这里监听信道、检测冲突;模拟场景用打印代替 print(f"发送 {len(frame)} 字节, 帧长(不含前导码)= {len(frame) - 8}") self.retry = 0 # 发送成功,重传计数清零两个方法值得单独说明。wait_ifg把96比特换算成实际sleep时间,它依赖初始化时传入的速率,这比写死sleep(0.0000096)要通用得多——把speed改成1000_000_000,同一份代码就适配千兆链路。backoff实现的是截断二进制指数退避:第一次冲突随机等0或1个时隙,第二次0到3个,第三次0到7个,以此类推。n用min(self.retry, 10)做上限,这是802.3标准里明确写的。课上如果只要求“模拟发送过程”而不要求冲突检测,backoff方法可以留空或者只打一条日志,但答辩时能说清楚它的计算逻辑,分数通常不一样。
4. 避坑记录:FCS范围、字节序与帧间隙,四个高频翻车点
课设文档里最值钱的不是框架代码,而是这些让项目“从能跑到跑对”的坑。以下四条是文档里明确标注、也是我实际复现时踩过的。
4.1 FCS算不对,抓包永远是Bad CRC
现象:用Wireshark抓自己发的包,协议栏里FCS显示红色Bad CRC,接收端直接丢帧,但发送端打印日志看起来一切正常。
原因:两处。其一,CRC计算范围错了,把前导码和SFD也算了进去;其二,zlib.crc32的结果用大端写入帧尾。
解决:CRC计算范围严格限定在“目的MAC到数据末尾”。zlib.crc32返回的uint32按小端字节序写入帧尾。校验方式可以写一个接收端解析函数,把收到的帧的FCS和重新计算的CRC比对,不一致就说明发送端封装有问题。这个坑的隐蔽之处在于:打印帧长度看不出来问题,只有上抓包工具才能暴露。
4.2 数据不足46字节不补Padding,帧被静默丢弃
现象:payload是20字节的ICMP回显,发送端显示发送成功,接收端抓包工具能看到这个帧,但Wireshark提示帧长invalid,或者接收端驱动直接丢弃。
原因:20字节payload加14字节帧头等于34字节,小于64字节最小帧长。很多交换机和网卡驱动对短帧直接丢弃,或者按错误帧处理。
解决:在封装阶段强制补齐。判断条件if len(payload) < 46就补0,把payload填充到46字节。注意补的是数据段,不是往帧头塞东西。补充一句:这个Padding在接收端不会被剥离,它属于frame的一部分,上层的IP解析会通过IP头里的总长度字段来识别真实数据的边界。
4.3 类型字段当成“协议号”随便填
现象:类型字段填了0x0000或0x0001,抓包工具把这一帧识别成802.3而不是Ethernet II,后面的解析全乱。
原因:类型字段同时承载“长度”和“协议类型”两种语义,分界线是0x0600(十进制1536)。小于等于0x05DC(十进制1500)按长度解析,大于等于0x0600按类型解析。
解决:模拟发送时填常用值:0x0800是IPv4,0x0806是ARP,0x86DD是IPv6。如果你想模拟的确实是802.3长度帧,那就故意填实际长度值——但课设题目里写的是“Ethernet帧”,默认就是Ethernet II格式。顺带提一句:带VLAN Tag的802.1Q帧是在源MAC和类型字段之间多塞了4字节(TPID 0x8100加2字节TAG),课设里先做untagged帧,别混。
4.4 帧间隙sleep时长大错位
现象:两台机器直连,发送端一次性发1000帧,接收端收到的少于1000,而且丢帧没有明显规律。
原因:帧间隙没等,或者等错了。有的同学把96 bit time当成96微秒用,在100Mbps下实际等了9.6倍的时间;有的干脆不等,导致接收方DMA还没处理完上一帧,新帧就来了。
解决:用bit_time = 1/speed换算,sleep(96 / speed)。如果是1000Mbps,96 bit time等于96纳秒,Python的time.sleep精度不够,这时候可以用忙等或者直接忽略IFG——但在文档里必须把这个换算关系写清楚,因为答辩时老师很可能问“你模拟的是几Mbps的链路?IFG等了多久?”
另外一条字节序相关的坑也值得记:MAC地址是逐字节发送,目的MAC的第一个字节(比如00:11:22里的0x00)最先上线路,内部不存在字节序问题。但有人习惯把MAC转成int再按大端pack,结果地址逐字节颠倒。处理方法很简单:MAC从头到尾当成bytes处理,不要转int,不要做任何字节序转换。
5. 验证发送结果:Wireshark对拍与接收端CRC复算
5.1 抓包对拍:确认帧被识别为Ethernet II
模拟代码写完,第一件事不是看打印日志,而是抓包。常见做法是本机回环验证:构造完帧后用raw socket发到lo回环接口,Wireshark里选择loopback接口抓包。正常结果应该看到:协议列显示“Ethernet”,帧头里的目的MAC、源MAC、类型0x0800都正确,FCS标记为Good。
这里有个值得注意的现象:Wireshark默认不显示前导码和SFD,因为它们在物理层已经被消费掉了。如果你抓到的包里有前导码,说明你发的不是标准帧。另外,如果你的帧被识别成“802.3”或者“Unknown”,优先检查类型字段——按照第4.3节的方法,把它改成0x0800再看。
5.2 写一个接收端解析函数:自己校验自己
抓包只能看外在表现,内部字段的校验还得靠代码。对文档里的模拟发送端,我补了一个对称的解析函数,形成一个完整的收发闭环:
def parse_frame(frame: bytes): """解析不含前导码的Ethernet帧,返回各字段和CRC校验结果""" # 帧格式: DA(6) + SA(6) + Type(2) + Payload + FCS(4) dst_mac = frame[0:6] src_mac = frame[6:12] ethertype = struct.unpack('!H', frame[12:14])[0] payload = frame[14:-4] fcs_recv = frame[-4:] # 重新计算CRC并与帧尾FCS比对 crc = struct.pack('<I', zlib.crc32(frame[:-4])) ok = (crc == fcs_recv) return { 'dst_mac': dst_mac.hex(), 'src_mac': src_mac.hex(), 'ethertype': hex(ethertype), 'payload_len': len(payload), 'crc_ok': ok }这个函数把解析流程拆成五个字段,和封装流程完全对称。fcs_recv取帧的最后4字节,crc从frame[:-4]重算,两者相等说明发送端的封装和接收端的解析对同一个帧格式达成了共识。把build_frame的输出喂给parse_frame,应该打印出crc_ok: True。这一步验证的是“自洽性”——课设答辩时被问“你怎么证明自己模拟的是对的”,抛这个函数比说一万句都有说服力。
5.3 边界用例:最小帧、广播帧与最大帧
验证完整性的另一个办法是跑边界用例。我一般会构造三帧来测试:第一帧payload只有1字节,验证Padding后帧长必须是64;第二帧目的MAC全FF,验证广播地址;第三帧payload 1500字节,验证最大帧长不越界。
广播帧需要多说一句:目的MAC = ff:ff:ff:ff:ff:ff,交换机收到后从所有端口转发,接收端看到全1的目的地址就知道这是广播,直接收下送交上层处理。代码里用b'\xff' * 6来构造,不是把字符串“ff”直接拼进去。这三帧跑过一遍,帧格式的边界条件就全部覆盖了。
6. 把模拟发送接到真实网卡:raw socket与硬件方向的两个延伸
文档停在软件模拟就够交课设了,但如果你想在答辩时多亮一手,或者想把这套逻辑用到真实链路上,有两个方向值得补。第一个是raw socket:在Linux下用AF_PACKET套接字,把build_frame构造出的字节流直接发到物理网卡,绕过内核协议栈的封装。第二个是向硬件PCS/PMA层延伸——在千兆、2.5G Ethernet的PCS/PMA或SGMII场景里,帧在进入线路之前还要经过8b/10b编码,帧间隙和最小帧长的计算单位从字节变成编码字符,这是另一个层面的事。
import socket def send_raw(ifname: str, frame: bytes): """通过AF_PACKET原始套接字发送Ethernet帧,frame从目的MAC开始,前导码由驱动补充""" sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW) sock.bind((ifname, 0)) # 绑定到指定网卡,0表示接收所有协议 sock.send(frame) # frame不含前导码,驱动会自动补齐SFD sock.close()这段代码有一个容易搞混的点:raw socket发送时,frame从目的MAC开始即可,前导码和SFD由网卡驱动补齐。这跟软件模拟有个差异——软件模拟把物理层职责也模拟了,前导码要自己拼;真实网卡驱动会接管物理层这部分工作。另外bind的第二个参数填0会接收该网卡上的所有协议,如果你这个程序还要同时收包,建议改成明确协议号,避免收进来一堆无关流量影响调试。
从那以后我每次写帧发送相关的课设或工具,都强制走一遍“封装→抓包对拍→边界用例”的流程,哪怕只是改了一个类型字段的值。Wireshark的CRC状态是绿色还是红色,是最诚实的结果反馈,代码里写得再漂亮都不如这一个小灯靠谱。希望帮到你。
本文还有配套的精品资源,点击获取