☰
燕大TCP协议栈实现:从三次握手到滑动窗口的纯C教学项目
2026/10/29 11:18:32 网站建设 项目流程

简介:本资源是燕山大学计算机网络课程三级实践项目——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解析IP+TCP首部,连校验和都手算。适合刚学完《计算机网络:自顶向下》第3章、正卡在“TCP到底怎么维护连接”的同学;也适合想给课程设计加硬核亮点的高年级学生——它不是Demo,是能跑通telnet客户端交互、支持并发连接、带完整日志追踪的最小可行TCP协议栈实现。

这个项目最反直觉的一点是:它没用epoll或select做I/O多路复用,而是用固定时间片轮询+环形缓冲区管理多个连接,反而让状态迁移逻辑异常清晰。你改一行状态判断(比如把ESTABLISHED → FIN_WAIT_1的条件从send_fin==1改成send_fin==2),立刻能在日志里看到整个连接卡死在FIN_WAIT_1,连重传定时器超时都打印得明明白白。这不是玩具代码,是能让你亲手拧开TCP黑匣子、看清每个螺丝怎么咬合的工程级教学实现。


2. 架构与目录结构:五层模块如何协作完成一次可靠传输

2.1 整体分层设计:从网卡到应用层的5个责任单元

项目采用清晰的分层架构,完全对应TCP/IP模型但剥离了物理层和链路层细节,聚焦于IP层之上的TCP核心逻辑:

  • Network Layer(网络层):负责raw socket初始化、IP首部构造/解析、ICMP错误处理(如目的不可达)
  • Transport Layer(传输层):核心模块,包含TCP首部编解码、校验和计算、状态机定义(11种状态)、定时器管理(RTO、PTO、2MSL)
  • Connection Manager(连接管理器):维护全局连接表(conn_table[CONN_MAX]),处理listen队列、accept队列、time_wait队列
  • Buffer & Flow Control(缓冲区与流控):双环形缓冲区(发送窗口+接收窗口),实现滑动窗口协议,支持动态窗口调整
  • Application Interface(应用接口):提供类BSD socket的API(tcp_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是对方iss,+1表示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_raw+ep # 若无cap_net_raw,临时赋予(重启失效,安全) sudo setcap cap_net_raw+ep ./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-dev

3.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_id=1 state=TCP_STATE_LISTEN -> TCP_STATE_SYN_RECEIVED # [2024-06-10 14:25:33] DEBUG: recv SYN from 127.0.0.1:54321, seq=2894732101 # [2024-06-10 14:25:33] DEBUG: send SYN-ACK to 127.0.0.1:54321, seq=1234567890, ack_seq=2894732102

在终端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_seq=1234567891 [2024-06-10 14:25:33] INFO: conn_id=1 state=TCP_STATE_SYN_RECEIVED -> TCP_STATE_ESTABLISHED [2024-06-10 14:25:33] DEBUG: Connection established! snd_nxt=1234567891 rcv_nxt=2894732102

提示:日志中的snd_nxt=1234567891正是SYN-ACK中seq+1,rcv_nxt=2894732102正是SYN包中seq+1——这就是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 len=100, seq=1234567891 -> 1234567991 # DEBUG: recv ACK ack_seq=1234567991 (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-client,3秒后必见: # DEBUG: RTO timeout for conn_id=1, retransmitting seq=1234567891 # DEBUG: retransmit count=1, new RTO=6000ms (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_raw+ep /usr/local/bin/tcp-server # 方案2(临时):手动赋权 sudo setcap cap_net_raw+ep ./tcp-server # 验证是否成功 getcap ./tcp-server # 应输出 ./tcp-server = cap_net_raw+ep

注意:切勿用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宏默认设为5(include/conn.h:#define CONN_MAX 5),连接表满后connection.c中conn_alloc()返回NULL,tcp_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 // 从120000ms(2分钟)改为30秒

生产环境应启用SO_LINGER选项强制关闭:

// 在tcp_close()中添加 struct linger ling = {1, 0}; // l_onoff=1, l_linger=0 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 Time)
  • rttvar:RTT方差估计值(Round-Trip Time Variation)
  • retrans_cnt:重传次数,用于指数退避(1→2→4→8倍RTO)
  • timer_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(buf+12, &hdr, sizeof(hdr)); hdr.check = tcp_checksum((uint16_t*)buf, 12 + sizeof(hdr)); // ... 后续发送 }

关键点:

  • 伪首部必须参与计算:源IP、目的IP、协议号、TCP长度(非IP总长!)
  • 字节序陷阱:IP地址和端口号传入前必须htons()/htonl(),但校验和计算时按网络字节序处理
  • 奇数长度补零:若TCP首部+数据长度为奇数,末尾补一个字节0再计算
  • 取反时机:必须在所有字段填好(包括check=0)后再计算,最后赋值

我一般会在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

核心改造点:

  1. 复用network.c的raw socket:UDP无需状态机,直接调用network_sendto()发送IP包
  2. 共享conn_t结构体:为UDP连接添加UDP_TYPE标志位,复用conn_table[]管理端口绑定
  3. 统一应用接口: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" "<h1>Welcome 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处修改:

  1. 新增src/epoll.c:封装epoll_create1()/epoll_ctl()/epoll_wait()
  2. 修改main.c主循环:用epoll_wait()替代usleep(),事件就绪时调用tcp_handle_event()
  3. 扩展conn_t:增加epoll_fd字段,注册到epoll实例
<p> <a href="https://download.csdn.net/download/sdx837740961/11249692" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询