探索DPDK:实现UDP接收与发送

目录

一、UDP接收

1、初始化环境

2、创建内存池

1)name

2) n

3)cache_size

4)priv_size

5)data_room_size

6)socket_id

3、网络端口配置

1)检查可用端口数量

2)配置网络端口

3)设置接收队列

4)启动端口

4、处理数据包

1)接收数据包

2)处理数据包 

二、发送UDP数据包

1、定义全局变量

2、网络端口配置增加发送队列

3、设置发送队列

4、 编码UDP数据包

1)构建以太网头部 (Ethernet Header)

2)构建IP头部 (IP Header)

3)计算IP头部校验和 

1.错误检测机制

2.如何计算校验和

3.重要性

4.局限性

4)构建UDP头部 (UDP Header)

5)计算UDP校验和

5、在主循环中处理和发送数据包


一、UDP接收

#include <stdio.h>
#include <rte_eal.h>               // 包含DPDK的Environment Abstraction Layer(EAL)函数
#include <rte_ethdev.h>            // 包含以太网设备操作相关的函数和结构定义
#include <arpa/inet.h>             // 包含用于网络地址转换的函数

int global_portid = 0;              // 全局变量,用于指定操作的网络端口

#define NUM_MBUFS  4096             // 定义MBUF池的大小
#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 Supported eth found\n");  // 如果没有端口可用,退出程序
	}

	const int num_rx_queues = 1;    // 接收队列数量设置为1
	const int num_tx_queues = 0;    // 发送队列数量设置为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, "Error with EAL init\n");  // 初始化EAL
	}

	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");  // 创建MBUF池
	}

	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, "Error receiving from eth\n");  // 接收错误检查
		}

		int i = 0;
		for (i = 0; i < num_recvd; i++) {
			struct rte_ether_hdr *ethhdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);  // 获取以太网头部
			if (ethhdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {
				continue;  // 只处理IPv4数据包
			}

			struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));  // 获取IP头部
			if (iphdr->next_proto_id == IPPROTO_UDP) {  // 检查是否为UDP数据包
				struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1);  // 获取UDP头部
				printf("udp : %s\n", (char*)(udphdr+1));  // 输出UDP数据内容
			}
		}
	}
	printf("hello dpdk\n");  // 程序启动提示信息
}

1、初始化环境

这里主要负责初始化DPDK的环境抽象层(EAL)。这是使用DPDK开发网络应用的第一步,因为EAL负责管理CPU核心、内存资源以及PCI设备的访问权限等。

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

"rte_eal_init"函数根据传入的参数初始化EAL。如果初始化失败,程序会打印错误信息并退出。

2、创建内存池

网络应用通常需要处理大量的数据包。为了提高性能,DPDK使用内存池来管理这些数据包,避免频繁的内存分配和释放。

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");
}

创建了一个名为"mbuf pool"的内存池,用于存储4096个数据包缓冲区(MBUF)。如果内存池创建失败,程序将打印错误信息并退出。

rte_pktmbuf_pool_create()

在代码中创建了一个名为 "mbuf pool" 的内存池,其中包含 NUM_MBUFS 个数据包缓冲区,每个CPU核心的缓存大小设置为0(使用默认设置),每个mbuf没有额外的私有空间,数据区大小设置为默认值,最后,内存池被分配在执行代码的CPU对应的NUMA节点上,六个输入参数这里详细解释下:

1)name

  • 类型: const char *
  • 描述: 内存池的名称。这个名字在调试时很有用,因为它可以帮助识别不同的内存池。

2) n

  • 类型: unsigned n
  • 描述: 内存池中mbuf的数量。这个数量定义了池中可以存储的最大数据包数。

3)cache_size

  • 类型: unsigned cache_size
  • 描述: 每个CPU核心的缓存大小。DPDK使用本地缓存来减少对全局数据结构的争用和访问,从而提高性能。设置为0时,将使用默认缓存大小。

4)priv_size

  • 类型: uint16_t priv_size
  • 描述: 每个mbuf的私有数据区大小。这是用户可以用于存储额外信息(例如,自定义控制信息)的空间。

5)data_room_size

  • 类型: uint16_t data_room_size
  • 描述: 每个mbuf中的数据区大小。这应该足够大,以存储最大的预期数据包。RTE_MBUF_DEFAULT_BUF_SIZE 通常用作这个参数,它定义了一个默认的缓冲区大小,足以容纳常见的网络数据包。

6)socket_id

  • 类型: int socket_id
  • 描述: 分配内存池的NUMA(非统一内存访问)节点。通常使用 rte_socket_id() 来获取当前执行代码的CPU所在的NUMA节点。这有助于优化内存访问性能,确保内存和CPU之间的局部性最大化。

3、网络端口配置

一旦内存池创建完成,下一步是配置网络端口。这包括设置接收和发送队列的数量、最大接收包长度等。

函数 ustack_init_port() 负责初始化和配置指定的网络端口。这个函数是网络应用的关键部分,因为它设置了网络设备的基本运行参数,包括端口的接收和发送队列。

调用rte_eth_dev_configure来设置端口参数,以及调用rte_eth_rx_queue_setup来设置接收队列。

1)检查可用端口数量

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

 首先,函数调用 rte_eth_dev_count_avail() 来获取系统中可用的以太网端口数量。如果没有可用的端口,函数会调用 rte_exit(),打印错误信息并退出程序。这是一个基本的错误检查步骤,确保了程序有可操作的网络设备。

2)配置网络端口

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);

在确定有可用端口后,函数配置这些端口。这里,设置接收队列数量为1,发送队列数量为0(只关注接收)。然后,使用 rte_eth_dev_configure() 函数和之前定义的端口配置 port_conf_default 来配置全局变量 global_portid 指定的端口。

rte_eth_dev_configure 是一个核心函数,在DPDK应用程序中用于配置网络设备(即网卡)。这个函数设置了网络端口的基本操作参数,包括接收和发送队列的数量以及其他端口相关的配置。

各参数的意义如下:

  1. port_id:

    • 类型:uint16_t
    • 说明:指定要配置的以太网设备的端口号。DPDK在系统启动时会枚举所有支持的网络设备,并为每个设备分配一个唯一的端口号。
  2. nb_rx_queue:

    • 类型:uint16_t
    • 说明:为指定的端口配置的接收队列数量。这个值决定了可以并行处理多少流量,通常与处理器核心数相关。每个接收队列可以绑定到特定的处理器核心上,以平衡负载和优化性能。
  3. nb_tx_queue:

    • 类型:uint16_t
    • 说明:为指定的端口配置的发送队列数量。同接收队列类似,发送队列的数量可以根据发送流量的需求和处理器核心数来设置。
  4. conf:

    • 类型:const struct rte_eth_conf *
    • 说明:指向 rte_eth_conf 结构的指针,该结构包含了更详细的网络端口配置信息。这个结构体允许你设置各种网络设备的高级特性,如下所示:
      struct rte_eth_conf {
          struct {
              struct {
                  uint64_t offloads;           // 定义接收时的一些特定操作,如校验和计算、分散/聚集等
                  uint32_t max_rx_pkt_len;     // 可接收的最大数据包长度
                  uint16_t split_hdr_size;     // 如果使用分散/聚集,定义头部分割的大小
                  uint8_t header_split;        // 是否启用头部分割功能
                  uint8_t hw_ip_checksum;      // 是否启用硬件IP校验和
                  uint8_t hw_vlan_filter;      // 是否启用硬件VLAN过滤
                  uint8_t hw_vlan_strip;       // 是否在接收时自动剥离VLAN标签
                  uint8_t hw_vlan_extend;      // 是否启用VLAN扩展处理
                  uint8_t jumbo_frame;         // 是否启用巨帧处理
                  uint8_t hw_strip_crc;        // 是否剥离接收数据包的CRC
                  uint8_t enable_scatter;      // 是否启用散布聚集接收
                  uint8_t enable_lro;          // 是否启用大接收脱载(LRO)
              } rxmode;
              struct {
                  uint64_t offloads;           // 定义发送时的一些特定操作,如校验和计算等
              } txmode;
          } rx_adv_conf, tx_adv_conf;
          uint64_t link_speeds;                // 定义支持的链路速度
          struct rte_eth_rss_conf rss_conf;    // 定义接收端扩展功能,如RSS(接收侧缩放)
      };
      
      static const struct rte_eth_conf port_conf_default = {
          .rxmode = {.max_rx_pkt_len = RTE_ETHER_MAX_LEN}
      };
      

      代码开头,这里只明确设置了 rte_eth_conf 结构体中的 max_rx_pkt_len 字段。其他的配置参数采用的是结构体初始化时的默认值,这意味着除非特别指定,否则这些参数将被初始化为零或默认状态。在DPDK中,结构体的未显式初始化字段通常会被设置为零或对应类型的默认值。通过设定 .max_rx_pkt_len = RTE_ETHER_MAX_LEN,配置网络端口准备接收标准以太网帧的最大长度的数据包。这是基本配置,用于确保端口能处理标准大小的以太网帧。

3)设置接收队列

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");
}

接下来,函数为端口设置接收队列。这里它指定队列索引为0,描述符数量为128,使用从 rte_eth_dev_socket_id() 获取的NUMA节点。内存池 mbuf_pool 被用作接收数据包的存储。如果队列设置失败,程序将打印错误信息并退出。

rte_eth_rx_queue_setup 是DPDK库中用来配置网络设备的接收队列的函数。下面是每个参数的详细说明:

  • uint16_t port_id

    • 说明: 网络端口的标识符。这是要配置的网络设备的端口号。
    • 用途: 用于指定要设置接收队列的具体网络端口。
  • uint16_t rx_queue_id

    • 说明: 接收队列的索引号。
    • 用途: 端口可以有多个接收队列,此参数用于指定哪一个接收队列将被设置。
  • uint16_t nb_rx_desc

    • 说明: 接收队列中描述符的数量。
    • 用途: 描述符是用于指定网络数据包内存位置的数据结构,此参数设置队列中可用的描述符数量,影响队列处理数据包的能力。
  • unsigned int socket_id

    • 说明: 指定接收队列绑定的NUMA节点的socket ID。
    • 用途: 对于多socket系统,绑定接收队列到具体的NUMA节点可以优化内存访问性能。
  • *const struct rte_eth_rxconf rx_conf

    • 说明: 指向接收队列配置结构的指针。
    • 用途: 该结构包含了一系列接收选项和配置,如接收模式和阈值等,用于细致调整接收行为和性能。
  • *struct rte_mempool mb_pool

    • 说明: 指向内存池的指针。
    • 用途: 此内存池用于分配接收数据包的内存缓冲区。设置正确的内存池是保证网络数据包正确接收的关键。

rx_conf 被设置为 NULL,表示使用默认的接收队列配置。通常,你可能需要根据特定应用的需求来调整这个配置,以达到最佳的接收性能。

问题:为什么用这个rte_eth_dev_socket_id(global_portid)获取id不是直接global_portid?

rte_eth_dev_socket_id(global_portid) 函数的使用中,我们需要区分两个重要的概念:端口标识符(port_id套接字标识符(socket_id。这两个标识符在DPDK中服务于不同的目的。

端口标识符 (port_id)

  • 描述port_id 是用来标识网络端口的编号。在DPDK中,每个物理或虚拟的网络接口被分配一个唯一的编号,称为 port_id。这个编号用于配置和管理特定的网络端口。
  • 用途:用来指定哪一个网络端口将接受配置指令,比如接收队列和发送队列的设置、启动和停止端口等。

套接字标识符 (socket_id)

  • 描述socket_id 指的是网络端口绑定的NUMA(Non-Uniform Memory Access)节点的套接字标识符。NUMA是一种内存设计,用于多处理器系统中,其中内存访问时间取决于内存的物理位置和访问该内存的处理器。
  • 用途:在配置DPDK网络端口时,将接收队列等绑定到具体的NUMA节点可以优化内存访问的性能。socket_id 用于指示数据结构和缓冲区应该在哪个NUMA节点上分配,以最小化跨节点的内存访问延迟。

使用 rte_eth_dev_socket_id(global_portid)

当调用 rte_eth_dev_socket_id(global_portid)

  • 这个函数返回与指定端口 (global_portid) 绑定的NUMA节点的套接字ID
  • 它是根据物理设备的位置和系统的NUMA架构自动确定的。
  • 在设置接收队列的内存池时,使用正确的 socket_id 确保了内存分配的效率和性能最优化,特别是在处理高速网络流量时。

因此,即使你已经有了端口号 (global_portid),你仍然需要使用 rte_eth_dev_socket_id 函数来获取对应端口的NUMA节点信息,以便正确地配置内存和队列,保证数据处理的性能。

4)启动端口

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

4、处理数据包

1)接收数据包

在无限循环中,使用 rte_eth_rx_burst 函数从指定的网络端口(global_portid)批量接收数据包。这个函数的功能是尝试从设备的接收队列中获取最多 BURST_SIZE(定义为128)个数据包。

uint16_t num_recvd = rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE);
  • global_portid:接收数据的端口号。
  • 0:指定从哪个接收队列获取数据包。
  • mbufs:一个指向 struct rte_mbuf * 数组的指针,用于存放接收到的数据包。
  • BURST_SIZE:此次调用尝试接收的最大数据包数量。

返回实际接收到的数据包数量。如果返回的数量超出了请求的数量,程序将输出错误信息并退出。

2)处理数据包 

for (i = 0; i < num_recvd; i++) {
    struct rte_ether_hdr *ethhdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
    if (ethhdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {
        continue;  // 忽略非IPv4类型的数据包
    }
    ...
}
  • rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *):将 mbuf 数据包转换为指向其数据开始位置的指针(此例中为以太网头部)。
  • ethhdr->ether_type:检查以太网帧的类型。如果不是IPv4数据包(以太网类型不等于RTE_ETHER_TYPE_IPV4),则跳过当前循环迭代。
  • rte_cpu_to_be_16():
    • 功能: 将CPU表示的16位数转换为大端格式。
    • 用途: 用于网络协议中的字节序转换。

对于是IPv4的数据包,程序进一步检查IP协议头,特别是协议类型是否为UDP:

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);
    printf("udp : %s\n", (char*)(udphdr+1));  // 打印UDP数据内容
}
  • rte_pktmbuf_mtod_offset:从数据包的指定偏移量处获取数据,这里是跳过以太网头部后的IP头部。
  • iphdr->next_proto_id:检查IP数据包的下一个协议是否为UDP。
  • udphdr:指向UDP头部的指针。
  • (char*)(udphdr+1):指向UDP数据内容的指针,这里+1意味着跳过UDP头部直接指向数据部分

二、发送UDP数据包

在接收UDP包的基础上增加了发送UDP包的代码逻辑,完整代码如下:

#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


#define ENABLE_SEND		1


#if ENABLE_SEND


uint8_t global_smac[RTE_ETHER_ADDR_LEN];
uint8_t global_dmac[RTE_ETHER_ADDR_LEN];


uint32_t global_sip;
uint32_t global_dip;

uint16_t global_sport;
uint16_t global_dport;


#endif


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) {
//number
	uint16_t nb_sys_ports = rte_eth_dev_count_avail();
	if (nb_sys_ports == 0) {
		rte_exit(EXIT_FAILURE, "No Supported eth found\n");
	}

	struct rte_eth_dev_info dev_info;
	rte_eth_dev_info_get(global_portid, &dev_info);
// eth0, 

	const int num_rx_queues = 1;
#if ENABLE_SEND
	const int num_tx_queues = 1;
#else
	const int num_tx_queues = 0;
#endif
	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 ENABLE_SEND

	struct rte_eth_txconf txq_conf = dev_info.default_txconf;
	txq_conf.offloads = port_conf_default.rxmode.offloads;
	if (rte_eth_tx_queue_setup(global_portid, 0, 512, rte_eth_dev_socket_id(global_portid), &txq_conf) < 0) {
		rte_exit(EXIT_FAILURE, "Could not setup TX queue\n");
	}
	
#endif 
	
	if (rte_eth_dev_start(global_portid) < 0) {
		rte_exit(EXIT_FAILURE, "Could not start\n");
	}

	return 0;
}

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

	//1 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);

	//1 ip header
	struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth + 1); //msg + sizeof(struct rte_ether_hdr);
	ip->version_ihl = 0x45;
	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;
	ip->hdr_checksum = rte_ipv4_cksum(ip);

	//1 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;
}

int main(int argc, char *argv[]) {

	if (rte_eal_init(argc, argv) < 0) {
		rte_exit(EXIT_FAILURE, "Error with EAL init\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, "Error receiving from eth\n");
		}


		int i = 0;
		for (i = 0;i < num_recvd;i ++) {

			struct rte_ether_hdr *ethhdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
			if (ethhdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {
				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);
#if ENABLE_SEND

				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, &udphdr->dst_port, sizeof(uint32_t));
				rte_memcpy(&global_dport, &udphdr->src_port, sizeof(uint32_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_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_udp_pkt(msg, (uint8_t*)(udphdr+1), total_len);

				rte_eth_tx_burst(global_portid, 0, &mbuf, 1);
#endif
				printf("udp : %s\n", (char*)(udphdr+1));

			} else if (iphdr->next_proto_id == IPPROTO_TCP) {

				struct rte_tcp_hdr *tcphdr = (struct rte_tcp_hdr *)(iphdr + 1);

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

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

			}

		}	
	}


	printf("hello dpdk\n");

}

1、定义全局变量

#define ENABLE_SEND		1

#if ENABLE_SEND
uint8_t global_smac[RTE_ETHER_ADDR_LEN];
uint8_t global_dmac[RTE_ETHER_ADDR_LEN];
uint32_t global_sip;
uint32_t global_dip;
uint16_t global_sport;
uint16_t global_dport;
#endif

定义了用于发送的全局变量,包括源和目的MAC地址、IP地址以及端口号。

ENABLE_SEND宏用于控制是否启用发送功能。

2、网络端口配置增加发送队列

#if ENABLE_SEND
	const int num_tx_queues = 1;
#else
	const int num_tx_queues = 0;
#endif
rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);

在ustack_init_port()函数中增加配置发送队列

3、设置发送队列

#if ENABLE_SEND
	struct rte_eth_txconf txq_conf = dev_info.default_txconf;
	txq_conf.offloads = port_conf_default.rxmode.offloads;
	if (rte_eth_tx_queue_setup(global_portid, 0, 512, rte_eth_dev_socket_id(global_portid), &txq_conf) < 0) {
		rte_exit(EXIT_FAILURE, "Could not setup TX queue\n");
	}
#endif

设置发送队列,使用从设备信息中获得的默认发送队列配置(dev_info.default_txconf)。这里还设置了队列的offload特性,以增强数据包处理性能。

rte_eth_txconf 结构体在 DPDK 中用于详细配置以太网设备的发送队列。这个结构体允许用户对网络端口的发送操作进行精细控制,包括如何处理网络流量、如何利用硬件加速功能等。这个结构体中的一些关键字段:

  • tx_thresh:这些阈值用于控制发送队列的缓冲行为,例如何时开始处理缓冲中的包,这可以帮助优化延迟和吞吐量。

    • pthresh:预取阈值。决定了在队列描述符被提交到硬件之前,描述符必须被预取到CPU缓存的最小数量。这是一个性能优化参数,可以减少因等待数据而导致的处理延迟。
    • hthresh:主机阈值。这是一个触发硬件开始处理包的描述符数量的阈值,与pthresh结合使用,可以更高效地管理描述符。
    • wthresh:写回阈值。这个参数控制数据包被发送到网络之前,多少描述符会被写回到内存。
  • tx_free_thresh

    • 控制何时释放一个发送描述符。如果设置为0,DPDK会在硬件确认数据包已发送后立即释放描述符。如果设置一个大于1的值,可以延迟描述符的释放,这在某些情况下可以提高性能。
  • tx_rs_thresh

    • 设置在写回阶段必须达到的发送描述符数量,之后才会设置RS(Report Status)位。这个功能有助于管理和监控发送队列的状态。
  • txq_flags

    • 提供对队列行为的低级控制,包括关闭或启用特定的硬件offload功能和其他高级配置。
  • offloads

    • 这是DPDK 17.11及更高版本中新增的字段,用于设置各种发送队列的硬件加速功能,如TCP/UDP校验和计算、分片、多队列散列等。这些offload功能可以显著提高性能,因为它们将计算负担从CPU转移至网络硬件。

4、 编码UDP数据包

static int ustack_encode_udp_pkt(uint8_t *msg, uint8_t *data, uint16_t total_len) {
	...
	udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp);
	return 0;
}

这个函数负责构建一个完整的UDP数据包,包括以太网头、IP头和UDP头,并计算IP和UDP的校验和。

当用户接收并处理完数据包后得到新的用户数据需要发送,此时我们只需要逆向操作接收数据包的过程即可。
一个UDP数据帧组成结构如图所示,在用户数据上添加UDP头,在此基础上再添加IP头,最后再添加以太网头,一个UDP数据帧就组装完毕,就可直接通过网卡发送。

1)构建以太网头部 (Ethernet 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);
  • struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg; 定义了一个指向消息缓冲区起始位置的以太网头部结构指针。
  • 使用rte_memcpy函数将全局定义的目的MAC地址(global_dmac)和源MAC地址(global_smac)复制到以太网头部结构中。
  • 设置eth->ether_type为IPv4,表示随后的数据是IPv4数据包。

2)构建IP头部 (IP Header)

struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth + 1);
ip->version_ihl = 0x45; // IPv4, header length = 5 words (20 bytes)
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; // Next protocol is UDP
ip->src_addr = global_sip;
ip->dst_addr = global_dip;
  • struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth + 1); 指向以太网头部后的位置,即IP头部的开始位置。
  • ip->version_ihl 设置IP版本号为4和头部长度(IHL)为5(即20字节)。0x45
    • 在IP协议的头部结构中,version_ihl 字段是一个 8 位的字段,由两部分组成:

      • Version (版本号):占用高 4 位,指定 IP 协议的版本。对于 IPv4 来说,这个值固定为 4。
      • IHL (Internet Header Length):占用低 4 位,表示 IP 头部的长度,以 32 位字为单位(每个字长 4 字节)。这个长度包括了所有的选项(如果有的话)。
      • 0x4:代表 IPv4。
      • 0x5:表示 IP 头部长度为 5 个 32 位字,即 5 x 4 = 20 字节。为什么是以4字节为最小单位?
        • 内存对齐:许多计算机系统更有效地处理对齐在4字节边界上的数据。将IP头部的长度设为4字节的倍数有助于这种高效处理。
        • 简单性和灵活性:以32位为单位允许头部长度容易计算并简化处理逻辑,因为网络通常在这种边界上操作字节。
  • ip->total_length 设置为整个数据包的长度减去以太网头部的长度。
  • ip->time_to_live 设置生存时间。
  • ip->next_proto_id 设置下一个协议为UDP。
  • ip->src_addrip->dst_addr 设置为全局定义的源和目的IP地址。

3)计算IP头部校验和 

ip->hdr_checksum = 0; // Clear checksum field for calculation
ip->hdr_checksum = rte_ipv4_cksum(ip);
  • 在计算校验和前,先将ip->hdr_checksum设置为0。
  • 使用rte_ipv4_cksum函数计算校验和。

在IP通信中,校验和是一个重要的特性,用于确保IP头部数据在传输过程中的完整性。这是一种错误检测机制,可以帮助接收端检测在传输过程中可能发生的数据损坏。校验的几个关键点:

1.错误检测机制
  • 目的:IP头部的校验和主要用于检测数据在传输过程中由于网络噪音、硬件错误等原因引起的任何错误或损坏。
  • 方法:通过对IP头部的所有16位字进行加和,然后取反得到的结果就是校验和。接收端将对接收到的IP头部进行同样的计算,并将计算结果与接收到的校验和字段进行比较。
  • 效果:如果计算结果与校验和匹配,则认为头部没有错误;如果不匹配,则说明数据在传输过程中可能被破坏,接收端通常会丢弃这样的数据包。
2.如何计算校验和
  • 计算校验和的标准步骤是首先将校验和字段设置为0,然后将头部中的每个16位整数相加,包括首部长度和版本、服务类型、总长度等字段。
  • 计算得到的总和再进行端转换(如果需要),并取一次反码(按位取反)得到最终的校验和。
  • 在发送数据包前,将这个校验和值填入IP头部的校验和字段中。
3.重要性
  • 可靠性:尽管IP协议本身是不可靠的,通过校验和提供了一种基本的数据完整性验证方式,增加了数据传输的可靠性。
  • 简易性:这种校验和计算方法简单,易于在硬件上实现,对处理器的计算负担较小。
  • 快速错误反馈:可以在数据包到达终点之前的任何一点检测到错误,从而减少不必要的数据传输。
4.局限性
  • 校验和只能检测到错误,不能纠正错误。它也可能不会捕捉到所有的错误,尤其是在出现多个错误时,这些错误可能相互抵消。
  • 校验和不涉及数据负载部分,只针对IP头部,因此不保证整个数据包的完整性。

在DPDK这样的高性能网络处理框架中,虽然可能有硬件支持直接计算校验和,但在软件层面上仍然需要正确实现这一机制,以确保在没有硬件支持的情况下也能保证数据的正确性。这就是为什么在构建网络数据包时需要计算并设置正确的IP头部校验和。

4)构建UDP头部 (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);
  • struct rte_udp_hdr *udp = (struct rte_udp_hdr *)(ip + 1); 指向IP头部后的位置,即UDP头部的开始位置。
  • 设置UDP源端口和目的端口。
  • udp->dgram_len 设置为除去以太网头和IP头后的数据长度。

5)计算UDP校验和

rte_memcpy((uint8_t*)(udp+1), data, udplen);
udp->dgram_cksum = 0; // Clear checksum field for calculation
udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp);
  • 将数据复制到UDP数据区。
  • 清除校验和字段,然后使用rte_ipv4_udptcp_cksum函数计算UDP的校验和。

5、在主循环中处理和发送数据包

#if ENABLE_SEND
	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);
	...
	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);
#endif

在接收处理循环中,代码首先复制接收到的数据包中的源和目的MAC地址、IP地址和端口号到全局变量。然后分配一个新的rte_mbuf缓冲区,调用ustack_encode_udp_pkt函数填充数据包内容,最后通过rte_eth_tx_burst函数发送出去。

https://xxetb.xetslk.com/s/2sff5t 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值