初始化等操作
环境、内存池、端口初始化等操作均与“DPDK单核数据包接收案例”一致,可参考:DPDK单核数据包接收案例
启动发送主线程
与接收一致,需要检查当前网络端口和所在线程是否位于同一个NUMA Socket。
|
|
在后续构造数据包时,需要将其放置到内存池中。因此,可通过rte_mempool_lookup()函数获取在初始化阶段创建的指定内存池,该函数的参数为所需内存池的名称。内存池在内部以类似链表的形式组织,查找过程中,函数会依次遍历所有已注册的内存池,并比较名称,若匹配成功则返回对应的内存池对象。
|
|
随后可准备以太网帧所需的MAC地址信息,其中的源MAC地址可借助rte_eth_macaddr_get()函数获取,并填充到rte_ether_addr结构体中,而目的MAC地址可手动填写。
|
|
随后准备网络层、传输层所需的IP地址、端口号、负载
|
|
主循环持续运行直至force_quit标志被置为true。每次循环调用rte_pktmbuf_alloc()从内存池mempool中分配一个数据包缓冲区mbuf。随后调用build_and_send_packet()函数,传入端口编号、缓冲区、源和目的MAC地址、源和目的IP地址、UDP端口号以及负载,完成数据包的构建和发送。每次发送后计数器递增,循环中通过 rte_delay_us(1000000) 进行1秒的延迟,控制发送频率。
需要注意的是,rte_delay_us()是一个忙等待(busy-wait)函数,会持续占用CPU资源进行精确的微秒级延迟,适合短时间、低延迟、高精度的等待。而标准的sleep()函数则会将线程挂起,释放CPU给其他进程使用,适合较长时间的延迟,但精度较低且响应延迟较大。
|
|
数据包构造与发送
build_and_send_packet()函数接收端口编号、缓冲区、源和目的MAC地址、源和目的IP地址、UDP端口号以及负载,以完成数据包的构建和发送。
|
|
首先计算数据包的总长度,包含以太网头、IPv4头、UDP头以及负载的长度。然后调用rte_pktmbuf_append()函数在分配好的缓冲区mbuf中为数据包分配所需空间,返回指向数据区的指针。
需要注意的是rte_pktmbuf_append()并不分配新的内存,而是在已有缓存区mbuf中调整有效数据的长度和位置,类似于指针操作。具体而言,该函数会检查剩余空间是否足够,如果足够,则更新mbuf的data_len(指示当前mbuf中实际包含的数据长度)和pkt_len(指示整个数据包的总长度,因为一个数据包可能会跨多个mbuf进行存储)属性字段,并返回指向新增数据区起始地址的指针,供上层代码写入数据。
|
|
在一块连续的内存区域pkt_data中,为以太网、IP、UDP头部及负载数据分别设置指针,便于逐层构造网络包的各个协议头。
|
|
填充以太网帧头部:使用rte_ether_addr_copy()函数将MAC地址复制到以太网头部中的相应字段中,并设置以太网帧的类型字段,以表示该帧封装的是 IPv4 协议的数据,由于类型字段是2字节大小,需使用rte_cpu_to_be_16()函数转换数值为网络字节序。
|
|
填充IPv4头部:
- version_ihl是组合字段,前4位是版本(IPv4是4),后4位是头部长度(单位是4字节,这里是5,表示20字节);
- type_of_service 是服务类型字段,通常设为0表示普通服务,无特殊优先级;
- total_length设置整个IP报文(IP头 + UDP头 + 负载)的总长度;
- packet_id是报文ID,通常用于分片标识,本示例中可简单设为0;
- fragment_offset是分片偏移和标志位,0表示不分片;
- time_to_live是TTL生存时间,表示报文可经过的最大路由跳数,通常设为64;
- next_proto_id是协议字段,填写17(IPPROTO_UDP)表示该IP报文封装的是UDP协议;
- src_addr和dst_addr填写源IP和目标IP地址,填写经过inet_addr()函数转换后的32位整数;
- hdr_checksum是校验和字段,可通过rte_ipv4_cksum()函数计算,但需要在计算前必须将校验和字段清零,这是因为校验和是对整个IP头部进行的一系列加法和反码运算,然而IP头本身包含一个校验和字段,如果不先将它清零,计算时这个旧值就会被错误地纳入计算,导致最终的校验和错误。
|
|
填充UDP头部:包括设置源端口号、目标端口号、UDP报文长度(UDP头部 + 负载的总字节数),UDP校验和字段可采用rte_ipv4_udptcp_cksum()函数计算得到,但同样需要提前预设为0。
|
|
将用户定义的UDP负载数据复制到数据包缓冲区中对应的位置,完成UD 数据部分的填充。
|
|
rte_eth_tx_burst()函数将数据包从指定网口发送出去,参数如下:
- portid:发送端口号;
- 0:发送队列号;
- &mbuf:指向要发送的rte_mbuf指针数组的地址;
- 1:表示发送一个数据包。
返回值nb_tx表示实际发送出去的数据包数量,根据返回值即可判断发送是否成功。
|
|
程序测试
接收端采用Scapy解析数据包,接收程序参见附录的完整代码recv_udp_packet.py,需要预先安装Scapy,随后运行脚本。
|
|
发送端编译运行该程序。
|
|
源码
task_send.c
|
|
recv_udp_packet.py
|
|
源码打包下载
源码打包下载:dpdk_simple_send.zip