简介:中南大学计算机网络实验源代码是一份面向高校网络专业学生的实践资源,聚焦2022年A1、A3两个实验的源码实现,适合作为计算机网络课程配套练习。A1覆盖Socket编程与TCP/UDP通信,包括连接建立、收发数据、异常处理及IP寻址等传输层基础内容;A3进一步深入HTTP等协议报文解析与服务器/客户端交互,帮助理解应用层到网络层的分层协作。压缩包共53个文件,以C源文件、头文件、目标文件为主体,辅以Makefile构建脚本、Python辅助脚本、实验指导docx和运行截图,整体仅1.06MB,目录结构清晰,便于按实验模块查找。已有743人浏览学习。通过阅读并运行这些代码,可直观掌握网络编程中的错误捕获、超时重试等关键机制,同时结合抓包工具加深对协议栈的认识,是一份兼顾理论与实战的参考材料。
1. 先从一包实验代码说起:A1 与 A3 到底藏着什么
这份资源不是“一个能交差的实验报告”,拆开后你会发现它实际上承包了中南大学计算机网络实验 A1 和 A3 两个题目的全部交付物。A1 那部分最值得看的是从 network.c 到 mysock_api.c 的一整条自研协议栈,配合 client.c 和 server.c 跑双端通信;A3 则是一个 Python 写的 HTTP 服务端,能把 Hello.html 和 images.jpg 发给浏览器。适用人群很明确:正在做或准备做这套实验的学生,想补 Socket 和 TCP 原理的 Devops 工程师,以及手头有“指导书 + 源码 + 运行截图”就能快速复现的人。
我的建议是别急着交作业,先把这条调用链走一遍,哪怕不写一行代码也值。湖科大教书匠、王道那类视频课讲的是通用框架,而这份源码是你能亲手跑起来的真实实例,每一层都有对应的 .c 文件和 Makefile 规则。对照《计算机网络自顶向下》的传输层章节来读,课本上的序号、确认、校验和都能在代码里找到落点,理解深度完全不一样。
2. 拆开 A1 的骨架:从 transport 到 mysock 的四层结构
2.1 压缩包里的文件怎么归类
打开压缩包第一眼会觉得乱:一堆 .c、.h、.o、.swp 混在一起。其实核心文件就三类,另两类是编译产物和编辑器残留,直接全部扔进 Ubuntu 编译大概率翻车。先把文件分成五组,心里就有数了。
| 类别 | 文件 | 说明 |
|---|---|---|
| 协议栈源码 | network.c/h、network_io.c/tcp.c/socket.c、transport.c/h、stcp_api.c/h、mysock.c/mysock_api.c/h、connection_demux.c/h、tcp_sum.c/h | 自研的简化 TCP/Socket 协议栈 |
| 应用层源码 | client.c、server.c、myserver.py、Hello.html | 双端通信演示程序和 HTTP 服务器 |
| 脚本与配置 | hello.sh、ENVCFG.MK、Makefile | 一键运行、编译环境配置 |
| 文档与报告 | 附录2-A类实验第1题指导书.docx、附录4-A类实验第3题指导书.docx、Report.txt | 实验要求与 A1 的文字报告 |
| 产物与残留 | 各 .o 文件、.swp、.~lock、images.jpg | 旧机器编译结果、vim 交换文件、A3 截图素材 |
这里面最容易误判的是 .swp 文件。.myserver.py.swp和.Hello.html.swp是 vim 编辑时产生的交换文件,不是源码;.~lock.附录2…docx#是 LibreOffice 的锁文件,也跟代码无关。编译前先make clean或者手动删掉所有 .o,不然旧环境的编译结果会干扰你本机的链接过程。images.jpg 是 A3 实验里需要服务器返回的图片资源,和 Hello.html 放在同级目录就行。
2.2 调用链与每层职责
A1 实验不是普通的“调 socket API 收发数据”,它的重点是自己在 socket 之上实现一层简化 TCP。整个调用链从应用层往下走,大致是这样一个顺序:
/* 应用层 client.c / server.c ↓ 调用 * Socket API mysock_api.c —— 与 POSIX socket 接口对齐的收发函数 * 简化 TCP stcp_api.c + tcp_sum.c —— 序号管理、确认应答、校验和计算 * 连接解复用 connection_demux.c —— 按四元组(源IP/源Port/目的IP/目的Port)分发数据 * IO 抽象 network_io_tcp.c / network_io_socket.c —— 决定数据走真实 socket 还是自制通道 * 底层模拟 network.c —— 模拟链路层/网络层的收包发包 */ /* 最终编译链: * client.o + server.o + mysock_api.o + stcp_api.o + * transport.o + connection_demux.o + tcp_sum.o + network_io.o + network.o */每层职责一句话能说清:mysock_api.c 负责把实验用的接口包装成你熟悉的socket()、bind()、listen()、accept()、send()、recv()的样子;stcp_api.c 负责在数据包上打序号和确认号,模拟 TCP 的可靠传输;tcp_sum.c 计算校验和,这是 TCP 头里最容易出错又最不直观的字段;connection_demux.c 维护一张哈希表(mysock_hash.h),按四元组把收到的包分发给对应的连接。
看代码的时候重点盯 tcp_sum.c 里的校验和函数。TCP 校验和要覆盖伪头部、TCP 头部和数据三部分,伪头部里包括源 IP、目的 IP、协议号、TCP 长度。很多人的包发出去对方没反应,不是网络不通,而是校验和算错,内核直接把包静默丢弃了。这个现象后面避坑章节会细说。
2.3 编译环境:ENVCFG.MK 与 Makefile 的配合
这套代码的 Makefile 写得比较教学化,把编译器路径和编译参数拆进了 ENVCFG.MK,主 Makefile 通过 include 引入。核心规则大致是这样一个结构:
# 引入环境配置文件:编译器路径、CFLAGS include ENVCFG.MK CC ?= gcc CFLAGS ?= -O0 -g -Wall -DDEBUG OBJS = network.o network_io.o transport.o stcp_api.o \ mysock_api.o connection_demux.o tcp_sum.o all: client server client: client.o $(OBJS) $(CC) $(CFLAGS) -o $@ $^ server: server.o $(OBJS) $(CC) $(CFLAGS) -o $@ $^ %.o: %.c $(CC) $(CFLAGS) -c -o $@ $< clean: rm -f *.o client serverENVCFG.MK 里一般配的是/usr/bin/gcc这类绝对路径和-DDEBUG宏。如果你在自己机器上编译找不到头文件,第一反应应该是去改 ENVCFG.MK 而不是动 Makefile。CFLAGS 里-O0是关闭优化,-g保留调试信息,-DDEBUG会打开代码里#ifdef DEBUG包裹的打印,建议保留这三个。
这里有个血泪经验:OBJS 的顺序不要乱调。链接器按从左到右的顺序解析符号,你把 stcp_api.o 放到 transport.o 后面,某些老版本 binutils 会报undefined reference to stcp_send。如果真遇到这种报错,把报错的函数名对应的 .o 文件往 OBJS 前面挪一行再试。
hello.sh 是配套的一键脚本,常见做法是:先 make,再后台启动 server,然后运行 client 收发一轮数据,最后把两端日志都打出来。用它验证环境是否正常最省事,如果 hello.sh 跑通了,说明协议栈主链路没问题。
3. A3 实验:myserver.py 是怎么把 Hello.html 交到浏览器手里的
3.1 单线程 HTTP 服务器的实现骨架
A3 实验的核心交付物是 myserver.py,配合 Hello.html 和 images.jpg 组成一个能跑的 HTTP 服务。压缩包里没有装 Flask、Django 这类框架,就是纯标准库 socket 写的。一个典型的单线程 HTTP 服务器骨架长这样:
#!/usr/bin/env python3 import socket import os HOST = '127.0.0.1' PORT = 8080 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) def render(path: str, conn: socket.socket) -> None: if path == '/': path = '/Hello.html' file_path = BASE_DIR + path if not os.path.exists(file_path): body = b'404 Not Found' conn.sendall( b'HTTP/1.0 404 Not Found\r\n' b'Content-Type: text/plain\r\n' + b'Content-Length: ' + str(len(body)).encode() + b'\r\n\r\n' + body ) return with open(file_path, 'rb') as f: body = f.read() ext = path.split('.')[-1] ctype = 'text/html' if ext == 'html' else 'image/jpeg' resp = ( b'HTTP/1.0 200 OK\r\n' b'Content-Type: ' + ctype.encode() + b'\r\n' b'Content-Length: ' + str(len(body)).encode() + b'\r\n\r\n' + body ) conn.sendall(resp) srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((HOST, PORT)) srv.listen(5) while True: conn, addr = srv.accept() data = conn.recv(1024) first_line = data.split(b'\r\n')[0] method, req_path, _ = first_line.split() print(method.decode(), req_path.decode()) render(req_path.decode(), conn) conn.close()这个骨架和压缩包里 myserver.py 的职责一致:单线程循环、解析请求行、按路径查文件、拼 HTTP 响应头、返回内容。几个参数值得说明:listen(5)是待连接队列长度,同时来了 5 个连接以内的请求会在内核里排队;recv(1024)一次最多读 1KB,对 Hello.html 这种小文件够用,但如果你把大文件放进去,这里必须要改成循环接收,否则浏览器会长时间转圈。SO_REUSEADDR这个选项很重要,它允许 server 停掉后立即重新绑定同一端口,否则你会被 TIME_WAIT 状态卡住几分钟。
Content-Type 的判断逻辑按扩展名走,html 返回text/html,jpg 返回image/jpeg。这正好对应压缩包里的 Hello.html 和 images.jpg——浏览器拿到图片后能不能正常渲染,就看这一行的值写没写对。
3.2 用 curl 和浏览器做验收
把服务跑起来后,最稳的验证方式不是开浏览器,而是先用 curl 看原始响应头,因为浏览器会把很多错误“美化”掉:
python3 myserver.py & curl -v http://127.0.0.1:8080/Hello.html curl -v http://127.0.0.1:8080/images.jpg正常响应应该长这样:状态行是HTTP/1.0 200 OK,响应头里有Content-Type: text/html和Content-Length,两者缺一个都算不完整。curl 的-v参数会把请求行、响应头全部打印出来,这是判断服务器行为最直接的手段。
浏览器验证就简单多了,地址栏输入http://127.0.0.1:8080/Hello.html,能看到一个带标题的页面;再访问http://127.0.0.1:8080/images.jpg,能直接显示图片。如果 200 返回了但页面白屏,先别怀疑 Python 代码,回去看 Content-Type 是不是写成了text/plain。
3.3 用 Wireshark 验证三次握手与 HTTP 请求
实验指导书里的验收往往不只看结果截图,还要你解释过程。打开 Wireshark,选回环接口lo(因为服务器绑定的是 127.0.0.1),在过滤器里填:
tcp.port == 8080然后重新执行一次 curl,你应该能在抓包列表里看到完整的三次握手:第一个包是SYN,第二个是SYN-ACK,第三个是ACK,之后跟着的是 HTTP GET 请求和响应分组。这里大部分人会犯一个低级错误:Wireshark 默认打开的是物理网卡接口,看不到回环流量,一定要手动切到 lo。
抓包文件最好存成 .pcap 格式,实验报告里贴上三次握手的截图比贴代码更说明问题。指导书里如果要求分析 TCP 报文段结构,展开抓包里那个 TCP 分组的Sequence Number和Acknowledgement Number字段,就能和 stcp_api.c 里定义的序号逻辑一一对应。
4. 避坑记录:验收前最容易翻车的五个问题
4.1 编译期:undefined reference 与被误认的 .swp
现象:make报错,链接阶段出现undefined reference to stcp_send或undefined reference to mysock_accept,但源码里明明有这些函数。
原因:OBJS 列表不完整,或者某个 .c 文件没被编译成 .o 放进链接。还有一个隐蔽原因:系统里残留了旧版本的 .o 文件,链接器优先拿了旧产物,符号表对不上。
解决:先make clean删掉所有 .o,再对照 Makefile 手动确认 OBJS 是否包含 tcp_sum.o、connection_demux.o、stcp_api.o。如果还报错,把报错符号所在的 .o 文件往 OBJS 列表前面挪。另外,copy 压缩包到 Linux 后不要vim直接打开源码又强退,留下的 .swp 文件会让人误以为是重复源码,ls -a看到.myserver.py.swp就删掉。
4.2 运行期:端口占用与数据乱序
现象:server 启动时报Address already in use,换端口能跑,原端口过几分钟才能再用。
原因:上一个 server 进程没有正常退出,端口处于 TIME_WAIT 状态。TCP 主动关闭的一方要等 2MSL,所以立刻重启必然撞车。
解决:代码里加srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1),这是最省事的方案。如果进程还活着,用lsof -i :8080查到 PID 再kill掉。别用kill -9先试kill,给进程一次释放资源的机会。
现象:client 连续发多包数据,server 端收到的顺序乱了或者丢数据。
原因:stcp 层只实现了发送,没有按序号缓存乱序到达的包,或者没有实现超时重传。这个实验里要求自己管序号,不是 TCP/IP 协议栈替你兜底。
解决:检查 stcp_api.c 里发送时是否给每个包赋了递增的 seq,接收时是否按 seq 排序再交给上层。对照 tcp_sum.c 的校验逻辑,确认收到的包没在校验和这步被丢掉。
4.3 验证期:白屏与抓不到回环流量
现象:HTTP 返回 200,浏览器却是白屏,curl 也看不到内容。
原因:Content-Length 没写,或者写成了和 bytes 长度不一致的值。浏览器等不到完整 body 就放弃渲染,表现就是白屏。
解决:用curl -v看响应头,确认Content-Length和 body 实际字节数一致。最稳妥的做法是把 body 读成 bytes 后用len(body)生成长度,而不是手写一个数字。
现象:Wireshark 打开后怎么抓都看不到本机的 HTTP 交换。
原因:抓包接口选错了,抓的是物理网卡而不是回环接口lo。另一个常见原因是过滤器写成了http.server之类不存在的过滤语法,导致没有包被显示。
解决:Wireshark 主界面先选Loopback: lo,过滤器用tcp.port == 8080。想看 HTTP 层的细节,再用http过滤器,但前提是前面 tcp 端口过滤能抓到包。
5. 进阶:把实验代码改成一台看得见的协议示波器
5.1 给 myserver.py 加请求日志
A3 实验跑通之后,我一般会先在 myserver.py 里加一段访问日志,把每次请求的方法、路径、状态码和时间都追加到文件里:
import time LOG_FILE = BASE_DIR + '/access.log' def log(method: str, path: str, status: int) -> None: line = f"{time.strftime('%Y-%m-%d %H:%M:%S')} {method} {path} {status}\n" with open(LOG_FILE, 'a') as f: f.write(line)然后在 render 函数的每个 return 前调用log(method, path, 200)或log(method, path, 404)。这样交实验报告时能附带一份真实的访问记录,比手画流程图有说服力。注意open(LOG_FILE, 'a')是追加模式,不会覆盖历史日志。
5.2 一键自动化验收脚本
A1 和 A3 的实验验收有时要跑多轮,每次都手动启动、手动 kill 很不值。pytest 级别的测试在这里没必要,写个 bash 脚本就够:
#!/usr/bin/env bash python3 myserver.py & SRV_PID=$! sleep 1 CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/Hello.html) kill $SRV_PID if [ "$CODE" == "200" ]; then echo "PASS: expected 200, got $CODE" else echo "FAIL: expected 200, got $CODE" exit 1 fi-w "%{http_code}"只输出 HTTP 状态码,-o /dev/null丢弃响应体。这样脚本的输出只有一个数字,比较逻辑可以被任何 CI 工具吃掉。以后改完代码,跑一遍这个脚本就知道有没有把服务改挂。
5.3 先抓包,再改代码
最后讲一个花过我整晚时间的教训。有一次我改 tcp_sum.c 里的校验和算法,怎么改对端都没反应,代码读了无数遍发现不了问题。后来拿 Wireshark 抓包才发现,问题根本不在校验和,而是我定义的结构体里少了#pragma pack,编译器按 4 字节对齐把校验和字段挤到了错误位置,发出去的包头部就废了。从那以后,我每次做网络实验都强制自己先抓一份基线包,确认正常路径长什么样,再动代码。改完再抓一次包,对比两次分组结构,问题往往一眼就能看出来。
这份资源里的 A1 和 A3 代码,足够你把这个流程完整走一遍:先跑通,再抓包,最后改代码。希望帮到你。
本文还有配套的精品资源,点击获取