
简介本资源是燕山大学计算机网络课程三级实践项目——TCP传输数据包的完整实现工程面向高校网络协议学习者与网络编程初学者聚焦TCP可靠传输核心机制的代码级理解与实操验证。项目涵盖三次握手建连、数据分段与序号管理、滑动窗口流量控制、拥塞控制算法如慢启动、ACK确认与重传、四次挥手断连等关键环节通过C/VC工程形式落地助学习者从协议原理直通Socket编程实践。压缩包含60个文件以4个cpp源码、2个h头文件、2个sln解决方案及配套exe可执行文件为主体辅以pdb调试信息、obj编译目标与vcxproj工程配置总大小27.88MB结构完整、开箱即用。已有1354人学习下载提供可直接编译运行的客户端/服务器双端示例、清晰的Debug目录组织及典型网络异常处理逻辑是深入掌握TCP协议栈行为与提升网络编程能力的优质教学工程范例。1. 燕大计算机网络三级项目TCP传输数据包一个能抓到SYN、ACK、FIN全过程的“透明盒子”你有没有试过在Wireshark里看到一串TCP流却说不清哪个包触发了重传、哪个ACK确认了哪段数据、为什么TIME_WAIT要等2MSL燕大计算机网络三级项目「TCP传输数据包」就是这样一个“透明盒子”——它不封装、不抽象用纯C语言在Linux用户态模拟完整TCP状态机从socket bind/listen/accept开始到三次握手、滑动窗口、超时重传、四次挥手全链路可单步调试。项目不依赖libpcap或内核模块所有数据包通过raw socket构造并发送接收端用recvfrom解析IPTCP首部连校验和都手算。适合刚学完《计算机网络自顶向下》第3章、正卡在“TCP到底怎么维护连接”的同学也适合想给课程设计加硬核亮点的高年级学生——它不是Demo是能跑通telnet客户端交互、支持并发连接、带完整日志追踪的最小可行TCP协议栈实现。这个项目最反直觉的一点是它没用epoll或select做I/O多路复用而是用固定时间片轮询环形缓冲区管理多个连接反而让状态迁移逻辑异常清晰。你改一行状态判断比如把ESTABLISHED → FIN_WAIT_1的条件从send_fin1改成send_fin2立刻能在日志里看到整个连接卡死在FIN_WAIT_1连重传定时器超时都打印得明明白白。这不是玩具代码是能让你亲手拧开TCP黑匣子、看清每个螺丝怎么咬合的工程级教学实现。2. 架构与目录结构五层模块如何协作完成一次可靠传输2.1 整体分层设计从网卡到应用层的5个责任单元项目采用清晰的分层架构完全对应TCP/IP模型但剥离了物理层和链路层细节聚焦于IP层之上的TCP核心逻辑Network Layer网络层负责raw socket初始化、IP首部构造/解析、ICMP错误处理如目的不可达Transport Layer传输层核心模块包含TCP首部编解码、校验和计算、状态机定义11种状态、定时器管理RTO、PTO、2MSLConnection Manager连接管理器维护全局连接表conn_table[CONN_MAX]处理listen队列、accept队列、time_wait队列Buffer Flow Control缓冲区与流控双环形缓冲区发送窗口接收窗口实现滑动窗口协议支持动态窗口调整Application Interface应用接口提供类BSD socket的APItcp_socket()/tcp_bind()/tcp_connect()/tcp_send()/tcp_recv()屏蔽底层细节提示所有模块通过tcp_context_t全局上下文结构体通信避免全局变量污染这是燕大导师在实验指导书里特别强调的“可测试性设计”。2.2 目录树与关键文件职责说明tcp-stack/ ├── include/ # 头文件集中地 │ ├── tcp.h # TCP首部定义、状态枚举、函数声明 │ ├── conn.h # 连接结构体conn_t、队列操作宏 │ └── timer.h # 定时器链表节点、RTO计算公式 ├── src/ │ ├── network.c # raw socket创建、sendto/recvfrom封装 │ ├── transport.c # TCP首部序列化、checksum计算、状态迁移主逻辑 │ ├── connection.c # listen/accept流程、连接表增删查、time_wait回收 │ ├── buffer.c # 环形缓冲区读写、窗口大小更新、ack_seq确认处理 │ └── main.c # 示例程序启动监听服务处理telnet连接 ├── test/ │ └── test_handshake.c # 单元测试验证SYN/SYN-ACK/FIN-ACK三阶段状态跳转 └── Makefile # 支持debug模式-DDEBUG_LOG和性能模式-O2注意transport.c是绝对核心——它不处理任何网络I/O只做“决策”收到一个包解析后调用tcp_state_machine()根据当前状态输入事件如EVENT_RECV_SYN决定下一步动作发SYN-ACK更新snd_nxt启动重传定时器。这种“纯逻辑纯数据”分离的设计让单元测试变得极其简单你甚至可以不用网络直接喂入伪造的TCP包结构体断言状态是否正确跳转。2.3 核心数据结构conn_t如何承载一次连接的生命conn_t结构体是整个项目的灵魂它把TCP连接的所有状态压缩在一个64字节结构里为缓存行对齐优化// include/conn.h typedef struct { uint8_t state; // 当前TCP状态TCP_STATE_LISTEN, TCP_STATE_ESTABLISHED... uint16_t local_port; // 本端端口host byte order uint16_t remote_port; // 对端端口host byte order uint32_t snd_una; // 发送未确认序号第一个未被ACK的字节 uint32_t snd_nxt; // 下一个要发送的序号含SYN/FIN占位 uint32_t rcv_nxt; // 下一个期望接收的序号含SYN/FIN占位 uint16_t snd_wnd; // 当前发送窗口大小network byte order uint16_t rcv_wnd; // 当前接收窗口大小network byte order uint32_t iss; // 初始发送序号随机生成 uint32_t irs; // 初始接收序号从SYN包中提取 uint8_t retrans_cnt; // 重传次数用于指数退避 uint8_t flags; // 标志位缓存FLAG_ACK|FLAG_SYN|FLAG_FIN } conn_t;关键点在于snd_una和snd_nxt的差值就是“已发送但未确认”的字节数而rcv_nxt直接决定了你回复ACK时的ack_seq字段——这正是滑动窗口协议的数学本质。项目里所有窗口更新逻辑如收到ACK后移动snd_una、收到新数据后收缩rcv_wnd都围绕这三个字段展开没有魔法全是整数运算。2.4 状态机实现11种状态如何用switch-case精准控制TCP状态机在src/transport.c中以tcp_state_machine()函数实现采用经典switch-case结构每个case处理一种状态下的所有可能事件// src/transport.c void tcp_state_machine(conn_t *conn, tcp_event_t event, const tcp_pkt_t *pkt) { switch (conn-state) { case TCP_STATE_CLOSED: if (event EVENT_ACTIVE_OPEN) { conn-state TCP_STATE_SYN_SENT; conn-iss rand() % 0xfffffffe 1; // 随机初始序号 tcp_send_syn(conn); // 构造并发送SYN包 } break; case TCP_STATE_SYN_SENT: if (event EVENT_RECV_SYN_ACK pkt-flags TCP_FLAG_ACK) { conn-state TCP_STATE_ESTABLISHED; conn-snd_una conn-iss 1; // SYN占用1字节序号 conn-snd_nxt conn-iss 1; conn-rcv_nxt pkt-seq 1; // SYN-ACK的seq是对方iss1表示SYN占位 tcp_send_ack(conn); // 确认对方SYN } else if (event EVENT_RECV_SYN) { conn-state TCP_STATE_SYN_RECEIVED; conn-irs pkt-seq; tcp_send_syn_ack(conn); } break; case TCP_STATE_ESTABLISHED: if (event EVENT_APP_CLOSE) { conn-state TCP_STATE_FIN_WAIT_1; tcp_send_fin(conn); } else if (event EVENT_RECV_FIN) { conn-state TCP_STATE_CLOSE_WAIT; tcp_send_ack(conn); } break; // ... 其余8种状态FIN_WAIT_2, TIME_WAIT, LAST_ACK等省略 } }参数说明tcp_event_t枚举类型包括EVENT_RECV_SYN,EVENT_RECV_ACK,EVENT_APP_SEND,EVENT_TIMEOUT_RTO等12种事件tcp_pkt_t解析后的TCP包结构体含seq,ack_seq,flags,window,data_len等字段所有状态跳转都伴随明确的动作发包tcp_send_xxx、更新序号snd_nxt、启动定时器timer_start(conn-rto_timer)这种实现方式让调试变得直观你在GDB里打断点在tcp_state_machine看conn-state和event就能预判下一行代码会做什么。比读RFC文档快十倍。3. 编译与运行从零构建可交互的TCP服务3.1 环境准备与依赖检查项目要求Linux环境推荐Ubuntu 20.04或CentOS 8需确保以下基础工具就位# 检查GCC版本必须≥9.0因使用了C17标准特性 gcc --version | head -1 # 检查root权限raw socket需要CAP_NET_RAW能力 sudo getcap /usr/bin/ping # 正常应输出 cap_net_rawep # 若无cap_net_raw临时赋予重启失效安全 sudo setcap cap_net_rawep ./tcp-server注意不要用sudo make项目Makefile已通过setcap机制规避root权限直接make即可。这是燕大实验报告里反复强调的“最小权限原则”。3.2 编译步骤与调试模式启用进入项目根目录执行标准三步# 1. 生成Makefile项目使用autotools风格但精简为单Makefile make clean # 2. 编译默认开启调试日志每包收发都打印 make # 3. 查看生成文件 ls -lh tcp-server tcp-client # 输出-rwxr-xr-x 1 user user 125K Jun 10 14:22 tcp-server* # -rwxr-xr-x 1 user user 89K Jun 10 14:22 tcp-client*关键编译选项说明-DDEBUG_LOG启用详细日志含时间戳、连接ID、seq/ack值、窗口大小-O0 -g关闭优化保留调试符号GDB必备-lpcap不依赖项目用socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP))直接抓包比libpcap更底层若编译报错error: ‘struct msghdr’ has no member named ‘msg_control’说明内核头文件版本过低升级linux-libc-dev即可sudo apt update sudo apt install linux-libc-dev3.3 启动服务端并验证三次握手在终端A中启动服务端监听本地8080端口# 启动服务-p指定端口-v开启verbose日志 ./tcp-server -p 8080 -v # 输出 # [2024-06-10 14:25:33] INFO: TCP server listening on 0.0.0.0:8080 # [2024-06-10 14:25:33] DEBUG: conn_id1 stateTCP_STATE_LISTEN - TCP_STATE_SYN_RECEIVED # [2024-06-10 14:25:33] DEBUG: recv SYN from 127.0.0.1:54321, seq2894732101 # [2024-06-10 14:25:33] DEBUG: send SYN-ACK to 127.0.0.1:54321, seq1234567890, ack_seq2894732102在终端B中用标准telnet连接证明兼容性telnet 127.0.0.1 8080 # 输出 # Trying 127.0.0.1... # Connected to 127.0.0.1. # Escape character is ^]. # Connection closed by foreign host.此时服务端日志将完整打印三次握手[2024-06-10 14:25:33] DEBUG: recv ACK from 127.0.0.1:54321, ack_seq1234567891 [2024-06-10 14:25:33] INFO: conn_id1 stateTCP_STATE_SYN_RECEIVED - TCP_STATE_ESTABLISHED [2024-06-10 14:25:33] DEBUG: Connection established! snd_nxt1234567891 rcv_nxt2894732102提示日志中的snd_nxt1234567891正是SYN-ACK中seq1rcv_nxt2894732102正是SYN包中seq1——这就是TCP序号同步的铁证。3.4 使用tcp-client进行可控数据交互项目自带轻量级客户端tcp-client支持发送任意长度数据并观察ACK行为# 发送100字节数据观察滑动窗口变化 ./tcp-client -s 127.0.0.1 -p 8080 -d Hello from燕大TCP stack! -l 100 # 日志关键行 # DEBUG: send data len100, seq1234567891 - 1234567991 # DEBUG: recv ACK ack_seq1234567991 (confirmed all 100 bytes) # DEBUG: snd_una updated to 1234567991, window now 65535若想触发重传可手动丢弃ACK包用iptables# 在另一终端执行丢弃发往本机8080端口的所有ACK包 sudo iptables -A INPUT -p tcp --dport 8080 --tcp-flags ACK ACK -j DROP # 然后运行tcp-client3秒后必见 # DEBUG: RTO timeout for conn_id1, retransmitting seq1234567891 # DEBUG: retransmit count1, new RTO6000ms (exponential backoff)这就是真实TCP重传机制的复现——不是模拟是实打实的网络行为。4. 常见问题排查那些让燕大学生熬夜到三点的坑4.1 现象服务端启动失败报错Operation not permitted原因raw socket创建需要CAP_NET_RAW能力但make生成的二进制文件未自动赋权。常见于首次编译后直接./tcp-server运行。解决# 方案1推荐用make install自动赋权 sudo make install # 会执行 setcap cap_net_rawep /usr/local/bin/tcp-server # 方案2临时手动赋权 sudo setcap cap_net_rawep ./tcp-server # 验证是否成功 getcap ./tcp-server # 应输出 ./tcp-server cap_net_rawep注意切勿用sudo ./tcp-server这会绕过能力机制且带来安全隐患燕大实验验收时此项直接扣分。4.2 现象telnet连接后立即断开日志显示recv FIN from client原因telnet客户端在建立连接后默认发送IAC WILL ECHO等协商包但本项目TCP栈未实现Telnet协议解析将这些字节误判为应用数据并触发tcp_recv()而tcp_recv()发现缓冲区为空实际是协议控制字符后主动发送FIN。解决修改src/main.c中服务端回调函数增加Telnet协商忽略逻辑// 在tcp_recv()调用前插入 if (len 3 buf[0] 0xff (buf[1] 0xfd || buf[1] 0xfc)) { printf(Ignore telnet negotiation: %02x %02x %02x\n, buf[0], buf[1], buf[2]); return; // 不交由上层处理 }或者更简单——用nc替代telnet测试echo test | nc 127.0.0.1 8080 # 完全符合TCP语义无协议干扰4.3 现象并发连接数超过5个后新连接无法建立日志卡在TCP_STATE_LISTEN原因CONN_MAX宏默认设为5include/conn.h:#define CONN_MAX 5连接表满后connection.c中conn_alloc()返回NULLtcp_accept()直接丢弃SYN包。解决修改include/conn.h// 将CONN_MAX从5改为64需同步调整内存分配 #define CONN_MAX 64 // 并在src/connection.c中检查malloc是否足够 conn_table calloc(CONN_MAX, sizeof(conn_t)); if (!conn_table) { perror(calloc conn_table); exit(1); }血泪经验燕大某届学生曾因忘记改CONN_MAX在答辩现场演示10并发时系统假死最后靠ulimit -n 1024强行扩容才救场。4.4 现象Wireshark抓包显示TCP Checksum Incorrect校验和错误原因Linux内核开启net.ipv4.tcp_invalid_ratelimit后会对校验和错误的包做速率限制导致部分包被静默丢弃更常见的是项目未关闭网卡校验和卸载Checksum Offload网卡硬件篡改了TCP校验和字段。解决# 关闭网卡校验和卸载以eth0为例 sudo ethtool -K eth0 tx off rx off sg off tso off gso off # 临时关闭内核校验和检查仅测试用 echo 0 | sudo tee /proc/sys/net/ipv4/tcp_invalid_ratelimit # 验证项目校验和计算是否正确关键 # 在tcp_send_syn()中添加打印 printf(Calculated checksum: 0x%04x, header checksum field: 0x%04x\n, tcp_checksum((uint16_t*)hdr, sizeof(tcp_hdr_t)), hdr-check); # 正常应两者相等4.5 现象TIME_WAIT状态连接堆积无法快速重启服务原因TCP规范要求TIME_WAIT持续2MSL默认2分钟项目严格实现此逻辑timer.c中time_wait_timer到期才释放连接。解决开发阶段可缩短2MSL时间仅限测试// include/timer.h #define TCP_TIMEWAIT_TIMEOUT_MS 30000 // 从120000ms2分钟改为30秒生产环境应启用SO_LINGER选项强制关闭// 在tcp_close()中添加 struct linger ling {1, 0}; // l_onoff1, l_linger0 setsockopt(sockfd, SOL_SOCKET, SO_LINGER, ling, sizeof(ling));5. 核心模块代码逐行解析滑动窗口与超时重传的实现细节5.1 滑动窗口更新buffer.c中如何动态收缩与扩张滑动窗口逻辑集中在src/buffer.c的tcp_update_window()函数它在每次收到ACK或发送数据时被调用// src/buffer.c void tcp_update_window(conn_t *conn, uint32_t ack_seq, uint16_t window_size) { // Step 1: 移动发送窗口左边界确认已送达 if (SEQ_GT(ack_seq, conn-snd_una)) { uint32_t old_una conn-snd_una; conn-snd_una ack_seq; // 计算新确认的字节数用于唤醒阻塞的tcp_send() int confirmed_bytes ack_seq - old_una; if (confirmed_bytes 0) { // 唤醒等待发送的应用线程此处简化为打印 printf(ACK confirmed %d bytes, new snd_una%u\n, confirmed_bytes, conn-snd_una); } } // Step 2: 更新接收窗口大小来自对端通告 // 注意window_size是network byte order需转换 conn-rcv_wnd ntohs(window_size); // Step 3: 计算当前可用发送窗口防回绕 uint32_t window_end conn-snd_una conn-rcv_wnd; if (SEQ_GT(conn-snd_nxt, window_end)) { // 发送指针超出窗口必须等待流量控制生效 printf(Flow control: snd_nxt(%u) window_end(%u), pause sending\n, conn-snd_nxt, window_end); conn-flags | FLAG_WINDOW_FULL; } else { conn-flags ~FLAG_WINDOW_FULL; } }参数说明SEQ_GT(a,b)安全比较序号的宏处理32位回绕#define SEQ_GT(a,b) ((int32_t)((a)-(b)) 0)ntohs()网络字节序转主机字节序TCP窗口字段为16位大端FLAG_WINDOW_FULL标志位供tcp_send()检查是否可发包关键逻辑在于Step 3——它不直接限制send()调用而是设置标志位让上层应用感知到“窗口已满”。这正是TCP“尽力而为”哲学的体现不阻塞系统调用而是让应用自己决定是等待还是丢弃数据。5.2 超时重传定时器timer.c中RTO的指数退避实现重传定时器管理在src/timer.c核心是rto_calculate()函数严格遵循RFC 6298的RTO计算公式// src/timer.c uint32_t rto_calculate(conn_t *conn, uint32_t rtt_sample) { // 初始化第一次采样 if (conn-srtt 0) { conn-srtt rtt_sample; conn-rttvar rtt_sample / 2; } else { // RFC 6298 Section 2.3: srtt (7/8)*srtt (1/8)*rtt_sample int32_t delta rtt_sample - conn-srtt; conn-srtt delta / 8; // rttvar (3/4)*rttvar (1/4)*|delta| uint32_t abs_delta (delta 0) ? -delta : delta; conn-rttvar (3 * conn-rttvar abs_delta) / 4; } // RTO srtt max(G, 4*rttvar) G为时钟粒度此处设为1ms uint32_t rto conn-srtt (conn-rttvar 2); if (rto 200) rto 200; // 下限200ms if (rto 60000) rto 60000; // 上限60秒 // 指数退避每次重传RTO翻倍上限60秒 if (conn-retrans_cnt 0) { rto conn-retrans_cnt; // 2^retrans_cnt * base_rto if (rto 60000) rto 60000; } return rto; } // 被调用处transport.c中 void tcp_retransmit(conn_t *conn) { conn-retrans_cnt; uint32_t new_rto rto_calculate(conn, 0); // 无新RTT样本用历史值 timer_start(conn-rto_timer, new_rto); tcp_send_data(conn, conn-snd_una); // 重发未确认数据 }参数说明srtt平滑RTT估计值Smoothed Round-Trip TimerttvarRTT方差估计值Round-Trip Time Variationretrans_cnt重传次数用于指数退避1→2→4→8倍RTOtimer_start()基于clock_gettime(CLOCK_MONOTONIC)的高精度定时器这段代码的价值在于它把RFC里拗口的数学公式转化成了可调试、可打印、可单步的C语言逻辑。你甚至可以在GDB里watchconn-srtt看着它随着每次ACK缓慢收敛——这才是理解TCP拥塞控制的起点。5.3 数据包构造network.c中如何手算TCP校验和校验和计算是TCP实现中最易出错的部分src/network.c的tcp_checksum()函数采用RFC 1071标准算法// src/network.c uint16_t tcp_checksum(const uint16_t *addr, size_t len) { uint32_t sum 0; const uint16_t *w addr; size_t nleft len; // Step 1: 16位累加 while (nleft 1) { sum *w; nleft - 2; } // Step 2: 处理剩余字节奇数长度 if (nleft 1) { uint16_t odd_byte 0; *(uint8_t*)odd_byte *(uint8_t*)w; // 强制转为低字节 sum odd_byte; } // Step 3: 折叠32位和为16位 while (sum 16) { sum (sum 0xffff) (sum 16); } // Step 4: 取反RFC要求 return (uint16_t)(~sum); } // 调用处transport.c中构造SYN包 void tcp_send_syn(conn_t *conn) { tcp_hdr_t hdr; memset(hdr, 0, sizeof(hdr)); hdr.src_port htons(conn-local_port); hdr.dst_port htons(conn-remote_port); hdr.seq htonl(conn-iss); hdr.ack_seq 0; hdr.doff 5; // 20字节首部 hdr.flags TCP_FLAG_SYN; hdr.window htons(65535); hdr.check 0; // 先置0 // 计算校验和伪首部 TCP首部 数据SYN无数据 uint16_t pseudo[6]; pseudo[0] htons(conn-local_ip 16); // IP伪首部 pseudo[1] htons(conn-local_ip 0xffff); pseudo[2] htons(conn-remote_ip 16); pseudo[3] htons(conn-remote_ip 0xffff); pseudo[4] htons(IPPROTO_TCP); pseudo[5] htons(sizeof(tcp_hdr_t)); // TCP首部长度 // 拼接伪首部TCP首部到临时缓冲区 uint8_t buf[100]; memcpy(buf, pseudo, 12); memcpy(buf12, hdr, sizeof(hdr)); hdr.check tcp_checksum((uint16_t*)buf, 12 sizeof(hdr)); // ... 后续发送 }关键点伪首部必须参与计算源IP、目的IP、协议号、TCP长度非IP总长字节序陷阱IP地址和端口号传入前必须htons()/htonl()但校验和计算时按网络字节序处理奇数长度补零若TCP首部数据长度为奇数末尾补一个字节0再计算取反时机必须在所有字段填好包括check0后再计算最后赋值我一般会在tcp_send_syn()末尾加一句printf(Final SYN checksum: 0x%04x\n, hdr.check);然后用Wireshark抓包对比——如果一致说明你的网络字节序和伪首部构造完全正确。6. 二次开发指南从添加UDP支持到集成HTTP服务器6.1 扩展协议栈在现有框架中添加UDP模块TCP项目结构天然支持协议扩展。UDP实现只需新增3个文件复用现有网络层和应用接口# 新增文件 src/udp.c # UDP首部构造、recvfrom/sendto封装 include/udp.h # UDP首部定义、udp_socket()等API声明 test/test_udp.c # 验证bind/connect/send/recv核心改造点复用network.c的raw socketUDP无需状态机直接调用network_sendto()发送IP包共享conn_t结构体为UDP连接添加UDP_TYPE标志位复用conn_table[]管理端口绑定统一应用接口udp_socket()返回的fd与tcp_socket()同类型上层应用无需区分// include/udp.h typedef enum { PROTO_TCP, PROTO_UDP } proto_type_t; // src/connection.c 中修改 conn_t 结构 typedef struct { // ... 原有字段 proto_type_t proto; // 新增标识TCP或UDP uint8_t udp_flags; // UDP特有标志 } conn_t; // src/udp.c 中实现 int udp_socket(int domain, int type, int protocol) { conn_t *conn conn_alloc(); conn-proto PROTO_UDP; conn-state UDP_STATE_BOUND; // 自定义UDP状态 return conn-id; // 返回连接ID作为fd }这样做的好处是当你要实现DNS查询UDP HTTP下载TCP混合应用时所有连接统一由connection.c管理select()可同时监听TCP和UDP fd——这才是工业级协议栈的设计思维。6.2 集成HTTP服务器用100行代码实现静态文件服务利用项目已有的tcp_recv()和tcp_send()可在src/main.c中快速搭建HTTP服务// src/main.c 中添加 http_handler() void http_handler(conn_t *conn) { char buf[1024]; int len tcp_recv(conn, buf, sizeof(buf)-1); if (len 0) return; buf[len] \0; if (strncmp(buf, GET / , 6) 0) { // 简单响应HTML const char *resp HTTP/1.1 200 OK\r\n Content-Type: text/html\r\n Content-Length: 42\r\n\r\n h1Welcome to YanDa TCP Stack/h1; tcp_send(conn, resp, strlen(resp)); } else if (strncmp(buf, GET /test.txt , 14) 0) { // 响应文件内容需提前创建test.txt FILE *f fopen(test.txt, rb); if (f) { fseek(f, 0, SEEK_END); long size ftell(f); fseek(f, 0, SEEK_SET); char *data malloc(size 1); fread(data, 1, size, f); fclose(f); char header[256]; snprintf(header, sizeof(header), HTTP/1.1 200 OK\r\n Content-Length: %ld\r\n\r\n, size); tcp_send(conn, header, strlen(header)); tcp_send(conn, data, size); free(data); } } }提示HTTP头部必须用\r\n换行且Content-Length必须精确否则浏览器会一直等待。这是燕大某次实验中80%学生翻车的点——他们用printf输出头部忘了printf默认\n而非\r\n。6.3 性能优化从单线程轮询到epoll支持当前项目用usleep(10000)轮询CPU占用率高。升级为epoll只需3处修改新增src/epoll.c封装epoll_create1()/epoll_ctl()/epoll_wait()修改main.c主循环用epoll_wait()替代usleep()事件就绪时调用tcp_handle_event()扩展conn_t增加epoll_fd字段注册到epoll实例p a hrefhttps://download.csdn.net/download/sdx837740961/11249692 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p