简介计算机网络课程设计作品包含网络协议编辑器与网络协议分析器采用Python编程实现面向计算机专业本科生、网络编程爱好者及需要完成类似课设的开发者。资源内包括两个主要Python脚本packet_sender.py用于创建、编辑和发送自定义报文packet_analysisor.py负责抓取网络流量并解析TCP/UDP/IP等协议字段同时附带README说明文档与gitignore文件压缩包共4个文件大小仅13KB体量轻量但功能完整。项目基于socket、struct及scapy库涵盖报文可视化预览、发送接收、数据包捕获、协议识别、统计与过滤等常见功能可作为理解OSI参考模型、TCP/IP协议栈及数据封装过程的直观教具。目前已有431人学习浏览在同类课设资源中具有一定参考价值。使用者可参照源码快速上手掌握Python网络编程、二进制打包解包、网络嗅探工具设计思路并能够将其中模块化、错误处理等实践迁移到自己的项目中对课程设计或协议分析进阶都很有帮助。1. 网络协议编辑器与协议分析器是什么一门课程设计最硬核的选题如果你在计算机网络课程设计题目里看到「网络协议编辑器与网络协议分析器使用 Python 编写」别急着把它当成又一个抓包软件的外壳作业。这个题目的本质是让你用自己写的代码把一台机器网卡上跑过的每一种网络协议从字节流还原成字段再反方向把一堆字段拼成字节流扔回网络。一个模块负责读一个模块负责写两个模块合起来就是一台微型协议实验台既能看清别人发的包也能亲手造一个包让别人抓。这类课程设计适合已经学完 TCP/IP 协议栈、但还没把协议当工具用过的同学。编辑器要解决的是怎么把 IP 头的每一字节安排明白分析器要解决的是怎么从随机抓到的帧里把协议看清。这两个模块也正好踩中课程设计答辩的核心考点能不能讲清楚字节序、能不能算对校验和、能不能说清每一层协议字段的含义。本文用一个不依赖第三方协议库的最小实现把抓包、解析、构造、发送、校验全链路走通并给出踩坑记录和验证方法。2. 协议分析器核心实现从原始套接字抓包到分层解析2.1 抓包的本质原始套接字与单网卡监听数据包在网卡上以帧frame的形式存在普通 socket 只能读到应用层 payload因为内核协议栈已经把以太网头、IP 头都剥掉了。要看到完整的帧必须用原始套接字绕过内核协议栈的解析过程。Linux 下最直接的做法是用AF_PACKET协议族、SOCK_RAW类型再bind到一个具体网卡接口。这段代码是分析器的入口import socket def create_sniffer(iface: str, buffer_size65536): # 创建原始套接字SOCK_RAW 让内核把完整帧交给用户态 sock socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(3)) # bind 到网卡第二个参数 0 表示接收所有协议类型 sock.bind((iface, 0)) sock.settimeout(10) return sockcreate_sniffer做了三件事创建原始套接字、绑定网卡、设置超时。socket.ntohs(3)中的 3 是 ETH_P_ALL 的数值表示接收所有协议类型。如果你只关心 IP 报文可以改成socket.ntohs(0x0800)只关心 ARP就改成0x0806。超时设成 10 秒是为了让外层循环能定期检查线程状态避免recv永久阻塞。buffer_size用 65536 完全够装下最大帧raw socket 一次recv只能拿到一个帧不存在一次读多个帧的用法。抓包循环通常在独立线程里跑每收到一帧就交给analyze(frame)函数。这里有个关键认知raw socket 收到的是从前导码之后、FCS 之前开始的完整数据最小 60 字节、最大 1514 字节不算巨型帧。分析器拿到这帧后第一件事是按以太网头格式拆字段。2.2 以太网帧解析前导码、MAC 地址与类型字段以太网帧头固定 14 字节目的 MAC 6 字节、源 MAC 6 字节、类型/长度 2 字节。用struct.unpack一次拆开需要特别注意格式串里的!前缀代表网络字节序否则多字节字段会倒序。解析代码import struct ETH_HEADER struct.Struct(!6s6sH) def parse_ether(frame: bytes): # 前14字节是固定头用预编译 Struct 拆包 dst_mac, src_mac, eth_type ETH_HEADER.unpack_from(frame, 0) # 字节串转 MAC 地址的常见写法每个字节转两位十六进制 dst :.join(f{b:02x} for b in dst_mac) src :.join(f{b:02x} for b in src_mac) return {dst: dst, src: src, type: eth_type, payload: frame[14:]}ETH_HEADER用预编译的struct.Struct比每次unpack重新解析格式串快不少。格式串!6s6sH表示网络字节序、6 字节字节串、6 字节字节串、2 字节无符号整数。unpack_from相对unpack的优势是只从指定偏移读取不要求len(frame)恰好等于结构长度这对解析被填充过的帧非常友好。拿到type字段后0x0800 表示载荷是 IPv40x0806 是 ARP0x86DD 是 IPv6据此决定下一步交给哪个解析器。2.3 IP 头解析版本、首部长度、校验和与分片标志IP 头最小 20 字节但首部长度字段IHL指示实际长度可能在 20 到 60 字节之间。解析时必须先读 IHL 再决定后续偏移不能用固定 20 字节一路拆到底。常见做法是先用一个 20 字节固定结构拆出关键字段再用 IHL * 4 作为网络层头的结束位置import socket IP_HEADER struct.Struct(!BBHHHBBH4s4s) def parse_ipv4(pkt: bytes): # pkt 是紧跟在以太网头后面的完整 IP 报文 ver_ihl, tos, total_len, ident, frag, ttl, proto, checksum, src, dst IP_HEADER.unpack_from(pkt, 0) version ver_ihl 4 # 高4位是版本号 ihl (ver_ihl 0x0F) * 4 # 低4位是首部长度单位是4字节 return { version: version, ihl: ihl, tos: tos, total_len: total_len, ident: ident, flags: (frag 13) 0x07, frag_offset: frag 0x1FFF, ttl: ttl, proto: proto, checksum: checksum, src: socket.inet_ntoa(src), dst: socket.inet_ntoa(dst), payload: pkt[ihl: total_len], }frag字段的高 3 位是标志位保留位、DF、MF低 13 位是片偏移必须分开解析。total_len是 IP 报文的总长度如果帧后面还有以太网填充字节payload 只取total_len - ihl多余部分直接丢弃。注意ver_ihl同时包含版本和首部长度不能当成普通数字直接用必须做位移和掩码。socket.inet_ntoa把 4 字节二进制 IP 转成点分十进制它只接受 bytes 对象!4s格式拆出来的正好是 bytes两者配合没问题。2.4 TCP/UDP 解析与协议树输出IP 层解完后根据proto字段分发到 TCP6或 UDP17解析器。TCP 头也是可变长度数据偏移字段占 4 位乘以 4 才是真实头长UDP 头固定 8 字节解析相对简单UDP_HEADER struct.Struct(!HHHH) def parse_udp(pkt: bytes): # pkt 是 IP 层 payload sport, dport, length, checksum UDP_HEADER.unpack_from(pkt, 0) return { sport: sport, dport: dport, length: length, checksum: checksum, payload: pkt[8: length], }为了让分析器输出像标准抓包工具那样层次清晰我习惯把每一层解析结果组织成一个字典树每个节点含层名、字段名、字段值。实际展示时按缩进输出四层结构以太网层 → IP 层 → TCP/UDP 层 → 应用层载荷。这个协议树是课程设计评分时最好用的展示点因为它一次性展示了我到底解析出几层、每层有哪些字段。解析到传输层后如果目标端口是 80 或 443还可以把 payload 当作文本打印出来但不要强行解析应用层协议因为报文可能只是三次握手的前两个包不携带完整数据。提示抓包时尽量先抓回环接口lo上的回环流量来调试协议字段齐全且不受外界环境影响确认解析逻辑正确后再换到物理网卡抓真实流量能省掉大量排查环境问题的精力。3. 协议编辑器核心实现用 struct 构造数据包并发送到网络3.1 编辑器的定位字段表单与十六进制预览联动协议编辑器和分析器正好是逆过程。分析器是字节流 → 字段编辑器是字段 → 字节流。课程设计里编辑器的常见做法是用 tkinter 做一个表单区和一个十六进制预览区表单区每个输入框对应一个协议字段比如 IP 层的 TTL、协议号、源地址、目的地址每当用户改一个字段编辑器调用build_packet()把字段拼成字节并同步刷新右侧的 hex 视图。这样用户能即时看到我改了 TTL 之后帧里哪几个字节变了这个联动效果在答辩演示时非常直观。实现联动时注意tkinter 的StringVar.trace_add可以监听输入变化但不要在每次按键都重建整个帧否则大报文会让界面卡顿。我的做法是字段变更 → 标记脏 → 延迟 200ms 后重建的去抖策略连续输入时只触发一次刷新。编辑器的核心不在界面而在那个把所有字段拼成字节的 builder界面只是调 builder 的外壳。3.2 构造以太网帧与 IP 报文的核心函数构造过程从内往外做先拼 TCP/UDP 头再封装 IP 头最后套以太网头。这样每一层都能在前一层建好的 payload 基础上补齐长度。为了不引入第三方协议库需要手写每个头的 struct 格式import struct import socket def build_ipv4(src_ip, dst_ip, proto, payload, ttl64, ident1): # 固定20字节标准IP头IHL5 version_ihl (4 4) | 5 total_len 20 len(payload) # 标志位设 DF10x4000表示不允许分片 frag 0x4000 # 校验和字段先填 0后面单独计算 header struct.pack(!BBHHHBBH4s4s, version_ihl, 0, total_len, ident, frag, ttl, proto, 0, socket.inet_aton(src_ip), socket.inet_aton(dst_ip)) csum checksum(header) # 重新打包把计算出的校验和填入第8个字段位置 return struct.pack(!BBHHHBBH4s4s, version_ihl, 0, total_len, ident, frag, ttl, proto, csum, socket.inet_aton(src_ip), socket.inet_aton(dst_ip))这段代码展示了 IP 报文构造的完整链条。第一遍打包时校验和字段填 0调用checksum()算出正确的 16 位校验和再整体重打一遍。inet_aton把点分十进制字符串转成 4 字节用户填了非法 IP 会抛异常上层要捕获并转成界面提示。ident是标识字段分片时用同一个值标识同一组片课程设计场景直接固定为 1 不会有问题。TTL 默认给 64 是 Linux 的惯例值路由器每经过一跳减一减到 0 就丢弃并回 ICMP 超时。3.3 校验和计算反码求和与回卷校验和是编辑器里最容易写错的函数。IP、TCP、UDP 的校验和算法是同一套路把校验和字段置 0把数据按 16 位分组反码求和再把高 16 位的进位回卷到低位最后取反。UDP 校验和还要额外加一个 12 字节的伪头源 IP、目的 IP、协议号、UDP 长度这一点常被忽略。下面是完整实现def checksum(data: bytes) - int: # 按16位分组不足16位的末尾补0 if len(data) % 2: data b\x00 total 0 for i in range(0, len(data), 2): total (data[i] 8) | data[i 1] # 回卷把高16位进位加到低16位直到高16位为0 while total 16: total (total 0xFFFF) (total 16) return (~total) 0xFFFF这个函数不到十行但每一步都有讲究。total累加过程中Python 整数没有溢出限制但仍要做显式回卷因为协议算法要求把进位加回低位。while total 16处理的是加完进位后又产生进位的情况最多循环两次就稳定了。最后(~total) 0xFFFF把结果截断成 16 位因为 Python 的按位取反对无限位整数操作会产生负数必须用掩码修正。计算 TCP/UDP 校验和时要先把伪头、TCP/UDP 头、payload 拼成一整块再将校验和字段本身的两个字节置 0最后调用checksum。3.4 发送与收尾raw socket 发包构造好帧之后发送是另一条路用 raw socket 把字节写回网卡。发送端 socket 与抓包端的创建方式不同要明确指定协议类型而且必须带着完整以太网头整体发出def send_frame(iface: str, frame: bytes): # SOCK_RAW AF_PACKET 可以从用户态直接写二层帧 sock socket.socket(socket.AF_PACKET, socket.SOCK_RAW) # bind 到网卡接口协议类型写 0 表示不限制 sock.bind((iface, 0)) sock.send(frame) sock.close()send_frame发送的是完整以太网帧包含目的 MAC、源 MAC 和类型字段。这里有个容易踩坑的地方很多教材里的例子写的是sock.sendto(frame, (iface, 0))但某些内核版本对sendto的地址参数处理方式不同可能把整个帧再次封一层导致对端收到两层以太网头。我的习惯是统一用bindsend避免地址参数在不同平台上的歧义。发送成功后可以用第 2 章写的分析器在另一台机器上抓回这个帧验证形成完整闭环。4. 课程设计避坑指南字节序、校验和与抓包权限的常见问题4.1 抓包一直空白权限与网卡切换现象分析器创建 socket 成功但recv一直超时或者程序直接抛PermissionError。原因raw socket 在 Linux 上需要 root 权限即使有权限绑定到lo回环接口时只能看到本机回环流量绑定到物理网卡时看不到用户自己从应用层发出去的包。不少同学把接口名写死成eth0但不同机器上可能是ens33、enp3s0写死就会报错或抓不到。解决程序启动时打印当前机器所有可用接口调用socket.if_nameindex()获取列表让用户选择如果抛权限错误用sudo运行进程或者给 Python 解释器单独加上网络抓包的系统能力setcap cap_net_rawep注意这会改变解释器的运行权限模型只在实验环境用。在物理机上演示时报权限错误是最常见的第一道坎。4.2 解析出的长度字段不对大小端与偏移量现象用分析器解析自己构造的帧时IP 总长度显示成0x1C00而不是0x001C端口号 8080 显示成 32895。原因网络字节序是大端而 x86 机器默认小端。struct.pack(!H, value)的!前缀已经处理了字节序但如果图省事手写(data[0] 8) | data[1]就会把高低字节拼反。另一个隐患是按偏移取字段时漏掉前面的头长度比如从第 13 字节开始取端口号结果取到的是 IP 头的某个字段。解决统一用带!前缀的struct.Struct禁止手写位移拼数所有偏移量都基于上层已解析出的头长度。一个简单的自检方法已知端口 8080 的十六进制是0x1F90如果解析结果变成0x901F立刻就能定位到字节序问题。4.3 校验和永远不对进位回卷被忽略现象编辑器构造的 IP 包发出去自己的分析器一算校验和就报错TCP 校验和尤其离谱怎么算都对不上。原因要么忘了回卷要么 TCP 校验和没加伪头。回卷指在 16 位分组求和后把超过0xFFFF的高位加回到低位伪头则是源 IP、目的 IP、协议号、TCP 长度等 12 字节必须参与 TCP/UDP 校验和计算。很多初学者只对 IP 头做校验忽略了传输层校验和依赖伪头。解决使用统一的checksum函数并保证循环回卷TCP/UDP 计算时先拼好伪头 TCP/UDP 头 payload整块数据再把校验和字段置 0。写完用一个标准抓包工具抓一个自己发的包对比校验和一致就说明算法正确不一致就把伪头拼装过程打出来逐字节检查。4.4 自己发的包抓不到回环接口与环路过滤现象编辑器发送帧后同一台机器的分析器绑定到物理网卡收不到刚发的包。原因发送和接收在同一个物理接口时报文一旦交给网卡就发走了本机的分析器监听的接口截获不到已经出站的帧。如果发送目标是回环地址分析器却绑定在物理网卡上更是完全收不到。解决如果只做本机验证把发送目标 IP 改成 127.0.0.1发送 socket 和分析器 socket 都绑定lo接口如果两个模块分属两台机器就分别绑各自的物理网卡。这也是课程设计现场演示时最容易被打回的一点自己的包抓不到整个流程看起来就是黑的务必提前确认发送接口和分析接口一致。4.5 Windows 下的 raw socket 限制现象程序在 Linux 上跑得好好的换到 Windows 就报错OSError: [WinError 10093]或者抓不到任何包。原因Windows 对AF_PACKET原生协议族的支持有限标准 Python raw socket 无法直接接收二层帧即使创建成功也只能收到本机发出的部分包。这个问题在课程设计答辩现场特别常见因为很多同学的日常开发机是 Linux答辩机却是 Windows。解决把抓包和分析拆成两条路径。Linux 端负责实时抓包并落盘成 pcap 文件Windows 端只做离线解析或者使用 Npcap 驱动配合轻量绑定库来访问二层数据。我在实际项目里采用离线 pcap 优先策略程序支持读取 pcap 文件作为统一输入源这样在任何操作系统上都能演示解析功能不容易卡在权限和驱动上。pcap 文件格式本身只有 24 字节文件头加每包 16 字节包头用struct完全能自己读。5. 让两个模块互相验证构造 ARP 请求并用分析器回读5.1 构造最小 ARP 请求帧ARP 请求只有 28 字节以太网头 ARP 头没有 IP 头是验证编辑器最廉价的协议。构造时容易漏掉 ARP 的硬件类型和协议类型字段这里给出可运行的实现import struct import socket def build_arp_request(src_mac: str, src_ip: str, dst_ip: str): # 以太网头: 目的 MAC 为广播地址 ff:ff:ff:ff:ff:ff dst_mac_b b\xff * 6 # MAC 字符串转 6 字节不能用 inet_aton那是给 IP 用的 src_mac_b bytes([int(x, 16) for x in src_mac.split(:)]) eth_type 0x0806 eth struct.pack(!6s6sH, dst_mac_b, src_mac_b, eth_type) # ARP 头: 硬件类型1(以太网), 协议类型0x0800(IPv4) # 硬件地址长度6, 协议地址长度4, 操作码1(请求) arp struct.pack(!HHBBH6s4s6s4s, 1, 0x0800, 6, 4, 1, src_mac_b, socket.inet_aton(src_ip), dst_mac_b, socket.inet_aton(dst_ip)) return eth arp构造过程中最容易写错的是 MAC 地址转换。socket.inet_aton只接受 IPv4 地址不能直接用于 MACMAC 字符串按冒号拆成十六进制整数后用bytes([...])重组为 6 字节。ARP 请求的目的 IP 填什么完全取决于你要探测谁目的 MAC 在请求里填全ff因为请求本身就是谁知道这个 IP 对应的 MAC请告诉我的广播。5.2 用分析器验证编辑器构造的帧自产自销的验证流程分三步发送前打印帧的十六进制在同一台机器或同一网络中启动第 2 章的分析器抓包将抓到的帧字段与发送时的输入字段逐一比对。比对时不要人工肉眼看几十个字节写个小脚本自动对比字段字典更可靠frame build_arp_request(aa:bb:cc:dd:ee:ff, 192.168.1.100, 192.168.1.1) send_frame(lo, frame) # 用分析器在 lo 接口上抓一个包解析后打印 captured sniffer.recv(65535) parsed parse_ether(captured) assert parsed[type] 0x0806, 以太网类型字段应为ARP # 继续用 ARP 解析器拆字段对比 opcode、src_ip、dst_ip 是否和构造时一致课程设计答辩时现场演示编辑器填一个源 IP 和一个目的 IP分析器立刻显示这两个 IP是最有说服力的闭环。注意断言条件要写清楚用assert或 if 语句都行但要在屏幕上打印验证通过或具体哪个字段不一致方便短时间内定位问题。5.3 与系统流量交叉验证判断实现正确性如果只用自己的编辑器发、自己的分析器收两个模块的错误可能互相掩盖编辑器按错误字节序组包分析器按同样错误的字节序解析两边一致但实际包是错的。更可靠的做法是引入系统真实流量作为基准。让 ping 命令产生 ICMP 包或者让浏览器访问本机服务产生 TCP 包再用自己的分析器去解析这些真实流量和系统抓包工具显示的字段逐一对照。判断实现正确性有三条硬性判据。字段语义正确指的是 IP 头里的 TTL、UDP 端口号等与已知值一致长度正确指的是 IP 头的total_len与帧实际长度兼容UDP 头的length等于头加 payload校验和正确指的是用第三方工具显示的 checksum 与自己算法算出的结果一致。三条都过了分析器才算真正可交付。单独一条过说明不了问题比如从不校验 TCP 伪头的人IP 校验和照样算得对。判据验证方法排查方向字段语义抓 ping 包源 IP 应为本机地址检查inet_ntoa使用和字段偏移长度一致IP total_len 与 payload 加头长度相等检查 IHL 乘法和 total_len 截断校验和一致与标准工具显示的 checksum 对比检查伪头是否参与、回卷是否执行6. 进阶把校验和计算与协议解析做成可复用组件6.1 增量校验与计算缓存校验和模块在课程设计里只算一次但在实际网络工具里转发节点要高频重算。常见优化是增量校验IP 包重传时 TTL 每次减 1校验和只需在旧值基础上做一次 16 位取反调整不必整包重算。感兴趣的话可以研究 RFC 1624 里的增量更新公式把校验和从 O(n) 降到 O(1)。另一个更简单的优化是加缓存字典以(方向, 数据长度, 数据开头若干字节)为键同一批高频小包可以命中缓存。把checksum函数单独抽到一个模块里不要和界面代码混在一起后续任何扩展都能直接复用。6.2 协议注册表新增协议只需注册解析函数分析器的分发逻辑如果写成if proto 6: parse_tcp(); elif proto 17: parse_udp()每加一个新协议就要改主函数。更好的做法是做一个协议注册表把解析函数按协议号登记进去主循环只查表PROTO_PARSERS {} def register(proto_num: int): def decorator(func): PROTO_PARSERS[proto_num] func return func return decorator register(6) def parse_tcp(pkt: bytes): # 完整 TCP 解析实现 pass register(17) def parse_udp(pkt: bytes): # 完整 UDP 解析实现 pass这样新增协议时原有代码一行都不用改。课程设计答辩中这个方法有额外加分效果因为它体现了开闭原则和插件化设计这两个高频考点。把同样的注册表思路用到编辑器就能按协议类型动态生成字段表单而不是为每种协议手写一套界面。我自己的经验是工具类项目没到一定复杂度时先把 if-else 写清楚跑通功能等你要加 ICMP、IGMP、DNS 这些协议时再重构注册表。重构过程中你还会发现当初把解析结果统一成字典结构是个多么正确的决定。做完整套项目我最大的教训是两个模块一定要从第一天就开始互相验证。我最初是分开写的分析器测了好几轮编辑器写了两个星期联调时才发现编辑器发的 ARP 包在分析器里全部解析失败原因竟是 IP 层的 TTL 字段填了字符串没做类型转换而分析器那边用struct.unpack处理无符号整数直接抛异常。后来改成每写完一个构造函数立刻用分析器抓回来解析的工作流这类低级错误基本当天就暴露了。希望帮到你。本文还有配套的精品资源点击获取