DPDK实现用户态tcp、ip协议栈

小捏哩 2025-12-01 18:50:44

目录

1. DPDK环境搭建

2. DPDK原理及架构

3. TCP/IP网络的五层架构

3.1 五层架构

3.2 数据在五层模型中的传输过程

3.3 网络协议栈

4. DPDK实现简单UDP的echo应用

4.1 DPDK环境初始化

4.2 UDP接受数据

4.3 UDP处理数据

4.4 UDP发送数据

5. DPDK实现简单TCP 的echo应用

5.1 TCP三次握手

5.2 TCP进行第一次握手

5.3 TCP第二次握手

5.4 TCP第三次握手

6. 完整代码


 

前言:

Linux操作系统内核提供的网络POSIX API依赖内核协议栈处理数据,数据的读写中频繁在内核和用户态间切换,效率极低。而DPDK直接对接网卡,对于网卡所接受到的数据实行零拷贝,即网卡中的数据缓存直接映射到计算机内存中,省去了内核作为中间桥梁。向上DPDK直接向用户态负责,所有的数据直接用户操作,效率大大提高。

1. DPDK环境搭建

DPDK的环境搭建可以参考我的另一篇博客:DPDK的原理以及环境搭建-CSDN社区

2. DPDK原理及架构

通过环境搭建部分,我们对DPDK有了一定的认识,知道它可以直接与硬件交互,同时因为大页的存在可以高效处理大数据包。但是大家有没有想过Linux原生架构比DPDK慢在哪里,换句话说:DPDK究竟在系统架构中扮演了一个什么角色使得仅它一个工具就能让整个网络性能大大提高?

 

传统的Linux网络协议栈中数据包的接收、TCP/IP协议栈解析数据完全由内核进行管理。当数据解析完成通知用户程序处理数据,此时就会发生频繁的内核/用户态的切换,导致效率很低。DPDK与传统内核架构不同,数据通过内存直接访问映射到内存中零拷贝放在用户态内存池mbuf中(此处mbuf就扮演了内核的sk_buff的角色)。由KNI控制数据与内核协议栈交互的流量。KNI仍然需要与内核交互,为了追求极致的性能,我们可以使用自定义协议栈,完全把数据放在用户态处理。

3. TCP/IP网络的五层架构

3.1 五层架构

  1. 应用层:为应用程序提供网络服务,如HTTP、FTP、SMTP等协议。它负责处理特定的应用程序细节。

  2. 传输层:提供端到端的数据传输服务,主要协议有TCP和UDP。TCP提供可靠的、面向连接的服务,而UDP提供不可靠的、无连接的服务。在您提供的代码中,tcphdr(TCP头部)的处理就是在传输层进行的,代码中检查了TCP的PSH标志,并提取了TCP数据部分。

  3. 网络层:负责数据包的路由和转发,主要协议是IP。它处理逻辑地址(IP地址)和路由选择。

  4. 数据链路层:负责在物理网络媒介上传输数据帧,处理物理地址(MAC地址)和错误检测。主要协议有以太网、PPP等。

  5. 物理层:负责传输原始比特流,处理物理连接和电气特性。它不关心数据的含义,只负责传输。

3.2 数据在五层模型中的传输过程

数据在网络五层模型中的传输过程是一个封装和解封装的过程,每一层都会添加自己的头部信息(有时还有尾部信息),以便下一层能够正确处理数据。以下是数据在发送方和接收方的详细传输过程:

3.3 网络协议栈

网络协议栈是网络通信中分层设计的概念,主要包括三个主要层次:以太网协议、IP协议和TCP/UDP协议。

3.3.1 以太网协议

以太网协议是数据链路层协议,负责在局域网内进行数据帧的传输和寻址。它定义了如何在物理网络上传输数据,包括帧格式、MAC地址寻址等内容。

3.3.2 IP协议

IP协议(网际协议)属于网络层协议,主要功能是提供数据传输、数据包编址、数据包路由和分段等1。IP协议通过编址约定,可以将数据通过路由传输到正确的网络或子网。

3.3.3 TCP/UDP协议

TCP/UDP协议属于传输层协议,为应用层提供服务1。TCP协议(传输控制协议)可以在网络用户启动的软件应用进程之间建立通信会话,实现数据流量控制和错误检测,在不可靠的网络上提供可靠的端到端数据传输1。而UDP协议(用户数据报协议)是一种无连接的协议,在传输数据之前不建立连接,不提供良好的可靠性和差错检查,只依赖校验来保证可靠性,但处理和传输数据的速度快,常用于传输关键的网络状态消息。

UDP协议头:

TCP协议头:

 

4. DPDK实现简单UDP的echo应用

4.1 DPDK环境初始化

DPDK环境初始化就是将DPDK各组件进行原始配置,包括初始化DPDK环境抽象层、绑定的网卡、收发队列的大小、收发队列的具体配置、启动指定端口ID的网络设备等。一般情况下都是一套完整的流程。

具体步骤:

1:首先创建用于收发数据的内存池,该内存池作为数据收发的载体;

2:接着初始化DPDK,首先设置环境抽象层(Environment Abstraction Layer, EAL),为后续DPDK组件提供支持;

3:然后初始化EAL之后,就是拿到具体的网卡进行配置。rte_eth_dev_count_avail()函数用于获取当前可用的网卡数量,返回一个nint16_t类型数据;

4:在拿到具体网卡,接下来就是配置接收和发送队列。对于接收队列来说它的任务仅仅是从内存池中拿取数据,所以只需要配置接收数据的大小以及关联到内存池;

5:收发队列初始化配置完成rte_eth_dev_start(global_portid)启动指定设备开始工作。

int ustack_init_port(struct rte_mempool * mbuf_pool){
    // number  代表我们绑定的网卡数量
    uint16_t nb_sys_ports = rte_eth_dev_count_avail();  // 返回可用的以太网设备数量
    printf("Available ports: %d\n", nb_sys_ports);  // 代表我们绑定的网卡数量
​
    if(nb_sys_ports == 0) {
        rte_exit(EXIT_FAILURE, "No Supported eth found\n");
    }
​
    const int num_rx_queues = 1; 
    const int num_tx_queues = 1; 
    // rte_eth_dev_configure()配置以太网设备参数,包括接收队列和发送队列的数量
    int ret = rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);
    if(ret < 0) {
        rte_exit(EXIT_FAILURE, "Could not configure device: %d\n", ret);
    }
    printf("Port configured successfully\n");
​
    // rte_eth_dev_socket_id(global_portid): 获取设备所在的CPU socket ID,用于内存分配优化
    ret = rte_eth_rx_queue_setup(global_portid, 0, 128, rte_eth_dev_socket_id(global_portid), NULL, mbuf_pool);
    if(ret < 0) {
        rte_exit(EXIT_FAILURE, "Could not setup RX queue: %d\n", ret);
    }
    printf("RX queue setup successfully\n");
​
​
    struct rte_eth_dev_info dev_info;  // 用于获取网络设备的详细信息
    rte_eth_dev_info_get(global_portid, &dev_info); // 获取指定端口(global_portid)的设备信息
    struct rte_eth_txconf txq_conf = dev_info.default_txconf;
    txq_conf.offloads = port_conf_default.rxmode.offloads; // 设置发送队列的卸载功能(offloads)为默认接收模式的卸载功能。
    // NUMA节点ID(通过rte_eth_dev_socket_id()获取)
    ret = rte_eth_tx_queue_setup(global_portid, 0, 512, rte_eth_dev_socket_id(global_portid), &txq_conf);
    if(ret < 0){
        rte_exit(EXIT_FAILURE, "Could not setup TX queue\n");
    }
​
    ret = rte_eth_dev_start(global_portid); // 启动指定端口ID的网络设备
    if(ret < 0) {
        rte_exit(EXIT_FAILURE, "Could not start device: %d\n", ret);
    }
    printf("Device started successfully\n");
​
    // 添加网卡信息打印 Link status: UP, Speed:  Mbps
    struct rte_eth_link link;
    rte_eth_link_get(global_portid, &link);
    printf("Link status: %s, Speed: %u Mbps\n", link.link_status ? "UP" : "DOWN", link.link_speed);
​
    return 0;
}

4.2 UDP接受数据

由上面我们可以知道网络协议栈中包括:以太网协议,IP协议和TCP/UDP协议,所以一帧数据包就包括以太网头,IP头,TCP或者UDP头和数据。

所以当我们接受到一个UDP数据包的时候,我们需要先解析出以太网头、IP头、UDP头后,就可以打印出我们接收到的UDP数据了:

int main (int argc, char *argv[]) {  
​
    int ret = rte_eal_init(argc, argv); // 初始化DPDK环境抽象层(EALL - Environment Abstraction Layer)
    if (ret < 0) {
        rte_exit(EXIT_FAILURE, "Error with EAL initialization\n");
    }
​
    // 创建内存池(mbuf_pool)用于存储网络数据包
    struct rte_mempool * mbuf_pool = rte_pktmbuf_pool_create("mbuf pool", NUM_MBUFS, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
​
    if(mbuf_pool == NULL) {
        rte_exit(EXIT_FAILURE, "Could not create mbuf pool\n");
    }
    ustack_init_port(mbuf_pool);
​
    while(1) {
        struct rte_mbuf *mbufs[BURST_SIZE] = {0};
​
        uint16_t num_recvd = rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE); // 零拷贝的高效接收方式(直接操作网卡DMA内存,减少数据拷贝)
        if (num_recvd > BURST_SIZE) {
            rte_exit(EXIT_FAILURE, "Error receiving from eth\n");
        }
        
        // printf("Received %d packets\n", num_recvd);  // 添加这行
​
        for (int i = 0; i < num_recvd; i++) {
            // rte_pktmbuf_mtod从mbuf中获取以太网头  14个字节
            struct rte_ether_hdr *ethhdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
            
            // 打印以太网类型 0x0800 rte_be_to_cpu_16将大端序转换为主机字节序(大小端--》两个字节以上都要转)
            // printf("Ethernet type: 0x%04x\n", rte_be_to_cpu_16(ethhdr->ether_type));
            
            if (ethhdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) { // rte_cpu_to_be_16 主机字节序转换为大端序
                printf("Not IPv4 packet, skipping\n");
                rte_pktmbuf_free(mbufs[i]);  // 释放非IPv4包
                continue;
            }
            // rte_pktmbuf_mtod_offset获取IP头(位于以太网头之后)
            struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));
            
            // 打印IP信息 使用inet_ntop将IP地址转换为可读字符串格式
            char src_ip[16], dst_ip[16];//IP Packet: 10.66.104.182 -> 10.66.189.155, Protocol: 17 UDP 6=TCP
            inet_ntop(AF_INET, &(iphdr->src_addr), src_ip, sizeof(src_ip)); // inet_ntop函数将网络字节序的IP地址转换为点分十进制字符串格式
            inet_ntop(AF_INET, &(iphdr->dst_addr), dst_ip, sizeof(dst_ip)); // AF_INET表示IPv4地址族
            // printf("IP Packet: %s -> %s, Protocol: %d\n", src_ip, dst_ip, iphdr->next_proto_id);
            
            if (iphdr->next_proto_id == IPPROTO_UDP) {  // Protocol: 17 UDP iphdr->next_proto_id是一个字节,所以不需要转
                struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1); // iphdr是指向IP头部的指针,UDP头部紧跟在IP头部之后 udp8个字节
                
                // 打印UDP数据
                char *udp_data = (char *)(udphdr + 1);
                int data_len = rte_be_to_cpu_16(udphdr->dgram_len) - sizeof(struct rte_udp_hdr);
                
                // 如果是文本,尝试打印为字符串
                if (data_len > 0) {
                    printf("UDP text: %.*s\n", data_len, udp_data);
                }
            }
            rte_pktmbuf_free(mbufs[i]);  // 释放内存
        }
    }
​
    printf("Hello, UStack!\n");
    return 0;
}

4.3 UDP处理数据

通过协议我们知道发送数据需要知道目的ip,目的端口,所以我们需要将解析到的目的ip,目的端口保存下来,以便组织UDP包进行回发。

// 刚开始的源地址是客户端那边的 ,后面的源地址要变成客户端的
rte_memcpy(global_smac, ethhdr->d_addr.addr_bytes, RTE_ETHER_ADDR_LEN); // global_smac是数组,本身就是地址
rte_memcpy(global_dmac, ethhdr->s_addr.addr_bytes, RTE_ETHER_ADDR_LEN);
​
rte_memcpy(&global_sip, &iphdr->dst_addr, sizeof(uint32_t));
rte_memcpy(&global_dip, &iphdr->src_addr, sizeof(uint32_t));
​
rte_memcpy(&global_dport, &udphdr->src_port, sizeof(uint16_t));
rte_memcpy(&global_dport, &udphdr->src_port, sizeof(uint16_t));  
​
struct in_addr addr;
addr.s_addr = iphdr->src_addr;
printf("sip %s:%d --> ", inet_ntoa(addr), ntohs(udphdr->src_port));
​
addr.s_addr = iphdr->dst_addr;
printf("dip %s:%d --> ", inet_ntoa(addr), ntohs(udphdr->dst_port));

4.4 UDP发送数据

我们通过接受到的UDP包可以解析出目的mac,目的ip,目的端口,然后组织成新的UDP包进行回发。

相较于TCP,UDP没有复杂的重传机制,组织发送数据时只需要按照协议格式依次填充,和解析数据包是相反的过程。

 

int ustack_encode_udp_pkt(uint8_t *msg, uint8_t *data, uint16_t total_len){
​
    // ether header
    struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg;
    rte_memcpy(eth->d_addr.addr_bytes, global_dmac, RTE_ETHER_ADDR_LEN);
    rte_memcpy(eth->s_addr.addr_bytes, global_smac, RTE_ETHER_ADDR_LEN);    
    eth->ether_type = htons(RTE_ETHER_TYPE_IPV4); // 用于将16位(短整型)数值从主机字节序转换为网络字节序。
    // 主机字节序:CPU处理数据的字节序
        // 小端序(Little-endian):低位字节存储在低地址(如Intel x86/x64)
        // 大端序(Big-endian):高位字节存储在低地址(如网络协议标准)
    // 网络字节序:大端序(高位在前)
​
​
    // ip header
    struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth + 1); //msg + sizeof(struct rte_ether_hdr);
    ip->version_ihl = 0x45;  // 5表示20个字节
    ip->type_of_service = 0;
    ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr)); // 需要进行网络字节序的转换
    ip->packet_id = 0;
    ip->fragment_offset = 0;
    ip->time_to_live = 64;
    ip->next_proto_id = IPPROTO_UDP;
    ip->src_addr = global_sip;
    ip->dst_addr = global_dip;
​
    ip->hdr_checksum = 0;  // 必须要清零 rte_ipv4_cksum会计算hdr_checksum的,不清零会导致错误
    ip->hdr_checksum = rte_ipv4_cksum(ip);
​
    // udp header
    struct rte_udp_hdr *udp = (struct rte_udp_hdr *)(ip + 1);
    udp->src_port = global_sport;
    udp->dst_port = global_dport;
    uint16_t udplen = total_len - sizeof(struct rte_ether_hdr) - sizeof(struct rte_ipv4_hdr);
    udp->dgram_len = htons(udplen);  // 需要进行网络字节序的转换
​
    rte_memcpy((uint8_t*)(udp+1), data, udplen);  // 数据也要写进去
​
    udp->dgram_cksum = 0;// 必须要清零 
    udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp);
​
    return 0;
}

最后将组织完成的udp包进行回送:

    rte_eth_tx_burst(global_portid,0,&mbuf,1);

注意:需要在sudo权限下进行!!!

效果如下图所示:

 

 

5. DPDK实现简单TCP 的echo应用

UDP是不可靠的传输,所以没有建议通信的过程,它使用进行大量数据传输的场合,但是TCP是科考传输,它需要建立可靠的通信,然后才可以进行发送数据。

5.1 TCP三次握手

TCP的连接建立需要进行三次握手。服务器作为被访问端,它收到连接请求,发送回应。其中对应的是三次握手中的第二次数据发送,此时协议头中SYN与ACK标识置为1,同时初始化自己的发送队列序号,对对端发送来的序号进行回应,即回应序号acknum值+1。

前面部分的源ip,目的ip解析与UDP相同此处不再赘述,首先需要拿到对方的seqnum和acknum:

    global_seqnum = ntohl(tcphdr->sent_seq);
    global_acknum = ntohl(tcphdr->recv_ack);

5.2 TCP进行第一次握手

在TCP连接中客户端与服务器端都有不同的状态,这里我们自然而然就想到了状态机,将TCP的不同状态对应不同事件。在客户端发起请求时只有当服务器处理LISTEN状态才能建立连接,第一次握手报文的flag字段中仅SYN值置为1。依据这两个条件判断服务器接收到的报文是否是发起请求的第一次握手报文:

if(global_flags & RTE_TCP_SYN_FLAG){ //SYN标志处理:当收到SYN包且当前状态为LISTEN时,构造SYN-ACK响应
                    if(tcp_status == USTACK_TCP_STATUS_LISTEN){
                        uint16_t total_len = sizeof(struct rte_tcp_hdr) + sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_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;
                    }  
                }

5.3 TCP第二次握手

当收到SYN包且当前状态为LISTEN时,构造SYN-ACK响应,需要将报文组织起来在flag字段中将SYN和ACK的值置为1,同时告诉对方自己的sendnum并将对方的acknum+1。

数据包中前面大部分操作与UDP相同,配置TCP数据报中的源地址目的地址,本端的发送序号随机生成,按照对方的序号将回应序号值+1,设置flag字段值以及接收窗口。接着将数据发送出去,将当前状态设为SYN_RCVD。

int ustack_encode_tcp_pkt(uint8_t *msg, uint16_t total_len){
​
    // ether header
    struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg;
    rte_memcpy(eth->d_addr.addr_bytes, global_dmac, RTE_ETHER_ADDR_LEN);
    rte_memcpy(eth->s_addr.addr_bytes, global_smac, RTE_ETHER_ADDR_LEN);    
    eth->ether_type = htons(RTE_ETHER_TYPE_IPV4); // htons是"host to network short"的缩写,用于将16位(短整型)数值从主机字节序转换为网络字节序。
    // 主机字节序:CPU处理数据的字节序
        // 小端序(Little-endian):低位字节存储在低地址(如Intel x86/x64)
        // 大端序(Big-endian):高位字节存储在低地址(如网络协议标准)
    // 网络字节序:大端序(高位在前)
​
​
    // ip header
    struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth + 1); //msg + sizeof(struct rte_ether_hdr);
    ip->version_ihl = 0x45;  // 5表示20个字节
    ip->type_of_service = 0;
    ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr)); // 需要进行网络字节序的转换
    ip->packet_id = 0;
    ip->fragment_offset = 0;
    ip->time_to_live = 64;
    ip->next_proto_id = IPPROTO_TCP;
    ip->src_addr = global_sip;
    ip->dst_addr = global_dip;
​
    ip->hdr_checksum = 0;  // 必须要清零 rte_ipv4_cksum会计算hdr_checksum的,不清零会导致错误
    ip->hdr_checksum = rte_ipv4_cksum(ip);
​
​
    // tcp header
    struct rte_tcp_hdr *tcp = (struct rte_tcp_hdr *)(ip + 1);
    tcp->src_port = global_sport;
    tcp->dst_port = global_dport;
    tcp->sent_seq = htonl(12345);
    tcp->recv_ack = htonl(global_seqnum + 1);
    tcp->data_off = 0x50;
    tcp->tcp_flags = RTE_TCP_SYN_FLAG | RTE_TCP_ACK_FLAG; //0x1 << 1;
    
    tcp->rx_win = TCP_INIT_WINDOWS; //htons(4096);  // rmem
    tcp->cksum = 0;
    tcp->cksum = rte_ipv4_udptcp_cksum(ip, tcp);
​
    return 0;
}

5.4 TCP第三次握手

客户端收到回应向服务器发送第三次握手,此时flag字段中仅ACK置为1。服务器收到数据报什么都不需要做将状态转为ESTABLISHED及连接建立成功。

当客户端向服务器发送数据,判断flag值中PSH字段是否为1。判断成功从数据部分提取数据:


                if(global_flags & RTE_TCP_PSH_FLAG){ // PSH标志处理:当连接已建立且收到PSH标志时,打印数据内容
                    printf("enter established: %d\n", tcp_status);
                    if (tcp_status == USTACK_TCP_STATUS_ESTABLISHED) {
​
                        uint8_t hdrlen = (tcphdr->data_off >> 4) * sizeof(uint32_t); // TCP头部长度
​
                        uint8_t *data = ((uint8_t*)tcphdr + hdrlen); // 数据部分的起始地址
​
                        printf("tcp data: %s\n", data);
                    }
                }

效果如下图所示:

第一个报文从客户端向服务器发送请求,flag值为2说明为SYN报文。接着服务器会送客户端发送第三次报文连接建立,开始发送数据,因为没有后续数据回应客户端回不断超时重传。

 

6. 完整代码



#include <stdio.h>
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <arpa/inet.h>


int global_portid = 0;  //绑定网卡的id从0开始,是一个逻辑数字,是绑定的顺序

#define NUM_MBUFS 4096
#define BURST_SIZE 128

#define TCP_INIT_WINDOWS		14600


uint8_t global_smac[RTE_ETHER_ADDR_LEN];  // 源mac 6个字节
uint8_t global_dmac[RTE_ETHER_ADDR_LEN];


uint32_t global_sip;  // 源ip
uint32_t global_dip;

uint16_t global_sport;  //源端口
uint16_t global_dport;



uint8_t global_flags;
uint32_t global_seqnum;
uint32_t global_acknum;

//--------------------tcp状态------------------11个状态
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;
//---------------------------------------------
static struct rte_eth_conf port_conf_default = { //struct rte_eth_conf用于以太网端口配置的结构体
    .rxmode = { .max_rx_pkt_len = RTE_ETHER_MAX_LEN}
};


int ustack_init_port(struct rte_mempool * mbuf_pool);
int ustack_encode_udp_pkt(uint8_t *msg, uint8_t *data, uint16_t total_len);
int ustack_encode_tcp_pkt(uint8_t *msg, uint16_t total_len);

int ustack_init_port(struct rte_mempool * mbuf_pool){
    // number  代表我们绑定的网卡数量
    uint16_t nb_sys_ports = rte_eth_dev_count_avail();  // 返回可用的以太网设备数量
    printf("Available ports: %d\n", nb_sys_ports);  // 代表我们绑定的网卡数量

    if(nb_sys_ports == 0) {
        rte_exit(EXIT_FAILURE, "No Supported eth found\n");
    }

    const int num_rx_queues = 1;
    const int num_tx_queues = 1; 
    // rte_eth_dev_configure()配置以太网设备参数,包括接收队列和发送队列的数量
    int ret = rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);
    if(ret < 0) {
        rte_exit(EXIT_FAILURE, "Could not configure device: %d\n", ret);
    }
    printf("Port configured successfully\n");

    // rte_eth_dev_socket_id(global_portid): 获取设备所在的CPU socket ID,用于内存分配优化
    ret = rte_eth_rx_queue_setup(global_portid, 0, 128, rte_eth_dev_socket_id(global_portid), NULL, mbuf_pool);
    if(ret < 0) {
        rte_exit(EXIT_FAILURE, "Could not setup RX queue: %d\n", ret);
    }
    printf("RX queue setup successfully\n");


    struct rte_eth_dev_info dev_info;  // 用于获取网络设备的详细信息
    rte_eth_dev_info_get(global_portid, &dev_info); // 获取指定端口(global_portid)的设备信息
    struct rte_eth_txconf txq_conf = dev_info.default_txconf;
    txq_conf.offloads = port_conf_default.rxmode.offloads; // 设置发送队列的卸载功能(offloads)为默认接收模式的卸载功能。
    // NUMA节点ID(通过rte_eth_dev_socket_id()获取)
    ret = rte_eth_tx_queue_setup(global_portid, 0, 512, rte_eth_dev_socket_id(global_portid), &txq_conf);
    if(ret < 0){
        rte_exit(EXIT_FAILURE, "Could not setup TX queue\n");
    }

    ret = rte_eth_dev_start(global_portid); // 启动指定端口ID的网络设备
    if(ret < 0) {
        rte_exit(EXIT_FAILURE, "Could not start device: %d\n", ret);
    }
    printf("Device started successfully\n");

    // 添加网卡信息打印 Link status: UP, Speed: 10000 Mbps
    struct rte_eth_link link;
    rte_eth_link_get(global_portid, &link);
    printf("Link status: %s, Speed: %u Mbps\n", link.link_status ? "UP" : "DOWN", link.link_speed);

    return 0;
}

// udp msg
int ustack_encode_udp_pkt(uint8_t *msg, uint8_t *data, uint16_t total_len){

    // ether header
    struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg;
	rte_memcpy(eth->d_addr.addr_bytes, global_dmac, RTE_ETHER_ADDR_LEN);
	rte_memcpy(eth->s_addr.addr_bytes, global_smac, RTE_ETHER_ADDR_LEN);    
    eth->ether_type = htons(RTE_ETHER_TYPE_IPV4); // 用于将16位(短整型)数值从主机字节序转换为网络字节序。
    // 主机字节序:CPU处理数据的字节序
        // 小端序(Little-endian):低位字节存储在低地址(如Intel x86/x64)
        // 大端序(Big-endian):高位字节存储在低地址(如网络协议标准)
    // 网络字节序:大端序(高位在前)


    // ip header
	struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth + 1); //msg + sizeof(struct rte_ether_hdr);
	ip->version_ihl = 0x45;  // 5表示20个字节
	ip->type_of_service = 0;
	ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr)); // 需要进行网络字节序的转换
	ip->packet_id = 0;
	ip->fragment_offset = 0;
	ip->time_to_live = 64;
	ip->next_proto_id = IPPROTO_UDP;
	ip->src_addr = global_sip;
	ip->dst_addr = global_dip;

	ip->hdr_checksum = 0;  // 必须要清零 rte_ipv4_cksum会计算hdr_checksum的,不清零会导致错误
	ip->hdr_checksum = rte_ipv4_cksum(ip);


    // udp header
	struct rte_udp_hdr *udp = (struct rte_udp_hdr *)(ip + 1);
	udp->src_port = global_sport;
	udp->dst_port = global_dport;
	uint16_t udplen = total_len - sizeof(struct rte_ether_hdr) - sizeof(struct rte_ipv4_hdr);
	udp->dgram_len = htons(udplen);  // 需要进行网络字节序的转换

	rte_memcpy((uint8_t*)(udp+1), data, udplen);  // 数据也要写进去

	udp->dgram_cksum = 0;// 必须要清零 
	udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp);

    return 0;
}

// tcp msg
int ustack_encode_tcp_pkt(uint8_t *msg, uint16_t total_len){

    // ether header
    struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg;
	rte_memcpy(eth->d_addr.addr_bytes, global_dmac, RTE_ETHER_ADDR_LEN);
	rte_memcpy(eth->s_addr.addr_bytes, global_smac, RTE_ETHER_ADDR_LEN);    
    eth->ether_type = htons(RTE_ETHER_TYPE_IPV4); // 用于将16位(短整型)数值从主机字节序转换为网络字节序。
    // 主机字节序:CPU处理数据的字节序
        // 小端序(Little-endian):低位字节存储在低地址(如Intel x86/x64)
        // 大端序(Big-endian):高位字节存储在低地址(如网络协议标准)
    // 网络字节序:大端序(高位在前)


    // ip header
	struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth + 1); //msg + sizeof(struct rte_ether_hdr);
	ip->version_ihl = 0x45;  // 5表示20个字节
	ip->type_of_service = 0;
	ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr)); // 需要进行网络字节序的转换
	ip->packet_id = 0;
	ip->fragment_offset = 0;
	ip->time_to_live = 64;
	ip->next_proto_id = IPPROTO_TCP;
	ip->src_addr = global_sip;
	ip->dst_addr = global_dip;

	ip->hdr_checksum = 0;  // 必须要清零 rte_ipv4_cksum会计算hdr_checksum的,不清零会导致错误
	ip->hdr_checksum = rte_ipv4_cksum(ip);


    // tcp header
	struct rte_tcp_hdr *tcp = (struct rte_tcp_hdr *)(ip + 1);
	tcp->src_port = global_sport;
	tcp->dst_port = global_dport;
	tcp->sent_seq = htonl(12345);
	tcp->recv_ack = htonl(global_seqnum + 1);
	tcp->data_off = 0x50;
	tcp->tcp_flags = RTE_TCP_SYN_FLAG | RTE_TCP_ACK_FLAG; //0x1 << 1;
	
	tcp->rx_win = TCP_INIT_WINDOWS; //htons(4096);  // rmem
	tcp->cksum = 0;
	tcp->cksum = rte_ipv4_udptcp_cksum(ip, tcp);

    return 0;
}

int main (int argc, char *argv[]) {   // DPDK (Data Plane Development Kit) 

    int ret = rte_eal_init(argc, argv); // 初始化DPDK环境抽象层
    if (ret < 0) {
        rte_exit(EXIT_FAILURE, "Error with EAL initialization\n");
    }

    // 创建内存池(mbuf_pool)用于存储网络数据包
    struct rte_mempool * mbuf_pool = rte_pktmbuf_pool_create("mbuf pool", NUM_MBUFS, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());

    if(mbuf_pool == NULL) {
        rte_exit(EXIT_FAILURE, "Could not create mbuf pool\n");
    }
    ustack_init_port(mbuf_pool);

    while(1) {
        struct rte_mbuf *mbufs[BURST_SIZE] = {0};

        uint16_t num_recvd = rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE); // 零拷贝的高效接收方式(直接操作网卡DMA内存,减少数据拷贝)
        if (num_recvd > BURST_SIZE) {
            rte_exit(EXIT_FAILURE, "Error receiving from eth\n");
        }
        

        for (int i = 0; i < num_recvd; i++) {
            // rte_pktmbuf_mtod从mbuf中获取以太网头  14个字节
            struct rte_ether_hdr *ethhdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
            
            // 打印以太网类型 0x0800 rte_be_to_cpu_16将大端序转换为主机字节序(大小端--》两个字节以上都要转)
            // printf("Ethernet type: 0x%04x\n", rte_be_to_cpu_16(ethhdr->ether_type));
            
            if (ethhdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) { // rte_cpu_to_be_16 主机字节序转换为大端序
                printf("Not IPv4 packet, skipping\n");
                rte_pktmbuf_free(mbufs[i]);  // 释放非IPv4包
                continue;
            }
            // rte_pktmbuf_mtod_offset获取IP头(位于以太网头之后)
            struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));
            
            // 打印IP信息 使用inet_ntop将IP地址转换为可读字符串格式
            char src_ip[16], dst_ip[16];//IP Packet: 10.66.104.182 -> 10.66.189.155, Protocol: 17 UDP 6=TCP
            inet_ntop(AF_INET, &(iphdr->src_addr), src_ip, sizeof(src_ip)); // inet_ntop函数将网络字节序的IP地址转换为点分十进制字符串格式
            inet_ntop(AF_INET, &(iphdr->dst_addr), dst_ip, sizeof(dst_ip)); // AF_INET表示IPv4地址族
            // printf("IP Packet: %s -> %s, Protocol: %d\n", src_ip, dst_ip, iphdr->next_proto_id);
            
            if (iphdr->next_proto_id == IPPROTO_UDP) {  // Protocol: 17 UDP iphdr->next_proto_id是一个字节,所以不需要转
                struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1); // iphdr是指向IP头部的指针,UDP头部紧跟在IP头部之后 udp8个字节
                
                // 打印UDP数据
                char *udp_data = (char *)(udphdr + 1);
                int data_len = rte_be_to_cpu_16(udphdr->dgram_len) - sizeof(struct rte_udp_hdr);
                
                // 如果是文本,尝试打印为字符串
                if (data_len > 0) {
                    printf("UDP text: %.*s\n", data_len, udp_data);
                }
// ----------------------------------------打印接受的udp数据------------------------------------------------
// --------------------------------------回发udp数据----------------------------
                // 刚开始的源地址是客户端那边的 ,后面的源地址要变成客户端的
                rte_memcpy(global_smac, ethhdr->d_addr.addr_bytes, RTE_ETHER_ADDR_LEN); // global_smac是数组,本身就是地址
                rte_memcpy(global_dmac, ethhdr->s_addr.addr_bytes, RTE_ETHER_ADDR_LEN);

                rte_memcpy(&global_sip, &iphdr->dst_addr, sizeof(uint32_t));
                rte_memcpy(&global_dip, &iphdr->src_addr, sizeof(uint32_t));

                rte_memcpy(&global_dport, &udphdr->src_port, sizeof(uint16_t));
                rte_memcpy(&global_dport, &udphdr->src_port, sizeof(uint16_t));  

				struct in_addr addr;
				addr.s_addr = iphdr->src_addr;
				printf("sip %s:%d --> ", inet_ntoa(addr), ntohs(udphdr->src_port));

				addr.s_addr = iphdr->dst_addr;
				printf("dip %s:%d --> ", inet_ntoa(addr), ntohs(udphdr->dst_port));

                uint16_t length = ntohs(udphdr->dgram_len);
                uint16_t total_len = length + sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_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_udp_pkt(msg, (uint8_t*)(udphdr+1), total_len);    
                
                rte_eth_tx_burst(global_portid, 0, &mbuf, 1);

// --------------------------------------回发udp数据----------------------------

            } 
// --------------------------------------处理tcp数据----------------------------            
            else if(iphdr->next_proto_id == IPPROTO_TCP){ // 检查IP头中的协议字段(next_proto_id)是否为TCP协议
                printf("TCP packet, protocol: %d\n", iphdr->next_proto_id);
                struct rte_tcp_hdr *tcphdr = (struct rte_tcp_hdr *)(iphdr + 1);

				rte_memcpy(global_smac, ethhdr->d_addr.addr_bytes, RTE_ETHER_ADDR_LEN);
				rte_memcpy(global_dmac, ethhdr->s_addr.addr_bytes, RTE_ETHER_ADDR_LEN);

				rte_memcpy(&global_sip, &iphdr->dst_addr, sizeof(uint32_t));
				rte_memcpy(&global_dip, &iphdr->src_addr, sizeof(uint32_t));

				rte_memcpy(&global_sport, &tcphdr->dst_port, sizeof(uint16_t));
				rte_memcpy(&global_dport, &tcphdr->src_port, sizeof(uint16_t));
				
                global_flags = tcphdr->tcp_flags;
				global_seqnum = ntohl(tcphdr->sent_seq);
				global_acknum = ntohl(tcphdr->recv_ack);
				
				struct in_addr addr;
				addr.s_addr = iphdr->src_addr;
				printf("tcp pkt sip %s:%d --> ", inet_ntoa(addr), ntohs(tcphdr->src_port));

				addr.s_addr = iphdr->dst_addr;
				printf("dip %s:%d , flags: %x, seqnum: %d, acknum: %d\n", inet_ntoa(addr), ntohs(tcphdr->dst_port), 
					global_flags, global_seqnum, global_acknum);
                
                if(global_flags & RTE_TCP_SYN_FLAG){ //SYN标志处理:当收到SYN包且当前状态为LISTEN时,构造SYN-ACK响应
                    if(tcp_status == USTACK_TCP_STATUS_LISTEN){
                        uint16_t total_len = sizeof(struct rte_tcp_hdr) + sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_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;
                    }  
                }

                if(global_flags & RTE_TCP_ACK_FLAG){ // ACK标志处理:收到ACK包时,将连接状态更新为ESTABLISHED
                    if (tcp_status == USTACK_TCP_STATUS_SYN_RCVD) {

						printf("enter established\n");
						tcp_status = USTACK_TCP_STATUS_ESTABLISHED;
					}
                }

                if(global_flags & RTE_TCP_PSH_FLAG){ // PSH标志处理:当连接已建立且收到PSH标志时,打印数据内容
					printf("enter established: %d\n", tcp_status);
					if (tcp_status == USTACK_TCP_STATUS_ESTABLISHED) {

						uint8_t hdrlen = (tcphdr->data_off >> 4) * sizeof(uint32_t); // TCP头部长度

						uint8_t *data = ((uint8_t*)tcphdr + hdrlen); // 数据部分的起始地址

						printf("tcp data: %s\n", data);
					}
                }
            }
// --------------------------------------回发tcp数据----------------------------               
            rte_pktmbuf_free(mbufs[i]);  // 释放内存
        }
    }

    printf("Hello, UStack!\n");

    return 0;
}

欢迎:https://github.com/0voice

...全文
138 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

544

社区成员

发帖
与我相关
我的任务
社区描述
零声学院,目前拥有上千名C/C++开发者,我们致力将我们的学员组织起来,打造一个开发者学习交流技术的社区圈子。
nginx中间件后端 企业社区
社区管理员
  • Linux技术狂
  • Yttsam
  • 零声教育-晚晚
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

请新加入的VIP学员,先将自己参加活动的【所有文章】,同步至社区:

【内容管理】-【同步至社区-【零声开发者社区】

试试用AI创作助手写篇文章吧