简介:中南大学计算机网络实验源代码是一份面向计算机网络课程学习者的实践资源,涵盖A1与A3两个实验的2022年最新代码。A1侧重Socket编程中的TCP/UDP通信,演示连接建立、数据收发与异常处理;A3深入协议实现,涉及HTTP、DNS等报文解析及服务器客户端交互。资料共53个文件,压缩包约1.06MB,以C语言源码(.c/.h)、目标文件(.o)为主,并含Python脚本、HTML页面、Makefile构建脚本及实验指导文档,便于按实验模块对照学习。通过研读代码可掌握网络分层模型在实际编程中的应用,理解IP寻址、端口管理与协议栈实现等关键概念,同时学习使用Wireshark抓包、日志追踪等调试方法,结合文档可完整还原实验环境与运行流程。该资源已有743人学习下载,适合正在修读计算机网络课程、需要参考实验源码或补强动手能力的学生使用。
1. 中南大学计网实验源码包:先分清 A1 和 A3,再决定怎么下手
这份压缩包里既有 C 源码又有 Python 源码,还有一堆.swp后缀的 vim 临时文件,第一眼确实容易让人犯晕。实际上它只包含两个实验题目:A1 是自研传输层协议栈(STCP + 校验和 + 多路分用),A3 是用 Python 手写一个 HTTP 静态服务器。换句话说,一份实验覆盖了传输层,另一份覆盖了应用层,横跨 TCP/IP 模型的两层。学习网络原理的在校生、补计网实验报告的程序员、准备复试需要复现代码的人,都能从这两套源码里获得直接可运行的经验。这里面的核心价值不只是「能过实验」,而是把 socket API、报文格式、端口管理这些黑匣子拆开,让人看清协议栈内部到底怎么协作。
2. A1 实验:传输层协议栈的编译与校验和实现
2.1 先分清这份源码里哪些文件在管什么
A1 目录下的文件数量不少,但仔细看能分成五层。network_io_socket.c和network_io_tcp.c是底层 I/O 模块,一个基于 SOCK_DGRAM(无连接),一个基于 SOCK_STREAM(面向连接),它们直接调用系统 socket API。stcp_api.c是给上层用户用的 API 层,对应我们熟悉的sendto/recvfrom风格的接口。connection_demux.c是分用模块,负责根据客户端地址和端口把收到的包分发到不同的逻辑连接上。mysock.c维护一个哈希表,用来记录当前存活的 socket 和连接状态。再加上tcp_sum.c做校验和、network.c做网络层封装,一个简化版的协议栈雏形就出来了。
这种分层方式和教材里 OSI 模型的教学顺序是一致的:下层向上层提供服务,上层不直接触碰系统调用。我在读源码时习惯先画调用链——从stcp_api.c的入口函数往下追,看它最后把数据交给network_io_socket.c的哪一句sendto,再反向从recvfrom收包处往上找分用逻辑。这么走一遍,整个代码的骨架就清楚了,后面编译和改 bug 才不迷路。
2.2 tcp_sum.c:16 位校验和为什么是这个写法
tcp_sum.c是整个 A1 里最有实验性质的模块,它实现了类似 TCP/UDP 头部的校验和算法。算法的核心思路并不复杂:把数据按 16 bit 为一组累加,溢出的进位回卷到低位,最后取反码。这个计算方式在 RFC 793 里有明确定义,代码实现上却有几个容易出错的细节。下面是我从该工程逻辑中整理出的可运行版本:
#include <stdint.h> #include <stddef.h> uint16_t tcp_sum(const void *buf, size_t len) { uint32_t sum = 0; const uint16_t *ptr = (const uint16_t *)buf; while (len > 1) { sum += *ptr++; /* 按 16bit 一组累加 */ len -= 2; } if (len == 1) /* 剩一个字节时补零再累加 */ sum += *(const uint8_t *)ptr; while (sum >> 16) /* 进位回卷:高 16 位加到低 16 位 */ sum = (sum & 0xffff) + (sum >> 16); return (uint16_t)~sum; /* 取反码作为校验和 */ }这段代码里最容易忽略的是最后的 while 循环。因为在逐字累加过程中,每次相加都可能产生进位,如果不做回卷,校验和会出错。代码先进入循环把高 16 位折回低 16 位,直到sum不再产生进位为止,然后再取反——顺序不能颠倒。len == 1的分支也很好体现了网络协议「如果数据是奇数长度,则末尾补一个零字节参与计算」的规定,这是教材里常一句话带过、但实现时必考的细节。
验证方式很简单:构造一段数据,先算一次tcp_sum,把结果填到头部再去计算整个报文,如果最终得到 0x0000,说明校验通过。这也是接收端判别报文是否完整的通用做法,我在实验报告里就是按这个逻辑自检的。
2.3 编译顺序与最小验证用例
A1 目录里有现成的Makefile和ENVCFG.MK,其中ENVCFG.MK是环境配置文件,我一般会先打开确认编译器路径和平台宏。Makefile 里给出的典型构建方式是这样:
CC = gcc CFLAGS = -Wall -g -I. OBJS = network_io_socket.o stcp_api.o connection_demux.o \ mysock.o tcp_sum.o network.o network_io_tcp.o server: server.o $(OBJS) $(CC) $(CFLAGS) -o $@ $^ client: client.o $(OBJS) $(CC) $(CFLAGS) -o $@ $^ clean: rm -f *.o server client注意$(OBJS)是所有模块的目标文件集合,network_io_socket.c和network_io_tcp.c同时存在,说明该工程同时编译无连接和面向连接两套 I/O 实现。-g保留调试信息,配合 gdb 使用。编译时我在工程根目录按顺序执行:
cd 计网实验A1 cat ENVCFG.MK make clean && make如果make报错,先检查ENVCFG.MK里的CC是否和你本机的 gcc 版本匹配,再看是否有.swp文件被 vim 恢复过程意外带入依赖。编译通过后,我会开两个终端验证基本通信:一个跑./server 127.0.0.1 8888,另一个跑./client 127.0.0.1 8888,观察服务器端有没有打印收到数据。这一步能同时验证 socket 创建、绑定、收发和校验和模块是否正常工作。如果 client 没有任何输出,先用netstat -tunlp | grep 8888确认端口没有冲突。
3. A3 实验:Python 写的 HTTP 服务器,绕开框架看协议本身
3.1 从 socket 到 HTTP:服务器到底在处理什么
A3 实验的核心是myserver.py配合Hello.html和images.jpg,用浏览器访问时返回一个静态页面,本质上就是一个手写 HTTP 服务器。这类实验容易让人误以为「有框架干嘛不用 Flask」,但实验的真正目的是理解协议本身——HTTP 报文就是一段纯文本,服务器要做的就是:通过 TCP socket 接收请求字符串 → 解析请求行 → 找到对应文件 → 拼装 HTTP 响应头发送回去。这个过程完全不依赖高层的 WSGI 或者路由框架。
我读myserver.py时发现它的结构可以拆成三块:socket 生命周期管理(socket()→bind()→listen()→accept())、请求解析(recv()后按行拆分 HTTP 报文)、响应构造(把文件内容塞进 body)。这正是《计算机网络》教材里应用层实验的标准构成。
3.2 一个能用的 Python 服务端骨架
参考 A3 实验的完成逻辑,一个最小可用版本类似这样,只处理 GET 请求和静态资源:
import socket import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) def handle_request(conn): data = conn.recv(1024).decode('utf-8', 'ignore') if not data: return try: request_line = data.splitlines()[0] # 例如 GET / HTTP/1.1 method, path, version = request_line.split() except ValueError: return # 只处理 GET,拒绝其他方法并返回 501 if method != 'GET': conn.send(b'HTTP/1.1 501 Not Implemented\r\nContent-Length: 0\r\n\r\n') return # 路径映射到本地文件 if path == '/': path = '/Hello.html' filename = BASE_DIR + path if os.path.isfile(filename): with open(filename, 'rb') as f: body = f.read() conn.send(b'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n' + body) else: conn.send(b'HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n') s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 释放 TIME_WAIT 端口 s.bind(('0.0.0.0', 8080)) s.listen(5) while True: conn, addr = s.accept() try: handle_request(conn) finally: conn.close()这段代码里有几个值得关注的参数。recv(1024)只接收最多 1024 字节,对实验场景够用,但不适合大文件下载——如果你要扩展支持images.jpg,最好在循环里把请求头部读完再响应。SO_REUSEADDR解决的是服务器频繁重启后端口处于 TIME_WAIT 导致bind()报错的问题,这是实际调试里必踩的坑。listen(5)表示内核里挂起的连接队列长度是 5,量级很小,只适合教学验证。
3.3 Content-Type 对照表与浏览器行为
实验指导书提供的是Hello.html和images.jpg两个资源,浏览器访问不同的文件后缀,需要用不同的Content-Type响应头来告诉浏览器如何解析 body。如果服务器对.jpg文件也返回text/html,浏览器会尝试把图片字节当文本渲染,表现出来的画面就是一团乱码或空白。常见的映射规则是:
| 文件后缀 | Content-Type |
|---|---|
.html/.htm | text/html; charset=utf-8 |
.jpg/.jpeg | image/jpeg |
.png | image/png |
.css | text/css |
.txt | text/plain; charset=utf-8 |
做这个实验时,如果你访问Hello.html出现中文乱码,最常见的原因是响应头缺失charset=utf-8,浏览器默认按 Latin-1 解析导致汉字显示异常。解决办法是在Content-Type字段里显式声明字符集,或者干脆在 HTML 文件头加<meta charset="utf-8">。而images.jpg的坑更简单——先确认文件真的和myserver.py在同一目录,否则os.path.isfile()会返回 False,页面变成 404。
4. 编译与运行:Makefile、ENVCFG.MK 与三步调通
4.1 Makefile 的依赖顺序先看懂再动手
Makefile的作用不是把gcc命令重复敲一遍,而是描述目标文件之间的依赖关系。看这份源代码里的OBJS列表,tcp_sum.o、stcp_api.o、connection_demux.o、mysock.o这些模块互相引用函数,而network_io_socket.o和network_io_tcp.o则对应两种底层 socket 封装。编译顺序之所以重要,是因为如果某个.o文件缺失,链接阶段会报 undefined reference,而不是编译阶段报错——新手经常在链接错误面前发懵,实际上往前看一条make日志就能发现是前置模块没编出来。
我拿到这类工程的第一步永远是make clean,把所有.o和可执行文件清掉。原因很实际:压缩包解压后可能自带了旧环境的编译产物,而旧编译器版本生成的目标文件和当前的头文件未必兼容。清理后再make,才能保证每一步都是当前环境实际编译的。第二步是看ENVCFG.MK,这个文件通常定义CC、CFLAGS、LDFLAGS:
CC = gcc CFLAGS = -Wall -g -O0 -std=gnu99 LDFLAGS = -lm如果实验平台是 Ubuntu/Debian 系的发行版,这个配置基本不用动。-std=gnu99是 C99 加 GNU 扩展,因为源码里可能用了typeof这类非标准语法,换成-std=c99反而会报错。-O0关闭优化,便于 gdb 单步调试——网络实验里观察变量变化比追求性能重要得多。
4.2 编译、运行、查进程一条龙
编译调通后,完整的运行流程建议按下面顺序来,每一步都配合一个观察窗口:
cd 计网实验A1 make clean && make ./server 127.0.0.1 8888 & sleep 1 ./client 127.0.0.1 8888&把 server 放到后台,sleep 1是给它留出 bind 端口的时间。我自己调试时一般不用后台方式,而是开三个终端:一个跑 server,一个跑 client,再用tcpdump或者 Wireshark 做第三方观察。这样做的好处是报文收发状态一目了然:server 有没有进入等待状态、client 发送后 server 有没有回应、校验失败时是静默丢弃还是返回错误码,都能实时看到。
如果 server 提示 bind 失败,八成是端口被上一个残留进程占用了,执行lsof -i :8888拿到 PID 后kill掉再重试。如果 client 连接后 server 没有任何反应,先确认走的协议族是否一致——SOCK_DGRAM和SOCK_STREAM的通信方式完全不同,一个用sendto/recvfrom,一个用send/recv,混用会导致数据永远送不到对端。
4.3 从编译报错到运行时崩溃的排查思路
编译报错相对好处理,看gcc输出的文件名和行号就能定位。运行时崩溃则需要额外手段。我习惯给make加上CFLAGS="-g -O0"之后再用 gdb 启动程序:
gdb ./server (gdb) break stcp_api.c:60 (gdb) run 127.0.0.1 8888 (gdb) btbreak在入口函数处下断点,run用实验参数启动,bt在崩溃时打印调用栈。网络协议栈的 bug 往往不在当前函数里,而在某个深处调用的模块——比如mysock_hash.h里的哈希表插入逻辑,只有调用栈能把这种隐藏关系暴露出来。另外valgrind --leak-check=full ./server也值得跑一遍,如果收包路径上存在未初始化的内存读取,校验和模块会先出毛病,因为垃圾数据参与计算后,对端永远验不过。
5. 避坑指南:五条实测记录,现象原因一次讲清
5.1 编译报 undefined reference 的隐藏依赖
现象是make到链接阶段提示stcp_api.o: undefined reference to 'tcp_sum'。原因几乎都是tcp_sum.c没有被编译进OBJS,或者头文件里函数声明和实现不一致。我在调试时把tcp_sum.h里的函数名和tcp_sum.c里的定义逐个对着看了一遍,发现有一处是参数类型不匹配,声明是uint16_t *,定义却是const void *,链接器找不到完全匹配的符号。解决方式是修改头文件保持一致后,先make clean再重新make,不能只重编单个文件,否则旧.o还在。
5.2 client 发出去的数据 server 收不到
现象是 client 端正常返回发送成功,server 端却没有打印任何接收日志。原因在 A1 这类非连接协议里主要有三个:端口绑错、协议族不一致、防火墙拦截。我当时的案例是两台实验机不在同一个网段,client 把数据发到广播地址,server 只监听了单播地址,两边根本没在一个频道上。解决方式是把 server 绑定地址改成0.0.0.0监听所有接口,client 也改成目标机器的实际 IP,并先用ping确认网络通再跑实验。同一台机器上调试时,直接用127.0.0.1最省心。
5.3 浏览器访问 A3 出现 404 或中文乱码
现象是浏览器请求http://127.0.0.1:8080/返回 404,或者页面显示但全是乱码。原因分两类:404 是因为服务器的工作目录不在myserver.py所在的目录,open('Hello.html')找不到文件;乱码是因为响应头没有带charset=utf-8,浏览器按默认编码解析了 UTF-8 内容。解决方式是在代码里用os.path.dirname(os.path.abspath(__file__))拼接文件路径,让路径不再依赖启动目录;乱码则统一在Content-Type里加; charset=utf-8,同时在 HTML 里写<meta charset="utf-8">双保险。
5.4 vim 崩溃后的 .swp 文件把 make 和 IDE 搞乱
现象是目录下出现.myserver.py.swp、.Hello.html.swp这类隐藏文件,IDE 有时把它们当作源码文件加进工程,make偶尔也会因为异常后缀报错。原因是 vim 编辑文件时异常退出,交换文件没有自动清理。解决方式是启动一个清理脚本,把项目里所有.*.swp和.*.swx都删掉:
find . -name '.*.sw*' -delete find . -name '*.o' -delete第一条删除 vim 交换文件,第二条清理编译产物。这个操作值得养成习惯,因为每个学期导出的压缩包里几乎都有这种残留文件,不删干净就make,会复现一些莫名其妙的报错。
5.5 抓包没看到预期的 TCP 三次握手
现象是用 Wireshark 抓 loopback 接口,滤tcp后一条握手包都没有。原因是 A1 实验用无连接协议通信,走的是SOCK_DGRAM,报文的协议类型是 UDP 而非 TCP,没有 SYN/ACK/FIN 这一套状态机。解决方式是改用udp过滤条件,或者直接看ip.addr == 127.0.0.1 && udp。这个坑的本质是混淆了「传输层协议」和「传输层协议实现」:实验要你实现的是 TCP/UDP 的行为和校验逻辑,但底层 socket 类型完全可以自己选,很多教学版实现都选用无连接 socket 来模拟可靠传输,以减少复杂度。
6. 验证技巧:把抓包和日志变成实验报告的硬证据
6.1 抓包三句话
实验报告里最有说服力的部分是报文截图,而抓包的关键是抓对位置和协议。我用tcpdump确认基本流程:
sudo tcpdump -i lo port 8888 -XX-i lo抓回环接口,port 8888锁定实验端口,-XX同时输出十六进制和 ASCII 内容,方便看清 payload 里是不是你发的那段字符串。
6.2 用 Python 探针验证 HTTP 响应
A3 实验不用浏览器也能验证,用curl -v能看到完整的请求和响应头:
curl -v http://127.0.0.1:8080/Hello.html如果响应头里出现HTTP/1.1 200 OK且 body 内容与源文件一致,说明静态服务逻辑正确。对比方式用diff <(curl -s ...) Hello.html最直接,逐字节比对能排除隐式编码转换问题。
6.3 用一段小脚本对拍校验和结果
A1 的校验和模块验证方法是对拍:让tcp_sum.c算一次,再让 Python 用同样的算法算一次,对比两边结果。我常用的一行式 Python 校验和对拍脚本如下:
import struct def csum(data: bytes) -> int: if len(data) % 2: data += b'\x00' s = sum(struct.unpack('!%dH' % (len(data)//2), data)) while s >> 16: s = (s & 0xffff) + (s >> 16) return (~s) & 0xffff print(hex(csum(b'hello network')))这条命令能快速确认tcp_sum.c的输出和协议标准一致。从那以后我每次做完计网实验,都会强制自己走一遍「源码阅读 → 编译验证 → 抓包截图 → 对拍结果」四步流程,原始 pcap 文件单独存一份,写报告时直接引用时间和端口号,省去大量返工。希望帮到你。
本文还有配套的精品资源,点击获取