本文还有配套的精品资源点击获取简介哈尔滨工业大学计算机网络课程2018年实验二配套Python源码完整实现选择重传SR和后退N帧GBN两种可靠数据传输协议。包含独立客户端、服务器及双向通信版本共7个核心文件sr.py、sr_client.py、sr_server.py、gbn.py、gbn_client.py、gbn_server.py、gbn_biconnect.py全部基于标准Socket编程适配课程实验运行环境。代码结构清晰关键逻辑处配有中文注释支持直接运行调试可用于观察ACK机制、超时重传、窗口滑动、乱序处理等协议行为。附带requirements.txt说明依赖项目录中还包含基础工程配置文件.gitignore、.inscode及参考子模块。注意不包含任何实验报告文档原始报告已缺失仅提供可验证的协议实现逻辑适合已有报告框架或需深入理解协议细节的学习者参考使用。1. 项目概述这不是一份“交作业代码”而是一套可触摸的协议教具哈工大计算机网络实验二对很多计网初学者来说是第一次真正把课本上那些抽象的“滑动窗口”“累积确认”“超时重传”从二维图示里拽出来、放到真实字节流中去观察和调试的实战关口。我带过三届本科生助教也自己重写过四轮这套实验——最深的体会是学生卡住的地方从来不是不会写for循环而是不理解为什么窗口大小设为4就刚好能撑住一个RTT为什么GBN在丢包后要回退到base而不是只重传丢失的那个包为什么SR的接收方要缓存乱序到达的帧却不能无限缓存。这份源码包正是为解决这些“为什么”而生的。它不是黑盒脚本而是用PythonSocket搭建的一套透明化协议沙盒你改一行窗口尺寸就能在Wireshark里看到ACK序列立刻变样你手动kill掉server进程几秒再重启就能亲眼看见client如何触发超时、如何重发、如何被新server的旧ACK干扰这就是著名的“重复ACK风暴”问题你故意在gbn_biconnect.py里注释掉某条send()就能复现双向连接中ACK与DATA交织导致的序列号错乱。关键词里的“SR协议”“GBN协议”“可靠传输”“Socket编程”“哈工大计网”每一个都不是标签而是你调试器里正在跳动的变量名、正在打印的日志行、正在抓包分析的TCP流片段。它面向两类人一类是已经搭好报告框架、只缺底层逻辑佐证的同学——你可以直接运行sr_client.py连sr_server.py用nc -u localhost 8080发测试数据看日志里seq5的包丢了之后client是否真的在timeout2.0s后重发了seq5~8另一类是想彻底吃透可靠传输内核的进阶者——你可以打开sr.py盯着self.recv_window这个字典结构看它是如何用{seq_num: data}实现乱序缓存的再对比gbn.py里那个简单的self.base整型变量体会“选择性重传”与“后退N帧”在内存模型上的根本差异。它不提供答案但给你一把显微镜让你亲手拆开协议的齿轮看清每一颗齿牙是如何咬合的。2. 协议设计与实现思路深度拆解2.1 为什么必须区分SR与GBN核心差异不在代码量而在状态机本质很多人初看代码会觉得“不就是改个重传逻辑吗GBN重传base到next_seq-1SR重传所有未确认的抄一遍就行。” 这种想法会直接导致在gbn_biconnect.py里栽跟头。真正的分水岭在于接收方状态管理模型。GBN的接收方极其“懒惰”它只维护一个expected_seq收到任何小于它的包都直接丢弃RFC 2018明确要求收到等于它的才交付上层并递增expected_seq。这种设计让接收方逻辑极简但代价是发送方必须“保守”——一旦窗口内任意一包丢失后续所有包都得重传哪怕它们早已安全抵达。SR则相反接收方是个“积极分子”它维护一个recv_window字典只要收到的seq在[rcv_base, rcv_base window_size)范围内就无条件缓存无论顺序并立即发送对应ACK。这带来了两个硬性约束第一接收方必须能区分“新ACK”和“重复ACK”所以每个ACK必须携带当前rcv_base第二发送方必须能处理“非累积”的ACK比如收到seq7的ACK但seq5还没到就不能移动send_base。翻看sr.py里的handle_ack()函数你会发现它不是简单地send_base ack_seq 1而是遍历所有已发送但未确认的包检查ack_seq是否覆盖了某个包的seq然后逐个标记为已确认——这才是SR“选择性”的灵魂。而gbn.py里对应的逻辑一行if ack_seq self.base: self.base ack_seq 1就搞定。这种状态机差异直接决定了双向连接的复杂度GBN双向通信时双方的base和expected_seq必须严格隔离否则ACK会互相污染SR则还要额外处理recv_window的跨连接同步问题。所以gbn_biconnect.py不是gbn_client.py和gbn_server.py的简单拼接而是重构了整个事件循环用独立线程分别处理收/发并为每个方向维护独立的base和expected_seq实例。这解释了为什么目录里有7个文件而非4个——多出的gbn_biconnect.py和sr.py/gbn.py这两个核心协议引擎才是理解哈工大实验设计意图的关键。2.2 Socket选型为什么坚持UDP而非TCP这不是偷懒而是教学必需看到sr_client.py里socket.socket(socket.AF_INET, socket.SOCK_DGRAM)这行新手常疑惑“老师讲可靠传输我们却用不可靠的UDP这不是自相矛盾吗” 这恰恰是哈工大实验最精妙的教学设计。TCP本身就是一个巨复杂的GBN/SR混合体带SACK的TCP更接近SR如果你直接用TCP写客户端那所谓的“实现可靠传输”就成了在轮子上再造轮子。UDP提供了干净的“字节管道”它保证单个报文的完整性IP层校验和但不保证送达、不保证顺序、不保证不重复——这正是可靠传输协议需要解决的全部问题域。用UDP你才能亲手实现-超时机制在sr_client.py的send_packet()里self.sock.settimeout(self.timeout)设置阻塞超时try...except socket.timeout:捕获后触发重传-序列号与ACK每个发送的Packet对象都带seq_num字段接收方解析后构造ACKPacket(seq_numreceived_seq)回传-窗口管理self.send_base和self.next_seq_num的更新逻辑完全由你控制不受操作系统内核干扰。如果换成TCPsend()调用成功只代表数据进了内核发送缓冲区你永远看不到“丢包”recv()返回的数据已经是按序重组好的你永远看不到“乱序”。这份代码的价值就在于它把网络协议栈里被层层封装的“不可靠性”赤裸地暴露给你——你必须直面IP层的丢包、链路层的误码、路由器的拥塞。这也是为什么requirements.txt里只有pycryptodome用于可选的校验和加密而没有其他网络库它刻意保持在POSIX Socket API的最底层确保你调试时看到的每一个sendto()和recvfrom()调用都对应着真实网络中的一次物理帧发送或接收。2.3 双向连接的设计陷阱为什么gbn_biconnect.py比单向版本难十倍单向传输client发→server收的逻辑是线性的client发包→server收包→server回ACK→client收ACK→更新窗口。双向连接client↔server互发数据瞬间引入三个维度的并发冲突1.序列号空间冲突client发给server的包用一套seq0,1,2…server发给client的包必须用另一套独立seq0,1,2…否则ACK会混淆。gbn_biconnect.py里为此定义了ClientSender和ServerSender两个独立类各自维护self.base和self.next_seq_num2.ACK语义歧义当client收到一个ACK它必须立刻判断这是对自己发出的DATA包的确认还是server发来的、针对server自己DATA包的ACK即“伪ACK”。解决方案是在每个ACK包里增加direction字段如C2S表示这是client→server方向的ACKhandle_incoming_ack()函数首先解析此字段再路由到对应sender实例3.超时定时器竞争client既要为自己的DATA包启动超时器又要为server的DATA包启动另一个超时器。gbn_biconnect.py采用单线程事件循环时间戳队列每次select()等待前计算所有待超时事件的最小剩余时间避免多线程锁开销。这些设计在gbn_biconnect.py的run()方法里体现得淋漓尽致它用while True:主循环内嵌select([sock], [], [], timeout)做I/O多路复用收到数据后先parse_packet()识别类型DATA/ACK/CONTROL再根据packet.direction分发给self.client_sender.handle_ack()或self.server_sender.handle_ack()。这种结构看似复杂但正是工业级协议栈如QUIC的简化雏形——它强迫你思考当网络不再是单向流水线而是一个多向交织的图谱时“可靠”二字该如何重新定义。3. 核心模块解析与实操要点3.1sr.py与gbn.py协议引擎的骨架与血肉这两个文件是整个项目的“心脏”它们不处理网络I/O只专注协议逻辑。以sr.py为例其核心是SelectiveRepeat类包含四个关键属性-self.window_size: 发送窗口大小直接影响吞吐量与内存占用-self.send_base: 当前窗口左边界即最早未确认的包的seq-self.next_seq_num: 下一个待分配的seq即窗口右边界1-self.sent_packets: 字典{seq_num: (packet, timestamp)}缓存所有已发未确认的包及发送时间用于超时检测。send_packet()方法的逻辑链条极为清晰1. 检查self.next_seq_num - self.send_base self.window_size确保不越界2. 构造Packet(seq_numself.next_seq_num, datadata, checksumcalc_checksum(data))3. 将包存入self.sent_packets[self.next_seq_num] (packet, time.time())4. 调用外部send_func(packet)由client/server注入实际发送5.self.next_seq_num 1。最关键的handle_ack()方法则展示了SR的精髓def handle_ack(self, ack_seq): # SR的ACK是“选择性”的可能确认任意一个已发包 if ack_seq in self.sent_packets: del self.sent_packets[ack_seq] # 移除已确认包 # 检查是否能向前滑动send_base while self.send_base in self.sent_packets: self.send_base 1 # 注意这里不是ack_seq1而是检查连续确认这段代码揭示了一个反直觉事实SR的send_base不是由ACK直接驱动的而是由sent_packets字典的连续性决定的。只有当send_base对应的包被确认且send_base1也在字典中意味着它已发但未确认send_base才不会移动一旦send_base被确认且send_base1不在字典中说明它还没发或已超时重发send_base就会跳到下一个存在的key。这与GBN中self.base max(self.base, ack_seq 1)的累积式更新形成鲜明对比。实操时你可以在sr_client.py的main()函数里加一行print(fACK received: {ack_seq}, send_base now: {sr.send_base})然后故意断开server观察send_base如何在超时重传后停滞不前——这就是SR“选择性”的代价它不强制要求ACK连续因此发送方无法仅凭单个ACK就推进窗口。3.2sr_client.py与sr_server.pySocket层的胶水与调试接口如果说sr.py是大脑那么sr_client.py就是手和脚。它的核心在于将协议引擎与网络世界连接起来。sr_client.py的main()函数流程如下1. 创建UDP socket并绑定端口2. 初始化SelectiveRepeat实例传入window_size4,timeout2.0等参数3. 启动一个独立线程start_receiver_thread()持续recvfrom(1024)监听ACK4. 主线程循环读取用户输入或文件调用sr.send_packet(data)发送5. 在start_receiver_thread()里每收到一个ACK就调用sr.handle_ack(ack_seq)更新状态。这里有个极易忽略的调试技巧在start_receiver_thread()的recvfrom()后立即打印原始字节流。例如data, addr sock.recvfrom(1024) print(f[DEBUG] Raw ACK bytes: {data.hex()}) # 输出类似 0000000500000000这样你能看到ACK包的实际结构前4字节是seq_num小端序后4字节是checksum。当你发现client收不到ACK时第一反应不该是查逻辑而是用这条print确认server是否真的发出了ACK发出的ACK的seq_num是否正确checksum是否为0表示server没计算校验和这种底层字节级的验证是绕过所有高级抽象直击问题根源的最快路径。同理sr_server.py的handle_data_packet()里在deliver_to_app_layer(packet.data)前加print(fServer received seq{packet.seq_num}, len{len(packet.data)})能让你实时看到乱序包的到来顺序验证recv_window是否真的在缓存它们。3.3gbn_biconnect.py双向通信的完整工作流实录这是全项目最值得逐行精读的文件。它实现了client与server之间真正的“对话”。其run()方法是事件驱动的典范def run(self): while self.running: # 计算所有待处理事件的最小超时时间 timeout self.get_min_timeout() # 使用select等待I/O或超时 ready, _, _ select.select([self.sock], [], [], timeout) if ready: # 有数据到达 data, addr self.sock.recvfrom(1024) packet parse_packet(data) if packet.type DATA: self.handle_data_packet(packet, addr) elif packet.type ACK: self.handle_ack_packet(packet) else: # 超时事件发生 self.handle_timeout_events()handle_data_packet()的处理逻辑揭示了双向连接的核心- 若packet.direction C2Sclient to server则调用self.server_sender.handle_ack(packet.ack_seq)更新server的发送窗口- 若packet.direction S2Cserver to client则调用self.client_sender.handle_ack(packet.ack_seq)更新client的发送窗口- 同时它会为该DATA包生成一个ACK并设置ack.direction packet.direction即C2S的DATA回C2S的ACK确保ACK语义闭环。实操中你可以用netcat模拟异常在client运行时执行echo corrupt | nc -u 127.0.0.1 8081向server端口发垃圾数据观察gbn_biconnect.py的parse_packet()如何因checksum失败而丢弃该包并触发self.server_sender.resend_from_base()——这就是协议鲁棒性的现场演示。注意此时client的发送窗口不受影响因为它只关心自己发出的DATA包的ACK这种隔离性正是双向连接稳定的基础。4. 实操过程与核心环节实现4.1 环境准备与一键运行指南无需复杂配置这套代码专为哈工大实验环境优化。实操步骤极度精简1.克隆仓库并进入目录bash git clone https://github.com/xxx/HIT-CNet-Lab2.git cd HIT-CNet-Lab22.安装依赖仅需标准库requirements.txt里只有一行pycryptodome3.9.9用于可选的校验和计算。若只需基础功能甚至可以删除该依赖将calc_checksum()函数改为return 0此时校验和失效但协议逻辑仍可运行。推荐安装bash pip install pycryptodome3.启动服务端任选一种- GBN单向python gbn_server.py --port 8080- SR单向python sr_server.py --port 8081- GBN双向python gbn_biconnect.py --client_port 8080 --server_port 80814.启动客户端对应服务端- 连GBN服务端python gbn_client.py --host 127.0.0.1 --port 8080- 连SR服务端python sr_client.py --host 127.0.0.1 --port 8081- 连GBN双向python gbn_client.py --host 127.0.0.1 --port 8080此时client既发又收提示所有脚本均支持--help查看参数。--window_size 4可动态调整窗口--timeout 1.5可修改超时时间。建议首次运行用默认值熟悉后再调整。4.2 关键行为观测实验用三分钟验证一个核心概念不要急于跑通整个流程先做三个微型实验亲手验证协议本质实验一验证GBN的“后退”特性1. 启动gbn_server.py --port 80802. 启动gbn_client.py --port 8080输入hello world3. 在server终端当看到Received packet seq0后立即按CtrlC终止server4. 等待约2秒默认timeout2.0client会打印Timeout! Resending from base0并重发seq0~35. 此时重启serverpython gbn_server.py --port 80806. 观察server日志它会收到seq0交付seq1丢弃因为expected_seq1seq2丢弃seq3丢弃——这就是GBN的“后退”client重发了整个窗口但server只收第一个其余全丢。实验二验证SR的“选择性”与“缓存”1. 启动sr_server.py --port 80812. 启动sr_client.py --port 8081输入Aseq0、Bseq1、Cseq2、Dseq33. 在server收到Aseq0后手动删除server进程4. client会超时重传seq0但此时你快速重启server5. server先收到seq0交付再收到seq2缓存因为rcv_base1再收到seq1交付并触发交付seq2最后收到seq3缓存——recv_window字典里始终只有{2: bC, 3: bD}直到seq1到达才清空。实验三验证双向连接的ACK隔离1. 启动gbn_biconnect.py --client_port 8080 --server_port 80812. 启动两个clientpython gbn_client.py --port 8080作为client Apython gbn_client.py --port 8081作为client B3. A发HelloB发World4. 观察A的log它只处理来自端口8081的ACK即server对A的DATA的确认完全忽略B发来的、目标端口8080的ACK那是server对B的DATA的确认——direction字段在此刻成为防火墙。4.3 日志与调试技巧让协议“开口说话”代码里预埋了大量调试钩子善用它们事半功倍-全局日志开关所有.py文件顶部都有DEBUG True常量设为False可关闭所有print()-分层日志sr_client.py里有CLIENT_LOG、SERVER_LOG、PROTOCOL_LOG三级标识可在print()前加if DEBUG and CLIENT_LOG:精确控制输出-Wireshark联动启动wireshark -f udp port 8080 or udp port 8081过滤出你的流量对照代码里的Packet结构seq_num占4字节data紧随其后用Tools → Protocol Preferences → UDP设置解码规则让Wireshark直接显示seq_num字段-时间戳锚点在send_packet()和handle_ack()里加入time.time()打印计算端到端延迟recv_time - send_time你会看到GBN的延迟波动远大于SR——因为GBN的“后退”导致有效吞吐率下降。注意gbn_biconnect.py的handle_timeout_events()里有一段被注释的print(fResending from base{self.client_sender.base}...)取消注释后你能清晰看到双向连接中client和server各自的重传事件如何交错发生这是理解并发协议行为的黄金视角。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象可能原因排查命令/操作解决方案client一直打印Timeout! Resending...server完全无日志client未正确发送或server端口错误netstat -an \| grep :8080确认server是否监听tcpdump -i lo udp port 8080确认是否有包发出检查client的--host是否为127.0.0.1非localhost避免IPv6解析确认server的--port与client的--port一致server收到包但不打印Received packetchecksum校验失败在server.py的handle_data_packet()里print(fRaw data: {data})后加print(fCalculated checksum: {calc_checksum(data[8:])})检查calc_checksum()是否被正确调用若用pycryptodome确认data[8:]切片是否准确前8字节为seqchecksum双向连接中client A发的数据client B收到了ACKdirection字段未正确设置或解析在gbn_biconnect.py的handle_ack_packet()里print(fACK direction: {packet.direction}, expected: {expected_dir})确认packet.direction在parse_packet()中是否从固定偏移读取检查client A和B是否使用了不同端口避免ACK路由错误修改window_size8后client崩溃或server丢包严重窗口过大导致UDP分片或buffer溢出ip link show查看MTUss -m \| grep udp查看socket buffer大小将window_size限制在min(8, (MTU-28)//(data_size8))其中28为IPUDP头8为seqchecksum5.2 我踩过的五个坑与独家修复技巧坑一UDP的“原子性”幻觉新手常以为sendto()发100字节recvfrom()一定收100字节。实际上UDP报文最大65507字节但链路MTU通常为1500超过则IP层分片。若任一分片丢失整个UDP报文就被丢弃。我在sr_client.py里曾用window_size16发大数据块结果大量超时——tcpdump显示server只收到部分分片。修复技巧在send_packet()前强制限制len(data) 1400并在注释里写明“避免IP分片确保单个UDP报文可达”。坑二时间精度陷阱time.time()返回浮点秒在毫秒级超时场景下误差显著。gbn_client.py里timeout0.5时time.time() - start_time timeout常因浮点误差提前触发。修复技巧改用time.perf_counter()它提供单调递增的高精度计时器start time.perf_counter(); ... if time.perf_counter() - start timeout:。坑三字节序的隐形杀手struct.pack(!I, seq_num)用网络字节序大端但若server用struct.unpack(I, data[:4])默认主机字节序在x86机器上就会错读seq。我在gbn_biconnect.py调试时client发seq1server解析成16777216。修复技巧统一用!I网络字节序struct.unpack(!I, data[:4])[0]并在Packet类文档字符串里强调“所有整数字段均为网络字节序”。坑四多线程资源竞争sr_client.py的receiver线程与主线程共享self.sr实例若主线程调用send_packet()同时receiver调用handle_ack()可能破坏sent_packets字典。修复技巧在SelectiveRepeat类中添加self._lock threading.Lock()所有修改sent_packets或send_base的操作前加with self._lock:。坑五双向连接的“心跳”缺失gbn_biconnect.py在长时间无数据时双方窗口会停滞。若client发完数据就静默server的base永远卡住。修复技巧在run()循环末尾添加心跳逻辑if time.time() - last_activity 30: self.send_heartbeat()发送一个typeHEARTBEAT的空包维持连接活性。6. 协议扩展与工程化思考6.1 从实验代码到工业级协议的鸿沟与桥梁这套代码是完美的教学载体但它离生产环境还有三道坎-拥塞控制缺失实验假设带宽无限而真实网络需实现慢启动、拥塞避免如TCP的cwnd机制。可在handle_ack()中加入self.cwnd min(self.cwnd 1, self.ssthresh)当连续收到3个重复ACK时触发快重传-安全性空白UDP无加密数据明文传输。requirements.txt中的pycryptodome已预留接口可扩展encrypt_payload()和decrypt_payload()用AES-GCM实现认证加密-连接管理粗放当前无握手/挥手gbn_biconnect.py靠进程启停模拟连接。真正的双向协议需三次握手SYN, SYN-ACK, ACK建立连接四次挥手FIN, ACK, FIN, ACK释放资源。这些扩展并非画蛇添足。我在某物联网项目中正是基于类似的SR原型增加了LWM2M协议的CoAP over UDP封装用gbn_biconnect.py的双向框架支撑设备与云平台的指令下发与状态上报。关键在于教学代码的价值不在于它多完美而在于它多“可生长”——它的模块化设计协议引擎与I/O分离、清晰的状态变量send_base, next_seq_num、标准化的Packet结构为所有扩展提供了稳固的基座。6.2 给哈工大学弟学妹的终极建议别把这份代码当答案抄把它当X光片看。我的建议是1.先删后写拿sr_client.py删掉所有sr.send_packet()调用自己重写一个极简版my_send()只处理seq_num分配和sendto再逐步加入超时、ACK处理2.逆向工程日志关掉所有DEBUG只留server的print(Delivered:, data)然后用nc -u 127.0.0.1 8080发hello观察输出。若没输出证明client根本没发出去——这时再开DEBUG一层层往上查3.用Wireshark代替脑补与其猜“ACK是不是发错了”不如直接看Wireshark里udp.dstport8080 udp.length12的包数seq_num是否连续checksum是否为04.接受“不完美”实验报告里写“在window_size6时吞吐量达到峰值但丢包率上升至12%”这比编造一个“100%可靠”的假数据更有价值——真实网络本就如此。最后分享一个小技巧在sr.py的__init__()里把self.window_size设为一个property添加setter方法property def window_size(self): return self._window_size window_size.setter def window_size(self, value): print(f[INFO] Window size changed to {value}) self._window_size value这样当你在client里动态调整窗口时控制台会实时告诉你协议状态正在变化——这小小的print就是你与协议对话的第一句问候。本文还有配套的精品资源点击获取简介哈尔滨工业大学计算机网络课程2018年实验二配套Python源码完整实现选择重传SR和后退N帧GBN两种可靠数据传输协议。包含独立客户端、服务器及双向通信版本共7个核心文件sr.py、sr_client.py、sr_server.py、gbn.py、gbn_client.py、gbn_server.py、gbn_biconnect.py全部基于标准Socket编程适配课程实验运行环境。代码结构清晰关键逻辑处配有中文注释支持直接运行调试可用于观察ACK机制、超时重传、窗口滑动、乱序处理等协议行为。附带requirements.txt说明依赖项目录中还包含基础工程配置文件.gitignore、.inscode及参考子模块。注意不包含任何实验报告文档原始报告已缺失仅提供可验证的协议实现逻辑适合已有报告框架或需深入理解协议细节的学习者参考使用。本文还有配套的精品资源点击获取