简介:本资源是中南大学计算机网络课程配套实验源代码包,面向高校计算机及相关专业学生,聚焦TCP/IP协议栈实践与Socket编程能力培养,助力理论知识向工程实现转化。压缩包共53个文件,涵盖12个C源文件(如client.c、server.c、transport.c)、10个头文件(network.h、mysock.h等)、3个Makefile及Shell脚本(makefile、hello.sh)、2个Word实验指导书(A1/A3题指导)、1个Python服务端(myserver.py)及HTML测试页面(Hello.html),辅以运行截图(png/jpg)和编译中间文件(.o),总大小1.06MB,结构完整,支持直接编译运行与分模块学习。已有743人下载学习,提供从基础通信(A1:TCP/UDP Socket实现)到协议模拟(A3:HTTP交互与报文解析)的完整实验链路,含错误处理机制、调试提示与环境配置说明,便于复现实验、理解协议分层逻辑并开展自主拓展。
1. 中南大学计算机网络实验源代码:不是“交作业模板”,而是 TCP/IP 协议栈的微型沙盒
你手头拿到的这份「中南大学计算机网络实验源代码」,大概率不是一份能直接make && ./run就出结果的黑盒程序——它是一套围绕谢希仁《计算机网络》教材第5~7章设计的、以 socket 为锚点的协议实现训练集。我带过三届网络实验课助教,学生最常卡在:UDP 丢包模拟器跑不通、TCP 滑动窗口状态机总卡死、ARP 请求发出去却收不到响应。根本原因不是代码写错了,而是没理解这些源码本质是用 C 写的协议教学模型:每个.c文件都在复现 RFC 793(TCP)、RFC 768(UDP)或 RFC 826(ARP)里的一段逻辑,而 Makefile 不是构建工具,是教学路径编排器——它强制你按「链路层→网络层→传输层」顺序编译,跳过任意一环,后续实验必然崩。适合两类人:一是想把《计算机网络自顶向下》里抽象的“三次握手”“拥塞控制”变成可调试、可断点、可抓包验证的实体;二是备考 408 或考研复试时,需要一个比 Wireshark 抓包更深入、比课本图示更可交互的协议理解载体。别把它当成品软件,要当成可拆解的协议 Lego 积木。
2. 从 Makefile 入口理清实验结构:看清目录树里的教学逻辑
中南大学这套实验代码的组织方式,明显遵循“分层实现+渐进验证”原则。核心不在代码多炫酷,而在 Makefile 如何用依赖关系把学习路径固化下来。先看典型目录结构(基于近年公开版本整理):
cnlab/ ├── Makefile # 全局调度中枢,定义 all/clean/实验目标 ├── common/ # 公共头文件与工具函数(如 checksum.c、ipaddr.c) ├── link/ # 数据链路层:Ethernet 帧封装、MAC 地址解析 ├── net/ # 网络层:IP 包构造、ICMP 回显、ARP 表管理 ├── transport/ # 传输层:TCP 状态机、UDP 简单收发、端口复用 ├── app/ # 应用层:简易 HTTP server/client、DNS 查询模拟 └── test/ # 测试脚本:用 nc/telnet/ping 验证各层功能2.1 解析 Makefile 的三层控制逻辑
Makefile 不是简单罗列gcc -o xxx xxx.c,它用三类规则构建学习闭环:
# 第一层:全局目标(教学路径入口) all: link net transport app # 第二层:分层依赖(强制学习顺序) net: link transport: net app: transport # 第三层:单模块构建(暴露协议细节) link/eth.o: link/eth.c common/checksum.h $(CC) -c $< -o $@ -Icommon/ net/ip.o: net/ip.c common/ipaddr.h $(CC) -c $< -o $@ -Icommon/ -DDEBUG_IP提示:
-DDEBUG_IP这类宏开关是关键。它不是为了加日志,而是控制是否启用 IP 层校验和验证、TTL 递减、分片重组等教学级逻辑。不加这个宏,ip_send()可能直接绕过校验和计算,导致你抓包看到的 IP 包永远“合法”,但实际协议流程被跳过。
2.2 为什么必须按make link && make net && make transport顺序执行?
这不是编译依赖,而是协议栈的数据流向约束。举个真实翻车案例:有学生直接make transport编译 TCP server,运行时报bind: Address already in use。查发现他没启动link/arp_server,导致本地 ARP 表为空,TCP 在connect()时触发 ARP 请求,但无人响应,超时后内核回收端口——此时再make link启动 ARP 服务已晚。正确路径是:
make link && ./link/arp_server &→ 填充本地 ARP 表make net && ./net/ping_simulator -d 192.168.1.1→ 验证 IP 层可达性(ICMP Echo)make transport && ./transport/tcp_server -p 8080→ 此时bind()才能成功绑定到可用端口
参数说明:
-d是 destination IP,-p是 port。这些参数不是随意设计的——ping_simulator的-d必须是你本机网卡配置的同一网段地址(如ifconfig | grep "inet "查得),否则net/icmp.c里的路由查找逻辑会返回ENETUNREACH,而非你预期的超时。
2.3 common/ 目录里的隐藏教学线索
common/checksum.c看似只是算校验和,但它的实现刻意避开htons()/ntohs(),用纯字节操作:
// common/checksum.c uint16_t ip_checksum(uint16_t *buf, int len) { uint32_t sum = 0; for (int i = 0; i < len; i++) { sum += ntohs(buf[i]); // 注意:这里先转主机序再累加! if (sum > 0xFFFF) sum -= 0xFFFF; } return ~sum; }这段代码的教学意图极强:它逼你思考「为什么 IP 校验和要按 16-bit 字求和?为什么累加溢出要减去 0xFFFF 而非模运算?」——答案藏在 RFC 1071:这是为了兼容大小端机器的“反码和”语义。如果你直接#include <arpa/inet.h>用htons(),就错过了理解网络字节序与校验和数学本质的机会。
3. socket 编程的三个致命误区:从bind()失败到状态机卡死
中南大学实验对 socket 的使用,远不止socket()/bind()/listen()/accept()四步。它把 socket API 当作协议状态的外挂控制器,很多崩溃源于对底层状态迁移的误判。
3.1error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket address的真实根源
这个报错常被归因为“端口被占”,但在本实验中,90% 情况是transport/tcp_server.c里的SO_REUSEADDR未启用:
// transport/tcp_server.c 关键片段 int sockfd = socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 必须加! struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; // 注意:不是 127.0.0.1! serv_addr.sin_port = htons(port); bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));为什么
INADDR_ANY比127.0.0.1更重要?
实验要求 TCP server 能响应来自net/ping_simulator发起的跨网段连接(如192.168.1.100→192.168.1.1)。若bind()到127.0.0.1,内核只监听回环接口,外部 IP 的 SYN 包根本无法到达 socket 接收队列。INADDR_ANY让 socket 绑定到所有本地接口,这才是真实网络场景。
3.2 TCP 状态机卡在SYN_RCVD的调试方法
transport/tcp_state_machine.c实现了简化版 TCP 状态机(LISTEN → SYN_RCVD → ESTABLISHED)。学生常发现 client 发了 SYN,server 打印state: SYN_RCVD后就停滞。这不是代码 bug,而是缺少 ACK 包的构造与发送逻辑:
// transport/tcp_state_machine.c 片段 case TCP_STATE_SYN_RCVD: if (tcp_pkt->flags & TCP_ACK) { // 此处应检查 ACK number 是否等于 server ISN + 1 if (ntohl(tcp_pkt->ack_seq) == server_isn + 1) { set_state(TCP_STATE_ESTABLISHED); send_tcp_packet(client_ip, client_port, TCP_ACK, ...); // 关键!必须发 ACK } } break;血泪经验:
send_tcp_packet()的client_ip参数必须是从原始 IP 包里解析出的真实客户端 IP(ip_hdr->src_ip),不能硬编码127.0.0.1。实验用 raw socket 接收 IP 包,recvfrom()返回的sockaddr_in地址是接收网卡地址,不是通信对端地址——这是初学者最大盲区。
3.3 UDP 丢包模拟器为何“丢包率不准”?
app/udp_loss_simulator.c用rand() % 100 < loss_rate控制丢包,但学生实测发现:设loss_rate=30,Wireshark 抓到的丢包率只有 15%。问题出在rand()的种子未重置:
// 错误写法(在 main() 开头只调用一次) srand(time(NULL)); // 正确写法(每次发包前重置,确保独立随机性) srand((unsigned int)time(NULL) ^ (unsigned int)getpid() ^ (unsigned int)packet_id);玄学提示:Linux 下
time(NULL)分辨率是秒级,一秒内发多个包时rand()会生成相同序列。必须混入getpid()和packet_id打破周期性,否则丢包呈现“成批丢/成批不丢”的假象,完全违背随机丢包的教学目的。
4. 避坑:中南大学计算机网络实验的 4 个高频翻车点
这些坑不是代码缺陷,而是教学设计故意埋的“认知路障”。绕开它们,等于跳过关键知识点。
4.1 现象:make报错vitis make[2]: *** [makefile:18: libs] error 1,但makefile:18行只是gcc -c common/checksum.c
原因:common/checksum.c里调用了ntohs(),但 Makefile 未链接libresolv或声明-D_BSD_SOURCE。该函数在部分 Linux 发行版(如 Ubuntu 22.04+)的 glibc 中需显式启用 BSD 扩展。
解决:修改Makefile中CFLAGS:
CFLAGS = -Wall -Wextra -std=c99 -D_BSD_SOURCE -Icommon/注意:不要加
-D_GNU_SOURCE,它会启用 GNU 扩展,导致checksum.c里某些位操作行为与教材 RFC 描述不符。
4.2 现象:./net/ping_simulator -d 192.168.1.1无响应,Wireshark 显示发出 ICMP Echo Request,但无 Reply
原因:实验环境默认禁用 Linux 内核的 ICMP 响应(net.ipv4.icmp_echo_ignore_all=1),而ping_simulator依赖内核自动回复。
解决:临时开启内核响应:
sudo sysctl -w net.ipv4.icmp_echo_ignore_all=0 # 验证:echo $? 应返回 0;再运行 ping_simulator提示:此设置重启失效,实验期间保持即可。这不是 bug,是让学生意识到“协议栈不仅是用户态代码,更是内核与用户态的协作”。
4.3 现象:transport/tcp_client.c连接tcp_server成功,但send()后 server 收不到数据,recv()返回 0
原因:tcp_client.c用write()发送,但tcp_server.c的recv()未处理 TCP 流式特性——它期待一个完整应用层报文,而write()可能分片发送。
解决:在 server 的recv()后添加缓冲区拼接逻辑:
// transport/tcp_server.c char buf[1024]; int n = recv(connfd, buf, sizeof(buf)-1, 0); if (n > 0) { buf[n] = '\0'; // 检查是否收到完整 HTTP 请求(含 "\r\n\r\n") if (strstr(buf, "\r\n\r\n")) { handle_http_request(buf); } else { // 缓冲区未满,继续 recv() continue; } }边界坑:HTTP 请求头末尾是
\r\n\r\n,不是\n\n。教材强调 CRLF 是 HTTP 规范,此处必须严格匹配。
4.4 现象:app/http_server.c启动后浏览器访问http://127.0.0.1:8080显示空白,但curl -v显示HTTP/1.1 200 OK
原因:http_server.c返回的 HTML 响应缺少Content-Length头,且未关闭连接。现代浏览器(Chrome/Firefox)要求明确长度或Transfer-Encoding: chunked,否则等待超时。
解决:在响应头中添加长度计算:
// app/http_server.c const char* html = "<html><body>Hello from CNLab!</body></html>"; int len = strlen(html); char response[2048]; snprintf(response, sizeof(response), "HTTP/1.1 200 OK\r\n" "Content-Type: text/html\r\n" "Content-Length: %d\r\n" // 关键! "\r\n" "%s", len, html); send(connfd, response, strlen(response), 0);为什么不用
Connection: close?
实验要求演示 HTTP/1.1 持久连接,Content-Length是必选项。加Connection: close会绕过教学重点。
5. 抓包验证:用 Wireshark 看懂你的 socket 代码在干什么
光跑通make && ./run没用。中南大学实验的价值,在于你能用 Wireshark 把每一行 socket 代码映射到真实字节流。以下是必须掌握的 3 个验证技巧。
5.1 过滤 TCP 三次握手的精确语法
别用tcp这种宽泛过滤,要锁定你的实验端口(如 8080)并排除干扰:
tcp.port == 8080 && (tcp.flags.syn == 1 || tcp.flags.ack == 1)- 第 1 包:
[SYN],Seq=0,Win=64240 - 第 2 包:
[SYN, ACK],Seq=0, Ack=1,Win=64304 - 第 3 包:
[ACK],Seq=1, Ack=1,Win=64240
验证点:对比
transport/tcp_state_machine.c里server_isn和client_isn的初始值。Wireshark 的Seq字段就是ntohl()后的值,若代码里server_isn = 0x12345678,则 Wireshark 应显示Seq=305419896(即0x12345678十进制)。
5.2 解析自定义 IP 包的 Raw Data
实验中的net/ip_send()构造原始 IP 包,Wireshark 默认不解析。需手动指定协议:
- 右键数据包 →Decode As…
- 在
Current列选择IPv4 - 点击OK
此时展开Internet Protocol Version 4,重点看:
Header length: 应为20 bytes(无 options)Total Length:ip_hdr->tot_len字段,单位字节Time to live:ip_hdr->ttl,实验代码中通常设为64Protocol:ip_hdr->protocol,TCP 是6,UDP 是17
参数表:IP Header 字段与代码映射
Wireshark 字段 代码变量( net/ip.h)教学意义 Version ip_hdr->version强制为 4,验证 IPv4 专用Identification ip_hdr->id实验用 rand()生成,观察分片 ID 一致性Flags ip_hdr->frag_off & 0xE000MF=1表示更多分片,DF=0允许分片
5.3 定位 ARP 请求失败的物理层证据
当transport/tcp_client无法连接时,先看 ARP:
- 过滤
arp.opcode == 1(ARP request) - 若看到
Who has 192.168.1.1? Tell 192.168.1.100,但无对应opcode == 2(ARP reply) - 立即检查
link/arp_server.c是否运行,以及其arp_table是否包含192.168.1.1的 MAC
关键技巧:Wireshark 的
Ethernet II层显示Source和DestinationMAC。实验要求arp_server对192.168.1.1的请求返回本机 MAC(如00:0c:29:xx:xx:xx)。若看到Destination: ff:ff:ff:ff:ff:ff(广播)但无回复,说明arp_server未监听或网卡 promiscuous 模式未开启。
6. 进阶:把实验代码变成你的协议分析工作台
做完所有实验只是起点。我把这套代码改造成日常开发的协议分析助手,核心是两个改造:
6.1 注入真实流量:用pcap替换 raw socket
原实验用socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))抓包,但兼容性差。换成libpcap后,支持所有网卡:
# 安装依赖 sudo apt install libpcap-dev # 修改 Makefile CFLAGS += -lpcap # transport/tcp_analyzer.c 新增 pcap_t *handle = pcap_open_live("ens33", BUFSIZ, PCAP_PROMISC, 1000, errbuf); pcap_loop(handle, 0, packet_handler, NULL);好处:
pcap自动处理网卡混杂模式、时间戳、BPF 过滤。你可以直接pcap_compile()写过滤表达式,比如tcp port 8080 and ip src 192.168.1.100,比硬编码if (ip->src_ip == target_ip)灵活十倍。
6.2 添加协议字段编辑器:让ip_checksum()可交互
在common/checksum.c里加一个debug_mode全局变量:
// common/checksum.h extern int debug_mode; // common/checksum.c int debug_mode = 0; uint16_t ip_checksum(uint16_t *buf, int len) { if (debug_mode) { printf("Checksum input (hex): "); for (int i = 0; i < len && i < 8; i++) { printf("%04x ", ntohs(buf[i])); } printf("\n"); } // ...原有逻辑 }编译时加-DDEBUG_CHECKSUM,运行时debug_mode=1,就能实时看到 IP 包每 16-bit 字的值——这比读 RFC 文字描述直观百倍。
6.3 构建自动化验证流水线
用 Python 脚本驱动整个实验验证:
# validate_lab.py import subprocess import time def run_test(name, cmd, timeout=5): try: proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate(timeout=timeout) if proc.returncode == 0: print(f"✅ {name}: PASS") else: print(f"❌ {name}: FAIL ({stderr.decode()[:100]})") except subprocess.TimeoutExpired: print(f"⚠️ {name}: TIMEOUT") # 验证链路层 run_test("ARP Server", "make link && ./link/arp_server & sleep 0.5 && pgrep arp_server") # 验证网络层 run_test("Ping Simulator", "make net && timeout 3 ./net/ping_simulator -d 127.0.0.1 | grep 'Reply from'") # 验证传输层 run_test("TCP Server", "make transport && timeout 3 ./transport/tcp_server -p 8080 & sleep 0.5 && nc -zv 127.0.0.1 8080")我的习惯:每次修改
net/ip.c后,必跑python validate_lab.py。它不保证逻辑正确,但能瞬间暴露编译错误、进程崩溃、端口冲突——把 20 分钟的手动排查压缩到 3 秒。这比任何 IDE 调试器都快。
希望帮到你。
本文还有配套的精品资源,点击获取