简介燕山大学计算机网络三级项目的 TCP 传输数据包实践资源面向正在学习 TCP 协议或需要完成网络编程课程设计的学生。项目以客户端与服务器之间的数据通信为场景覆盖三次握手、数据分片与重组、滑动窗口流量控制、拥塞控制以及慢启动、快速重传等关键机制通过 C/C 源码与可执行程序让学习者直观对照协议原理与实际实现。压缩包内共有 60 个文件大小约 27.88MB主要内容包含 cpp/h 源代码、sln/vcxproj 工程文件、可直接运行的 exe以及 obj、pdb、tlog 等编译中间产物与日志便于自行重新编译和调试。其中 server1 与 client1 为两个完整的 Visual Studio 工程服务端和客户端代码齐全直接运行即可观察 TCP 数据包传输过程。已有 1352 人学习下载适合作为计算机网络课程设计参考、协议分析实验素材或 TCP 网络编程入门实践。1. 燕大计算机网络三级项目里的 TCP 传输数据包先看清交什么再动手拿到“燕大计算机网络三级项目TCP传输数据包”这个题目多数人的第一反应是写一个 socket demo客户端发给服务端一段字符串跑通、截图、交差。这个做法在答辩时很容易被一个问题击穿——你代码里那个 send() 在链路上到底生成了几个 TCP 数据包三次握手各自带了什么标志位应用载荷为什么是 PSH/ACK 而不是单独一个 PSH这个三级项目真正要交的东西不是“能跑的代码”而是代码、抓包证据、以及一份能讲清 TCP 传输过程的报告。它适合正在做课程设计的学生也适合想借一个完整流程补齐 TCP 协议栈细节的入门从业者。2. TCP 数据包长什么样三次握手、报文段格式与状态流转2.1 先把“数据包”这个词对齐字节流与 TCP 报文段“TCP 传输数据包”这个说法里最容易被忽略的词是“包”。很多人把应用层一次性 send 出去的 buffer 误认为是一个 TCP 数据包实际上 TCP 是面向字节流的协议数据从 send() 进入内核发送缓冲区后会被按 MSS最大报文段长度切分加上 TCP 头、交给 IP 层封装成 IP 包再走链路层。Wireshark 和 tcpdump 里看到的 TCP segment才是“包”。在常见的 MTU1500 的以太网上MSS 默认是 1460 字节也就是 1500 减去 20 字节 IP 头、再减去 20 字节 TCP 头。如果 send() 一次写入超过 1460 字节内核会把这次写入切分成多个报文段依次发送。这个点答辩时经常被追问一次 send 不一定等于一个包一个大包也不一定等于一次 send。项目报告里能在开头把这个概念写清楚后面的抓包分析才有说服力。2.2 三次握手和四次挥手在包上是四组标志位三次握手对应链路上的三个报文段。第一个是客户端发出的 SYN携带初始序号 seqA第二个是服务端回应的 SYNACK携带自己的初始序号 seqB同时确认号 ackA1第三个是客户端回应的 ACKseqA1ackB1。注意第三次握手在常规场景下不带应用载荷载荷长度为 0。TCP Fast Open 可以让第三次握手携带数据但这是特殊机制三级项目不需要做答辩时反而要能解释“为什么这里是 0”。四次挥手同样在包上非常直观主动关闭方先发 FINACK对端回一个 ACK然后对端也发 FINACK主动关闭方再回 ACK随后进入 TIME_WAIT。这里容易讲错的是 FIN 包通常带 ACK 标志因为 TCP 是全双工的关闭发送方向之前接收方向的数据可能还没确认完。项目报告里写状态转移时别只写 FIN 两个字要把 ACK 标志位一起标出来。2.3 报文段格式里要会画会背的 6 个字段TCP 报文段格式是报告里必须出现的一张表。抓包里我们实际能用到的字段集中在下面这张表把这几个字段说清楚答辩基本就稳了。字段位数tcpdump/Wireshark 里的对应需要写进报告的解释源端口 / 目的端口各 16 bitsport / dport标识应用进程本项目用 45678序号32 bitseq本报文段数据在发送流中的字节位置确认号32 bitack期望对端下一个发送的字节序号数据偏移4 bitheader lengthTCP 头长度单位是 4 字节选项存在时大于 5标志位9 bitSYN / ACK / FIN / PSH / RST连接管理位握手挥手全看这里窗口16 bitwin接收窗口告诉对端还能收多少字节确认号这个字段写报告时最容易翻车。它的含义是“期待对端下一个字节的序号”不是“我已经收到的最后一个字节序号”。抓包时看到 ack100意思是“你下一个带序号的字节请从 100 开始发”也就是序号 0 到 99 我已经收齐了。把这个定义写进报告并在抓包分析里对照一次比背十遍八股文都管用。2.4 从 LISTEN 到 TIME_WAIT状态机只记关键 5 态TCP 状态机本身很庞杂三级项目不需要全部展开但几个关键状态必须能和代码对应上。服务端调用 listen() 后进入 LISTEN客户端调用 connect() 阻塞返回后双方进入 ESTABLISHED主动关闭方发 FIN 后进入 FIN_WAIT_1收到对端 ACK 后进 FIN_WAIT_2最后发出对 FIN 的确认后进入 TIME_WAIT。TIME_WAIT 持续 2MSL在 Linux 上通常是 60 秒这也是后面避坑章里端口复用的根源。这些状态在项目运行过程中可以直接观察。客户端连接上后在另一个终端执行ss -tan能看到一条ESTAB记录程序退出后再执行一次主动关闭方那条记录会短暂保持TIME_WAIT。把这个观察结果截图放进报告状态机部分就不只是背概念了。3. 从 socket 到可提交代码用 C 写一个 TCP 传输 demo3.1 环境与端口WSL 也能跑端口避开 1024 以下代码在纯 Linux 和 WSL 里都能跑。需要确认两点系统里有 gcc以及没有别的进程占用测试端口。端口建议选 1024 以上的高位端口比如 45678避开 HTTP、SSH 这类常见服务免得抓包时混进无关流量。如果用的是 WSL回环地址 127.0.0.1 可以直接用不需要额外配虚拟网卡。3.2 服务端最小代码socket → bind → listen → accept → recv → send下面这段是服务端完整可编译的代码。它只接收一次数据、原样回显一次然后关闭连接足够支撑抓包分析。#include stdio.h #include string.h #include stdlib.h #include unistd.h #include arpa/inet.h #include sys/socket.h #define PORT 45678 #define BUF_SIZE 4096 int main(void) { int listen_fd, conn_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len sizeof(client_addr); char buf[BUF_SIZE]; // 创建 IPv4 TCP socket listen_fd socket(AF_INET, SOCK_STREAM, 0); if (listen_fd 0) { perror(socket); exit(1); } // 端口复用避免 TIME_WAIT 导致重启失败 int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr htonl(INADDR_ANY); server_addr.sin_port htons(PORT); if (bind(listen_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { perror(bind); exit(1); } // 监听队列长度为 5演示场景够用 if (listen(listen_fd, 5) 0) { perror(listen); exit(1); } printf(server listening on port %d\n, PORT); // 阻塞等待一个客户端连接 conn_fd accept(listen_fd, (struct sockaddr *)client_addr, client_len); printf(client connected: %s:%d\n, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 接收客户端发来的数据 ssize_t n recv(conn_fd, buf, sizeof(buf), 0); if (n 0) { perror(recv); close(conn_fd); close(listen_fd); exit(1); } buf[n] \0; printf(received %zd bytes: %s\n, n, buf); // 原样回显让客户端确认链路是通的 send(conn_fd, buf, n, 0); close(conn_fd); close(listen_fd); return 0; }逻辑说明socket(AF_INET, SOCK_STREAM, 0)创建流式 socket第三个参数 0 表示让内核选 TCPlisten(fd, 5)里的 5 是未完成和已完成连接队列总和的上限不是最大连接数accept返回的conn_fd才是收发数据的 socketlisten_fd只负责接收新连接。接收用recv(conn_fd, buf, sizeof(buf), 0)这里是一次性读缓冲区大小 4096 字节超过这个长度的数据会留在内核缓冲区等下一次 recv。3.3 客户端最小代码socket → connect → send → recv → close#include stdio.h #include string.h #include stdlib.h #include unistd.h #include arpa/inet.h #include sys/socket.h #define SERVER_PORT 45678 #define BUF_SIZE 4096 int main(void) { int sock_fd; struct sockaddr_in server_addr; char buf[BUF_SIZE]; sock_fd socket(AF_INET, SOCK_STREAM, 0); if (sock_fd 0) { perror(socket); exit(1); } memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; // 本机测试回环地址 server_addr.sin_addr.s_addr inet_addr(127.0.0.1); server_addr.sin_port htons(SERVER_PORT); // connect 内部完成 TCP 三次握手成功才返回 if (connect(sock_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { perror(connect); exit(1); } printf(connected to server\n); const char *msg hello tcp packet from yanshan; // send 返回发送成功的字节数不等于对端 recv 到的字节数 ssize_t sent send(sock_fd, msg, strlen(msg), 0); if (sent 0) { perror(send); close(sock_fd); exit(1); } printf(sent %zd bytes\n, sent); // 等待服务端回显 ssize_t n recv(sock_fd, buf, sizeof(buf), 0); if (n 0) { perror(recv); close(sock_fd); exit(1); } buf[n] \0; printf(echo received: %s\n, buf); close(sock_fd); return 0; }逻辑说明client_addr.sin_addr.s_addr inet_addr(127.0.0.1)把点分十进制的回环地址转成网络字节序整数connect()会阻塞直到三次握手完成或超时失败所以 connect 能返回值本身就说明握手协商成功了。send()返回的是成功写入发送缓冲区的字节数不代表对端已经 recv 到这些数据TCP 的可靠性由协议栈保证不体现在 send 的返回值上。这个区别项目报告里值得写一笔。3.4 编译与验证顺序先起服务端再起客户端再对着抓包编译命令在项目根目录执行gcc -Wall -o tcp_server tcp_server.c gcc -Wall -o tcp_client tcp_client.c-Wall打开常见警告结构体字段赋值遗漏、返回值忽略这类问题会直接暴露出来。运行顺序有讲究先启动服务端./tcp_server看到server listening on port 45678后在另一个终端启动客户端./tcp_client客户端输出connected to server服务端输出client connected: 127.0.0.1:端口号和收到的消息内容然后客户端输出回显内容。到这里 demo 是通的但这只是程序层面通了。下一步要抓包验证 TCP 传输数据包的真实形态这才是三级项目评分的关键。3.5 关键参数怎么改缓冲区、监听队列、端口BUF_SIZE只影响应用层读写缓冲区不影响 TCP 报文段切分把 4096 改成 8192 或 65536 都行但 recv 一次最多只能读到缓冲区大小的数据超过的部分留在内核等下一次调用。listen(fd, 5)里的 5 在并发量小时够用如果项目扩展成多客户端连接可以调到 128同时给 accept 套上循环。端口号直接改两处PORT宏注意服务端和客户端要一致。SO_REUSEADDR建议服务端固定加上后面避坑章会解释为什么没有它会翻车。4. 抓包验证传输数据包tcpdump 过滤、握手与挥手判读4.1 tcpdump 抓回环包的过滤表达式-i lo、-S、port抓包工具用系统自带的 tcpdump 就够了不需要额外安装。运行服务端和客户端之前先在一个独立终端启动抓包sudo tcpdump -i lo -nn -S tcp port 45678 -w tcp_demo.pcap参数说明-i lo指定回环接口本机 127.0.0.1 的流量全走 lo不指定的话默认抓第一块网卡回环包一个都看不见-nn关闭主机名和端口名解析避免把 45678 解析成未知服务拖慢输出-S显示绝对序号而不是相对序号三次握手判读时更直观tcp port 45678是过滤表达式只抓与测试端口相关的 TCP 包-w tcp_demo.pcap把原始包写入文件避免终端刷屏丢包。注意必须在客户端运行之前启动否则漏掉前几个握手包。再给一个只抓 SYN 包的过滤表达式答辩时可以用它演示“我在几十个包里精确定位连接建立”sudo tcpdump -i lo -nn -S tcp port 45678 and tcp[13] 0x02 ! 0tcp[13]是 TCP 头第 13 个字节也就是标志位所在的字节0x02是 SYN 位掩码。这个写法是抓包报告里很好的加分细节。4.2 抓包结果怎么对应三次握手前三包逐行看抓完后用回放命令直接看tcpdump -nn -S -r tcp_demo.pcap输出大致是三段。前三行对应三次握手每行字段可以拆成下面这样理解第一包客户端 → 服务端标志位 SseqA。这是 SYN客户端随机生成初始序号 A。第二包服务端 → 客户端标志位 S.seqBackA1。这是 SYNACK服务端携带自己的初始序号 B同时确认客户端序号 A。第三包客户端 → 服务端标志位 .seqA1ackB1。这是纯 ACK载荷长度为 0确认服务端序号 B。这里要理解为什么第三条 ackB1B 是服务端的初始序号虽然 SYN 包本身没有应用数据但 SYN 标志要消耗一个序号位所以确认号要加 1。同理第一条的 SYN 也消耗一个序号位所以客户端第三条的 seq 是 A1 而不是 A。这个“标志位消耗序号”的细节是答辩时最常被追问的点。4.3 数据传输段判读PSH/ACK 载荷长度与序号推进握手完成后抓包文件中间几行就是应用数据传输。客户端调用send()发送 “hello tcp packet from yanshan”链路上出现类似这样的一行标志位 P.seqA1ackB1length32。P. 表示 PSHACK载荷长度就是应用层消息的字节数。服务端回显时再出现一行反向的 PSHACK。数据传输段的判读要点是序号推进。握手结束后客户端的 seq 是 A1这个值加上载荷长度就是下一条数据的起始序号。抓包里能看到 seq 从 A1 跳到 A33中间没有空洞。这就是 TCP 按字节流编号的直接证据。另外 PSH 标志表示“请对端尽快把数据交给应用层”小数据包一次 send 时通常能看到如果开了 Nagle 算法且数据量小PSH 不一定每次都出现报告里不必纠结。4.4 四次挥手与 TIME_WAIT 的证据ss -tan 配合看程序退出时抓包尾部出现四行收尾客户端发 FINACK服务端回 ACK服务端发 FINACK客户端回 ACK。注意这个 demo 里服务端 recv 完、send 完就 close服务端反而是主动关闭方时序会反过来先看到服务端的 FIN 包。谁先 close谁就是主动关闭方这一点的判断依据是代码顺序而不是客户端/服务端身份。四次挥手里还有个细节收到 FIN 的一方如果还有数据要发可以合并到后续发送里这个 demo 里两边都没额外数据所以挥手是标准的四包。抓包结束后立刻执行ss -tan | grep 45678能看到主动关闭方那条 TCP 连接处于TIME_WAIT状态会保留约 60 秒。如果动作够快能拍到 ESTAB 消失、TIME_WAIT 出现的瞬间。这个截图放在报告里比任何文字描述都直观。5. 避坑与常见问题粘包、端口占用、recv 返回 0、抓不到回环包5.1 一次 send 不等于一次 recv粘包与拆包现象客户端连续两次 send 小数据服务端一次 recv 把两份数据都读出来了看起来像“粘包”。反过来的情况是 send 一大块数据服务端分好几次 recv 才读完叫“拆包”。原因TCP 是字节流协议没有消息边界。内核只保证字节顺序不保证 send 和 recv 的次数一一对应。Nagle 算法会把多个小数据合并成一个报文段网络拥塞时大报文段又会拆成多个小段都在应用层表现为粘包/拆包。解决应用层必须自己加消息边界。常见做法是每个消息前面加 4 字节长度头发送端先写长度再写内容接收端先读 4 字节解析出长度再按长度把内容读满。发送端演示如下uint32_t len htonl((uint32_t)strlen(msg)); send(sock_fd, len, 4, 0); // 先发长度 send(sock_fd, msg, strlen(msg), 0); // 再发内容htonl把主机字节序转成大端网络字节序接收端用ntohl转回来。注意两个 send 之间不能合写成一个 send否则对端读到的前 4 字节可能是半个长度加半个内容。5.2 bind 报 Address already in useTIME_WAIT 后悔药现象服务端跑完一次 demoCtrlC 杀掉进程立刻重启时报bind: Address already in use。原因主动关闭方进入了 TIME_WAIT 状态连接的四元组还没完全释放端口被占用。TIME_WAIT 要持续 2MSLLinux 上通常 60 秒左右这是 TCP 保证可靠关闭的机制不是 bug。解决在 bind 之前设置 SO_REUSEADDR。注意必须在 bind 之前调用才有效这就是第 3 章服务端代码里setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt))存在的意义。如果项目里服务端频繁重启调试这行代码能省掉大量等待时间。5.3 本机实验抓不到包回环流量不在 eth0 上现象tcpdump 开了客户端服务端都在运行终端始终没有输出。原因127.0.0.1 的流量走的是 loloopback接口不是 eth0 也不是 wlan0。默认抓包接口抓不到回环流量这个问题在新手实验里出现率极高。解决抓包命令里显式指定-i lo或者先用ip addr查看本机网卡列表确认接口名。另外 WSL2 里接口命名可能不同同样以ip addr输出为准。5.4 recv 返回 0 被当成错误半关闭是正常语义现象客户端发送完数据后 close服务端 recv 返回 0代码里走 perror 分支打印错误。原因recv 返回 0 表示对端正常关闭了发送方向TCP 连接进入半关闭状态。这不是网络错误更不是读失败。只有返回 -1 才需要查 errno。解决recv 返回 0 时应把对应 socket 关闭、退出读取循环。很多多线程网络框架里recv 返回 0 就是连接断开的标准信号。项目代码里写成if (n 0) { perror; }是错的正确判断是if (n 0) { close; break; }。5.5 传大文件速度上不去Nagle 与缓冲区上限现象传输几百 MB 文件时带宽利用率很低CPU 也不高速度就是上不去。原因两个常见因素。一是 Nagle 算法小数据被延迟等待确认交互式传输时吞吐受损二是 socket 内核收发缓冲区默认值偏小对端没有及时 recv 时发送方 buffer 填满就会阻塞。解决对已知要传大文件的连接可以关闭 Naglesetsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, on, sizeof(on));。缓冲区可以在两端调大比如setsockopt(sock_fd, SOL_SOCKET, SO_SNDBUF, size, sizeof(size));。调大缓冲区不是万能的接收方应用层要配合及时读取否则缓冲区再大也会被标满接收窗口、逼停发送方。这个组合是传文件类项目能跑出吞吐量的底线配置。6. 把项目从能跑做到能讲trace 证据表与 iperf 吞吐验证6.1 把 pcap 整理成一张 trace 证据表答辩时直接放 tcpdump 原始输出评委扫一眼很难看出门道。我一般建议把抓包结果整理成一张表每个关键阶段选一两个代表包列成下面这种格式包序方向标志位载荷长度关键字段说明1客户端→服务端SYN0seqA发起连接2服务端→客户端SYNACK0seqB, ackA1同意连接3客户端→服务端ACK0seqA1, ackB1握手完成4客户端→服务端PSHACK32seqA1应用数据5服务端→客户端PSHACK32ackA33回显数据表中序号统一写相对序号比如第一条 SYN 记为 seqA第三条写成 seqA1评委会更快看懂序号推进逻辑。这条表的生成过程不复杂就是对照 tcpdump 输出逐行抄但它是报告里最有分量的一页。6.2 用 iperf 验证传输速率与 MSS 上限如果想让项目多一个数据支撑点可以用 iperf3 测一次吞吐。服务端终端跑iperf3 -s -p 45678客户端终端跑iperf3 -c 127.0.0.1 -p 45678 -t 5-t 5表示测 5 秒输出会包含带宽、重传次数、MSS 等。回环口的吞吐会远高于真实网卡测出来的值不能代表外网性能但能验证一个问题TCP 的传输上限由窗口和 RTT 决定回环下 RTT 极小带宽可以冲得很高。报告里写结论时把这个限制讲清楚显得你理解测试边界而不是只会跑命令。这个项目到最后真正的分水岭不在于 send 了几行代码而在于被问“第三次握手为什么载荷是 0”时能立刻答出来被问“seq 和 ack 差多少”时能指着抓包说清序号推进。我自己做这类实验早期翻过一次车代码跑通就以为完事了答辩现场被追问两次握手区别时脑子里只有 socket API 的返回状态连包都没抓过。那次之后养成的习惯是——任何网络 demo 跑通后先抓包把包数、标志位、载荷长度和代码逐行对齐再截图存档这个流程也一直在用。希望帮到你。本文还有配套的精品资源点击获取