ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

中南大学计算机网络实验源代码:从Socket到TCP状态机的协议实现指南

中南大学计算机网络实验源代码:从Socket到TCP状态机的协议实现指南 简介一份面向中南大学计算机网络课程实验的源代码合集覆盖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(host127.0.0.1, port8901): 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(host127.0.0.1, port8901, payloadbhello): with socket.create_connection((host, port), timeout5) as cli: cli.sendall(payload) received cli.recv(4096) print(f[client] sent {len(payload)} bytes, received {len(received)} bytes)timeout5很重要它避免了对端不响应时客户端无限阻塞。调这个参数时需要注意单位是秒实验环境里路由器延时高的话可以放大到 10但不要在局域网环境里写 60否则一旦代码有 bug调试时你会浪费一分钟才能等到异常抛出来。参数我常用的值说明recv缓冲区4096普通教学实验够用不需要改大timeout5 秒局域网内合理广域网可视情况调大port8901避开 1024 以下端口也避开常见的 8080SO_REUSEADDR1开启服务端重启不报 Address already in use3. 用十个函数把 TCP 三次握手改成可运行状态机从 recv 日志反推 SYN-ACK3.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 SYNACK), (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(finvalid 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}) # 第二轮服务端 SYNACK 到达客户端 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 SYNACK [client] send ACK [server] connection established [result] clientESTABLISHED, serverESTABLISHED这段代码最重要的一点是状态转移表里没有写“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.syn1 || (tcp.flags.syn1 tcp.flags.ack1) || (tcp.flags.ack1 tcp.len0) -T fields -e frame.number -e tcp.srcport -e tcp.dstport -e tcp.seq_raw -e tcp.ack_raw这个命令输出的每一行就是一个握手报文你会看到 TCP 的 seq 和 ack 并不是从 0 开始而是有一个初始序号 ISN。很多实验报告里写“seq0”那是在相对序号模式下看到的真实报文里 seq 是一个随机的 32 位无符号整数。这个差异值得单独写一段它解释了为什么学校实验手册里总强调“不要把抓包看到的 seq 当成课本里的seq client_isn 1”。拿到 tshark 的字段输出后你把它和上面的状态机日志放在一起看客户端发 SYN 时 seq 假设为 A服务端回 SYNACK 时 seq 为 B、ack 为 A1最后客户端发 ACK 时 seq 为 A1、ack 为 B1。这个“加一”的逻辑在代码里体现为“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)不退出CtrlC 结束以后发现服务端日志里出现过 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 seq3002903810 ack0 flagsSYN 0.27 ms 8901 - 52010 seq910736482 ack3002903811 flagsSYNACK 0.41 ms 52010 - 8901 seq3002903811 ack910736483 flagsACK把这段输出和你的状态机日志并排贴在报告里说服力远大于截图。我个人的习惯是每次实验完毕把 pcap 文件和这次的时间线解析结果一起提交下一次复习时只需要重新跑一次 tshark代码行为就全部还原了。这个习惯曾经帮我在复核实验时发现某次代码因为改了重传定时器导致 SYN 重传了三次而应用层日志里完全没有体现。希望帮到你。本文还有配套的精品资源点击获取
返回列表