☰
用Python从零实现网络协议编辑器与解析器:TCP/IP报文构造与抓包分析
2026/10/13 4:02:25 网站建设 项目流程

简介:一套基于Python编写的计算机网络课程设计项目,同时实现网络协议编辑器与协议分析器,适合计算机网络专业学生、网络编程初学者用于课程设计或实践练习。项目核心是让学习者亲手构造、修改、发送并解析常见网络报文,从而深入理解IP、TCP、UDP等协议字段及数据封装过程。压缩包共4个文件,大小约13KB,包含2个Python脚本(packet_sender.py负责报文编辑与发送,packet_analysisor.py负责抓包与分析)、1个README.md说明文档和1个gitignore配置文件。Python脚本基于socket与struct库实现底层网络接口和二进制数据的打包解包,README则提供项目结构与使用指引,方便快速上手和二次开发。该资源已有431人学习下载,代码量精炼且模块划分清晰,能帮助读者掌握Python网络编程的基本流程,并学会设计简单的协议构造和抓包工具,为后续使用Wireshark或深入网络排障打下基础。

1. 网络协议编辑器与分析器:这个课程设计选题为什么值得认真做

很多人交计算机网络课程设计,用的是现成库两三个 API 调完,抓一个包截个图交差。答辩老师问一句“TCP 校验和怎么算”“IP 头长度字段在哪”,基本就卡住了。这个项目的价值和区分度恰恰在于:它逼你把网络报文从比特开始重新拼一遍,再重新拆一遍。网络协议编辑器负责把可读字段编码成二进制帧,网络协议分析器负责把二进制帧解码回可读字段,一编一析刚好形成一个完整闭环。适合的人群有三类:课程设计想拿高分、不想靠截图糊弄的在校生;打算投网络方向岗位、需要一份能讲清楚原理的面试项目的人;以及单纯想把 TCP/IP 吃透的开发者。下面按我实际做过的技术路线拆开讲。

2. 协议编辑器和分析器的公共地基:字节序与首部布局

2.1 网络字节序和结构体对齐:Python 解析二进制帧的第一关

网络协议在传输层以下的二进制格式全部采用大端字节序(Big-Endian),也就是网络字节序。端口号、序列号、校验和这些多字节字段,在网络上传输时都是高位字节在前。这和 x86 机器默认的小端序正好相反,所以实现协议编辑器、协议分析器时第一件事就是把二进制读写方向统一。

Python 里做这件事有两种主流工具。第一种是struct模块,用!前缀表示网络字节序;第二种是ctypes里的BigEndianStructure,可读性更好但灵活度稍差。我个人在课程设计里用struct更多,因为它可以直接把打包、解包写在一行,而且对字节流的切片操作更贴近 C 语言的memcpy思维。下面是struct的典型写法:

import struct # 打包一个 TCP 头:源端口、目的端口、序列号、确认号、首部长度/标志位、窗口、校验和、紧急指针 tcp_header = struct.pack( "!HHIIBBHHH", 12345, # 源端口 80, # 目的端口 1000, # 序列号 0, # 确认号 0x50, # 数据偏移 5,表示 TCP 头 20 字节 0x02, # 标志位:SYN 64240, # 窗口大小 0, # 校验和,后面再算 0 # 紧急指针 ) print(len(tcp_header)) # 输出 20 print(tcp_header.hex())

!HHIIBBHHH这个格式字符串的每个字符都有明确含义:!指网络字节序,H是无符号 16 位整数,I是无符号 32 位整数,B是无符号 8 位整数。四个H、两个I、三个B加起来正好 20 字节,对应去掉选项字段的 TCP 固定首部。这里要注意一点:TCP 头里的“数据偏移”字段只有 4 位,而“保留字段”占 4 位,它们合起来才是 1 字节,所以0x50这个值实际表示“头部 5 个 32 位字,共 20 字节”。初学容易把偏移和标志位拆开算,这是第一个需要死记的布局细节。

2.2 从上到下数包:IP 头什么时候结束,TCP 头从哪里开始

一个完整的网络包在以太网里是“以太网帧头 + 网络层头 + 传输层头 + 应用层数据”。抓包拿到原始字节后,先要按固定偏移切出每一层。以太网帧头固定 14 字节,从第 0 字节开始:6 字节目的 MAC、6 字节源 MAC、2 字节以太网类型。以太网类型是 0x0800 时,说明后面跟的是 IPv4 数据报。

IPv4 头长度不固定,但有一个固定规则:第 0 字节的高 4 位是版本,低 4 位是 IHL(Internet Header Length),单位是 4 字节。比如0x45表示版本 4、头部长度 5×4=20 字节。只要读出第 14 个字节再取低 4 位乘 4,就能定位 IP 头结束的位置。TCP 头从这里开始,而 TCP 头自己的长度又记录在它第 12 个字节的高 4 位里,同样是乘以 4。这种层层嵌套的“长度自描述”结构,是整个协议分析器解包逻辑的地基。

我见过不少初学者在解 IP 头时直接固定取data[34:54]当作 TCP 头,这在没有 IP 选项字段时恰好是正确的,一旦源路由或时间戳选项出现就全错位。正确姿势永远是动态计算偏移量,不能依赖固定位置。协议编辑器也一样:填完 IP 选项之后,总长度、头部长度、校验和都要联动变化,否则生成的报文本身就是非法的。

2.3 编辑器与分析器为什么必须成对出现

单独做分析器,抓一个包回来解出字段,功能上没问题,但没法验证解包逻辑是不是自洽。单独做编辑器,能构造任意报文,但看不到它在真实网络环境里是否被正常处理。两者拼在一起,价值就出来了:用自己写的编辑器构造一个 TCP SYN 包发出去,再用自己写的分析器抓回来解析,如果字段完全一致、校验和能通过,就证明双向逻辑都正确。

这种闭环思路比写一万行代码都有效。我在设计项目时把两个工具做成一个主程序下的两个子命令,共享同一套字段定义和校验和函数。编辑器侧调用打包函数,分析器侧调用解包函数,两边共用同一份“字段表结构”,避免出现打包和解包各搞一套偏移量把整个人绕晕的情况。后面两章会分别给出这两个子功能的完整实现思路和可运行代码。

3. 用 Python 写网络协议编辑器:从字段输入到二进制帧

3.1 字段表优先:把协议头拆成 13 个可填写字段

协议编辑器核心不是画界面,而是把“协议头”翻译成“字段表”。以 IPv4 + TCP 这一段为例,IP 头里需要用户提供的字段包括:源 IP、目的 IP、服务类型、标识符、生存时间 TTL。TCP 头里需要的是:源端口、目的端口、序列号、确认号、标志位、窗口大小、选项数据。每一层都有一个“载荷”字段,指下一层协议或应用数据。用字段表驱动的好处是,以后想支持 UDP、ICMP 或 IPv6,只需增加一张表,不必改主流程。

字段表的定义我采用字典数组,每个字段记录名称、偏移、字节序、默认值。像这样设计:

ip_fields = [ {"name": "version_ihl", "size": 1, "default": 0x45}, {"name": "tos", "size": 1, "default": 0}, {"name": "total_length", "size": 2, "default": 0}, {"name": "identification", "size": 2, "default": 0x1234}, {"name": "flags_frag", "size": 2, "default": 0x4000}, {"name": "ttl", "size": 1, "default": 64}, {"name": "protocol", "size": 1, "default": 6}, {"name": "header_checksum", "size": 2, "default": 0}, {"name": "src_ip", "size": 4, "default": "0.0.0.0"}, {"name": "dst_ip", "size": 4, "default": "0.0.0.0"}, ] tcp_fields = [ {"name": "src_port", "size": 2, "default": 12345}, {"name": "dst_port", "size": 2, "default": 80}, {"name": "seq", "size": 4, "default": 1000}, {"name": "ack", "size": 4, "default": 0}, {"name": "offset_reserved", "size": 1, "default": 0x50}, {"name": "flags", "size": 1, "default": 0x02}, {"name": "window", "size": 2, "default": 64240}, {"name": "checksum", "size": 2, "default": 0}, {"name": "urgent_ptr", "size": 2, "default": 0}, ]

这里的flags_frag默认值 0x4000 是把“不分组”位置 1,对应 IP 头的“Don’t Fragment”位。offset_reserved的默认值 0x50 在上面解释过,是“数据偏移为 5”。维护字段表而不是直接写死打包逻辑,后续给界面或命令行传参都只用迭代这张表就行。

3.2 用 struct 编码:校验和函数先写对

校验和是协议编辑器的核心难点,也是容易在答辩时被追问的点。IPv4 头校验和算法是这样的:先把校验和字段置为 0,然后把整个 IP 头按 16 位一组求和,如果结果超过 16 位就回卷进位,最后取反。TCP 校验和更麻烦一点,它除了 TCP 段本身,还要把一个 12 字节的伪首部纳入计算范围,伪首部包含源 IP、目的 IP、协议号、TCP 段长度,但伪首部本身不发送。

这两个校验和函数一定要在编辑器里实现一次,因为分析器里还要用同样的算法做校验比对。下面是能直接用的校验和函数:

def checksum(data: bytes) -> int: if len(data) % 2: data += b"\x00" # 补齐为偶数长度 s = 0 for i in range(0, len(data), 2): word = (data[i] << 8) + data[i + 1] s += word s = (s & 0xffff) + (s >> 16) # 回卷进位 return (~s) & 0xffff # 使用示例 ip_header_without_checksum = bytes.fromhex("450000400000000040060000C0A80101C0A80102") result = checksum(ip_header_without_checksum) print(hex(result)) # 打印计算出的校验和

注意这里先要保证参与计算的数据长度为偶数,如果不满足就在末尾补一个字节的零,否则 16 位对齐会错位。回卷进位那行是重点:如果s超过 65535,就把多出来的高位再加回低位,这是一个循环过程,无法用普通加法替代。

TCP 校验和需要先拼接伪首部:

def tcp_checksum(src_ip: str, dst_ip: str, tcp_segment: bytes) -> int: src = socket.inet_aton(src_ip) dst = socket.inet_aton(dst_ip) pseudo_header = struct.pack("!4s4sBBH", src, dst, 0, 6, len(tcp_segment)) return checksum(pseudo_header + tcp_segment)

计算时伪首部里的“TCP 长度”是 TCP 头加数据的总长,不是 TCP 头的固定 20 字节。很多人首次写的时候传的是 TCP 头部长度,导致抓包工具报“checksum offload error”。协议编辑器的自测方法很简单:把一个用合法工具生成的 TCP 包拿来,把自己编辑器生成的同字段包和它逐字节对比。如果只有校验和位不同,说明这个函数写对了。

3.3 编辑器完整实现:构造一个 TCP SYN 帧并发送

编辑器主体流程是“收集字段 → 先拼一个校验和置零的包 → 算校验和 → 重拼最终包 → 发送”。下面这个函数接收源 IP、目的 IP、端口和载荷,返回完整的 IPv4+TCP 报文:

import socket import struct def build_syn_packet(src_ip, dst_ip, src_port, dst_port, seq): # 1. 构造 IP 头,校验和先填 0 ip_header = struct.pack( "!BBHHHBBH4s4s", 0x45, # 版本 4 + IHL 5 0, # 服务类型 40, # 总长度 = 20 + 20,暂定,后面会重算 0x1234, # 标识符 0x4000, # 标志位 + 分段偏移 64, # TTL 6, # 上层协议:TCP 0, # IP 校验和 socket.inet_aton(src_ip), socket.inet_aton(dst_ip) ) # 2. 构造 TCP 头,校验和也先填 0 tcp_header = struct.pack( "!HHIIBBHHH", src_port, dst_port, seq, 0, # ACK 序号,SYN 包不需要 0x50, # 数据偏移 5 0x02, # SYN 标志 64240, 0, # TCP 校验和 0 ) # 3. 计算 IP 校验和并重写 ip_sum = checksum(ip_header) ip_header = struct.pack( "!BBHHHBBH4s4s", 0x45, 0, 40, 0x1234, 0x4000, 64, 6, ip_sum, socket.inet_aton(src_ip), socket.inet_aton(dst_ip) ) # 4. 计算 TCP 校验和,需要包含伪首部 tcp_sum = tcp_checksum(src_ip, dst_ip, tcp_header) tcp_header = struct.pack( "!HHIIBBHHH", src_port, dst_port, seq, 0, 0x50, 0x02, 64240, tcp_sum, 0 ) return ip_header + tcp_header # 发送:Linux 下使用原始套接字 sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW) packet = build_syn_packet("192.168.1.10", "192.168.1.1", 12345, 80, 1000) sock.sendto(packet, ("192.168.1.1", 0))

前面 40 的总长度是 IP 头 20 字节加 TCP 头 20 字节,没有算应用层载荷。如果带了载荷,这里必须改成20 + 20 + len(payload),同时 TCP 长度和伪首部的长度参数也要跟着变。切记:总长度字段描述了 IP 数据报的全体,校验和计算必须等所有字段都确定下来再执行。先算 TCP 校验和再改 IP 总长度,会导致抓包工具始终报 TCP 校验和错误,因为伪首部里的长度变了。这个顺序问题至少能坑掉一半第一次写编辑器的人。

4. 用 Python 写网络协议分析器:原始套接字抓包与逐层解析

4.1 原始套接字:拿到网卡上的“原汁原味”帧

分析器的第一步是拿到原始数据。普通 UDP 或 TCP 套接字交给应用层的已经是去掉了协议头的“净果”,分析器需要的是从 IP 头开始的完整报文。Linux 下可以用socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)创建原始套接字接收 TCP 报文,但这只能拿到 IP 层开始的数据,没有以太网帧头。如果想同时解析 Ethernet 头,需要socket.AF_PACKET加SOCK_RAW,它能看到链路层最原始的一帧。

课程设计里我建议做两层抓取模式:默认模式抓 IP 层,进阶模式抓链路层。下面这段代码演示链路层抓包:

import socket import struct # 注意:Linux 下需要 root 权限;Windows 上此方式不适用 sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(0x0003)) sock.bind(("eth0", 0)) # 绑定到指定网卡 data, addr = sock.recvfrom(65535) print(f"捕获 {len(data)} 字节") # 解析以太网帧头 dst_mac, src_mac, ether_type = struct.unpack("!6s6sH", data[:14]) print("目的 MAC:", ":".join(f"{b:02x}" for b in dst_mac)) print("源 MAC:", ":".join(f"{b:02x}" for b in src_mac)) print("以太网类型: 0x{:04x}".format(ether_type))

AF_PACKET加ntohs(0x0003)表示接收所有协议类型,从普通 TCP 到 ARP、ICMP 都能收到。拿到帧头后判断ether_type,0x0800 走 IPv4 解析,0x0806 走 ARP 解析,其余类型可以直接跳过或打印“未识别协议”。这一步是协议分析器的门面,解析错一层,后面所有层的偏移量都会错位。

4.2 逐层解包:从以太网帧头到 TCP 载荷

链路层之后是网络层和传输层。解包时,我建议每一步都返回“剩余字节”和“解析结果字典”,让上层逻辑不用关心这一层到底用了多长的选项字段。下面是一个完整的 IPv4+TCP 解析函数:

def parse_ipv4_tcp(data: bytes): # 跳过以太网帧头,data 从 IP 头开始 ip_start = 0 version_ihl = data[0] version = version_ihl >> 4 ihl = (version_ihl & 0x0f) * 4 if version != 4: return None ip_fields = { "protocol": data[9], "src_ip": socket.inet_ntoa(data[12:16]), "dst_ip": socket.inet_ntoa(data[16:20]), } tcp_start = ip_start + ihl src_port, dst_port, seq, ack, offset_flags = struct.unpack( "!HHIIB", data[tcp_start:tcp_start + 13] ) offset = (offset_flags >> 4) * 4 flags = offset_flags & 0x3f payload_start = tcp_start + offset return { **ip_fields, "src_port": src_port, "dst_port": dst_port, "seq": seq, "ack": ack, "tcp_flags": flags, "payload": data[payload_start:] } # 使用示例 frame = sock.recvfrom(65535)[0] if frame[12:14] == b"\x08\x00": parsed = parse_ipv4_tcp(frame[14:]) print(parsed)

这里有个细节容易错:struct.unpack("!HHIIB", data[tcp_start:tcp_start + 13])只取了 TCP 头前 13 个字节,因为偏移和标志位恰好在这 13 字节内结束。前 12 字节依次是源端口(2)、目的端口(2)、序列号(4)、确认号(4),第 13 字节就是“数据偏移+保留位+标志位”。先取出这一个字节,再通过移位和掩码拆成 offset 和 flags,比直接解 20 字节清爽得多。flags取值 0x02 是 SYN,0x12 是 SYN+ACK,0x10 是 ACK,0x01 是 FIN。熟悉这套标志位,分析器才能输出“这是握手包”这种直观结论。

4.3 过滤和展示:像 Wireshark 一样能定位问题

分析器不能只打印原始结构体,那样等于把十六进制文本换了一种排列方式。要有基本的过滤能力和可读输出。常见过滤维度是:源 IP 等于某个值、目的端口等于某个值、协议等于 TCP 或 UDP、标志位含 SYN 等。我把过滤条件放到一个字典里,循环抓包过程中逐项匹配。

filters = {"dst_port": 80, "tcp_flags": 0x02} packet_count = 0 while packet_count < 10: data, _ = sock.recvfrom(65535) if data[12:14] != b"\x08\x00": continue info = parse_ipv4_tcp(data[14:]) if not info: continue matched = True for key, value in filters.items(): if info.get(key) != value: matched = False break if matched: packet_count += 1 print( f"[{packet_count}] {info['src_ip']}:{info['src_port']} -> " f"{info['dst_ip']}:{info['dst_port']} " f"flags={info['tcp_flags']:#04x} payload={len(info['payload'])}B" )

打印行里的flags={info['tcp_flags']:#04x}会输出形如0x02的十六进制格式,方便对照标志位表。payload长度能直接看出这个包是纯握手包还是携带了应用数据。过滤条件设立后,分析器变成一台“只关心目标流量”的监视器,课程设计演示时效果比满屏刷包强得多。如果需要更完整,可以把每个解析结果同时以 JSON 格式落盘,方便后续写报告时引用。

5. 协议编辑器与分析器避坑:六个能让你翻车的细节

5.1 校验和字段没有先置零就开算,抓包工具看着都挠头

现象:编辑器生成的包,抓包工具显示 TCP 校验和错误;或者接收方直接把包丢弃,编辑端却以为自己发成功了。

原因:校验和算法规定“将要校验的字段本身先填 0 再求和取反”,如果拿一个随机初值参与计算,算出来的校验和自然不合法。IP 头和 TCP 头有各自的校验和字段,在计算前都必须先清零。TCP 校验和更坑,伪首部里的“长度”若是用了未填充载荷前的值,即使校验和字段清零也会算错。

解决:把“打包 → 清零 → 计算 → 重新打包”四步固定成模板,任何头部类型都按这个顺序走。我写编辑器时专门封装了一个build_with_checksum函数,先构造checksum=0的头部,计算后再调用struct.pack重写一次,彻底杜绝顺序问题。

5.2 捕获到的包长度和实际长度对不上:recvfrom 拿了 65471 字节

现象:分析器recvfrom(65535)返回的数据长度固定是 65535,完全不像一个正常 TCP 包头,解包结果乱成一团。

原因:原始套接字在收到大于缓冲区剩余空间的报文时,会做截断处理。缓冲区给多大就只能收多大,收满 65535 不代表“刚好一个包”,而是“缓冲区满了”。另一个常见原因是网卡开启了 jumbo frame,真实帧大于 1518 字节,而程序只按标准以太网帧长去解析。

解决:把接收缓冲区设为一个帧的最大长度,例如 65535,然后用“以太网帧头的总长度字段”去判断实际帧长。解析时不要盲目信任len(data),要以 IP 头里的总长度字段为准,超出部分视为填充或截断数据。

5.3 Windows 和 Linux 的原始套接字根本不是同一个脾气

现象:同一个分析器脚本,在 Linux 上能抓到包,拿到 Windows 上直接报权限错误;或者在 Windows 上能创建套接字,但recvfrom一直阻塞收不到任何数据。

原因:Windows 的原始套接字限制比 Linux 严格得多,默认不能接收所有流经网卡的数据包,需要用SIO_RCVALL开启混杂模式,而且必须管理员权限运行。Linux 下AF_PACKET可以直接绑定网卡,Windows 则没有这个接口。

解决:课程设计演示统一在 Linux 虚拟机里跑,省去平台适配工作量。代码里在最前面检测操作系统,如果是 Windows 就打印一句明确提示并尝试用SIO_RCVALL,不要让用户面对“卡住不动”的假象。交叉开发时,分析器核心解析逻辑和平台无关,只有抓包入口需要分平台实现。

5.4 Tkinter 抓包循环把 GUI 拖死,界面假死只好杀进程

现象:给分析器加了一个 Tkinter 界面,点“开始抓包”后窗口马上无响应,过一会儿操作系统提示“程序未响应”。

原因:recvfrom是阻塞调用,把它直接放在 Tkinter 主循环里,GUI 事件循环被卡住,窗口自然假死。这是所有网络工具做图形界面时都会踩的经典坑。

解决:抓包循环放进独立线程,用queue.Queue把解析好的数据传回主线程,主线程用after定时从队列取数据刷新控件。只要记住“网络 I/O 不进 UI 线程”这条铁律,界面就不会翻车。篇幅有限,可以用一个最简单的threading.Thread(target=sniff_loop, daemon=True)起步,后续再加停止事件。

5.5 TCP 头的选项字段让固定偏移全部失效

现象:分析器对普通包解析一切正常,一旦抓到带时间戳选项的 TCP 包,解析出的端口、标志位全部错乱。

原因:TCP 头长度由第 12 字节高 4 位决定,取值范围是 5 到 15,对应 20 到 60 字节。很多初版代码直接固定取 20 字节,忽略了选项字段的存在。带选项的包在真实网络上非常常见,尤其是 Linux 默认开启时间戳的情况下。

解决:解析时永远先读offset = (data[tcp_start + 12] >> 4) * 4,再根据 offset 来切 TCP 头和载荷。编辑器在生成带选项的包时,同样要同步修改“数据偏移”字段,不能只在装箱时加选项数据却忘记调整偏移值。

6. 端到端验证:让自己编的包,被自己写的分析器看穿

代码写完不算完,最关键的一步是拿自己写的两个工具互相对话,验证整个链路。最简单的实验是:在本地启动一个监听 8000 端口的服务,用编辑器构造一个 TCP SYN 包发往本机,再用分析器抓回响应包,确认看到 SYN-ACK 或 RST 标志位。SYN 到开放端口回 SYN-ACK,到关闭端口回 RST,这个结果是检验编辑器是否正确的最强证据。

具体操作是:开一个终端跑python -m http.server 8000作为监听服务;另一个终端运行编辑器,填源端口为 50000、目的端口为 8000、标志位 SYN;再用分析器抓本机回环接口。如果分析器在tcp_flags里看到 0x12(SYN+ACK),说明从 IP 头到 TCP 头再到校验和全部正确。如果收到 RST(0x04),说明编辑器的包能被系统识别,只是目标端口没监听,这也是一个有效结果。如果什么都收不到,优先查校验和函数和发送用的套接字类型。

验证通过后,再给分析器加一个小功能让效果更进一步:对每个包重新计算 IP 头和 TCP 头的校验和,跟包内字段比对,一致就打“校验通过”,不一致打“校验失败,期望值与实际值”。这个“自检”功能在课程设计答辩时特别能打,因为视觉上比一排十六进制数字直观得多,也直接说明你理解校验和的完整语义。

我自己的习惯是把编辑器做成“支持从十六进制字符串导入报文再修改”的形态,这样可以把抓到的真实报文丢进去改一个字段,再发出去看网络反应。比如抓到一个正常的 HTTP 请求,把目的端口改成 81,立刻能看到大量 TCP 重传,这种“动手改网络”的体感远胜过只写静态解析。最后想提醒一句:这个项目贵在两块逻辑闭环,不贵在功能多。把 IPv4 加 TCP 的封装、解封、校验、发送跑通,已经抵得上那些堆了十几种协议却个个都是半吊子的做法。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询