简介面向中南大学计算机网络课程实验这份资源收录了2022年A1与A3两个实验的完整源代码适合正在学习Socket编程、TCP/UDP通信及网络协议实现的同学对照实践。压缩包共53个文件以C语言源码.c/.h、编译生成的.o目标文件为主另含Python脚本、Makefile构建配置、实验指导书docx和运行结果截图整体大小约1.06MB目录按A1、A3实验分组便于快速定位与筛选。目前已有743人学习下载。从A1的客户端/服务器连接与数据收发到A3的HTTP等协议报文构建及交互模拟这套代码覆盖了从传输层到应用层的核心知识点并附带编译脚本、报告文本和运行截图可作为课程设计、期末复习与实际网络编程调试的参考范例。1. 为什么“中南大学计算机网络实验源代码”成了期末季的硬通货每到期末中南大学计算机网络实验源代码就会成为搜索热词很多人其实不是想做研究而是想找一份能直接交差、能跑通、能应付答辩的参考实现。这门课通常搭配谢希仁《计算机网络》第八版教材实验方向绕不开 Socket 编程、可靠传输协议、路由算法和 HTTP 抓包分析。你搜到的资料里十个压缩包有八个是残缺工程要么缺头文件要么目录里只有一份写了一半的报告。这篇文章不承诺给你一个现成的压缩包而是按我实际做这套实验的思路教你怎么拿到任何一份“实验源码”后判断它能不能跑、怎么改参数、怎么让代码和实验报告自洽。适合还在补实验的学生也适合想用最短路径把教材里的协议栈落到本机的工程师。2. 先把实验框架搭起来从谢希仁第八版里拆出典型实验点2.1 计算机网络实验的四个高频方向与选型理由中南大学计算机网络的实验题很少会要求你做一个完整的应用层项目。根据过往公开的实验任务和课程大纲核心考察点基本是四个方向实验方向教材对应章节常见验收要求Socket 编程TCP/UDP第 5 章 运输层实现一个简单的回显服务能收发文件可靠传输协议停等 / GBN第 5 章 可靠传输原理模拟丢包、超时重传打印状态距离向量路由算法第 4 章 网络层实现 Bellman-Ford 路由表更新HTTP 与抓包分析第 6 章 应用层用 Wireshark 抓包分析三次握手和报文选型上我见过用 C 写的也见过用 Python 写的。C 语言的好处是能和教材里的伪代码一一对应指针、结构体都能用上适合本科阶段训练Python 的好处是开发速度快实验报告里可以直接贴代码片段答辩时容易讲清楚状态机。如果你时间紧我建议选 Python 版本因为写停等协议和滑动窗口时Python 的动态类型能少碰很多内存越界问题。但要注意中南大学的实验环境可能是 Windows 机房也可能让你用 Linux选型之前先确认代码要在哪个环境跑。另一个关键选择是是否用现成库。Socket 实验用标准库别引第三方框架。有一个常被忽略的“雷区”很多学生喜欢用select或epoll做高并发觉得这样能加分但实验要求里往往要求你手工处理阻塞收发你一旦引入事件循环反而不符合题目的“模拟丢包”场景。所以我一般建议先跑通一个阻塞式的版本再考虑要不要优化。2.2 拿到一份“实验源代码”后先看入口文件目录结构和编译环境你在各种渠道找到的源码包名字可能叫csu_net_exp或者TCP实验但真正决定能不能跑的不是文件名是目录结构。我拿到任何一份实验源码第一件事不是打开主函数而是看三处README、Makefile或CMakeLists.txt、main.c或server.py。这三处能告诉你这份代码的目标平台、编译方式和入口。一个常见的 Linux 下 C 语言实验工程目录结构大概是. ├── README.md ├── Makefile ├── src │ ├── main.c │ ├── socket_util.c │ └── socket_util.h ├── report │ └── 实验报告模板.doc └── test └── test_client.py如果你拿到的是这么完整的东西先跑一遍make看有没有编译错误。很多时候你遇到的是缺少-lm链接数学库这类小问题不要急着改代码先看 Makefile 里的CFLAGS和LDLIBS。例如一个典型的 MakefileCC gcc CFLAGS -Wall -g -O2 LDLIBS -lpthread all: server client server: src/server.o $(CC) $(CFLAGS) -o server src/server.o $(LDLIBS) src/server.o: src/server.c src/socket_util.h $(CC) $(CFLAGS) -c src/server.c -o src/server.o clean: rm -f server client src/*.o这段Makefile的要点是-Wall打开警告-g生成调试信息-O2做优化。对实验代码来说-g比-O2重要因为你要用 GDB 调试分段错误优化反而会把变量优化掉让你在print时看到莫名其妙的值。如果编译报错说pthread_atfork未定义多半是忘记加-lpthread这时候只需要在LDLIBS里补上不必改源码逻辑。如果你的源码是 Python那更简单看requirements.txt和__main__.py。很多实验源码其实不需要第三方库只用socket和threading但有人会多写一个requirements.txt里面列了numpy其实代码里没用这时候直接忽略它省得装出一个环境差异。2.3 用最小命令跑通一个 TCP Socket 实验不管你的实际实验题目是什么TCP Socket 回显是最常见的起点。我建议你先把下面这个最小服务端跑起来再替换成你手头的源码。# tcp_echo_server.py import socket def main(): # AF_INET 表示 IPv4SOCK_STREAM 表示 TCP server socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SO_REUSEADDR 允许 TIME_WAIT 状态下快速重起端口 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((0.0.0.0, 8888)) server.listen(1) print(listening on 8888) conn, addr server.accept() with conn: print(connected from, addr) while True: data conn.recv(1024) if not data: break conn.sendall(data) # 原样返回 if __name__ __main__: main()这段代码的核心逻辑是bind绑定到0.0.0.0意思是监听本机所有网卡地址而不是只有127.0.0.1recv(1024)每次最多读 1024 字节但 TCP 是流协议一次recv返回的未必是完整的一个报文所以后面要用循环处理。sendall和send不同sendall会一直发送完所有字节才返回避免你只发了一半就退出。对应的最小测试命令不需要写客户端代码直接用nc或telnetpython3 tcp_echo_server.py # 另一个终端 echo hello | nc 127.0.0.1 8888如果你能看到终端回显hello说明服务端和客户端的 Socket 链路是通的。这一步要特别注意nc的版本有些系统自带的是netcat-openbsd它连接建立后会把 EOF 也发出去导致服务端recv返回b然后退出这是正常的不是 bug。关于端口参数实验里经常会让客户端传入一个任意端口。常见做法是把端口写在命令行参数里而不是写死。你可以在代码里这样处理import sys port int(sys.argv[1]) if len(sys.argv) 1 else 8888这样你做验收时能快速换端口测试避免上一个实验残留的TIME_WAIT连接占用端口。记住端口号范围是 1 到 65535但通常实验环境只允许用大于 1024 的端口否则没有 root 权限会Permission denied。3. 可靠传输协议实验停等协议与滑动窗口的代码实现要点3.1 停等协议的状态机与超时重传可靠传输协议往往是中南大学实验里最让人头疼的一项因为它不是单纯写一个网络程序而是要你模拟“不可靠信道下的可靠传输”。最基础的是停等协议发送方发一个包然后停下来等确认收到确认再发下一个。这个协议的状态机只有四个状态发送、等待确认、收到确认、超时重传。下面我用 Python 写一个模拟发送方的最小实现重点在状态转移# stop_and_wait.py import time import random TIMEOUT 1.0 # 超时值单位秒 MAX_RETRY 3 # 最大重传次数 def send_packet(seq, fake_loss_rate0.2): 模拟发送数据包返回是否发送成功这里丢包用随机数模拟 if random.random() fake_loss_rate: return False # 模拟丢包 return True def send_stop_and_wait(data): send_base 0 retry 0 while send_base len(data): # 状态发送 if send_packet(send_base): print(fsend packet seq{send_base}) # 状态等待确认 time.sleep(0.01) # 假设RTT很短 # 这里用timer模拟确认超时 start time.time() # 实际实验里线程会异步等待ACK # 这里简化如果在TIMEOUT内没收到判超时 time.sleep(0.2) # 假装等待确认 if time.time() - start TIMEOUT: print(ftimeout, retransmit seq{send_base}) retry 1 if retry MAX_RETRY: raise Exception(too many retries) continue send_base 1 retry 0 else: print(fpacket lost, retransmit seq{send_base}) print(all packets sent) if __name__ __main__: send_stop_and_wait(hello)这个实现的逻辑说明send_base是当前发送的序号只有在收到确认后才加一否则一直重传同一个包。fake_loss_rate是模拟丢包率你可以调整这个参数观察吞吐变化。TIMEOUT是最关键的超时值如果设得太小原本没丢包也会超时重传设得太大遇到真丢包时信道利用率暴跌。实际实验里你需要把time.sleep(0.2)换成真实的socket等待并用select或timer来实现超时。参数上我给出的TIMEOUT 1.0在模拟环境里够用但真实局域网 RTT 可能只有几毫秒你应该按RTO 2 * 平均RTT的经验值来设而不是固定写死。还有MAX_RETRY实验报告里一般要求你解释为什么连续三次超时后要放弃这对应教材里的“重传次数有限”这一概念。3.2 滑动窗口的发送缓冲区与接收缓冲区设计停等协议的效率太低所以实验进阶题会要求你实现滑动窗口典型的是 Go-Back-N 或选择性重传。这里的难点不是协议算法本身而是发送缓冲区和接收缓冲区的数据结构设计。很多学弟学妹写出来的代码窗口一变大就数组越界原因是用了一个固定数组但没有做环形处理。发送方需要维护三个指针base窗口基址、next_seq下一个待发序号和last_seq窗口上界。窗口大小W必须满足W 序号空间的一半否则接收方无法区分新包和重传包。下面是核心的发送逻辑// sliding_window.c #define WINDOW_SIZE 4 #define SEQ_MOD 8 // 序号空间取2的幂 int base 0; int next_seq 0; void send_frame(int seq, char *data) { // 实际编码里在这里添加校验和然后通过模拟信道发送 printf(send frame seq%d\n, seq); } void handle_ack(int ack_num) { if (ack_num base ack_num base WINDOW_SIZE) { // 累计确认所有小于等于ack_num的帧都被确认 base (ack_num 1) % SEQ_MOD; printf(ack %d, new base%d\n, ack_num, base); } else { printf(invalid ack range\n); } } void send_loop(char *data, int len) { while (base ! (next_seq - 1) % SEQ_MOD || next_seq len) { // 判断窗口是否已满 if (next_seq base WINDOW_SIZE next_seq len) { send_frame(next_seq, data next_seq); next_seq; } // 这里应该有接收ACK、超时重传的逻辑 // 实际代码需要放在一个线程或事件循环中 break; // 示意完整实现需要循环 } }这段代码的WINDOW_SIZE 4SEQ_MOD 8正好满足“发送窗口不能超过序号空间一半”的条件。base和next_seq都在[0, SEQ_MOD-1]区间内循环所以要用模运算。很多坑都出在base WINDOW_SIZE上如果序号超过SEQ_MOD而没有取模就直接越界了。正确写法是把窗口判断改成if (((next_seq - base SEQ_MOD) % SEQ_MOD) WINDOW_SIZE next_seq len)这个模运算的系数SEQ_MOD是必须调的关键参数。实验里如果你用的是 8 位序号SEQ_MOD256窗口大小建议不要超过 128如果你用的是 4 位序号SEQ_MOD16窗口最大只能到 8。你可以在报告里写一组对比实验窗口大小 1、4、8 时的吞吐变化这比贴一堆代码更能得分。接收方缓冲区设计更直接一般用一个数组存已经收到的包再用一个布尔数组标记对应槽位是否有效。收到乱序包时不立刻提交而是等到前一个序号连续后再提交。这个“连续性判断”最容易写错我见过一个典型案例接收方收到序号 3 后直接放入buf[3]没有做“当前期待序号”校验结果窗口滑动错位后面的包全部被当成重复包丢弃。3.3 参数设置超时时间、窗口大小、序号空间怎么调这部分其实是很多实验报告缺失的内容。代码能跑不算完你还得解释为什么这么设参数。我把三个核心参数的调法列出来参数典型取值范围调大影响调小影响超时时间 RTO1 到 2 倍平均 RTT丢包后等待时间长信道利用率下降容易误判超时触发不必要重传窗口大小 W小于等于序号空间一半吞吐提升但缓冲区压力大接近停等协议效率低序号空间 SEQ_MOD2 的整数幂如 16/256抗重复包能力强但报文头开销大高负载时容易回绕歧义实验里你可以在模拟信道中设置丢包率p然后画出吞吐率 有效数据量 / 发送数据量与p的关系曲线。常见结果是丢包率从 0 升到 20% 时停等协议吞吐量直线下降而滑动窗口能撑到更高丢包率。写报告时把丢包率放在 x 轴把序号空间利用率放在 y 轴再对比三种协议的曲线比贴十页代码更有说服力。还有一个很容易忽略的参数ACK 的确认粒度。停等协议用逐包确认滑动窗口用累计确认。如果你在代码里把handle_ack写成“只确认单个包”那窗口永远不会正确滑动这是初学者最容易“翻车”的地方。记住累计确认的语义ack_num表示该序号之前的所有帧都已正确接收而不是只确认这一个帧。4. HTTP 与路由协议实验抓包验证和距离向量算法的代码细节4.1 用 Wireshark 验证 HTTP 实验跑通只是第一步HTTP 实验往往是所有实验里看起来最简单但验收时翻车率最高的。因为很多人的代码本身就基于 Python 的http.server模块跑通三分钟但问到底层报文结构答不上来。实验要求的“抓包分析”不是让你截一张图而是要看懂三次握手、HTTP 请求行和响应状态码。我常用的验证流程是先启动一个最简单的 HTTP 服务然后用tcpdump或 Wireshark 抓包。如果你在 Linux 终端环境用tcpdump更方便sudo tcpdump -i lo -A tcp port 8080 -c 20 # 另开一个终端 curl -v http://127.0.0.1:8080/hello-i lo抓回环接口-A以 ASCII 显示数据包内容tcp port 8080过滤指定端口-c 20抓到 20 个包后自动停止。抓到的包里你应该能看到三次握手第一次SYN第二次SYNACK第三次ACK。特别注意回环接口的抓包不会显示以太网头但这不影响 TCP 层分析。很多人在这一步犯的错是在 Windows 的 Wireshark 里抓包结果发现过滤器写成tcp.port8080这是错的Wireshark 的过滤语法是tcp.port 8080。如果你在命令行里做实验记得用curl -v而不是浏览器因为浏览器会额外发出favicon.ico请求干扰分析。HTTP 报文分析的核心参数是请求行里的方法、URL、版本响应行里的状态码和原因短语。实验报告里一般要求你标注出Host头、Content-Length头和Connection头。你可以在代码里故意设置一个错误的Content-Length然后观察浏览器行为这个反向验证能帮你理解报文长度字段的重要性。4.2 距离向量路由算法的 Bellman-Ford 实现与“计数到无穷”的坑距离向量算法是网络层实验里的“硬骨头”它看起来只是一个迭代公式但实现后常常跑不出正确结果。核心方程是Dx(y) min_v { c(x,v) Dv(y) }。每个节点只与邻居交换距离向量然后更新自己的转发表。我用 Python 写一个最小的 Bellman-Ford 更新逻辑重点是让你看清“每次交换后表项如何收敛”# dv_routing.py import copy # 拓扑三个节点 A, B, C # cost: {A: {A:0, B:1, C:4}, ...} cost { A: {A: 0, B: 1, C: 4}, B: {A: 1, B: 0, C: 2}, C: {A: 4, B: 2, C: 0}, } def update_table(node, neighbors, table): 收到邻居向量后更新本节点路由表 for dst in table: best table[dst] for nbr in neighbors: # 核心公式通过邻居到dst的距离 via cost[node][nbr] table.get(nbr, {}).get(dst, float(inf)) if via best: table[dst] via return table # 初始化每个节点只知道直接相邻 tA {A: 0, B: 1, C: 1} # 注意C是直接链路cost4这里故意写成1是个错误示例 print(initial, tA) # 模拟一次交换 tA[B] update_table(A, [B], tA) print(after update, tA)这个例子里我故意把A-C的初始值写成 1而真实链路是 4这是为了演示算法会收敛到正确值。update_table的计算中table.get(nbr, {}).get(dst, float(inf))是防止邻居向量里没有目标节点的标准写法避免KeyError。float(inf)在 Python 里表示不可达这个语义要保留到实验报告里。距离向量算法最著名的坑是“计数到无穷”。如果B-C链路断开A 和 B 会互相告知一个错误距离并每次加一直到超过最大跳数才停止。解决方法是毒性逆转和水平分割。你在实验里必须在代码里至少实现split horizonB 不会告诉 A“通过 A 能到 C”因为那是由 A 自己发出去的路径。实现方式是在向邻居发送向量时把下一跳是该邻居的表项置为inf。实践里我建议你添加一个MAX_HOP 16的限制对应 RIP 协议的经典 16 跳不可达。当某个距离超过 16直接视为不可达。这个参数能避免死循环也符合教材里的做法。4.3 从 Dijkstra 到 OSPF实验报告里常考的对比点如果你的实验题是“实现链路状态路由算法”那你要写的是 Dijkstra而不是 Bellman-Ford。Dijkstra 算法用全局拓扑计算最短路径和距离向量的最大区别是LS 算法有完整的网络拓扑信息而 DV 只知道邻居。实验报告里老师一定让你对比这两种机制的收敛速度。Dijkstra 的实现要点是优先队列import heapq def dijkstra(graph, src): dist {node: float(inf) for node in graph} dist[src] 0 pq [(0, src)] parent {} while pq: d, u heapq.heappop(pq) if d dist[u]: continue for v, w in graph[u].items(): if d w dist[v]: dist[v] d w parent[v] u heapq.heappush(pq, (dist[v], v)) return dist, parent参数说明graph是邻接表src是源节点。注意heapq是最小堆每次弹出距离最小的节点。if d dist[u]是跳过过期项的关键否则你会重复处理旧的距离。这段代码的复杂度是O(E log V)实验报告里应该提到这一点并且和 DV 的O(N^2)收敛时间对比。实验验收时老师一般会问DV 算法为什么会有“坏消息传播慢”正确答案是距离向量需要迭代更新每个节点只知道邻居的信息链路故障要经过多轮交换才能扩散到全网。你可以在报告里画一条曲线比较在相同拓扑下 Dijkstra 和 DV 收敛所需的轮数Dijkstra 一次计算得到DV 需要 N-1 轮。5. 中南大学计算机网络实验源代码避坑编译、跨平台和报告格式5.1 现象Windows 下getsockopt返回值正常但数据发不出去原因很多人拿到 Linux 版源码后直接在 Visual Studio 里编译调试时发现getsockopt(SO_ERROR)返回 0但send一次只能发一个字节。问题通常出在send的返回值没有循环处理Windows 下send对大数据包可能只发送一部分而 Linux 默认行为也可能是部分发送但你只调了一次send。解决实现一个send_all函数循环调用send直到发送完所有字节int send_all(int sockfd, const char *buf, int len) { int sent 0; int n; while (sent len) { n send(sockfd, buf sent, len - sent, 0); if (n -1) { perror(send); return -1; } sent n; } return sent; }同时Windows 使用 Winsock 需要先WSAStartup否则socket()会返回INVALID_SOCKET。这是最典型的“能编译但运行失败”原因。5.2 现象CRC 校验码正确但接收端总报校验失败原因常见做法是把数据按位进行计算但发送端把校验码追加在数据末尾后接收端计算时把校验码也一起当作数据参与运算。教材里的标准做法是发送端计算CRC后把校验码放在数据的后面一起发送接收端用整个“数据校验码”去除生成多项式余数应为 0。如果你在接收端只对数据部分重新计算而不包括校验码本身结果会不匹配。解决把发送端生成的校验字接在数据后接收端对data crc整体做模二除法检查余数是否为 0。写代码时注意多项式长度的位数例如 CRC-16 的生成多项式是0x8005对应 16 位移位次数是 16 次而不是 15 次。5.3 现象距离向量路由算法跑起来后表项不收敛甚至死循环原因大多数实现没有处理“下一跳是自己”的表项或者没有做水平分割。当 A 告知 B 可以通过 A 到达 CB 又把这条路径原样传回 AA 会认为自己经过 B 的路径更短于是出现环路。解决在发送路由更新前检查所有表项如果下一跳是接收这个更新的邻居则把该表项的距离置为inf。另外每个节点要记录自己的启动时间收到的更新里如果时间戳比自己更新才接受否则丢弃。实现这两个机制后你的路由表会在几轮迭代内稳定。5.4 现象实验报告里的运行截图和自己的代码对不上原因常见问题是截图里显示的 IP 是192.168.1.100而代码里写死的是127.0.0.1老师一眼就能看出截图不是在你机器上跑的。另一个问题是截图时间戳和文件修改时间相差一天以上。解决实验报告用到的截图必须在最终代码版本上重新跑一遍并且用date命令把系统时间显示在终端提示符里。我自己的习惯是每次修改代码后重新截图不偷懒用旧图。另外如果实验要求打印日志把时间戳写进日志文件这能让验收老师确认你是完整跑通而不是伪造数据。5.5 现象中文注释编译后乱码甚至导致编译失败原因Windows 下 GBK 编码的注释传到 Linux 后用 UTF-8 编译GCC 会报stray \246 in program之类的错误。中南大学实验机房可能同时有 Windows 和 Linux 两种环境你从同学那里拷贝源码时很容易带上编码问题。解决统一使用英文注释或者在代码第一行添加编码声明PythonC 语言则用#pragma comment不解决实际问题建议直接在编辑器里把文件转换成 UTF-8 无 BOM。下面的代码风格能避开编码问题// logger.h #define LOG(fmt, ...) \ do { fprintf(stderr, [%s:%d] fmt \n, __FILE__, __LINE__, ##__VA_ARGS__); } while (0)这样你不需要在源码里写中文日志内容用英文或数字标记避免乱码。同时LOG宏能帮你把代码行号打出来这对后面的调试很有用。6. 从“能交实验”到“能答辩”用日志和自动化测试把代码变成自己的6.1 给协议实验加一个事件日志层很多实验代码只输出最终结果验收老师问“重传了几次”你答不上来。我一般在所有协议实验里加一个简单的日志层把关键事件按时间顺序记录import datetime def log_event(event, seq, extra): ts datetime.datetime.now().strftime(%H:%M:%S.%f)[:-3] print(f[{ts}] {event} seq{seq} {extra}, flushTrue)flushTrue很关键否则在管道重定向或程序崩溃时最后的日志会丢失。你在发送、收到确认、超时、丢包四个节点各调用一次log_event答辩时直接分段展示日志老师立刻能看出协议状态机是否完整。6.2 用 shell 脚本做协议正确性回归测试实验环境里可能没有 pytest但一定有 shell。你可以为可靠传输协议写一个简单的自动化测试# test_reliable.sh #!/bin/bash set -e for rate in 0 0.1 0.2 0.3; do echo loss rate $rate python3 stop_and_wait.py --loss-rate $rate --samples 100 # 检查是否发了足够多的重传包 python3 -c import sys; assert all packets sent in open(log.txt).read() done--loss-rate和--samples要提前在你的代码里写成参数解析这个设计比把参数写死在代码里更符合实验要求。脚本返回值0代表通过任何一步失败都会因为set -e退出验收时你可以直接让老师看终端输出。6.3 用 Wireshark 加字段过滤快速定位问题这里一个实用技巧是抓包时给协议加一个自定义标记。比如你在 TCP 数据段的开头放一个魔数0xCAFE然后在 Wireshark 里用tcp.payload[0] ca:fe过滤就能秒筛出你的实验流量避免被其他进程的广播包干扰。tcpdump -i eth0 -X tcp[34:4] 0xcafebabe这个表达式的意思是从 TCP 头的第 34 字节开始取 4 个字节匹配你的魔数。抓包验证时能精确证明你的数据帧格式正确比单纯说“我跑通了”更有说服力。把日志、自动化测试和抓包三个能力组合起来你手里的实验源码就不再是一个只能跑一次的玩具。答辩时你甚至可以现场改一个参数再跑一轮展示代码的可维护性。我自己的习惯是每次实验结束前把代码里的所有print都整理成带时间戳的日志再把测试脚本放进test/目录确保换一台机器也能复现。这个习惯曾经让我的路由协议实验在验收现场改掉一个MAX_HOP常量后重新收敛最终顺利通过。希望你也能从一份“源代码”里得到的不只是分数而是动手排查网络问题的思路。希望帮到你。本文还有配套的精品资源点击获取