ARTICLE DETAIL

资讯详情

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

TCP数据包传输项目实战:从socket编程到Wireshark抓包验证

TCP数据包传输项目实战:从socket编程到Wireshark抓包验证 简介一份面向燕山大学计算机网络三级项目的TCP传输数据包实践资源适合正在学习TCP协议与网络编程的高校学生也适合课程设计、期末项目或竞赛备赛参考。包内提供完整的C/C工程实现包含client1与server1两个Visual Studio解决方案可分别编译运行覆盖三次握手、数据分片与重组、滑动窗口流量控制、拥塞控制、ACK确认及四次挥手等关键机制。资源共60个文件以cpp/h源码、sln/vcxproj工程配置、exe可执行程序为主辅以pdb调试符号、tlog/log构建日志与obj中间文件压缩包大小约27.88MB便于直接打开运行和对照调试。已有1352人学习下载对于想要快速跑通TCP通信流程、分析数据包收发细节、对照代码理解慢启动与快速重传、快速恢复等算法并完成课程报告的同学是一份可直接参考的完整实现。1. 燕大计算机网络三级项目TCP传输数据包到底要把什么做通燕大计算机网络三级项目拿到手里题目就一行字TCP传输数据包。很多同学第一反应是去写几千字论文讲TCP三次握手结果答辩时被问一句“你的程序抓包给我看”就卡住了。这个项目的本质不是写论文是让一个数据包从客户端进程出发穿过传输层、网络层、数据链路层再完整落到服务端进程手里并且你能在 Wireshark 里亲眼看到整个过程。它最适合作业临近、需要一份能跑通又能讲清楚的工程的人也适合想补 TCP 细节的从业者拿来练手核心就三件事——socket 编程、数据包格式设计、抓包验证三件都通了这个项目就稳了。2. 从三次握手到字节流TCP数据包项目的协议底子2.1 TCP可靠传输的四个保障序号、确认、重传与连接管理TCP 和 UDP 最大的差别不是快慢而是可靠传输。可靠不是说网络不出错而是收发双方通过序号、确认、重传把出错兜住。做这个项目如果只把 socket 当管道用发完就完事那跟 UDP 随意 sendto 没有本质区别——所以答辩时老师最爱问一句你凭什么说你的传输是可靠的答不出 TCP 报文头里那几个字段程序再花哨也白搭。先看连接怎么建立。客户端 connect 发出 SYN携带一个随机初始序号 ISN服务端回应 SYNACK带上自己的 ISN同时对客户端的 ISN 做确认客户端再回一个 ACK双方进入 ESTABLISHED。写代码时看不到这三个报文但它们决定了 connect 的返回值。如果中间有防火墙把 SYN 丢弃connect 会卡在 SYN_SENT 状态十几秒甚至更久这就是最典型的“程序没问题但连不上”的场景。TCP 是字节流没有消息边界。客户端连续 send 两个 100 字节的包服务端 recv 一次可能读走 200 字节也可能分三次读成 50、50、100。这是后面粘包问题的根源。所以题目叫“传输数据包”时双方必须约定包格式——这是自定义协议头的价值所在。课程项目的常见做法是定一个固定结构体头部放长度字段接收方按长度把包拆出来。我对照的是谢希仁《计算机网络第8版》传输层这一章。重点记住下面这几个字段后面抓包、写代码都用得上。TCP报文头字段作用抓包里怎么看源端口 / 目的端口标识两端进程显示为 9000、9000序号 Sequence本报文段第一个字节的偏移SEQ1 表示相对序号 1确认号 Acknowledgment期望对端下一个字节的序号ACK2 表示 1 号字节已收到标志位 SYN/ACK/FIN/PUSH控制连接管理握手包标志列里看还有一个和抓包直接相关的数字MSS。以太网 MTU 是 1500 字节IPv4 头占 20TCP 头占 20所以最大段长度是 1460 字节。如果把数据包定义成 4KB 的 struct内核会把一个逻辑包拆成三个 TCP 段recv 就要拼三次。项目里把 payload 控制在 512 字节以内抓包更干净recv 逻辑也更直观。动手前建议先在终端里跑一条netstat -an | grep 9000观察连接在 LISTEN、SYN_SENT、ESTABLISHED 之间的状态迁移。状态机跑一遍比你背十遍状态转换图都管用。2.2 项目结构与选型为什么用C语言socket而不是Python燕大计算机网络三级项目的常见要求是独立完成、能演示收发数据、能说明数据包格式、最好有抓包记录。评分点落在协议理解上而不是界面多花哨。所以我选了 C 语言加 Linux 的 Berkeley socket。原因是这套接口的每个系统调用都直接对上教材里那张用户态到内核态的调用图socket、bind、listen、accept、connect 一一对应讲起来清楚。如果换成 Pythonsocket 库把细节全包住答辩时老师追问“底层发生了什么”容易露怯。项目可以拆成三块客户端发包、服务端收包、抓包验证。工程结构建议下面这样紧凑不散。模块文件职责客户端tcp_client.c构造数据包、建立连接、发送服务端tcp_server.c监听、接受连接、按包头拆包构建Makefile编译两个可执行文件验证记录pcap/放导出的抓包文件、截图端口我选 9000避开 1-1024 的特权端口也避开 8080、3306 这类容易被本机服务占用的端口。两台 Linux 虚拟机联调时9000 端口只要在防火墙里放行就能通。注意主机字节序和网络字节序不同端口号要用htons()转成网络序否则抓包看到的端口就会是反的。3. 用C语言把数据包送出去服务端与客户端的完整实现3.1 服务端socket、bind、listen、accept 每一环都在干什么服务端这条链路是固定顺序socket()创建套接字setsockopt()设置地址复用bind()绑定端口listen()进入监听accept()接受连接然后循环recv()收数据。每一步都有典型翻车点我直接给出可编译的代码。#include stdio.h #include string.h #include unistd.h #include arpa/inet.h #define PORT 9000 #define BUFSZ 1024 typedef struct { int seq; /* 包序号网络字节序 */ int len; /* payload 长度 */ char payload[512]; /* 数据内容 */ } DataPacket; /* 按固定结构体收满一个包防止读到半个包 */ int recv_packet(int fd, DataPacket *pkt) { unsigned char *dst (unsigned char *)pkt; int need sizeof(DataPacket); int got 0; while (got need) { int n recv(fd, dst got, need - got, 0); if (n 0) return -1; got n; } pkt-seq ntohl(pkt-seq); pkt-len ntohl(pkt-len); return 0; } int main(int argc, char *argv[]) { int raw argc 1 strcmp(argv[1], --raw) 0; int lfd socket(AF_INET, SOCK_STREAM, 0); int opt 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(PORT); addr.sin_addr.s_addr htonl(INADDR_ANY); if (bind(lfd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(bind); return 1; } listen(lfd, 10); printf(listening on port %d\n, PORT); int cfd accept(lfd, NULL, NULL); printf(client connected\n); if (raw) { char buf[BUFSZ]; int n; while ((n recv(cfd, buf, sizeof(buf), 0)) 0) { buf[n] \0; printf(raw recv: %s\n, buf); } } else { DataPacket pkt; while (recv_packet(cfd, pkt) 0) { printf(recv seq%d len%d payload%s\n, pkt.seq, pkt.len, pkt.payload); if (pkt.seq -1) break; } } close(cfd); close(lfd); return 0; }代码里recv_packet是核心它记录了已经读到的字节数每次只向内核要“剩余还没读满”的那部分。TCP 是字节流一次 recv 可能返回半个包也可能返回两个包所以必须靠循环凑够sizeof(DataPacket)才返回。这个函数也是后面拆粘包的雏形。SO_REUSEADDR不是可选项是必选项。没有它程序被 CtrlC 杀掉后端口会停留在 TIME_WAIT 状态下次 bind 直接报 Address already in use。memset清零sockaddr_in是为了避免结构体里有残留垃圾。INADDR_ANY表示监听所有网卡地址这样虚拟机里的客户端连过来也能接到。还要注意结构体对齐问题。DataPacket默认 4 字节对齐两端都用 gcc 编译没问题如果另一侧是 Windows 程序建议加#pragma pack(1)并且显式做字节序转换否则对端按不同的对齐方式解析结构体读出来的 seq 和 len 全是乱码。3.2 客户端connect 之后数据包是怎么组装的客户端要做的比服务端少socket、connect、构造数据包、send。但数据包组装是这里最容易写错的环节。我按“字段字节序转换、发送、退出包”的顺序写。#include stdio.h #include string.h #include unistd.h #include arpa/inet.h #define PORT 9000 #define SERVER_IP 127.0.0.1 typedef struct { int seq; int len; char payload[512]; } DataPacket; int send_packet(int fd, int seq, const char *msg) { DataPacket pkt; pkt.seq htonl(seq); pkt.len htonl((int)strlen(msg)); memset(pkt.payload, 0, sizeof(pkt.payload)); strncpy(pkt.payload, msg, sizeof(pkt.payload) - 1); return send(fd, pkt, sizeof(DataPacket), 0); } int main(void) { int fd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(PORT); inet_pton(AF_INET, SERVER_IP, addr.sin_addr); if (connect(fd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(connect); return 1; } for (int i 0; i 10; i) { char msg[64]; snprintf(msg, sizeof(msg), packet-data-%d, i); int n send_packet(fd, i, msg); if (n ! (int)sizeof(DataPacket)) { printf(send %d partial, only %d bytes\n, i, n); break; } printf(sent seq%d bytes%d\n, i, n); usleep(100000); /* 100ms方便抓包时区分每个包 */ } send_packet(fd, -1, quit); close(fd); return 0; }客户端最关键的细节是把seq、len用htonl()转到网络字节序再发服务端收到后用ntohl()转回主机序。很多课设翻车就翻在这里同一台机上跑通换两台机型序不一致就解析乱。usleep(100000)是刻意加的参数。它让每个数据包间隔 100ms 发出好处是抓包文件里一个包对应一个 TCP 段答辩时一帧一帧讲得清楚。如果去掉10 个包可能被 Nagle 算法合并成一两个 TCP 段抓包记录就不好看了。inet_pton把字符串形式的 IP 转成二进制网络序地址比老式的inet_addr更安全它支持 IPv6 地址格式。SERVER_IP默认127.0.0.1如果是两台虚拟机联调改成服务端那台机器的 IP 即可。3.3 编译与联调Makefile参数、双终端运行和tcp调试助手把两个源文件放在同一目录配合下面的 Makefile 构建。-Wall -Wextra打开警告-O2做优化加减乘除这类表达式不会有什么坑但警告必须清零再提交。CC gcc CFLAGS -Wall -Wextra -O2 ALL tcp_server tcp_client all: $(ALL) tcp_server: tcp_server.c $(CC) $(CFLAGS) -o $ $^ tcp_client: tcp_client.c $(CC) $(CFLAGS) -o $ $^ clean: rm -f $(ALL) *.o跑法的顺序很讲究先起服务端再起客户端。终端一执行./tcp_server终端二执行./tcp_client服务端窗口会依次打印recv seq0 len... payloadpacket-data-0这样的日志说明完整包已经到达。想在 Windows 上用 tcp 调试助手 1.17 连 Linux 虚拟机里的服务端做连通性测试执行./tcp_server --raw然后调试助手填上虚拟机 IP 和 9000 端口点连接后发普通文本。注意调试助手发的是原始字节和自定义结构体不兼容所以用 raw 模式看文本就行别拿它验证包格式要验证结构化数据包还是要跑自己的客户端。4. 用Wireshark验证数据包三次握手、序列号与延迟调参4.1 抓包三件事选接口、写过滤表达式、看懂握手程序跑通只是第一步答辩时能把抓包记录甩出来才是真正的加分项。Wireshark 里只有三件事要做对选对接口、过滤表达式写干净、认得出三次握手。先选接口。客户端连127.0.0.1时所有流量都走回环接口Wireshark 里叫 lo 或 Loopback选它才能抓到包。如果两台虚拟机联调选实际通信的那块网卡不要凭感觉选 eth0先用ip addr确认 IP 落在哪块接口上。接口选错是抓包里最常见的玄学问题抓半天一个包都看不见其实流量根本不经过你选的接口。过滤表达式这样写tcp.port 9000。如果想只看某一次完整的连接流加tcp.stream eq 0stream 编号在右键任一 TCP 包里的 “Follow TCP Stream” 可以看到。过滤写好后重新跑一遍客户端立刻能看到四组包一个 SYN、一个 SYNACK、一个 ACK然后是十几组数据包和确认包。三次握手三帧的特征非常固定帧序号报文方向标志位序号 / 确认号1客户端 → 服务端SYNSEQ0无ACK2服务端 → 客户端SYNACKSEQ0ACK13客户端 → 服务端ACKSEQ1ACK1这里 Wireshark 默认显示的是相对序号从 0 开始数。真正理解知识点的人会点开第二帧在 Sequence Number 里看到服务端的 ISN 是随机值Acknowledgment Number 等于客户端的 ISN 加 1。答辩时讲到这一层比背一百句“可靠传输”都管用。不想开图形界面的环境下用 tcpdump 也能出同样的结果sudo tcpdump -i lo tcp port 9000 -w tcp.pcap -c 20-i lo指定回环接口-w tcp.pcap把结果写成 Wireshark 可打开的 pcap 文件-c 20表示抓到 20 个包就退出。跑完客户端后 CtrlC 结束 tcpdump把 pcap 文件拷回 Windows 用 Wireshark 分析效果完全一致。4.2 参数调优TCP_NODELAY、接收缓冲区与吞吐量抓包除了看握手还要看数据包的发送节奏。如果你发现客户端循环里明明send了 10 次Wireshark 里却只有四五个数据段别慌这是 Nagle 算法在工作当一个小包发出后还没收到 ACK内核会把后面几个小包攒起来合并发送。这对交互型协议是延迟灾难但如果你传的是大文件、写的是字节流反而省带宽。这个项目我建议显式关掉 Nagle让数据包一个包一个 TCP 段地发抓包记录更干净。代码加在 connect 之后int flag 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, flag, sizeof(flag));一个容易忽略的点是TCP_NODELAY 必须在连接建立后设置写在 socket 创建后、connect 前也行但最稳妥的位置是 connect 成功之后。关掉 Nagle 后send的粒度直接决定 TCP 段的粒度一个 520 字节的DataPacket就是一个 TCP 段。接收缓冲区参数也值得调。默认的收发缓冲区通常够用但如果要传大文件把缓冲区调大能减少 ACK 交互次数int rbuf 1 20; /* 1MB */ setsockopt(fd, SOL_SOCKET, SO_RCVBUF, rbuf, sizeof(rbuf)); int sbuf 1 20; setsockopt(fd, SOL_SOCKET, SO_SNDBUF, sbuf, sizeof(sbuf));注意setsockopt对收发缓冲区的大小有上下限约束内核会把传入的值取与系统限制的交集。设了 1MB 不代表一定拿到 1MB抓包时看到窗口扩大也就够了。窗口字段本身就是 TCP 流量控制的体现这句话记下来答辩直接答到点子上。5. 常见问题与避坑TCP传数据包最容易翻车的四个点5.1 端口被占用bind 报 Address already in use现象服务端刚跑一次CtrlC 结束再启动就报bind: Address already in use。最迷惑的是用netstat -an | grep 9000根本看不到进程在监听。原因TCP 连接的主动关闭方会进入 TIME_WAIT 状态一般持续 2MSL约 2 到 4 分钟。此时虽然进程已经退出端口还被内核占着直接 bind 会失败。解决在listen前加SO_REUSEADDR这允许端口处于 TIME_WAIT 时重新绑定。如果加了还报错用ss -tanp | grep 9000查出占用进程并kill掉。我从那以后每个服务端程序默认都带上这个选项已经成了肌肉记忆。5.2 收包收到半个TCP粘包与拆包的最简单解法现象客户端发 10 个包服务端打印只有 7 条日志有的 payload 是两条数据拼在一起的有的 payload 只剩半截。原因TCP 是字节流没有消息边界。send两次的数据可能被内核合并在一个 TCP 段里发出接收方一次recv读回来就拼成了一个数据块反过来一个大数据包也可能被拆成两段到达第一次recv只读到前半截。解决约定固定报头接收方按字节数凑满一个包再返回。就是我实现里recv_packet的做法——用一个while循环记录got计数recv只读剩余的部分直到got need。这是最简单的拆包方案固定包长没有歧义。进阶场景里如果包长不固定就把前 4 字节设为长度字段先收 4 字节知道 len再收 len 字节。5.3 对端关了还写SIGPIPE 把客户端进程抬走现象服务端先退出客户端再send终端直接打出一句Broken pipe进程消失。没有任何错误码可以让你读进程就这么没了。原因send到了一个已经关闭的连接上内核发送 SIGPIPE 信号默认动作是终止进程。这在客户端进程要长时间运行、需要重连的场景里非常致命——写了一半的日志、没保存的数据全丢。解决在程序开头忽略 SIGPIPE再把send的返回值当作错误来源signal(SIGPIPE, SIG_IGN);忽略之后send会返回 -1配合errno里的EPIPE代码就能感知到连接已经断开然后走重连或退出流程。这个坑特别隐蔽因为不触发时你根本意识不到它的存在。5.4 connect 卡死在 SYN_SENT防火墙与网络拓扑排查现象客户端connect等了二三十秒最后超时返回 Connection timed out。用 netstat 看连接状态停在 SYN_SENT。原因SYN 报文发出去没人应答。最常见的情况是服务端防火墙丢弃了 SYN或两台机器不在同一个局域网段IP 根本路由不过去。解决先把服务端iptables -L -n | grep 9000看一眼或者干脆临时关闭防火墙做对照实验。再确认两台机器能互相 ping 通。这里有个血泪经验虚拟机用 NAT 网络时宿主机能 ping 通虚拟机不代表另一台虚拟机也能 ping 通用桥接模式且同一网段联调才顺。判断“防火墙到底丢没丢包”在服务端跑tcpdump -i any port 9000如果只看到 SYN 反复重传没有 SYNACK 回应问题就锁定在服务端侧跟客户端代码无关。6. 进阶把单包传输改成分块文件传输并模拟一次重传6.1 在数据包上叠文件传输协议单包收发跑通后再往前走一步把payload当成文件的分块一次传一块。这个升级能让你真正理解 TCP 可靠传输的价值。先读文件按固定块大小切分每块带上全局序号服务端收一块回一个 ACK客户端收到 ACK 才发下一块。这就是最简停等协议。#define CHUNK 512 unsigned char buf[CHUNK]; int seq 0; while (1) { size_t n fread(buf, 1, CHUNK, fp); if (n 0) break; send_block(sockfd, seq, buf, n); /* 发送一个数据包 */ if (recv_ack(sockfd, seq) 0) { /* 等 ACK超时重发 */ continue; } seq; }这个循环用真实代码实现了教材里讲的“停止等待协议”。recv_ack里用setsockopt(SO_RCVTIMEO)设 1 秒超时收不到 ACK 就把同一个块重发。你会发现网络一有丢包这个最简单协议的吞吐量立刻降得很难看——然后你就明白为什么 TCP 要引入滑动窗口和选择性确认。这一层想通了课设的深度直接上一个档次。6.2 用 md5sum 做收尾自检传文件最怕“看起来传完了内容对不上”。服务端收完文件后用md5sum两边对比这一步能验证你的分块、重组逻辑有没有丢字节md5sum original.tar.gz received.tar.gz两个哈希值一致说明每个包头的 len 字段、payload 拷贝、文件追加写入全部正确。不一致时优先检查len字节序转换再检查最后一次fread不足 CHUNK 时有没有把多余字节带进文件。这个自检习惯我后来做任何传输协议都保留着。从那以后只要涉及“协议”二字的作业我都会强制自己先抓包、再写码把交付标准定成“Wireshark 里能看到完整的三次握手、完整的数据流和四次挥手”比写一万字报告都实在。希望帮到你祝你答辩顺利。本文还有配套的精品资源点击获取
返回列表