☰
TCP数据包构造与解析:滑动窗口、超时重传与抓包验收实战
2026/11/6 6:45:02 网站建设 项目流程

简介:燕山大学计算机网络三级项目的TCP传输数据包实践材料,面向计算机网络课程学习者与网络编程初学者,围绕TCP可靠传输展开,涉及三次握手建立连接、数据分片与重组、滑动窗口流量控制、拥塞控制及四次挥手释放连接等关键机制。压缩包共60个文件、约27.88MB,包含C++源码与头文件、Visual Studio解决方案及工程配置,也有可执行程序、调试数据库与运行日志,便于对照客户端与服务器两端实现,观察连接建立、数据发送确认和异常处理过程。已有1355人学习,适合用作课程实验参考、网络编程练习和协议分析素材。读者可从中梳理TCP通信程序的目录组织,结合源码理解慢启动、快速重传、快速恢复等算法的落地方式,并通过运行结果与日志排查收发顺序、确认重传等问题,提升套接字编程与协议调试能力。

1. 先想清楚「发一个包」到底要交什么

计算机网络三级项目里,TCP 数据包这道题最容易被低估。很多人以为写个 socket 客户端发几行字符串就交差,结果验收时被问一句“你发的这个包首部第 13 个字节是什么”、或者“这条 1460 字节的段为什么序号跳了 2920”,当场卡住。这个项目真正要交的,是一套能自己构造、自己抓、自己解释的 TCP 数据包收发与解析方案:从首部 20 字节逐字段打包,到三次握手、滑动窗口、超时重传,再到抓包时序和状态迁移能一一对上。它适合正在做课程设计、需要一份能跑又能讲的学生,也适合想把传输层从“背八股”变成“看得见”的从业者。下面按需求、选型、实现、验收的顺序,把这套方案完整拆开。

2. 需求拆解与技术选型:四个模块的职责边界

2.1 把「发一个包」拆成四层交付物

课程三级项目通常不会只要求“写个客户端和服务端通信”。按我见过的评分表,得分点基本落在四层交付物上,缺一层就要被扣一档。

层次交付物验收方式常见失分点
报文层手工构造的 TCP 段 + 字段解析器抓包工具能识别、逐字段打印正确校验和错、字节序反、数据偏移写错
连接层三次握手 / 四次挥手状态机状态迁移表与抓包时间线一致只画图不落地、SYN 的 seq 消耗漏算
传输层滑动窗口 + 累积确认 + 超时重传在 10% 丢包下把文件完整传完用固定超时、重传后 RTO 不翻倍
观测层日志、时序图、统计指标能回答“重传了几次、平均 RTT 多少”只 print 收发,没有度量

报文层是地基。TCP 首部就 20 字节固定部分,加上选项最多 60 字节,字段不多,但每个字段都有坑:序号和确认号是 32 位、要按网络字节序;数据偏移以 4 字节为单位,写 20 就错了;标志位里 SYN 和 FIN 各消耗一个序号,算窗口的时候忘了这一条,后面全乱。

连接层考的是你能不能把“状态”讲清楚。很多人握手挥手背得滚瓜烂熟,真让他把 TIME_WAIT 为什么是 2MSL 说清楚,或者让他解释为什么主动关闭方的最后一个 ACK 丢了会重传,就答不上来。项目里我的做法是把状态迁移做成一张表,每一条边对应一个事件和一个动作,代码里就是if/elif配状态变量,表能画出来,代码就能写出来。

传输层是拉开差距的地方。要求一般写成“实现可靠传输”,翻译成人话就是:允许丢包、允许乱序的情况下,接收方拿到的字节流必须和发送方完全一致。这一层要落三样东西:发送窗口、累积确认、超时重传。窗口决定一次能发多少不被确认的数据,累积确认决定接收方怎么回 ACK,超时重传决定丢了之后多久补发。

观测层最容易被忽略,但答辩时最救场。老师问“你怎么证明重传真的发生了”,你翻出一张重传次数随时间变化的日志,比说十句“我实现了”都管用。

2.2 抓包侧与构造侧要分开选型

这是整个方案里最容易翻车的一次决策。很多人上来就选“最硬核”的那条路,结果卡在权限上,两天没跑通第一个包。

三种常见技术路线,特点和风险差别很大:

路线首部可控权限要求代码量主要风险
纯 SOCK_STREAM 传文件 + 抓包分析否无少首部由内核填,“自定义数据包”拿不到分
Scapy 构造 + sniff 抓包是抓包需管理员/root中第三方依赖、性能差
Raw socket 自己组 IP/TCP是必须管理员/root大内核回 RST、跨平台差异大

纯 SOCK_STREAM 那条路只适合“抓包分析”型题目。它的优点是零门槛,socket.socket(AF_INET, SOCK_STREAM)建连、sendall发数据,配上抓包工具就能看到完整的三次握手和滑动窗口。缺点是首部全部由内核填,你想改个窗口大小、想看窗口缩放因子,都只能被动观察。如果题目明确写了“构造 TCP 报文”,这条路直接判为不合格。

Scapy 是性价比最高的选择。构造一个带自定义标志位的段就是一行:

from scapy.all import IP, TCP, send, sniff # 手工构造一个 SYN 段:源端口 40000,目的端口 8080,初始序号取随机值 pkt = IP(dst="127.0.0.1") / TCP(sport=40000, dport=8080, flags="S", seq=1000, window=64240) send(pkt, verbose=0) # 抓本机回环上的 TCP 流量,只留前 20 个包,避免刷屏 pkts = sniff(iface="lo", filter="tcp port 8080", count=20) for p in pkts: print(p[TCP].sport, p[TCP].dport, p[TCP].flags, p[TCP].seq, p[TCP].ack)

flags="S"是 SYN,"SA"是 SYN+ACK,"FA"是 FIN+ACK,这几个简写记住能省很多时间。iface参数在 Linux 上是lo,在 Windows 上要用抓包库给出的网卡名,写死会直接报错。filter用的是 BPF 语法,tcp port 8080比抓全量再过滤效率高一个量级。

Raw socket 那条路只在一种情况下值得走:题目明确要求“不依赖第三方库,从字节开始构造”。走之前先想清楚两件事。Linux 下内核收到一个不属于任何已知连接的 TCP 段,会回一个 RST 把你的连接掐断,得用防火墙规则把出站的 RST 丢掉;Windows 下要开SIO_RCVALL才能收到目的地址不是本机的包,而且管理员权限是硬要求。这两条我都在不同机器上踩过,一旦卡住,排查方向很容易跑偏到代码上。

我的建议是主方案用 Scapy,把 raw socket 做成可选的加分模块,用一个配置开关切换。这样演示环境没权限也能跑,有权限就展示更底层的实现。

2.3 工程结构与可调参数

目录不追求复杂,按“构造 / 抓包 / 传输 / 展示”四块切,每个文件职责单一,答辩时指哪打哪。

文件职责关键函数
packet_builder.py构造 TCP 首部与选项build_tcp_header、build_options
packet_parser.py解析字节流为字段字典parse_tcp_header、decode_flags
checksum.py伪首部与反码求和校验tcp_checksum
capture.py抓包、过滤、落盘 pcapstart_capture
reliable.py窗口、确认、重传、RTOSenderWindow、ReceiverWindow
conn_state.py握手挥手状态机ConnectionFSM
report.py时序图与统计输出plot_timeline、dump_stats
config.py全部可调参数集中管理常量

环境准备用一条命令就够,把依赖钉在版本上,避免换机器跑不起来:

# 建议在虚拟环境里装,抓包库对 Python 版本比较敏感 python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install scapy==2.5.0 matplotlib==3.8.2 # Linux 下抓包需要 capability,或者直接用 sudo 跑 sudo setcap cap_net_raw,cap_net_admin+eip $(readlink -f $(which python))

setcap那条是省事的做法,给解释器本身授了原始套接字权限,之后不用每次sudo。评估机不允许改权限的话,就在capture.py里做降级:抓不到包就回放预先准备好的 pcap 文件,演示流程不受影响。

参数集中在config.py里,这几个值会被反复调:

参数含义建议值调整影响
MSS单段最大载荷1460调小便于观察分段,调大吞吐高但丢包代价大
WINDOW_SIZE接收窗口字节数65535太小跑不满,太大会话多内存
INIT_RTO初始重传超时1.0 s太小误重传,太大恢复慢
LOSS_RATE测试用丢包率0.0 ~ 0.1验证重传是否真的生效
CAPTURE_IFACE抓包网卡回环写错就抓不到任何包

MSS取 1460 是因为以太网 MTU 1500 减去 20 字节 IP 首部和 20 字节 TCP 首部。只在自己机器回环上跑的话没有 MTU 限制,取 536 反而更容易在一屏日志里看清楚分段过程,答辩时更好讲。

3. TCP 报文构造与解析:20 字节首部的每个字段都得对得上

3.1 首部字段布局与打包顺序

固定首部 20 字节,字段顺序不能随意调整,网络字节序(大端)不能忘。这张表贴在代码注释里,比翻书快:

偏移长度字段说明
02源端口发送方端口
22目的端口接收方端口
44序号 seq本报文段数据第一个字节的编号
84确认号 ack期望收到的下一个字节编号
121数据偏移+保留高 4 位是首部长度/4,中 3 位保留
131标志位CWR ECE URG ACK PSH RST SYN FIN
142窗口接收窗口大小,单位字节
162校验和含伪首部
182紧急指针URG 置位时才有意义
20变长选项最多 40 字节,需 4 字节对齐

Python 里用struct一次打包固定部分,格式串!HHIIHHHH对应上表的八个字段,!表示网络字节序:

import struct # 固定首部 20 字节的格式串:端口(2) 端口(2) seq(4) ack(4) 偏移+标志(2) 窗口(2) 校验和(2) 紧急指针(2) TCP_HEADER_FMT = "!HHIIHHHH" TCP_HEADER_LEN = struct.calcsize(TCP_HEADER_FMT) # 应为 20 # 标志位按 bit 定义,方便用按位或组合 FIN, SYN, RST, PSH, ACK, URG, ECE, CWR = 0x001, 0x002, 0x004, 0x008, 0x010, 0x020, 0x040, 0x080 def build_tcp_header(src_port, dst_port, seq, ack, flags, window, checksum=0, urg_ptr=0, data_offset=5): """data_offset 以 4 字节为单位,5 表示 20 字节、不带选项。""" # 高 4 位放数据偏移,低 9 位放标志,中间 3 位保留填 0 offset_flags = ((data_offset & 0x0F) << 12) | (flags & 0x01FF) return struct.pack(TCP_HEADER_FMT, src_port, dst_port, seq & 0xFFFFFFFF, ack & 0xFFFFFFFF, offset_flags, window, checksum, urg_ptr)

seq & 0xFFFFFFFF这个掩码不是可有可无。序号是 32 位无符号数,在 2^32 处回绕,如果不做掩码,算到 43 亿以后struct.pack直接抛异常。窗口字段是 16 位,超过 65535 要记得用窗口缩放选项,否则打包会溢出。

带选项的时候,data_offset要改成实际长度除以 4,并且在尾部补齐到 4 字节边界。MSS 选项长这样:

def build_mss_option(mss=1460): """MSS 选项:kind=2, len=4, 值占 2 字节,总长 4 字节,天然对齐。""" return struct.pack("!BBH", 2, 4, mss) def build_nop_padding(target_len): """选项总长必须是 4 的倍数,不够用 NOP(kind=1) 补齐。""" pad = target_len % 4 return b"\x01" * pad if pad else b""

选项顺序在真实实现里是有讲究的:MSS 一般在 SYN 里发,窗口缩放和时间戳跟在后面。自己造包时顺序不影响解析,但如果你要和真实协议栈互通,顺序和长度都得以对齐为前提,否则对方会因为读不懂选项把包丢掉。

3.2 校验和:伪首部这 12 个字节不能漏

校验和算错的典型症状是:包发出去了,抓包工具显示bad checksum,接收方静默丢弃,你以为是防火墙问题,其实就是一个字节算错。

TCP 校验和覆盖三部分:伪首部 12 字节、TCP 首部、TCP 数据。伪首部不真的传输,只参与计算,包含源 IP、目的 IP、一个保留字节、协议号 6 和 TCP 段总长度。

import socket, struct def tcp_checksum(src_ip, dst_ip, tcp_segment): """tcp_segment 是首部+数据,首部里的校验和字段要先置 0。""" # 伪首部:源 IP(4) 目的 IP(4) 保留(1) 协议(1, TCP=6) TCP 长度(2) pseudo = struct.pack("!4s4sBBH", socket.inet_aton(src_ip), socket.inet_aton(dst_ip), 0, 6, len(tcp_segment)) data = pseudo + tcp_segment # 奇数长度补一个 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 位,可能需要折两次 while total >> 16: total = (total & 0xFFFF) + (total >> 16) return (~total) & 0xFFFF

调用顺序很关键:先build_tcp_header(..., checksum=0)拿到首部,拼上数据算出校验和,再重新打一次首部把校验和填进去。如果先把校验和写进去再算,等于把自己算进去,结果永远不对。

还有一处容易忽略:IPv4 的校验和只覆盖 IP 首部,不含伪首部;TCP 的校验和必须含伪首部。两者算法一样,覆盖范围不一样,混用会得到一个“看起来很像正确值”的错误值。

3.3 反向解析:从字节流还原字段与标志位

构造和解析是一体两面。验收时老师最常做的动作,是甩给你一个 pcap 让你现场解。解析器要能处理三件事:定长首部、变长选项、标志位可读化。

FLAG_NAMES = [(0x100, "NS"), (0x080, "CWR"), (0x040, "ECE"), (0x020, "URG"), (0x010, "ACK"), (0x008, "PSH"), (0x004, "RST"), (0x002, "SYN"), (0x001, "FIN")] def decode_flags(offset_flags): """把 16 位的偏移+标志字段拆成 (首部长度, 标志列表)。""" data_offset = (offset_flags >> 12) & 0x0F bits = offset_flags & 0x01FF names = [name for bit, name in FLAG_NAMES if bits & bit] return data_offset * 4, names def parse_tcp_header(raw): """raw 至少 20 字节,返回字段字典;带选项时一并解出。""" if len(raw) < TCP_HEADER_LEN: raise ValueError("长度不足 20 字节,不是合法的 TCP 首部") (sport, dport, seq, ack, offset_flags, window, checksum, urg) = struct.unpack(TCP_HEADER_FMT, raw[:TCP_HEADER_LEN]) hdr_len, flags = decode_flags(offset_flags) return { "sport": sport, "dport": dport, "seq": seq, "ack": ack, "header_len": hdr_len, "flags": flags, "window": window, "checksum": hex(checksum), "urg_ptr": urg, "options": raw[TCP_HEADER_LEN:hdr_len], # 变长部分 "payload": raw[hdr_len:], # 数据部分 }

header_len必须从包里读,不能写死 20。带时间戳选项的段首部是 32 字节,写死 20 会把 12 字节选项当成数据,后续所有长度计算全错,而且错得很隐蔽——校验和会告诉你不对,但不会告诉你错在哪。

解析结果建议做一次十六进制回显,把它和抓包工具的原始视图并排看。SHA 对不上、校验和不对、字段值诡异,一眼就能发现。这一步花不了多少代码,排查时省下的时间却是成倍的。

4. 可靠传输实现:滑动窗口、超时重传与 RTT 估算

4.1 发送窗口、累积确认与字节流编号

TCP 的可靠性建立在“字节流编号 + 累积确认”上。序号编的是字节不是段,这一点必须先钉死。发 1000 字节、MSS 是 500,就是三个段,序号分别是 base、base+500、base+1000。

发送方维护一个窗口,窗口内是“已发送未确认”和“可发送未发送”两部分:

class SenderWindow: def __init__(self, init_seq, window_size, mss): self.base = init_seq # 最早一个未被确认的字节序号 self.next_seq = init_seq # 下一个要发送的字节序号 self.window_size = window_size self.mss = mss self.buffer = b"" # 待发送数据,按字节流理解 self.sent_at = {} # seq -> 发送时间,用于 RTT 采样 def can_send(self): """窗口未满且还有数据可发。""" return (self.next_seq - self.base) < self.window_size and \ (self.next_seq - self.base) < len(self.buffer) def next_segment(self): """切出下一个不超过 MSS 的段。""" offset = self.next_seq - self.base chunk = self.buffer[offset: offset + self.mss] seg = (self.next_seq, chunk) self.next_seq += len(chunk) return seg def on_ack(self, ack_no): """累积确认:ack_no 之前的所有字节都算收到。""" if ack_no > self.base: self.base = ack_no # 窗口整体右移,之前发过的确认过的段可以从缓存里清掉 self.buffer = self.buffer[ack_no - self.base:] if False else self.buffer

最后那行if False是提醒自己:字节流缓冲的裁剪要用独立的start_offset变量做,不要直接对buffer切片,否则base和next_seq的相对关系会乱。我第一版就是栽在这,跑小文件没事,传 2 MB 之后序号莫名其妙倒退,日志里next_seq < base,非常难查。

累积确认的好处是不用为每个段单独回 ACK,坏处是一个 ACK 丢了可能触发不必要的重传。工程上一般配快速重传:连续收到三个重复 ACK,不等超时直接重发那个缺失段。

def on_dup_ack(self, ack_no): """三次重复 ACK 触发快速重传,阈值 3 是经验值。""" if ack_no == self.base: self.dup_count += 1 if self.dup_count == 3: self.retransmit(self.base) self.dup_count = 0 else: self.dup_count = 0

4.2 RTO 估算:为什么不能用固定超时

固定超时是这类项目里最常见的错误做法。设成 1 秒,回环上跑得好好的,一到有延迟的环境就疯狂误重传,日志里一片重传,看着像丢包率 50%,实际一个包没丢。

正确的做法是动态估算。Jacobson/Karels 算法用一阶差分维护平滑 RTT 和 RTT 偏差:

class RtoEstimator: ALPHA, BETA = 0.125, 0.25 # 标准权重 MIN_RTO, MAX_RTO = 0.2, 60.0 def __init__(self, init_rto=1.0): self.srtt = None self.rttvar = None self.rto = init_rto def sample(self, rtt): """只在没有重传过的段上采样,避免 Karn 算法歧义。""" if self.srtt is None: self.srtt = rtt self.rttvar = rtt / 2.0 else: self.rttvar = (1 - self.BETA) * self.rttvar + self.BETA * abs(self.srtt - rtt) self.srtt = (1 - self.ALPHA) * self.srtt + self.ALPHA * rtt self.rto = min(max(self.srtt + 4 * self.rttvar, self.MIN_RTO), self.MAX_RTO) return self.rto def backoff(self): """超时后指数退避,防止拥塞时连环重传。""" self.rto = min(self.rto * 2, self.MAX_RTO) return self.rto

三个细节决定这个模块能不能用。第一,rttvar初始值取 RTT 的一半,不是 0,否则第一个样本会让 RTO 偏小。第二,重传过的段不能拿来采样 RTT,因为你分不清 ACK 对应的是原包还是重传包,这就是 Karn 算法要解决的问题。第三,MIN_RTO不要设太小,回环环境测出来 0.05 秒的 RTO 换个机器就误重传,0.2 秒是实践中比较稳妥的下限。

超时后backoff把 RTO 翻倍,这一点很多人漏掉。没有退避,拥塞时重传风暴会把链路彻底压死,日志里能明显看到重传次数指数上涨却传不完数据。

4.3 握手与挥手的有限状态机

连接管理建议做成一张显式状态表,代码只是表的翻译。这样答辩时你可以直接指着表讲,而不是对着 if-else 现推。

当前状态事件动作下一状态
CLOSED主动打开发送 SYN,seq=xSYN_SENT
SYN_SENT收到 SYN+ACK发送 ACK,ack=y+1ESTABLISHED
SYN_SENT收到 SYN(同时打开)发送 SYN+ACKSYN_RCVD
SYN_RCVD收到 ACK无ESTABLISHED
ESTABLISHED应用关闭发送 FIN,seq=uFIN_WAIT_1
FIN_WAIT_1收到 ACK无FIN_WAIT_2
FIN_WAIT_2收到 FIN发送 ACKTIME_WAIT
TIME_WAIT等待 2MSL关闭CLOSED
class ConnectionFSM: def __init__(self): self.state = "CLOSED" def on_event(self, event, seg=None): # 表驱动:把 (状态, 事件) 映射到 (动作, 新状态) key = (self.state, event) if key == ("CLOSED", "ACTIVE_OPEN"): self.state = "SYN_SENT" return "send_syn" if key == ("SYN_SENT", "RCV_SYN_ACK"): self.state = "ESTABLISHED" return "send_ack" if key == ("ESTABLISHED", "APP_CLOSE"): self.state = "FIN_WAIT_1" return "send_fin" if key == ("FIN_WAIT_1", "RCV_ACK"): self.state = "FIN_WAIT_2" return None if key == ("FIN_WAIT_2", "RCV_FIN"): self.state = "TIME_WAIT" return "send_ack" return None # 未定义迁移,记录日志而不是静默忽略

两个必须讲清楚的点。SYN 和 FIN 各占一个序号,所以收到 SYN 后回的 ACK 是seq+1,这一步写错,后面所有序号偏移一位,整条流全废。TIME_WAIT 持续 2MSL,是为了让最后一个 ACK 有重传机会,同时让旧连接的重复段在网络里消散,这个理由答辩时能说出来,基本就稳了。最后一个分支返回None而不是抛异常,是为了在演示时能继续跑下去,但这个None要写进日志,否则状态卡死你都不知道是哪一步没收到的。

4.4 丢包注入:怎么证明重传真的生效

没有丢包测试的“可靠传输”是不成立的。在回环上跑一万次都不会丢包,你的重传代码可能一行都没执行过。

import random def maybe_drop(payload, loss_rate): """按概率丢包,返回 None 表示这个段被丢弃。""" if random.random() < loss_rate: print(f"[DROP] len={len(payload)}") # 必须打日志,否则事后无法复盘 return None return payload # 分段测:0% / 5% / 10% / 20%,每档传同一个 1MB 文件并记录完成时间 for rate in (0.0, 0.05, 0.10, 0.20): random.seed(42) # 固定种子,保证结果可复现 run_transfer(loss_rate=rate, file_size=1024 * 1024)

random.seed固定种子这一行看着不起眼,实际很关键。答辩现场重跑一次,如果结果和报告里的不一样,解释起来非常被动。固定种子之后,丢包序列可复现,重传次数、完成时间都能对上。

测试还要覆盖乱序。实现上简单一点,把两个段在发送队列里交换位置就行;接收方靠序号重排,把乱序到达的段放回正确位置再交付。这一步做完,“可靠传输”才算闭环。

5. 避坑与排查:跑不通的时候先看这几处

5.1 抓不到包,先分清是权限问题还是网卡问题

现象:sniff跑了十秒,返回空列表,程序不报错。

原因是两类问题混在一起了。权限不足时,抓包库有的版本抛异常,有的版本静默返回空,很容易误判。网卡名写错同样返回空,两者从结果上看一模一样。

解决分两步走。先用ifconfig或ip addr确认本机有哪些网卡,回环流量必须抓lo,抓eth0是看不到 127.0.0.1 的。再确认权限,Linux 下用setcap给解释器授权,或者临时用sudo跑一次对比结果。如果sudo能抓到、普通用户抓不到,就是权限问题,不用再怀疑代码。

5.2 校验和全 0 或全 F

现象:抓包工具标红显示bad checksum,值是0x0000或0xffff。

原因是计算顺序错了,或者伪首部漏了。把校验和字段先置 0 再算,这个顺序反了就会得到恒定值;伪首部里的协议号写成 17(UDP)而不是 6,也会算出一个“稳定但错误”的值。

解决:写一个最小用例验证。构造一个已知正确的段,把算出来的校验和和抓包工具显示的比,一致了再接其他模块。另外注意奇数长度补的 0 字节只参与求和、不计入长度字段,这两处混了也会出错。

5.3 小文件能传,大文件序号倒退

现象:传 1 KB 完全正常,传 2 MB 之后日志里出现next_seq < base,或者接收端数据错位。

原因有两个。一是缓冲区直接用切片裁剪,base和next_seq的相对偏移没同步更新,累计几轮之后错位。二是序号回绕没处理,2^32 之后从 0 重新开始,用>比较新旧序号会判断反。

解决:缓冲区用起始偏移量管理,不要对原对象做切片赋值;序号比较统一走一个函数,用(a - b) & 0xFFFFFFFF < 2^31判断先后,而不是直接比大小。这个函数写一次,全项目复用,能省掉大量排查。

5.4 握手发出去就被自己掐断

现象:SYN 发出去了,紧接着收到一个 RST,连接建不起来,代码里没有任何地方发 RST。

原因:在 Linux 用原始套接字构造 TCP 段时,内核收到一个不属于任何已知连接的段,按协议规范会回 RST。这个 RST 是内核发的,不是你的程序发的,所以日志里看不到。

解决:加一条防火墙规则把出站 RST 丢掉,只在你指定的端口上生效,别全局屏蔽。同时在程序里过滤入站的 RST,避免误判为对端拒绝。这个问题最坑的地方在于,你以为是对端在拒绝你,实际是自己的机器在拒绝自己。

6. 验收与答辩:把抓包截图讲成一个闭环

验收前一晚,我会强迫自己走一遍顺序固定的自检流程,这套流程比反复检查代码有用得多。

第一步是清理环境重跑。删掉缓存的 pcap、清空日志目录,从零跑一遍完整流程:构造 SYN、握手、传 1 MB 文件、挥手、生成时序图。这一步能暴露九成的“在我机器上能跑”问题,比如依赖没写进依赖文件、临时路径写死、种子没固定。

第二步是对数据。准备一张对照表,把答辩要讲的每个指标和它的来源固定下来:

答辩问题回答依据数据来源
首部字段怎么验证的解析结果与抓包工具逐字段比对packet_parser.py日志
握手几个包、顺序对不对时序图前三条report.py生成的图
重传真的发生了吗丢包率 10% 下重传次数[DROP]与[RETX]日志
RTO 怎么算的平滑 RTT 与偏差的收敛曲线RtoEstimator采样输出
吞吐多少文件大小除以完成时间传输脚本计时

第三步是准备两个失败演示。主动关掉抓包权限,展示降级到 pcap 回放的路径;主动把丢包率调到 20%,展示重传和退避仍在工作。答辩时最怕被问“如果环境跑不起来怎么办”,有这两个预案,问题就变成了加分项。

讲的时候顺序很重要:先说“我这个包要解决什么问题”,再说“首部怎么构造”,最后说“怎么证明它对了”。不要一上来就翻代码,评委看不懂缩进,但看得懂时序图。把时序图和你的状态迁移表并排放,指着说“这条线对应表里这一行”,说服力比念十行代码强。

还有一个小技巧:把 20 字节首部的字段表打印出来贴在屏幕边上,讲的时候用手指着讲。这个动作看着笨,但它让评委确信你知道每个字节在哪,而不是背下来的。

从那以后我每次做这类网络项目,都强制走一遍“清理环境重跑 + 数据对照表 + 两个失败预案”的流程,再没在验收现场翻过车。希望帮到你。

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

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

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

立即咨询