在传统网络系统中,TCP 协议栈长期运行在操作系统内核之中,应用层只需要简单地调用 connect() 或 accept() 即可完成连接建立。然而,这种由内核统一处理网络协议的模式,也伴随着上下文切换和 copy 开销。随着高性能网络处理需求不断增长,越来越多系统开始采用 DPDK(Data Plane Development Kit)在用户态直接驱动网卡,从而绕过内核网络协议栈,获得数量级提高的吞吐能力与更低延迟。
在用户态模式中,由于内核不再参与 TCP/IP 解析和连接管理,所有协议逻辑都必须在程序中完全自行实现。从解析以太网头开始,一直到构造 TCP 报文、维护序列号、处理状态机、计算校验和,这些原本由内核网络子系统自动完成的工作,都必须手工实现。因此,TCP 三次握手便成为构建用户态 TCP 协议栈的关键起点。
虽然 TCP 三次握手是网络基础知识,但在用户态实现时,需要从底层的角度重新理解其意义与行为。
握手的核心不仅是"交换三条消息",而是为后续可靠传输奠定结构基础。它实际上完成了多个关键任务:
首先,它让双方确认各自的收发能力是否正常。客户端通过 SYN 表示自身准备建立连接,并提供自己的初始序列号。服务器通过 SYN+ACK 证明也具备通信能力,同时发送自己的序列号。
其次,它将双方各自的初始序列号同步到对方,使得后续 TCP 数据段都带有可确认的序列信息。这是可靠传输的核心机制。
第三,通过握手,双方正式进入 ESTABLISHED 状态,协议栈内部的 TCB(Transmission Control Block)被初始化,各项资源得以准备完成。
这种同步机制决定了 TCP 的连接语义,而在用户态环境中,这种状态转换必须在代码中显式体现出来。
本文将围绕示例代码中关于 TCP 三次握手的部分,系统讲述该程序如何在用户态完成连接建立。文章从协议原理出发,分析 DPDK 环境下的收包与发包机制,再逐步拆解握手的三个阶段,使整个过程既清晰又具工程实战意义。
一、代码中 TCP 状态机的设计
TCP 协议依赖状态机驱动,因此示例代码首先定义了一个简化版的 TCP 状态集合:
typedef enum __USTACK_TCP_STATUS {
USTACK_TCP_STATUS_CLOSED = 0,
USTACK_TCP_STATUS_LISTEN,
USTACK_TCP_STATUS_SYN_RCVD,
USTACK_TCP_STATUS_SYN_SENT,
USTACK_TCP_STATUS_ESTABLISHED,
USTACK_TCP_STATUS_FIN_WAIT_1,
USTACK_TCP_STATUS_FIN_WAIT_2,
USTACK_TCP_STATUS_CLOSING,
USTACK_TCP_STATUS_TIMEWAIT,
USTACK_TCP_STATUS_CLOSE_WAIT,
USTACK_TCP_STATUS_LAST_ACK
} USTACK_TCP_STATUS;
uint8_t tcp_status = USTACK_TCP_STATUS_LISTEN;
程序启动后处于 LISTEN 状态,这与服务器端监听套接字的行为一致。当接收到一个 TCP SYN 报文后,状态会从 LISTEN 转入 SYN_RCVD,随后在收到 ACK 后进入 ESTABLISHED。这个状态切换过程正是 TCP 三次握手的核心。
二、第一次握手:接收 SYN 报文
在主循环中,DPDK 使用 rte_eth_rx_burst() 从网卡接收数据包。对于每一个报文,通过多段偏移解析以太网头、IP 头和 TCP 头。当检测到报文类型为 TCP 时,程序提取标志位、序列号与 ACK 号:
struct rte_tcp_hdr *tcphdr = (struct rte_tcp_hdr *)(iphdr + 1);
global_flags = tcphdr->tcp_flags;
global_seqnum = ntohl(tcphdr->sent_seq);
global_acknum = ntohl(tcphdr->recv_ack);
其中,global_flags 包含 SYN/ACK/PSH 等 TCP 标志位;global_seqnum 是对端发送的序列号。
随后检测是否为 SYN 包:
if (global_flags & RTE_TCP_SYN_FLAG) {
if (tcp_status == USTACK_TCP_STATUS_LISTEN) {
uint16_t total_len = sizeof(struct rte_tcp_hdr) + sizeof(struct rte_ipv4_hdr) + sizeof(struct rte_ether_hdr);
struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool);
if (!mbuf) {
rte_exit(EXIT_FAILURE, "Error rte_pktmbuf_alloc\n");
}
mbuf->pkt_len = total_len;
mbuf->data_len = total_len;
uint8_t *msg = rte_pktmbuf_mtod(mbuf, uint8_t *);
ustack_encode_tcp_pkt(msg, total_len);
rte_eth_tx_burst(global_portid, 0, &mbuf, 1);
tcp_status = USTACK_TCP_STATUS_SYN_RCVD;
}
}
当收到 SYN 且处于 LISTEN 状态时,程序执行以下动作:
-
提取对端 MAC、IP、端口,作为回包时的反向参数
-
保存对端的初始序列号(用于 ACK)
-
切换状态 LISTEN → SYN_RCVD
-
准备发送 SYN+ACK
这一步对应 TCP 三次握手的第一步:
客户端 → 服务端:SYN
三、第二次握手:发送 SYN+ACK
第二次握手的关键在于构造一个合法的 SYN+ACK TCP 报文。示例程序使用 ustack_encode_tcp_pkt() 完成该任务:
tcp->sent_seq = htonl(12345);
tcp->recv_ack = htonl(global_seqnum + 1);
tcp->tcp_flags = RTE_TCP_SYN_FLAG | RTE_TCP_ACK_FLAG;
这一小段代码浓缩了 TCP 协议的核心:
-
sent_seq = 12345:服务器选取自己的初始序列号(ISN) -
recv_ack = global_seqnum + 1:确认客户端的 SYN -
tcp_flags = SYN + ACK:表明这是第二次握手
构造 IP 头后,再计算 TCP/IPv4 校验和:
tcp->cksum = 0;
tcp->cksum = rte_ipv4_udptcp_cksum(ip, tcp);
之后,将构造好的 mbuf 发出:
rte_eth_tx_burst(global_portid, 0, &mbuf, 1);
到此为止,TCP 第二次握手完成:
服务端 → 客户端:SYN + ACK
四、第三次握手:接收 ACK 并进入 ESTABLISHED 状态
客户端收到 SYN+ACK 后,会发送第三个握手报文 ACK。程序在接收时检测 ACK 标志位:
if (global_flags & RTE_TCP_ACK_FLAG) {
if (tcp_status == USTACK_TCP_STATUS_SYN_RCVD) {
printf("enter established\n");
tcp_status = USTACK_TCP_STATUS_ESTABLISHED;
}
}
只有两项条件同时满足时,才能认为握手完成:
-
当前处于 SYN_RCVD(半连接状态)
-
收到了 ACK(确认服务器 SYN 的到达)
此时状态机进入 ESTABLISHED,TCP 连接建立成功。
这正是三次握手中的最后一步:
客户端 → 服务端:ACK
从协议角度看,此时双方的序列号已同步,可以进入数据传输阶段。
五、握手后的数据接收:解析 PSH 报文与 TCP Payload
示例程序还进一步展示了建立连接后的数据接收过程。当收到 PSH 标志(表示传输负载数据)时,程序解析 TCP 头长度字段以找到数据起始位置:
uint8_t hdrlen = (tcphdr->data_off >> 4) * sizeof(uint32_t);
uint8_t *data = ((uint8_t*)tcphdr + hdrlen);
TCP 头长度以 4 字节为单位,通过该偏移值可以准确访问 payload 内容。之后简单打印 TCP 数据:
printf("tcp data: %s\n", data);
这部分并不是握手的必需部分,但完整展示了用户态 TCP 收包流程,为后续进一步扩展协议栈提供基础。
六、从协议到实现:示例代码中的握手过程总结
将三次握手流程与代码逻辑对照,可清晰看到完整的对应关系:
第一次握手(客户端 SYN)
-
收包、检测 SYN
-
获取序列号
-
状态机 LISTEN → SYN_RCVD
第二次握手(服务器 SYN+ACK)
-
构造以太网/IP/TCP 头
-
设置 seq、ack、flags
-
发送 SYN+ACK
第三次握手(客户端 ACK)
-
收包、检测 ACK
-
状态机 SYN_RCVD → ESTABLISHED
整个逻辑精炼而完整,呈现了一个可运行的最小 TCP 连接建立过程。
七、用户态实现 TCP 握手的意义
通过 DPDK 手动实现 TCP 握手,与传统 socket 编程相比有本质差异。在 socket API 中,三次握手被完全封装,而在 DPDK 用户态环境中,需要显式构造每个报文、管理每个序列号、维护每个状态。正是这种透明性,使得用户能够深入理解协议运行机制。
这样的极简实现为后续构建完整的用户态 TCP 协议栈奠定了基础。协议栈的其他部分,例如重传机制、滑动窗口、拥塞控制、超时管理等,都可以在此框架上逐步增加,从而构建具备高性能和可控性的 TCP 实现。
更多相关知识可查看https://github.com/0voice。