☰
中南大学计算机网络实验源代码:从Socket到TCP状态机的协议实现指南
2026/10/8 6:16:38 网站建设 项目流程

简介:一份面向中南大学计算机网络课程实验的源代码合集,覆盖A1与A3两道2022年版实验题目,适用正在学习网络原理或需要完成同类实验的本/专科学生。A1部分基于Socket编程实现客户端与服务器间的TCP/UDP通信,涉及连接建立、数据收发与异常处理;A3部分则深入HTTP、DNS等协议报文解析与交互模拟,帮助读者将TCP/IP协议栈理论落到实际代码中。资源共53个文件,以C源码(.c/.h)、编译中间文件(.o)为主,另含实验指导书docx、运行截图png/jpg、Makefile及辅助脚本,压缩包整体约1.06MB,目录按A1/A3分区,便于对照查阅。此前已有743人学习下载。通过研读这些代码,可同时获得网络编程范式、调试工具使用(如Wireshark抓包)和协议实现细节,是一份值得反复阅读的实验参考资料。

1. 为“中南大学计算机网络实验源代码”正本清源:它不是答案,是一套协议脚手架

搜“中南大学计算机网络实验源代码”的同学,大多是在 deadline 前想找一份能直接跑的脚本,但真正让实验报告拿高分的,从来不是“能通”,而是你在验收时能说清楚“为什么这个 seq 号要加 1,为什么校验和要这样折叠”。计算机网络实验看着是敲代码,实际上考的是协议实现。把这串源码定位成脚手架,而不是救命稻草,你的收益会完全不同:套接字负责收发,你负责把 TCP 状态机、IP 头结构、CRC 校验这些课本体感变成可调试的东西。这篇文章适合正在做谢希仁第五版配套实验、用 Wireshark 抓包交报告、以及想从“会调 socket”跨到“看得懂协议栈”的同学。

2. 动手前先会搭骨架:实验源代码的目录、抓包文件和通用复用模块

2.1 一套能过验收的代码仓库该长什么样,文件怎么摆

中南大学计算机网络实验的交付物通常分成三个部分:可运行的源码、抓包文件、实验报告。我见过太多同学把三个东西塞进一个 zip,打开以后源码路径是绝对路径,换台机器直接跑不起来,老师验收时还得帮你改路径。我一般会按下面这个结构归档:

network-lab/ ├── README.md # 每个实验怎么运行,依赖什么库,结果输出到哪 ├── src/ │ ├── crc.py # 数据链路层:CRC 校验实现 │ ├── ip_checksum.py # 网络层:IP 首部校验和 │ ├── tcp_state.py # 传输层:三次握手状态机模拟 │ ├── tcp_echo.py # 传输层:最小 TCP echo 例子 │ └── sniffer.py # 抓包辅助脚本 ├── pcaps/ │ ├── handshake.pcap # 用 Wireshark 抓的三次握手 │ └── http.pcap # HTTP 请求抓包 └── report/ ├── 实验1_数据链路层.md ├── 实验2_网络层.md └── 实验3_传输层.md

这个结构的核心思路是让代码和结果分离。src目录里的每个脚本只做一件事,report目录里写结论和截图,pcaps目录保留证据。实验报告里如果贴的是“某次运行成功的终端输出”,我建议改成贴 Wireshark 的过滤结果,老师会更愿意相信代码真的经历了协议栈,而不是 print 出来的一段假数据。

2.2 为什么很多 socket 代码看起来能跑,但一验收就翻车

我审过不少同学交上来的“计算机网络实验源代码”,最典型的问题不是 bug,而是“能跑”和“可验收”之间差了三条边界:

第一,代码写死了 IP 和端口,换一台实验机器就要改源码。正确做法是把监听地址、端口、抓包文件名这些参数放到config.ini或命令行参数里。第二,程序用sys.exit()把异常吞掉了,重复运行时端口还被上一个进程占着,界面上一片红。第三,代码把收到的字节直接按 ASCII 解码,遇到二进制协议的\x00就乱了。这三点不解决,就算功能全对,验收印象也会打折扣。

我给学生做演示时经常说一句话:实验代码的可用性,不是“我这边跑通了”,而是“在一个干净的终端里,输入一条命令就能复现你的结果”。你不用搞 CI/CD,但要具备最基础的可复现意识。

2.3 最小可用实现:一个不会把自己卡死的 TCP echo 服务端

下面这段代码是很多传输层实验的地基,我在教学里反复用,它比直接抄socket教程多处理了两个容易被忽略的边界。

# tcp_echo.py # 最小 TCP echo:服务端把客户端发来的字节原样返回 import socket def start_server(host="127.0.0.1", port=8901): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv: # SO_REUSEADDR 解决 TIME_WAIT 状态下的端口占用,开发期必备 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((host, port)) srv.listen(1) print(f"[server] listening on {host}:{port}") conn, addr = srv.accept() with conn: print(f"[server] accept from {addr}") while True: data = conn.recv(4096) if not data: print("[server] client closed connection") break conn.sendall(data)

代码里recv(4096)返回空字节串,表示对端已经关闭,这是 TCP 里的 EOF 语义。很多第一次写 socket 的同学在这里用while True死等,对端关闭后程序既不退出也不报错,看起来像“卡死”。sendall和send的区别也要知道,send可能只发送部分字节,sendall会循环发完,实验报告里如果涉及“可靠传输”,这两个函数的取舍可以展开写一段。

再补一个对应的客户端:

# tcp_echo_client.py import socket def start_client(host="127.0.0.1", port=8901, payload=b"hello"): with socket.create_connection((host, port), timeout=5) as cli: cli.sendall(payload) received = cli.recv(4096) print(f"[client] sent {len(payload)} bytes, received {len(received)} bytes")

timeout=5很重要,它避免了对端不响应时客户端无限阻塞。调这个参数时需要注意,单位是秒,实验环境里路由器延时高的话可以放大到 10,但不要在局域网环境里写 60,否则一旦代码有 bug,调试时你会浪费一分钟才能等到异常抛出来。

参数我常用的值说明
recv缓冲区4096普通教学实验够用,不需要改大
timeout5 秒局域网内合理,广域网可视情况调大
port8901避开 1024 以下端口,也避开常见的 8080
SO_REUSEADDR1开启,服务端重启不报 Address already in use

3. 用十个函数把 TCP 三次握手改成可运行状态机:从 recv 日志反推 SYN-ACK

3.1 为什么实验题里要模拟握手,而不是直接调用 connect

很多同学拿到传输层实验题,第一反应是“用 socket 连一下不就行了”。但老师真正想考察的是你对 TCP 状态转移的理解,而不是你会不会调connect。操作系统在内核里已经把三次握手做掉了,你从应用层根本感知不到 SYN、SYN-ACK、ACK 的中间过程。所以,实验代码的常见做法是写一个状态机模拟器,把客户端和服务端各自的状态搬进用户态,用字典或枚举去推动状态流转。

这个模拟器不需要真实发包,但必须体现四个关键点:状态集合、事件驱动、超时处理、序号推进。做到了这四点,你在报告里就能对着状态转移图讲清楚“为什么主动打开方要进入 SYN_SENT,为什么服务端收到 SYN 后要同时设置 ACK 和 SYN 两个标志位”。

3.2 状态机最小实现:客户端/服务端各一个字典,驱动十一个状态

我习惯直接用一个枚举加一个转移表来做,比堆if-else更直观,也方便在实验报告里画图。

# tcp_state.py # 模拟三次握手的状态转移,不经过真实 socket from enum import Enum class TCPState(Enum): CLOSED = 0 LISTEN = 1 SYN_SENT = 2 SYN_RCVD = 3 ESTABLISHED = 4 def next_state(current, event): # 状态转移表:每种状态只处理它能接受的事件 table = { (TCPState.CLOSED, "passive_open"): (TCPState.LISTEN, "listen()"), (TCPState.LISTEN, "recv_syn"): (TCPState.SYN_RCVD, "send SYN+ACK"), (TCPState.SYN_RCVD, "recv_ack"): (TCPState.ESTABLISHED, "connection established"), (TCPState.CLOSED, "active_open"): (TCPState.SYN_SENT, "send SYN"), (TCPState.SYN_SENT, "recv_syn_ack"): (TCPState.ESTABLISHED, "send ACK"), } try: new_state, action = table[(current, event)] except KeyError: raise ValueError(f"invalid transition: {current.name} + {event}") return new_state, action def run_handshake(): client = TCPState.CLOSED server = TCPState.CLOSED client, action = next_state(client, "active_open") print(f"[client] {action}") server, action = next_state(server, "passive_open") print(f"[server] {action}") # 第一轮:客户端 SYN 到达服务端 server, action = next_state(server, "recv_syn") print(f"[server] {action}") # 第二轮:服务端 SYN+ACK 到达客户端 client, action = next_state(client, "recv_syn_ack") print(f"[client] {action}") # 第三轮:客户端 ACK 到达服务端 server, action = next_state(server, "recv_ack") print(f"[server] {action}") print(f"[result] client={client.name}, server={server.name}")

运行结果如下:

[client] send SYN [server] listen() [server] send SYN+ACK [client] send ACK [server] connection established [result] client=ESTABLISHED, server=ESTABLISHED

这段代码最重要的一点是状态转移表里没有写“CLOSED + recv_syn”这种非法组合,而是让不合法的转移直接抛异常。这个设计是刻意的,因为实验报告的加分项就是你能指出“如果客户端在 SYN_SENT 状态下收到一个不带 SYN 标志的包,应该丢弃并重新计时”。你把非法转移暴露出来,比全返回None更有教学价值。

参数上,TCPState枚举值从 0 开始,方便你后续接state.value做图表展示;状态名用全大写是为了和 Wireshark 里的SYN-SENT、ESTABLISHED对应起来,报告里两边一对照,别人一眼就能看明白。

3.3 用 Wireshark 抓包反推刚才的模拟结果,验证状态机没写错

模拟器终归是模拟,你还需要真实抓包来验证。我常用的做法是把实验代码跑起来的同时,用 Wireshark 抓 loopback 接口的流量,然后过滤三次握手那三个包:

tshark -r handshake.pcap -Y "tcp.flags.syn==1 || (tcp.flags.syn==1 && tcp.flags.ack==1) || (tcp.flags.ack==1 && tcp.len==0)" -T fields -e frame.number -e tcp.srcport -e tcp.dstport -e tcp.seq_raw -e tcp.ack_raw

这个命令输出的每一行就是一个握手报文,你会看到 TCP 的 seq 和 ack 并不是从 0 开始,而是有一个初始序号 ISN。很多实验报告里写“seq=0”,那是在相对序号模式下看到的,真实报文里 seq 是一个随机的 32 位无符号整数。这个差异值得单独写一段,它解释了为什么学校实验手册里总强调“不要把抓包看到的 seq 当成课本里的seq = client_isn + 1”。

拿到 tshark 的字段输出后,你把它和上面的状态机日志放在一起看:客户端发 SYN 时 seq 假设为 A,服务端回 SYN+ACK 时 seq 为 B、ack 为 A+1,最后客户端发 ACK 时 seq 为 A+1、ack 为 B+1。这个“加一”的逻辑在代码里体现为“received SYN + ACK => send ACK”,在抓包里体现为 ack 字段精确递增。两边能对上,你的传输层实验才算闭环。

4. 避坑:改这五个点,实验源代码才敢提交验收

4.1 大端字节序让你拼出来的 IP 头面目全非

现象:你手工拼了一个 IP 首部,bytes.fromhex('4500 003c ...')看着没问题,抓到包却发现版本号变成了 0,或者总长度变成了 15360。

原因:网络字节序是大端,而 x86 机器默认小端。你如果直接拿struct.pack("H", length)去打包,得到的是小端字节序,被接收方解析时高低位对调,数值就错了。

解决:所有多字节字段统一用struct.pack("!HH"),感叹号就是网络字节序。我见过一份代码里 90% 的字段都用了!,唯独校验和那一个字段忘了,ICMP echo 请求直接发不出去。这是一个排查起来非常隐蔽的坑,字符“!”很容易被复制时漏掉。

4.2 IP 校验和计算范围比想象中多算或者少算一层

现象:代码里把整个 IP 包都算进校验和,结果 ping 不通;把校验和字段清零后算完往里填,填入的位置又不对。

原因:IP 首部校验和只覆盖首部本身,不覆盖数据部分;而 UDP/TCP 校验和要加上伪首部。很多人把这两条混淆了,用同一套函数算完 IP 又去算 TCP,必然出错。

解决:分开写函数。IP 校验和计算前先把校验和字段置零,每 16 位做二进制反码求和,最后取反。TCP/UDP 校验和则额外把源 IP、目的 IP、协议号、TCP 长度凑成 12 字节伪首部加进去。代码里加一行注释标注“此处不含数据”,能省下你调半天错的时间。

4.3 recv 返回空字节导致客户端“假死”

现象:客户端一直卡在recv(4096)不退出,Ctrl+C 结束以后发现服务端日志里出现过 EOF。

原因:服务端sendall之后没有关闭连接,客户端以为数据还没发完,继续阻塞等待。或者是客户端在循环里recv,但没判断空字节,返回b''后还在处理,结果死循环。

解决:if not data: break是 TCP socket 编程的基本盘,这段必须出现在任何 recv 循环里。同时给 socket 设置超时,双保险。

cli.settimeout(5) try: data = cli.recv(4096) except socket.timeout: print("no data in 5s, abort")

4.4 Windows 下 SO_REUSEADDR 对 TCP 和 UDP 的行为不一致

现象:TCP 服务端重启没问题了,照搬到 UDP 实验里却出现端口绑定失败。

原因:Windows 上SO_REUSEADDR对 TCP 和 UDP 语义不同,UDP 里允许多个 socket 绑定同一端口,数据包会随机到达其中一个,反而让程序复盘时行为不可预期。

解决:UDP 实验不要盲目设置SO_REUSEADDR,除非你的确需要多播接收。局域网教学环境还是保持“一进程一端口”更清晰,也方便抓包。

4.5 日志时间戳和 Wireshark 抓包时间对不上

现象:代码打印“handshake done”的时间早于抓包里出现 SYN 包的时间,老师质疑你的实验是编的。

原因:很多同学把connect()返回当作握手完成,但connect返回意味着协议栈收到了服务端的 ACK,抓包时间戳以路由器/本机网卡收包为准,socket API 的用户态日志会有几十到几百微秒的延迟。这不是错误,但报告里如果没有任何抓包证据,审阅方只能认为你在自嗨。

解决:实验报告的“验证”部分,用 Wireshark 里的Time since previous frame和代码日志里的单调递增时间戳对齐。不要只贴一次运行结果,至少抓三次,证明状态转移是可稳定复现的。

5. 验证手法:用 Wireshark 把抓包时间戳和代码日志对齐

代码写完以后,真正让它有可信度的是一套验证脚本。我不会只在报告里贴一张 Wireshark 截图,而是会做一件事:把抓包里的 seq/ack 时间线导出来,和程序日志逐行对齐。

tshark -r pcaps/handshake.pcap -Y "tcp" -T fields -e frame.time_relative -e tcp.srcport -e tcp.dstport -e tcp.seq_raw -e tcp.ack_raw -e tcp.flags.syn -e tcp.flags.ack > timeline.csv

拿到 timeline.csv 以后,我习惯写一个十余行的小脚本去解析,打印出一条人类可读的三次握手时间线:

import csv with open("timeline.csv") as f: for row in csv.reader(f): time_ms = float(row[0]) * 1000 flags = [] if row[5] == "1": flags.append("SYN") if row[6] == "1": flags.append("ACK") print(f"{time_ms:8.2f} ms {row[1]} -> {row[2]} seq={row[3]} ack={row[4]} flags={'+'.join(flags)}")

输出长这样:

0.00 ms 52010 -> 8901 seq=3002903810 ack=0 flags=SYN 0.27 ms 8901 -> 52010 seq=910736482 ack=3002903811 flags=SYN+ACK 0.41 ms 52010 -> 8901 seq=3002903811 ack=910736483 flags=ACK

把这段输出和你的状态机日志并排贴在报告里,说服力远大于截图。我个人的习惯是每次实验完毕,把 pcap 文件和这次的时间线解析结果一起提交,下一次复习时只需要重新跑一次 tshark,代码行为就全部还原了。这个习惯曾经帮我在复核实验时发现某次代码因为改了重传定时器,导致 SYN 重传了三次,而应用层日志里完全没有体现。希望帮到你。

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

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

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

立即咨询