TCP可靠传输原理:从RDT2.2状态机到重传定时器实战
简介这份资源是面向计算机网络课程学习者与实验实践者的TCP可靠数据传输模拟项目聚焦RDT 2.2协议版本帮助理解TCP在不可靠信道上实现可靠传输的核心机制。压缩包共15个文件约1.04MB以Java源码与编译后的class文件为主体辅以txt日志、tcp配置、project与classpath等工程文件构成一个可直接导入IDE运行的完整实验工程。内容围绕RDT 2.0到2.2的演进展开重点解决ACK确认包位错问题涉及累计确认、超时重传与ACK纠错编码等策略通过发送方与接收方的交互模拟呈现序列号、校验和与RTT超时设置等关键设计。已有322人学习适合希望动手实现可靠传输协议、加深对TCP工作原理理解的学生与开发者可据此调试收发流程、观察日志输出并验证重传逻辑。1. TCP-RDT2.2.zip 里藏着的可靠传输逻辑从三次握手到重传定时器很多人第一次看到TCP-RDT2.2.zip这个命名会以为它只是一个普通的课程作业压缩包。但把 RDT 和 TCP 放在一起指向的其实是一个经典问题在不可靠的链路上怎么用一套可验证的机制把数据可靠地送到对端。RDT 是 Reliable Data Transfer 的缩写2.2 通常对应“带超时重传 序号 确认 去重”的那一版状态机。它和真实 TCP 协议栈的关系就像飞行模拟器和真飞机逻辑同源但复杂度差了两个数量级。如果你正在学网络协议、准备做 TCP 调试、或者想自己写一个能跑通的可靠传输 demo这个包值得拆开看。它解决的不是“怎么调通一个 socket”而是“为什么丢包之后连接还能恢复”“为什么重复包不会污染数据”“为什么定时器设短了会雪崩”。适合已经会写基础 TCP 通信、但对重传、确认、窗口这些机制只停留在背诵层面的人。下面我按“先立住模型再落到代码最后讲翻车点”的顺序把这条链路拆开。2. RDT2.2 的状态机到底在解决什么问题2.1 从“停等”到“连续发送”的动机RDT1.0 假设信道完全可靠发出去就完事。RDT2.0 引入差错检测接收方收到坏包要发 NAK发送方重传。但 NAK 本身也可能丢于是 RDT2.1 给每个包加序号接收方只认期望序号。RDT2.2 去掉了 NAK改成对最后一个正确收到的包发 ACK发送方收到重复 ACK 就知道对端没收到新包。这个演进的核心动机只有一个让发送方在不确定对端状态时有一个可判断的依据。停等协议下发送方每发一个包就启动定时器超时没收到 ACK 就重传。序号的作用是让接收方区分“这是新包”还是“这是重传包”。真实 TCP 在此基础上做了三件大事把停等改成滑动窗口、把序号按字节编号、把定时器做成自适应 RTO。但如果你连停等状态机都没手写过直接看 TCP 的拥塞控制只会觉得像玄学。2.2 用 Python 写一个最小可跑的 RDT2.2 发送端下面这段代码模拟发送方逻辑不涉及真实 socket只把状态迁移写清楚。你可以直接复制运行观察每个事件下的动作。# rdt22_sender.py # 模拟 RDT2.2 发送方停等 序号 超时重传 重复 ACK 处理 import time class RDTSender: def __init__(self, timeout1.0): self.seq 0 # 当前发送序号0 或 1 self.timeout timeout # 重传超时时间单位秒 self.timer_start None # 定时器启动时刻 self.waiting_ack False # 是否在等待 ACK def send_packet(self, data): 构造并‘发送’一个带序号的包 packet {seq: self.seq, data: data, checksum: self._checksum(data)} print(f[SEND] seq{self.seq}, data{data}) self.timer_start time.time() self.waiting_ack True return packet def _checksum(self, data): 极简校验字符 ASCII 求和仅用于演示 return sum(ord(c) for c in data) % 256 def on_ack(self, ack_seq): 收到 ACK 后的处理 if not self.waiting_ack: print(f[IGNORE] 收到 ACK{ack_seq}但当前没有待确认包) return if ack_seq self.seq: print(f[OK] ACK{ack_seq} 正确切换到下一个序号) self.seq 1 - self.seq # 0/1 翻转 self.waiting_ack False self.timer_start None else: print(f[DUP] 收到重复 ACK{ack_seq}触发重传) self._retransmit() def check_timeout(self): 在事件循环中周期调用检查是否超时 if self.waiting_ack and self.timer_start: if time.time() - self.timer_start self.timeout: print([TIMEOUT] 超时未收到 ACK重传) self._retransmit() def _retransmit(self): print(f[RETX] 重传 seq{self.seq}) self.timer_start time.time() if __name__ __main__: sender RDTSender(timeout0.5) sender.send_packet(hello) time.sleep(0.2) sender.on_ack(0) # 正确 ACK sender.send_packet(world) time.sleep(0.6) sender.check_timeout() # 模拟超时这段代码里最关键的是seq只有 0 和 1 两个值。RDT2.2 用 1 bit 序号就够因为停等协议同一时刻只有一个未确认包。on_ack里判断ack_seq self.seq才推进否则视为重复 ACK 并重传。check_timeout需要外部循环调用真实实现里通常用select或独立线程做定时器。参数上timeout设得太短会导致无谓重传设得太长会让恢复变慢。真实 TCP 用 RTT 采样动态计算 RTO公式是RTO SRTT 4 * RTTVAR。你在 demo 里可以先固定 0.5 秒等跑通后再换成自适应。2.3 接收端怎么处理重复包和乱序包接收端逻辑比发送端简单但有一个容易写错的点收到重复包时必须重发 ACK而不是丢弃后沉默。因为发送方之所以重传就是因为没收到 ACK如果你沉默发送方会一直重传。# rdt22_receiver.py # 模拟 RDT2.2 接收方期望序号 去重 重复 ACK class RDTReceiver: def __init__(self): self.expected_seq 0 # 期望收到的下一个序号 def receive(self, packet): seq packet[seq] data packet[data] if self._corrupt(packet): print(f[CORRUPT] 包损坏丢弃并重发 ACK{1 - self.expected_seq}) return {ack: 1 - self.expected_seq} if seq self.expected_seq: print(f[DELIVER] 交付数据: {data}) self.expected_seq 1 - self.expected_seq return {ack: seq} else: print(f[DUP] 重复包 seq{seq}重发 ACK{1 - self.expected_seq}) return {ack: 1 - self.expected_seq} def _corrupt(self, packet): 模拟校验失败实际应重新计算 checksum 比对 return False if __name__ __main__: rcv RDTReceiver() rcv.receive({seq: 0, data: hello, checksum: 0}) rcv.receive({seq: 0, data: hello, checksum: 0}) # 重复包 rcv.receive({seq: 1, data: world, checksum: 0})接收端维护expected_seq只有序号匹配才交付并翻转期望值。重复包走 else 分支返回的 ACK 是1 - expected_seq也就是上一个正确包的 ACK。这个细节在真实 TCP 里对应“重复 ACK”机制发送方收到三个重复 ACK 就触发快速重传不用等超时。3. 把 RDT2.2 映射到真实 TCP序号、ACK 和定时器怎么对齐3.1 字节序号 vs 包序号为什么 TCP 不按包编号RDT2.2 的序号是“包序号”一个包一个号。TCP 的序号是“字节序号”假设你发 1000 字节起始序号是 1那下一个包的序号就是 1001。这样做的好处是重传时可以只重传丢失的那一段而不是整个包。比如发送方发了 seq1(500字节)、seq501(500字节)、seq1001(500字节)接收方只收到 1 和 1001那它 ACK 501发送方只需要重传 501 到 1000 这一段。如果按包编号你没法表达“这个包的前半段收到了后半段没收到”。在 Wireshark 里抓包时你会看到Seq和Ack都是大整数那就是字节序号。相对序号可以在 Wireshark 设置里打开显示成 1、501、1001 这种方便对照。3.2 超时重传定时器RTO 怎么算才不会雪崩RDT2.2 的定时器是固定值真实 TCP 必须自适应。核心公式变量含义典型计算方式SRTT平滑 RTTSRTT (1 - α) * SRTT α * RTTα 通常 0.125RTTVARRTT 偏差RTTVAR (1 - β) * RTTVAR β *RTO重传超时RTO SRTT 4 * RTTVAR且不小于 1 秒RFC 6298如果你在 Linux 上调过tcp_retries1和tcp_retries2那两个参数控制的是重传次数上限不是 RTO 本身。RTO 由内核根据 RTT 动态算你可以用ss -i看当前连接的rto值。提示在局域网做 TCP 调试时RTO 通常很小几毫秒到几十毫秒一旦跨公网RTO 会明显变大。如果你自己写可靠传输 demo先固定 RTO 跑通再换成自适应否则连“超时重传”这个动作都观察不到。3.3 用 iperf 和 Wireshark 验证重传行为光看代码不够你得在真实链路上看到重传。下面是一组我常用的命令# 服务端启动 iperf3 iperf3 -s # 客户端发 10 秒 TCP 流带 -V 输出详细日志 iperf3 -c 192.168.1.100 -t 10 -V # 同时用 tcpdump 抓包只抓 TCP 重传相关 tcpdump -i eth0 tcp[tcpflags] tcp-syn ! 0 or tcp[13] 0x04 ! 0 -w retx.pcaptcpdump的过滤表达式里tcp[13] 0x04匹配 RST 标志tcp-syn匹配 SYN。想看重传更直接的方式是在 Wireshark 里用过滤器tcp.analysis.retransmission。如果抓到大量重传先看tcp.analysis.rto字段确认是超时重传还是快速重传。参数说明-t 10表示持续 10 秒-V输出更多统计。tcpdump的-w把包写入文件方便用 Wireshark 打开分析。注意tcpdump需要 root 权限生产环境抓包要控制文件大小加-C 10限制每个文件 10MB。4. 避坑与排查RDT2.2 到 TCP 落地时最容易翻车的 4 个点4.1 序号翻转后 ACK 对不上现象发送方 seq 从 1 翻转到 0 后接收方一直回 ACK1发送方认为 ACK 错误反复重传。原因接收方的expected_seq没有跟着翻转或者翻转逻辑写成了expected_seq (expected_seq 1) % 2但发送方用的是1 - seq两边不一致。解决统一用1 - seq或(seq 1) % 2不要混用。在接收端打印每次expected_seq的变化和发送端日志对照。4.2 重复 ACK 被当成新 ACK 处理现象发送方收到重复 ACK 后没有重传而是直接推进序号导致数据丢失。原因on_ack里只判断了ack_seq self.seq没有处理ack_seq ! self.seq的情况或者把重复 ACK 直接忽略了。解决重复 ACK 必须触发重传。真实 TCP 里连续三个重复 ACK 触发快速重传RDT2.2 里一个重复 ACK 就可以重传因为停等协议没有乱序问题。4.3 定时器没停导致重复重传现象发送方收到 ACK 后定时器还在跑过一会儿又触发一次重传。原因on_ack里没有把timer_start置空或者waiting_ack没有置 False。解决收到正确 ACK 后必须同时做三件事翻转序号、waiting_ack False、timer_start None。少做一件都会导致定时器误触发。4.4 校验和算错导致好包被丢现象接收方一直回重复 ACK发送方一直重传但数据本身没坏。原因发送端和接收端的校验和算法不一致比如一个用 ASCII 求和一个用 CRC或者字节序处理不同。解决校验和算法必须两端完全一致。demo 里可以用最简单的求和但要在注释里写清楚。真实 TCP 用 16 位反码求和你可以用scapy或dpkt库直接算不要手写。5. 进阶把 RDT2.2 扩展成滑动窗口的 3 个关键改动如果你已经把停等版本跑通下一步就是把它改成滑动窗口。这不是简单加个循环有三个地方必须改。第一发送方要维护一个窗口[base, next_seq]base是最早未确认的序号next_seq是下一个要发的序号。窗口大小 N 表示最多允许 N 个未确认包。发送方在next_seq base N时持续发包收到 ACK 后base前移。第二接收方要维护接收窗口可以缓存乱序到达的包。RDT2.2 里接收方只认期望序号滑动窗口里接收方要维护一个缓冲区等缺失的包到达后再按序交付。第三定时器要从“一个包一个定时器”改成“只给最早未确认的包设定时器”。真实 TCP 里只有一个重传定时器超时后重传base指向的包并重启定时器。下面是一个滑动窗口发送方的骨架# sw_sender.py # 滑动窗口发送方骨架窗口大小 N累积 ACK class SlidingWindowSender: def __init__(self, window_size4): self.base 0 self.next_seq 0 self.window_size window_size self.buffer {} # 已发送未确认的包 self.timer_start None def send(self, data_list): while self.next_seq self.base self.window_size and self.next_seq len(data_list): pkt {seq: self.next_seq, data: data_list[self.next_seq]} self.buffer[self.next_seq] pkt print(f[SEND] seq{self.next_seq}) if self.base self.next_seq: self.timer_start time.time() self.next_seq 1 def on_ack(self, ack_seq): 累积 ACKack_seq 之前的所有包都确认 if ack_seq self.base: self.base ack_seq self.buffer {k: v for k, v in self.buffer.items() if k self.base} if self.base self.next_seq: self.timer_start None else: self.timer_start time.time() print(f[ACK] base 前移到 {self.base})这段代码里on_ack处理的是累积 ACKack_seq表示“这个序号之前的所有包都收到了”。真实 TCP 的 ACK 就是累积的所以发送方收到 ACK1001 就知道 1000 之前的所有字节都到了。buffer里只保留未确认的包确认后清理。窗口大小 N 不能无限大否则接收方缓冲区会爆。真实 TCP 用接收窗口rwnd做流控发送方实际能发的数据量是min(cwnd, rwnd)。你在 demo 里可以先固定 N4跑通后再加流控。我自己的习惯是每改一版先用print把base、next_seq、buffer大小打出来确认状态迁移符合预期再上真实网络。直接上 socket 调出了问题你连是逻辑错还是网络错都分不清。希望帮到你。本文还有配套的精品资源点击获取