基于dpdk的用户态协议栈的实现(三)—— TCP的三次握手实现

在传统网络系统中,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 状态时,程序执行以下动作:

  1. 提取对端 MAC、IP、端口,作为回包时的反向参数

  2. 保存对端的初始序列号(用于 ACK)

  3. 切换状态 LISTEN → SYN_RCVD

  4. 准备发送 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;
    }
}

只有两项条件同时满足时,才能认为握手完成:

  1. 当前处于 SYN_RCVD(半连接状态)

  2. 收到了 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

相关推荐
我要升天!3 小时前
QT -- 网络编程
c语言·开发语言·网络·c++·qt
Felven3 小时前
盛科工业千兆网交换机端口计数查看
运维·网络·盛科交换机
天赐学c语言4 小时前
Linux - 网络基础概念
linux·服务器·网络·socket
sugar__salt4 小时前
网络编程套接字(二)——TCP
java·网络·网络协议·tcp/ip·java-ee·javaee
sc.溯琛5 小时前
计算机网络:概论学习1
网络·智能路由器·php
独行soc5 小时前
2025年渗透测试面试题总结-273(题目+回答)
网络·python·安全·web安全·网络安全·渗透测试·安全狮
独行soc5 小时前
2025年渗透测试面试题总结-274(题目+回答)
网络·python·安全·web安全·网络安全·渗透测试·安全狮
真正的醒悟5 小时前
图解网络13
网络
电子_咸鱼5 小时前
【QT SDK 下载安装步骤详解 + QT Creator 导航栏使用教程】
服务器·开发语言·网络·windows·vscode·qt·visual studio code