【DPDK实战】编写一个高性能 UDP 抓包程序

在高性能网络编程领域,DPDK (Data Plane Development Kit) 是绕不开的一座大山。它通过绕过操作系统内核(Kernel Bypass),直接在用户态接管网卡,实现了极高的数据包处理效率。

很多初学者配置好环境后,面对复杂的 API 无从下手。今天这篇文章,我们将从零开始,编写一个基于 DPDK 的 用户态协议栈雏形(ustack)

本文目标: 实现一个程序,能够从网卡捕获 IPv4 下的 UDP 数据包,并解析打印出 Payload 内容。

核心概念:为什么 DPDK 这么快?

在看代码之前,我们需要理解 DPDK 的三个核心,这也是我们代码中各个模块存在的意义:

  1. UIO / VFIO(绕过内核) : 传统 socket 编程,数据要从网卡 -> 驱动 -> 内核协议栈 -> 用户空间,发生了多次内存拷贝。DPDK 直接把网卡映射到用户空间,**零拷贝(Zero Copy)**直接读写。

  2. Hugepages & Mempool(大页内存与内存池) : 为了避免频繁的 malloc/free 造成性能抖动,DPDK 在启动时就申请好一大块内存(大页),切割成固定大小的 mbuf 放在池子里,随用随取,用完归还。

  3. Polling Mode(轮询模式) : 传统网卡使用中断(Interrupt)通知 CPU,高并发下 CPU 会被中断淹没。DPDK 使用 while(1) 死循环不断查询网卡:"有包吗?有包吗?",虽然占用了 CPU,但消除了中断开销,吞吐量极大。

代码全解析

我们将代码分为三个部分:环境初始化网卡配置业务循环

1. 头文件与宏定义

cpp 复制代码
#include <stdio.h>          // 标准输入输出库,用于 printf
#include <rte_eal.h>        // DPDK 核心库:EAL (Environment Abstraction Layer) 环境抽象层
#include <rte_ethdev.h>     // DPDK 网卡设备库:提供网卡配置、收发包 API
#include <arpa/inet.h>      // 提供网络字节序转换等函数 (如 htons)

// 指定我们要使用的网卡端口 ID,0 代表系统识别到的第一个 DPDK 网卡
int global_portid = 0;

// 内存池大小:申请 4096 个 mbuf (内存块)
// 如果包处理得慢,或者收包太快,这个池子可能会枯竭,导致丢包
#define NUM_MBUFS    4096

// 批量处理大小:每次从网卡"抢" 128 个包
#define BURST_SIZE   128

注意BURST_SIZE 设置为 128 或 32 是常见做法。批量处理可以分摊函数调用的开销(类似批发比零售便宜)。

2. 网卡初始化函数 (ustack_init_port)

这个函数负责告诉网卡:"我要怎么用你"。

cpp 复制代码
// 网卡的基础配置结构体
static const struct rte_eth_conf port_conf_default = {
    // 设置接收模式:最大包长度为以太网标准最大长度 (1518字节)
    // 注意:旧版本是用 RTE_ETH_MAX_PKT_LEN,新版本用 RTE_ETHER_MAX_LEN
    .rxmode = { .max_rx_pkt_len = RTE_ETHER_MAX_LEN }
};

// [函数:初始化网卡端口]
// static 关键字:表示这是内部函数,防止对外暴露符号,避免命名冲突
static int ustack_init_port(struct rte_mempool *mbuf_pool){

    // 1. 查询系统里有几个可用的 DPDK 网卡
    uint16_t nb_sys_ports = rte_eth_dev_count_avail();
    if(nb_sys_ports == 0){
        rte_exit(EXIT_FAILURE, "No available ports\n"); // 如果没有网卡,直接报错退出
    }

    // 2. 配置网卡参数
    const int num_rx_queues = 1; // 启用 1 个接收队列 (收货口)
    const int num_tx_queues = 0; // 启用 0 个发送队列 (只收不发,做抓包工具)
    
    // 下发配置给 global_portid 号网卡
    rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);

    // 3. 绑定接收队列 (RX Queue) 和 内存池 (Mempool)
    // 意思:0 号网卡的 0 号队列,你以后收到的包,都往 mbuf_pool 里的空箱子里装
    // 参数 128 是指接收描述符的数量 (Ring Size),即网卡硬件缓冲区的大小
    if(rte_eth_rx_queue_setup(global_portid, 0, 128, rte_eth_dev_socket_id(global_portid), NULL, mbuf_pool) < 0){
        rte_exit(EXIT_FAILURE, "Could not setup RX queue\n");
    }

    // 4. 正式启动网卡 (Link Up)
    // 这一步之后,网卡指示灯通常会亮起,开始工作
    if(rte_eth_dev_start(global_portid) < 0) {
        rte_exit(EXIT_FAILURE,"Could not start\n");
    }

    return 0; // 初始化成功
}

3. Main 函数:EAL 初始化与内存池

cpp 复制代码
int main(int argc, char *argv[]){

    // [Step 1] EAL 初始化 (Environment Abstraction Layer)
    // 这是 DPDK 的入口,负责接管 CPU、内存和网卡硬件
    // 如果这一步失败,说明环境没配好 (比如没有大页内存,或者没加载 vfio-pci 驱动)
    if(rte_eal_init(argc,argv) < 0){
        rte_exit(EXIT_FAILURE, "EAL initialization failed\n");
    }

    // [Step 2] 创建内存池 (Mempool)
    // 名字叫 "mbuf pool",一共有 NUM_MBUFS (4096) 个块
    // 这是为了避免频繁 malloc/free,提升性能
    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");
    }

    // [Step 3] 调用上面的辅助函数,初始化端口
    ustack_init_port(mbuf_pool);
    
    // ... 进入核心循环 ...
}

4. 核心循环:收包、解析、释放(重点!)

这是程序最繁忙的地方,也是最容易出错(比如内存泄漏)的地方。

cpp 复制代码
// [Step 4] 进入死循环,开始"干苦力" (收包-处理-释放)
    while(1){

        // 定义一个指针数组,用来存放收到的一批包
        struct rte_mbuf *mbufs[BURST_SIZE] = {0};

        // A. [核心收包 API]
        // 从 global_portid 的 0 号队列,尝试"抢"最多 BURST_SIZE (128) 个包
        // num_recvd 是实际抢到的包数量 (可能为 0,也可能为 128)
        uint16_t num_recvd = rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE);
        
        // 理论上不应该超过 BURST_SIZE
        if(num_recvd > BURST_SIZE){
            rte_exit(EXIT_FAILURE, "Received more packets than burst size\n");
        }

        // B. 遍历处理抢到的每一个包
        int i = 0;
        for(i = 0; i < num_recvd; i++){
            
            // [零拷贝解析 - 第 1 层:以太网头]
            // rte_pktmbuf_mtod (Message to Data): 把 mbuf 转成以太网头结构体指针
            struct rte_ether_hdr *eth_hdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
            
            // 过滤:检查以太网类型是否为 IPv4 (0x0800)
            // rte_cpu_to_be_16: 因为网络协议是大端序 (Big Endian),x86 CPU 是小端序,需要转换
            if(eth_hdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)){
                // 如果不是 IPv4 (比如是 ARP 或 IPv6),就不处理
                rte_pktmbuf_free(mbufs[i]); // 释放内存,把箱子还给池子
                continue;                   // 跳过,看下一个包
            }

            // [零拷贝解析 - 第 2 层:IP 头]
            // mtod_offset: 在开头的基础上,向后偏移 sizeof(ether_hdr) 长度,找到 IP 头
            struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));
            
            // 过滤:检查 IP 协议号是否为 UDP (17)
            if(iphdr->next_proto_id == IPPROTO_UDP){

                // [零拷贝解析 - 第 3 层:UDP 头]
                // 这里的指针加法 (iphdr + 1) 实际上是地址向后移动 sizeof(struct rte_ipv4_hdr)
                struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1);

                // [打印数据]
                // (char*)(udphdr + 1) 指向 UDP 头后面的数据部分 (Payload)
                // 注意:如果 Payload 不是以 \0 结尾的字符串,这里可能会打印乱码
                printf("udp : %s\n", (char*)(udphdr + 1));
            }

            // [释放内存]
            // 无论刚才是否打印了 UDP,处理完当前包后,必须手动归还内存
            // 如果忘了这行,4096 个箱子很快用完,程序就会无法再收包 (内存泄漏)
            rte_pktmbuf_free(mbufs[i]);
        }

    }

完整可运行代码

cpp 复制代码
#include <stdio.h>
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <arpa/inet.h>

int global_portid = 0;

#define NUM_MBUFS    4096
#define BURST_SIZE   128

static const struct rte_eth_conf port_conf_default = {
    .rxmode = { .max_rx_pkt_len = RTE_ETHER_MAX_LEN }
};

static int ustack_init_port(struct rte_mempool *mbuf_pool) {
    uint16_t nb_sys_ports = rte_eth_dev_count_avail();
    if (nb_sys_ports == 0) {
        rte_exit(EXIT_FAILURE, "No available ports\n");
    }

    const int num_rx_queues = 1;
    const int num_tx_queues = 0;
    rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);

    if (rte_eth_rx_queue_setup(global_portid, 0, 128, rte_eth_dev_socket_id(global_portid), NULL, mbuf_pool) < 0) {
        rte_exit(EXIT_FAILURE, "Could not setup RX queue\n");
    }

    if (rte_eth_dev_start(global_portid) < 0) {
        rte_exit(EXIT_FAILURE, "Could not start\n");
    }

    return 0;
}

int main(int argc, char *argv[]) {
    if (rte_eal_init(argc, argv) < 0) {
        rte_exit(EXIT_FAILURE, "EAL initialization failed\n");
    }

    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);
        if (num_recvd > BURST_SIZE) {
            rte_exit(EXIT_FAILURE, "Received more packets than burst size\n");
        }

        int i = 0;
        for (i = 0; i < num_recvd; i++) {
            struct rte_ether_hdr *eth_hdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
            
            if (eth_hdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {
                rte_pktmbuf_free(mbufs[i]);
                continue;
            }

            struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));
            if (iphdr->next_proto_id == IPPROTO_UDP) {
                struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1);
                // 建议:实际项目中请使用 HexDump 打印二进制数据
                printf("udp payload : %s\n", (char*)(udphdr + 1));
            }
            
            // 务必释放 mbuf
            rte_pktmbuf_free(mbufs[i]);
        }
    }

    printf("hello world\n");
    return 0;
}

总结

通过这个简单的 Demo,我们完成了从网卡抓包到解析 UDP 的全过程。虽然代码短,但它包含了 DPDK 最精髓的思想:EAL 初始化环境、Mempool 管理内存、Rx Burst 批量收包、Zero Copy 指针解析

0voice · GitHub

相关推荐
fy zs2 小时前
网络层IP协议的初步认识
服务器·网络·tcp/ip
克里斯蒂亚诺更新2 小时前
https写一个定位当前位置获取经纬度的H5页面
css·网络协议·https
网安小白的进阶之路2 小时前
B模块 安全通信网络 第二门课 核心网路由技术-2-BGP-邻居-全互联
网络·安全·智能路由器
learning-striving2 小时前
ospf综合配置实验
网络·ensp
上海云盾-高防顾问3 小时前
DDoS防护方案性价比分析:不同企业该怎么选?
网络·ddos
小快说网安3 小时前
拆解 DDoS 攻击套路:抗 D 防护的主动防御与应急响应机制
网络·ddos·网络攻击·高防ip
小快说网安3 小时前
硬核解析:高防 IP 是如何拦截 DDoS 攻击的?从清洗中心到流量调度
网络·tcp/ip·网络安全·ddos
2301_765715143 小时前
TCP/IP协议深度解析与应用场景
网络·tcp/ip·php
北京耐用通信3 小时前
耐达讯自动化Profibus总线光纤中继器:破解石油化工分析仪器通讯难题
网络·人工智能·科技·物联网·网络协议·自动化·信息与通信