ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

计算机网络实验源代码实战:从Socket通信到CRC校验的完整指南

计算机网络实验源代码实战:从Socket通信到CRC校验的完整指南 简介中南大学计算机网络实验源代码是一份面向计算机网络课程学生的实操型资源针对A1和A3两道实验题提供2022年最新版源码适合需要将TCP/UDP通信、Socket编程、协议解析等理论落地的学习者。资源包共53个文件压缩后仅1.06MB以C语言源文件.c/.h和编译产物.o为主附带makefile、shell脚本、Python脚本、实验指导书docx/txt及运行结果截图png/jpg各类型文件分工明确便于按需查阅。A1实验代码涵盖network_io_socket、client/server等模块展示了连接建立、数据收发与异常处理的完整流程A3实验通过myserver.py、Hello.html等服务端与页面文件演示HTTP应用与网络服务的实现方式。同时目录中还保留了stcp_api、tcp_sum等传输层相关实现可辅助深入理解协议栈工作机制。目前已有743人学习下载无论是期末实验参考还是自学网络编程这套代码都能提供真实的工程视角和排错思路。1. 中南大学计算机网络实验源代码这门课到底要交什么代码搜「中南大学计算机网络实验源代码」的人多半是谢希仁《计算机网络》配套实验课卡在了某个环节。这门课的实验从来不考你背了多少概念而是要求你把课本里的三次握手、滑动窗口、CRC校验用C或Python写成能编译、能运行、能抓到网络报文的东西。源代码质量直接决定实验报告能不能过也决定期末复习时你手里有没有一份能讲清楚的参照物。我见过太多人从网上随便扒一份代码回来一编译全是warning一运行就段错误最后靠玄学调通。这文章就按我做这套实验的顺序从环境搭建、Socket聊天程序、CRC与停等协议一路写到避坑和进阶验证。不管你是中南大学在读、还是外校要做同类实验照着这条路径走比漫无目的拖代码靠谱得多。2. 先把环境立住Linux下编译运行Socket实验代码的两条必走路径2.1 Socket API在Windows与Linux下的关键差异别让环境坑了你计算机网络实验的参考代码绝大多数是Linux版本原因是Linux的Socket API直接暴露在C标准库的系统调用里写起来干净。Windows的Winsock要额外做两件事一是调用WSAStartup初始化Winsock库二是链接ws2_32.lib否则会报一堆undefined reference。很多同学在Windows上编译Linux代码翻车根本不是逻辑错是环境不匹配。我一般建议直接装一个Ubuntu虚拟机或者用WSL。判断你的程序跑在哪种环境看头文件就够了Linux用#include sys/socket.h和#include netinet/in.hWindows用#include winsock2.h而且Linux里关闭socket是close()Windows里是closesocket()。如果你只有Windows环境也不是不能做但要在代码开头包一层条件编译把这两处差异吞掉。2.2 一份带Makefile的最小工程gcc参数与调试宏实验代码不需要多复杂但一个能一键编译的Makefile能帮你省掉大量重复敲命令的时间。下面是我常用的最小工程三个文件放在同一目录server.c、client.c、Makefile。CC gcc CFLAGS -g -Wall -O0 -DDEBUG LDFLAGS -lpthread all: server client server: server.c $(CC) $(CFLAGS) -o server server.c $(LDFLAGS) client: client.c $(CC) $(CFLAGS) -o client client.c clean: rm -f server client *.o .PHONY: all clean代码逻辑说明-g生成调试符号让gdb能告诉你段错误出现在哪一行这个参数在实验调试期必须开-Wall把常见警告全亮出来别嫌吵警告往往就是隐患-O0关闭优化防止调试时变量被优化掉导致看不到真实值-lpthread链接线程库TCP聊天实验要用多线程不加会报pthread_create未定义。参数说明如果你的实验只用单线程比如只做CRC校验可以去掉-lpthread。还有一点容易被忽略某些在线判题或实验平台上gcc默认不开-stdc11你代码里用了C11的_Generic或者柔性数组就会编译失败在CFLAGS里显式加一行-stdgnu11能省不少心。2.3 用Python对照实现一份同样的逻辑调不通时的参照物C语言网络实验最痛苦的不是逻辑是指针和内存。我一般建议同一份Socket逻辑用Python各写一遍Python版本作为行为参照C版本作为提交版本。两边跑的结果不一致时问题基本出在C的缓冲区管理上而不是协议理解错了。import socket server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((127.0.0.1, 8888)) server.listen(5) while True: conn, addr server.accept() data conn.recv(1024) if data: print(frecv from {addr}: {data.decode()}) conn.send(data) # 回显 conn.close()逻辑说明Python的socket接口和Linux C的调用几乎一一对应bind对应bindlisten(5)对应listen(fd, 5)conn.send(data)对应send(client_fd, buf, n, 0)。先用Python把「发什么、收什么、什么时候关闭」跑通再回C里用perror定位哪一步返回值是-1思路会清晰很多。参数说明SO_REUSEADDR在Python里用setsockopt设置对应C里的setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt))它解决的是服务端重启时端口被TIME_WAIT占住的问题下面避坑章节会细说。3. 核心实验一TCP Socket聊天程序——从三次握手到粘包处理3.1 实验要求的标准套路一个服务端、两个客户端、多线程收发中南大学这类的计算机网络实验第一个必做项目几乎都是TCP Socket通信。实验要求一般是实现一个服务端和多个客户端客户端连上后能发消息服务端能接收并回显多个客户端之间不能互相阻塞。这就意味着服务端必须支持并发最常见的实现是accept循环里给每个连接开一个pthread线程。这个实验表面考的是Socket API实际考的是三件事第一是否理解三次握手发生在connect和accept之间而不是在send里第二是否理解阻塞调用会让程序卡死所以必须多线程第三是否理解TCP是字节流协议没有消息边界所以发送多条消息时会粘包。实验报告想拿高分不是把代码跑通就行而是要把第三点写清楚。3.2 能直接编译运行的C语言TCP聊天代码关键函数逐个说明我给出一个服务端最小实现配合上面的Makefile就能编过。注意这里故意保留了最简形态方便你在上面加自己的功能。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include stdint.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include pthread.h #define PORT 8888 #define BACKLOG 5 #define BUF_SIZE 1024 void *handle_client(void *arg) { int fd *(int *)arg; free(arg); char buf[BUF_SIZE]; ssize_t n; while ((n recv(fd, buf, sizeof(buf) - 1, 0)) 0) { buf[n] \0; printf(recv %zd bytes: %s\n, n, buf); send(fd, buf, n, 0); // 回显 } close(fd); printf(client fd %d closed\n, fd); return NULL; } int main(void) { int server_fd; struct sockaddr_in addr; int opt 1; server_fd socket(AF_INET, SOCK_STREAM, 0); setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_addr.s_addr htonl(INADDR_ANY); addr.sin_port htons(PORT); if (bind(server_fd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(bind); exit(1); } if (listen(server_fd, BACKLOG) 0) { perror(listen); exit(1); } printf(listening on 0.0.0.0:%d\n, PORT); while (1) { struct sockaddr_in client_addr; socklen_t len sizeof(client_addr); int client_fd accept(server_fd, (struct sockaddr *)client_addr, len); if (client_fd 0) { perror(accept); continue; } int *fd malloc(sizeof(int)); *fd client_fd; pthread_t tid; pthread_create(tid, NULL, handle_client, fd); pthread_detach(tid); } close(server_fd); return 0; }逻辑说明socket()创建TCP套接字setsockopt设置地址重用bind把套接字绑定到0.0.0.0:8888listen进入监听状态。accept每返回一个新的client_fd就用pthread_create开一个线程处理这个连接主线程继续阻塞在accept上等下一个客户端。handle_client里的循环不断recv读到0表示对端关闭读到-1表示出错。参数说明BACKLOG设为5是实验惯例含义是内核为等待accept的连接队列预留的槽位数。BUF_SIZE设为1024就能覆盖绝大多数课程实验的短消息不要设成1或2字节那会让你的recv频繁触发把日志刷到没法看。这里用htons(PORT)把端口号转成网络字节序很多新人漏掉这步导致bind失败属于最常见的翻车点之一。3.3 粘包与半包实验报告里必须写的两个改进点上面的代码能跑但有个隐藏问题如果客户端连续发两条消息服务端可能一次recv就收到了两条这就是粘包。还有可能一条消息分两次到达第一次recv只收到一半这就是半包。TCP是字节流不保消息边界实验报告里写清楚这个再给出解法分数立刻不一样。// 发送侧先发4字节消息长度再发消息本体 uint32_t len htonl((uint32_t)strlen(msg)); send(fd, len, 4, 0); send(fd, msg, strlen(msg), 0); // 接收侧先收满4字节解析出len再收len字节 uint32_t net_len; recv_full(fd, net_len, 4); uint32_t msg_len ntohl(net_len); recv_full(fd, buf, msg_len);代码逻辑说明这个方案的思路是给每条消息加一个固定长度的头部头部里记录消息长度。发送侧先发长度后发数据接收侧先收长度再根据长度循环收数据直到收满为止。recv_full要自己写循环因为单次recv不能保证收满指定字节数。参数说明长度字段用4字节无符号整数是为了兼容长度超过64KB的实验数据如果你确定实验里每条消息不超过255字节可以用1字节但4字节是通用做法。注意htonl和ntohl这组字节序转换网络协议规定多字节整数使用大端序实验报告里能写出这行代码的含义说明你真的理解了网络字节序。4. 核心实验二CRC校验与停等协议——把数据链路层搬到用户态4.1 CRC-32查表法多项式、初值与输出异或的配合第二个常见实验是实现数据链路层的差错检测通常要求用CRC-32。很多网上代码直接把0x04C11DB7塞进多项式但查表法里异或用的却是0xEDB88320这两个数是什么关系写报告时说不清楚的人一大把。简单说0x04C11DB7是CCITT标准的原始多项式0xEDB88320是它的反转形式因为查表法从低位开始移位必须用反转多项式。#include stdint.h #include stddef.h static uint32_t crc_table[256]; void crc32_init(void) { for (uint32_t i 0; i 256; i) { uint32_t c i; for (int k 0; k 8; k) { if (c 1) { c (c 1) ^ 0xEDB88320; } else { c 1; } } crc_table[i] c; } } uint32_t crc32_calc(const uint8_t *data, size_t len) { uint32_t crc 0xFFFFFFFF; for (size_t i 0; i len; i) { crc crc_table[(crc ^ data[i]) 0xFF] ^ (crc 8); } return crc ^ 0xFFFFFFFF; }逻辑说明crc32_init一次生成256个表项每个表项代表一个字节的所有可能取值在校验过程中的变化。crc32_calc里初始值是0xFFFFFFFF每一步先用CRC当前值的低字节和待校验数据异或索引查表再和右移8位后的原CRC异或。最后再异或0xFFFFFFFF输出这一步叫输出异或很多实现省略它导致和标准zlib算出的结果不一致。参数说明三个参数必须配套——多项式0xEDB88320、初值0xFFFFFFFF、输出异或0xFFFFFFFF。只改其中任一项算出来的结果都会和标准库不一致。实验里想验证自己算法对不对最简单的方法是计算字符串123456789的CRC-32标准结果是0xCBF43926对不上就是参数配错了。4.2 停等协议实现发送、确认、超时重传的最小闭环光校验还不够实验经常要求在此基础上实现停等协议。停等协议的逻辑是发送方发一帧等接收方回ACK收到ACK再发下一帧没收到就超时重传。用Python写这个逻辑最直观而且能演示丢包和重传的流程。import socket import struct import time import zlib def send_frame(sock, seq, data): length len(data) crc zlib.crc32(data) 0xFFFFFFFF frame struct.pack(!BI, seq, length) data struct.pack(!I, crc) sock.send(frame) def recv_frame(sock): header recv_exact(sock, 5) seq, length struct.unpack(!BI, header) payload recv_exact(sock, length) crc_recv struct.unpack(!I, recv_exact(sock, 4))[0] crc_calc zlib.crc32(payload) 0xFFFFFFFF valid (crc_recv crc_calc) return seq, payload, valid def recv_exact(sock, n): buf b while len(buf) n: chunk sock.recv(n - len(buf)) if not chunk: raise ConnectionError(peer closed) buf chunk return buf逻辑说明发送方用struct.pack(!BI, seq, length)打包帧头包含1字节的序号和4字节的长度!表示网络字节序big-endian。接收方先收5字节帧头解析出序号和长度再按长度收数据最后收4字节CRC把收到的CRC和本地重新计算的CRC比对valid字段就是接收方要做差错判断的依据。参数说明这里故意没写超时重传循环因为实验要求里重传策略各不相同。有的要求固定超时2秒有的要求动态估算RTT。你先跑通单帧的发送接收和校验再往外面包一层while循环和select超时逻辑就清楚了。4.3 验证停等协议正确性用模拟丢包而不是靠运气停等协议最容易翻车的点不是正常流程而是丢包后的状态恢复。实验里想验证重传逻辑不需要真的在网络上丢包可以用一个简单的包装函数模拟丢包。import random def lossy_send(sock, frame, loss_rate0.3): if random.random() loss_rate: sock.send(frame) return True return False # 模拟丢包 # 发送方循环里使用 send_frame(sock, seq, data) try: ack recv_frame(sock) if ack[2] and ack[0] seq: pass # 收到正确ACK推进seq except socket.timeout: lossy_send(sock, frame, loss_rate0.0) # 重传时不再丢包逻辑说明lossy_send用随机数决定这一帧是否真的发出去loss_rate设成0.3表示30%的帧会被丢弃。这样你不需要物理拔网线就能看到发送方在超时后重传的日志。重传时把loss_rate传0.0是为了避免连续丢包导致实验一直跑不完。参数说明超时时间设置有个血泪经验不要设太短。本机回环RTT不到1毫秒但虚拟机里CPU调度抖动可能让ACK晚到几十毫秒超时设500毫秒比较稳妥。如果设成100毫秒你会在日志里看到大量不必要的重传反而掩盖了真正的bug。5. 中南大学计算机网络实验避坑指南5个典型的翻车点与排查路径5.1 服务端重启就报Address already in use换端口才能跑现象服务端程序崩溃后重新启动bind直接失败报Address already in use换一个端口就能跑起来。原因TCP连接关闭后主动关闭方会进入TIME_WAIT状态默认持续60秒。这时内核还保留着端口和连接的残留信息如果你没有设置SO_REUSEADDRbind就会拒绝。解决在socket之后、bind之前调用setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt))。这就是第2章Python代码里那一行的价值。注意这个选项必须在bind之前设置放在listen之后再设置不生效。5.2 客户端connect卡住不动程序像死了一样现象客户端执行到connect就停住没有报错也没有返回CtrlC才能杀掉。原因服务端没有启动、服务端IP或端口写错、防火墙拦截SYN包。connect默认是阻塞模式它会在内核里重发SYN超时时间很长看起来就像死循环。解决先确认服务端真的在监听用netstat -tlnp | grep 8888查看端口状态。再在客户端代码里用getpeername或getsockopt(SO_ERROR)拿到更具体的错误码。排查顺序是先ping服务端IP再telnet端口最后才怀疑代码。绝大多数情况是服务端根本没起来或者客户端把IP写成了服务器主机名。5.3 多线程printf输出乱序实验截图没法看现象多个客户端同时发消息时服务端终端里打印的日志顺序错乱两行消息挤在一起甚至出现乱码。原因printf内部有行缓冲但多线程同时调用时输出是一个非原子操作两个线程的缓冲内容会交叉写入。这不是Socket问题是并发问题。解决给打印加一把互斥锁pthread_mutex_t在printf前后lock和unlock。或者用一个全局日志函数内部统一加锁。实验报告里能写出这个细节说明你真的考虑过多线程安全比那些把日志打乱然后截图交上去的同学强不少。5.4 CRC校验该报错却没报隐藏的字节序问题现象发送方计算CRC并附在帧尾接收方校验时数据明明被改了但CRC比对仍然通过。原因如果帧里的CRC字段用htonl发送接收方却没有用ntohl解析直接按本地字节序读取就会出现这个诡异结果。当传输数据没被破坏时这种错误反而可能碰巧一致一旦破坏就校验不出来。解决收发两侧全部使用struct.pack(!I, crc)和struct.unpack(!I, ...)!强制网络字节序。同时写一个自测用例手动翻转数据里的一个字节确认接收方valid变成False。这一步不做你的校验逻辑在黑匣子里永远不知道对不对。5.5 实验报告和代码被判雷同的隐形风险现象明明是自己写的代码但实验报告和某位同学相似度超过阈值被判雷同。原因同一个班的同学用同一份网上模板改变量名没改干净注释里还留着原作者的名字连return 0前面空几行都一样。特殊源文件里的注释和函数命名习惯会成为查重特征。解决我一般会拿模板代码自己重写一遍记忆里的逻辑写成自己的表达方式变量名用自己习惯的命名函数拆分也不一样。做法是「不参考网络代码自己对着课本的流程图写写完再对照模板修正细节」这样既安全也真正理解。实验报告里流程图自己画代码贴自己版本的说明文字用自己的话写。6. 验证与进阶给实验代码加自动化测试和心跳重传6.1 用一个Shell脚本把TCP实验的三个用例全跑通每次改完代码手动敲命令验证太容易漏我习惯写一个最小的回归脚本把「服务端启动、客户端连接、消息回显、断开重连」四个动作合成一条命令。#!/bin/bash make server client /dev/null 21 || exit 1 (./server server.log 21 ) SERVER_PID$! sleep 1 echo hello csu network lab | ./client 127.0.0.1 | grep -q hello echo test1 pass || echo test1 fail kill $SERVER_PID逻辑说明这个脚本把服务端放到后台运行输出写进server.log然后启动客户端发送一条测试消息用grep -q判断回显是否成功。测试完杀掉服务端避免端口残留。配合第5章的SO_REUSEADDR重复执行也不会报端口占用。6.2 给停等协议加一个心跳重传从实验代码走向工程能力停等协议实验做完了可以再往前一步实现一个心跳包机制让接收方在一定时间内没收到任何数据帧就知道对端掉线了。这个方向在热词里叫「心跳包重传源代码」也是面试八股里常被追问的点。class Heartbeat: def __init__(self, sock, interval3, timeout10): self.sock sock self.interval interval self.timeout timeout self.last_ack time.time() def send_beat(self): try: self.sock.send(b__beat__) self.last_ack time.time() except OSError: raise ConnectionError(send beat failed) def check(self): if time.time() - self.last_ack self.timeout: raise ConnectionError(fno heartbeat ack in {self.timeout}s)逻辑说明send_beat每3秒发一个心跳帧check检查距离上次ACK的时间是否超过10秒超过就判定对端掉线。参数interval和timeout配合的含义是连续错过3到4次心跳才认为连接死亡可以过滤偶发的网络抖动。把这段代码附在实验报告末尾说明你理解了应用层保活与TCP keepalive的区别这对复试面试很有帮助。我自己的习惯是每个实验都留一个分阶段的git记录先提交能编译的空架子再提交单客户端跑通最后提交多线程和粘包处理。这样每一步翻车都知道改了什么导致的而不是最后一次性面对一堆错误。这套思路也是我反复强调「环境先行、验证跟上」的原因希望帮到你。本文还有配套的精品资源点击获取
返回列表