环境

  • Ubuntu20.04.6

  • Visual Studio Code

  • VMware17

UDP/IP数据包

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240717224949.png

  • Ethernet 2:以太网头

  • Destination:目的MAC地址

  • Source:源MAC地址

  • Type:上层协议

    • 0x0800:IP协议

    • 0x0806:ARP协议

    • 0x86DD:IPv6协议

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240717232141.png

  • Internet Protocol Version 4:ipv4头

  • Version:版本号,用来表明IP协议实现的版本号,当前一般为IPv4,即0100。

  • Header Length:首部长度,因为头部长度不固定(Option可选部分不固定),所以需要标识该分组的头部长度多少,用4bit表示,以4byte为单位,取值范围:5-15,即20(5_4)-60(15_4)byte(其他字段也是类似的计算方式,因为bit位是不够表示该字段的值)

  • Differentiated Services Field:服务类型,前3比特为优先权子字段(Precedence,现已被忽略)。第8比特保留未用。第4至第7比特分别代表延迟、吞吐量、可靠性和花费。当它们取值为1时分别代表要求最小时延、最大吞吐量、最高可靠性和最小费用。这4比特的服务类型中只能置其中1比特为1。可以全为0,若全为0则表示一般服务。服务类型字段声明了数据报被网络系统传输时可以被怎样处理。例如:TELNET协议可能要求有最小的延迟,FTP协议(数据)可能要求有最大吞吐量,SNMP协议可能要求有最高可靠性,NNTP(Network News Transfer Protocol,网络新闻传输协议)可能要求最小费用,而ICMP协议可能无特殊要求(4比特全为0)。实际上,大部分主机会忽略这个字段,但一些动态路由协议如OSPF(Open Shortest Path First Protocol)、IS-IS(Intermediate System to Intermediate System Protocol)可以根据这些字段的值进行路由决策。RFC2474的ToS取消了IP precedence字段而使用了DSCP,QoS里有描述,给QoS用来打标签。

  • Total Length:总长度,指明整个数据报的长度(以字节为单位,含头长度)。最大长度为65535字节。可用总长度减去头部长度获得实际报文数据的长度,取值范围0-65535byte,链路只允许1500byte,所以一般都需要MTU分片 。

  • Identification:标识,来唯一地标识主机发送的每一份数据报。通常每发一份报文,它的值会加1。通常与标记字段和分片偏移字段一起用于IP报文的分片。当原始报文大小超过MTU,那么就必须将原始数据进行分片。每个被分片的报文大小不得超过MTU,而这个字段还将在同一原始文件被分片的报文上打上相同的标记,以便接收设备可以识别出属于同一个报文的分片,“类似于进程号”,有时候电信会用他来识别流量是否是同一台主机(因为做了PAT后源ip都是一样的,鸡贼!)

  • Flags:标志,第1位没有被使用;第2位D是不分片位(DF),Do not fragment,顾名思义,不要分片,当DF位设置为1时,表示路由器不能对报文进行分片处理;- 第3位M表示还有后继分片(MF),More fragment,多分片,当路由器对报分进行分片时,除了最后一个分片的MF位设置为0外,其他所有分片的MF位均设置1,以便接收者直到收到MF位为0的分片为止;

  • Fragment Offset:片偏移,如果一份数据报要求分段的话,此字段指明该段偏移距原始数据报开始的位置。

  • Time to Live:生存时间,用来设置数据报最多可以经过的路由器数。由发送数据的源主机设置,通常为32、64(win7)、128、255(linux)等。每经过一个路由器,其值减1,直到0时该数据报被丢弃。

  • Protocol:协议,指明IP层所封装的上层协议类型,如ICMP(1)、IGMP(2) 、TCP(6)、UDP(17)等。

  • Header checksum status:首部校验和,内容是根据IP头部计算得到的校验和码。计算方法是:对头部中每个16比特进行二进制反码求和。(和ICMP、IGMP、TCP、UDP不同,IP不对头部后的数据进行校验)。

  • Source Address | Destination Address:用来标明发送IP数据报文的源主机地址和接收IP报文的目标主机地址。

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240717232222.png

  • User Datagram Protocol:udp头

  • Source Port:源端口号

  • Destination Port:目的端口号

  • Length:udp数据包总长度

  • Checksum:校验

  • UDP payload:数据负载

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240717235714.png

数据头定义

// myheader.h

#ifndef __MY_HEADER_H__
#define __MY_HEADER_H__

#define LN_HWADDR_LEN    6
typedef struct __ln_ether_hdr {

    char src_hwaddr[LN_HWADDR_LEN];
    char dst_hwaddr[LN_HWADDR_LEN];
    short type;
} __attribute__((packed))ln_ether_hdr;

typedef struct __ln_ipv4_hdr {

    char version_hdrlen;
    char service_type;
    short total_len;
    short packet_id;
    short packet_slice;
    char time_to_live;
    char next_proto;
    short sum_check;
    unsigned int src_ip;
    unsigned int dst_ip;
} __attribute__((packed))ln_ipv4_hdr;

typedef struct __ln_udp_hdr {

    short src_port;
    short dst_port;
    short total_len;
    short sum_check;
}

#endif

代码

宏定义

#define NUM_MBUFS (4096-1) // 内存池元素数目
#define BURST_SIZE 32 // 接收数组大小

主要函数

struct rte_mempool *
rte_pktmbuf_pool_create(const char *name, unsigned int n,
    unsigned int cache_size, uint16_t priv_size, uint16_t data_room_size,
    int socket_id)
  • name:内存池的名称。

  • n:内存池中要分配的元素数目(对象个数)。

  • cache_size:预先分配到每个slab上的对象数量,提高访问效率。

  • priv_size:私有数据区域大小。

  • data_room_size:数据区域大小。

  • socket_id:套接字标识,指定内存分配在哪个NUMA节点。

static inline uint16_t
rte_eth_rx_burst(uint16_t port_id, uint16_t queue_id,
         struct rte_mbuf **rx_pkts, const uint16_t nb_pkts)
  • port_id:指定要从哪个以太网端口接收数据包。

  • queue_id:指定要从哪个队列接收数据包。

  • rx_pkts:一个指向存储接收到的数据包的内存缓冲区数组的指针。

  • nb_pkts:表示缓冲区数组大小,即最大能够存储多少个数据包。

int
rte_eth_rx_queue_setup(uint16_t port_id, uint16_t rx_queue_id,
               uint16_t nb_rx_desc, unsigned int socket_id,
               const struct rte_eth_rxconf *rx_conf,
               struct rte_mempool *mp)
  • port_id:指定要配置的以太网端口。

  • rx_queue_id:指定要配置的接收队列ID。

  • nb_rx_desc:表示该队列使用的接收描述符数量。

  • socket_id:表示分配内存时使用的NUMA节点。

  • rx_conf:一个指向接收配置结构体(rte_eth_rxconf)的指针,用于配置接收队列参数。可以为空指针,表示使用默认配置。

  • mb_pool:一个指向内存池结构体(rte_mempool)的指针,用于存储从硬件接口上接收到的数据包。

整体流程

少量代码的时候可以放上来,了解一下流程。

/*
 * 版权所有: Copyright (c) 2024-2025  XXX Company. All rights reserved.
 * 系统名称: Ubuntu20.04.6
 * 文件名称: recv.c
 * 内容摘要: 实现单线程的UDP接收
 * 作    者: 黄彦杰 Lenn
 * 设计日期: 2024-07-17
*/

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

#define NUM_MBUFS (4096-1)
#define BURST_SIZE 32

static const struct rte_eth_conf port_dev_conf_default = {

    .rxmode = {.max_rx_pkt_len = RTE_ETHER_MAX_LEN}
};

int nDevPortId = 0;


/*
 * 函数名称: ln_init_port
 * 作    者: 黄彦杰 Lenn
 * 设计日期: 2024-07-17
 * 功能描述: 初始化虚拟网口,配置收发队列
 * 返 回 值: None
*/
static void ln_init_port(struct rte_mempool* mbuf_pool) {

    int nb_sys_port = rte_eth_dev_count_avail();

    if (nb_sys_port == 0) {

        rte_exit(EXIT_FAILURE, "No available system port");
    }

    struct rte_eth_dev_info dev_info;
    rte_eth_dev_info_get(nDevPortId, &dev_info);

    int nb_rx_queues = 1;
    int nb_tx_queues = 0;
    struct rte_eth_conf port_conf = port_dev_conf_default;
    rte_eth_dev_configure(nDevPortId, nb_rx_queues, nb_tx_queues, &port_conf);
    if (rte_eth_rx_queue_setup(nDevPortId, 0, 128, rte_eth_dev_socket_id(nDevPortId), NULL, mbuf_pool) < 0) {

        rte_exit(EXIT_FAILURE, "Setup rx queue failed");
    }

    if (rte_eth_dev_start(nDevPortId) < 0) {

        rte_exit(EXIT_FAILURE, "Could not start eth dev");
    }
}

/*
 * 函数名称: main
 * 作    者: 黄彦杰 Lenn
 * 设计日期: 2024-07-17
 * 功能描述: while(1)主循环,收发
 * 返 回 值: 0
*/
int main(int argc, char* argv[]) {

    rte_eal_init(argc, argv);

    struct rte_mempool* mbuf_pool = rte_pktmbuf_pool_create("mbuf pool", NUM_MBUFS, 0, 0,NUM_MBUFS, rte_socket_id());

    if(mbuf_pool == NULL) {

    rte_exit(EXIT_FAILURE, "Membuf pool create failed");
    }

    ln_init_port(mbuf_pool);

    while (1) {

        struct rte_mbuf* mbufs[BURST_SIZE];

        int nb_recv = rte_eth_rx_burst(nDevPortId, 0, mbufs, BURST_SIZE);

        unsigned i = 0;
        for (i = 0; i < nb_recv; i++) {

            struct rte_ether_hdr* ehdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr*);
            if (ehdr->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);

                int length = ntohs(udphdr->dgram_len);
                *((char*)udphdr + length) = '\0';

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

                addr.s_addr = iphdr->dst_addr;
                printf("dst %s:%d %s\n", inet_ntoa(addr), ntohs(udphdr->dst_port), (char*)(udphdr + 1));

                rte_pktmbuf_free(mbufs[i]);
            }
        }
    }


    return 0;
}

启动服务

  • 进入编译目录配置环境变量

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240717164130.png

  • 接管网卡
ifconfig eth0 down
./usertool/dpdk-setup.sh
# 执行插入和绑定操作

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240718000558.png

编译

Makefile

# SPDX-License-Identifier: BSD-3-Clause
# Copyright(c) 2010-2014 Intel Corporation

# binary name
APP = recv

# all source are stored in SRCS-y
SRCS-y := recv.c

# Build using pkg-config variables if possible
ifeq ($(shell pkg-config --exists libdpdk && echo 0),0)

all: shared
.PHONY: shared static
shared: build/$(APP)-shared
    ln -sf $(APP)-shared build/$(APP)
static: build/$(APP)-static
    ln -sf $(APP)-static build/$(APP)

PKGCONF=pkg-config --define-prefix

PC_FILE := $(shell $(PKGCONF) --path libdpdk)
CFLAGS += -O3 $(shell $(PKGCONF) --cflags libdpdk)
CFLAGS += -DALLOW_EXPERIMENTAL_API
LDFLAGS_SHARED = $(shell $(PKGCONF) --libs libdpdk)
LDFLAGS_STATIC = -Wl,-Bstatic $(shell $(PKGCONF) --static --libs libdpdk)

build/$(APP)-shared: $(SRCS-y) Makefile $(PC_FILE) | build
    $(CC) $(CFLAGS) $(SRCS-y) -o $@ $(LDFLAGS) $(LDFLAGS_SHARED)

build/$(APP)-static: $(SRCS-y) Makefile $(PC_FILE) | build
    $(CC) $(CFLAGS) $(SRCS-y) -o $@ $(LDFLAGS) $(LDFLAGS_STATIC)

build:
    @mkdir -p $@

.PHONY: clean
clean:
    rm -f build/$(APP) build/$(APP)-static build/$(APP)-shared
    test -d build && rmdir -p build || true

else # Build using legacy build system

ifeq ($(RTE_SDK),)
$(error "Please define RTE_SDK environment variable")
endif

# Default target, detect a build directory, by looking for a path with a .config
RTE_TARGET ?= $(notdir $(abspath $(dir $(firstword $(wildcard $(RTE_SDK)/*/.config)))))

include $(RTE_SDK)/mk/rte.vars.mk

ifneq ($(CONFIG_RTE_EXEC_ENV_LINUX),y)
$(error This application can only operate in a linux environment, \
please change the definition of the RTE_TARGET environment variable)
endif

CFLAGS += -O3
CFLAGS += -DALLOW_EXPERIMENTAL_API
CFLAGS += $(WERROR_FLAGS)

include $(RTE_SDK)/mk/rte.extapp.mk
endif

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240718001428.png

运行

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240718001740.png

  • 要向虚拟机192.168.1.165发送消息。

问题bug

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240718001903.png

你向虚拟机发送数据,但是你会发现这玩意发不过去,没想到吧嘿嘿嘿。这里有个坑。

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240718002115.png

netsh i i show in
netsh -c i i add neighbors 4 192.168.1.165 00-0c-29-05-6b-82

https://imagehyj.oss-cn-hangzhou.aliyuncs.com/blog/20240718002722.png

项目地址

roboteth-recv