一、网络协议栈
如何实现网络协议栈,首先需要拿到网络数据,有以下几种方式
1)原生socket
2)netmap
3)dpdk
1、网络通信过程
物理网卡将模拟信号转化为数据信号包;
NIC为网卡过来的数据包分配一个数据结构sk_buffer,
指出数据包中以太网头、IP头等信息的位置;
协议栈根据sk_buffer解析、处理数据包;
VFS作为接口(如socket),便于应用对数据包进行读、写等操作。
2、dpdk
dpdk本质是接管网卡到驱动的通信,交由dbdk自己处理。
有两种处理方式:1、基于dbdk实现自己的用户态协议栈;2、将数据继续交与内核协议栈处理。
1)dpdk是否有助于提高网络并发量?
答案:否。回顾之前做百万并发的时候,主要在于网络连接的优化。因此并发主要跟协议栈有关。
2)能否通过dpdk解决低延迟的问题?
答案:否。延迟主要是业务引起的。
二、dpdk环境
1、dpdk环境开启
cd dpdk路径 sudo su #export RTE_SDK=dpdk路径 export RTE_SDK=/home/king/share/dpdk/dpdk-stable-19.08.2/ export RTE_TARGET=x86_64-native-linux-gcc
./usertools/dpdk-setup.sh
依次执行:
43(加载IGB UIO module,是一种drive,dpdk接管网卡的方式)
44(加载VFIO module,是一种driver,dpdk接管网卡的方式)
45(加载KNI module,是内核网络接口,将数据写回内核协议栈)
46(设置巨页,可以不需要频繁页交换),可输入512
47(设置巨页),可输入512
49(执行之前需要eth0 down掉,执行sudo ifconfig eth0 down)pci地址=对应eth0的(如0000:03:00.0)
60(退出)
至此,dpdk接管了物理网卡。
2、Windowe下配置IP和MAC地址的映射
以管理员权限运行cmd
1)查看静态表接口
arp -a
2)查看适配器
netsh i i show in
关注结果中的以太网对应得Idx=12
Idx Met MTU 状态 名称 --- ---------- ---------- ------------ --------------------------- 1 75 4294967295 connected Loopback Pseudo-Interface 1 14 40 1500 connected WLAN 22 25 1500 disconnected 本地连接* 9 17 25 1500 disconnected 本地连接* 10 11 65 1500 disconnected 蓝牙网络连接 2 35 1500 connected VMware Network Adapter VMnet1 21 35 1500 connected VMware Network Adapter VMnet8 12 25 1500 connected 以太网 3
3)新添静态IP
netsh -c i i add neighbors Idx IP地址 Mac地址 // netsh -c i i add neighbors 12 192.168.42.133 00-0c-29-54-10-bb
4)通过arp -a
检查是否添加成功
5)清除静态表
netsh i i delete neighbors Idx
三、实现用户态协议栈ustack
接下来编写ustack,大致流程是
1)初始化:rte_eal_init()
2)创建一个创建内存池mbuf_pool:rte_pktmbuf_pool_create()
3)配置以太网设备,包括配置队列的个数、接口的配置信息:rte_eth_dev_configure()
4)分配并设置以太网设备的接收队列:rte_eth_rx_queue_setup()
5)分配并设置以太网设备的发送队列:rte_eth_tx_queue_setup()
6)启动以太网设备:rte_eth_dev_start()
7)从以太网接口接收数据包:rte_eth_rx_burst()
1、UDP协议
用户层经过网络各层,分别会在数据包前面添加
(UDP)协议首部,用于支持UDP的无连接、高效传输
IP首部:用于使数据能在互联网中传输,也就是能被路由器转发
以太网帧首部:用于使帧能够在一段链路或者网络上传输,被相应的目的主机接收。
dp pkt ------------------------------------------------------------------- | rte_ether_hdr | rte_ipv4_hdr | rte_udp_hdr | data |
2、代码
#include <rte_eal.h> #include <rte_ethdev.h> #include <rte_mbuf.h> #include <stdio.h> #include <arpa/inet.h> //UDP协议发送 #define ENABLE_SEND 1 #define NUM_MBUFS (4096-1) #define BURST_SIZE 32 #if ENABLE_SEND static uint32_t gSrcIp; //源 IP 地址 static uint32_t gDstIp; //目的 IP 地址 static uint8_t gSrcMac[RTE_ETHER_ADDR_LEN]; //源 MAC 地址 static uint8_t gDstMac[RTE_ETHER_ADDR_LEN]; static uint16_t gSrcPort; //UDP 数据包的源端口号 static uint16_t gDstPort; #endif int gDpdkPortId = 0; static const struct rte_eth_conf port_conf_default = { .rxmode = {.max_rx_pkt_len = RTE_ETHER_MAX_LEN } }; //初始化以太网设备 static void ng_init_port(struct rte_mempool *mbuf_pool) { //返回可用的 Ethernet 设备数量 uint16_t nb_sys_ports= rte_eth_dev_count_avail(); if (nb_sys_ports == 0) { rte_exit(EXIT_FAILURE, "No Supported eth found\n"); } //获取指定 Ethernet 设备的设备信息 struct rte_eth_dev_info dev_info; rte_eth_dev_info_get(gDpdkPortId, &dev_info); // //配置以太网设备 const int num_rx_queues = 1; //接收队列数量 const int num_tx_queues = 1; //发送队列数量 struct rte_eth_conf port_conf = port_conf_default; //struct rte_eth_conf 结构体包含了各种网卡相关的配置参数和属性 rte_eth_dev_configure(gDpdkPortId, num_rx_queues, num_tx_queues, &port_conf); //分配并设置以太网设备的接收队列 if (rte_eth_rx_queue_setup(gDpdkPortId, 0 , 1024, rte_eth_dev_socket_id(gDpdkPortId),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; //struct rte_eth_txconf 结构体中包含了各种发送队列相关的配置参数和属性 txq_conf.offloads = port_conf.rxmode.offloads; //使用 port_conf.rxmode.offloads 来初始化 txq_conf.offloads,以便发送队列的属性和接收队列保持一致。 //分配并设置以太网设备的传输队列 if (rte_eth_tx_queue_setup(gDpdkPortId, 0 , 1024, rte_eth_dev_socket_id(gDpdkPortId), &txq_conf) < 0) { rte_exit(EXIT_FAILURE, "Could not setup TX queue\n"); } #endif //启动以太网设备。 if (rte_eth_dev_start(gDpdkPortId) < 0 ) { rte_exit(EXIT_FAILURE, "Could not start\n"); } } /*将负载数据封装成UDP数据包 */ static int ng_encode_udp_pkt(uint8_t *msg, unsigned char *data, uint16_t total_len) { // 以太网头 struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg; rte_memcpy(eth->s_addr.addr_bytes, gSrcMac, RTE_ETHER_ADDR_LEN); rte_memcpy(eth->d_addr.addr_bytes, gDstMac, RTE_ETHER_ADDR_LEN); eth->ether_type = htons(RTE_ETHER_TYPE_IPV4); // IPV4 头 struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr *)(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; // ttl = 64 ip->next_proto_id = IPPROTO_UDP; ip->src_addr = gSrcIp; ip->dst_addr = gDstIp; ip->hdr_checksum = 0; ip->hdr_checksum = rte_ipv4_cksum(ip); // UDP 头 struct rte_udp_hdr *udp = (struct rte_udp_hdr *)(msg + sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_hdr)); udp->src_port = gSrcPort; udp->dst_port = gDstPort; uint16_t udplen = total_len - sizeof(struct rte_ether_hdr) - sizeof(struct rte_ipv4_hdr); udp->dgram_len = htons(udplen); //要先填充 UDP 报文,再进行校验和计算 rte_memcpy((uint8_t*)(udp+1), data, udplen); udp->dgram_cksum = 0; udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp); printf("````````````````````send````````````````````\n"); struct in_addr addr; addr.s_addr = gSrcIp; printf("源IP和端口: %s:%d, ", inet_ntoa(addr), ntohs(gSrcPort)); addr.s_addr = gDstIp; printf("目的IP和端口: %s:%d\n", inet_ntoa(addr), ntohs(gDstPort)); return 0; } /*将待发送的负载数据封装成UDP数据包,存入rte_mbuf 参数;内存池、负载数据的指针、负载数据的大小 */ static struct rte_mbuf * ng_send(struct rte_mempool *mbuf_pool, uint8_t *data, uint16_t length) { //计算 UDP 数据包的总长度 = 以太网头部(14字节)+ IPv4头部(20字节) + UDP头部(8字节) + 负载数据 // const unsigned total_len = length + 42; uint16_t total_len = length + sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_hdr) + sizeof(struct rte_udp_hdr); //分配一个 rte_mbuf 数据包缓冲区 struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool); if (!mbuf) { rte_exit(EXIT_FAILURE, "rte_pktmbuf_alloc\n"); } mbuf->pkt_len = total_len; mbuf->data_len = total_len; //将 mbuf 转换成一个 uint8_t* 类型的指针变量pktdata,并返回缓冲区数据的起始地址 uint8_t *pktdata = rte_pktmbuf_mtod(mbuf, uint8_t*); //将负载数据封装成UDP数据包 ng_encode_udp_pkt(pktdata, data, total_len); return mbuf; } int main(int argc, char *argv[]) { //初始化EAL环境 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"); } //初始化以太网设备 ng_init_port(mbuf_pool); //rte_eth_macaddr_get(gDpdkPortId, (struct rte_ether_addr *)gSrcMac); while (1) { //从以太网接口的接收队列中读取数据包,存入mbufs struct rte_mbuf *mbufs[BURST_SIZE]; unsigned num_recvd = rte_eth_rx_burst(gDpdkPortId, 0, mbufs, BURST_SIZE); if (num_recvd > BURST_SIZE) { rte_exit(EXIT_FAILURE, "Error receiving from eth\n"); } unsigned i = 0; for (i = 0;i < num_recvd;i ++) { /* udp pkt ------------------------------------------------------------------- | rte_ether_hdr | rte_ipv4_hdr | rte_udp_hdr | data | */ //获取以太网帧头部 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)) { //判断以太网帧中的网络层协议是否为IPv4协议 continue; } //获取IP头 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) { //判断传输层协议是否为UDP,IPPROTO_UDP是一个常量,代表传输控制协议(UDP)的协议编号 //获取udp头 struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1); //在负载数据末尾插入了一个字符串结束符 '\0' uint16_t length = ntohs(udphdr->dgram_len); *((char*)udphdr + length) = '\0'; printf("````````````````````receive````````````````````\n"); struct in_addr addr; addr.s_addr = iphdr->src_addr; //源IP地址 printf("源IP和端口: %s:%d, ", inet_ntoa(addr), ntohs(udphdr->src_port)); addr.s_addr = iphdr->dst_addr; //目的IP地址 printf("目的IP和端口: %s:%d, 接收数据: %s\n", inet_ntoa(addr), ntohs(udphdr->dst_port), (char *)(udphdr+1)); #if ENABLE_SEND //接收和回传时候,源地址和目的地址,是相反的 rte_memcpy(gSrcMac, ehdr->d_addr.addr_bytes, RTE_ETHER_ADDR_LEN); rte_memcpy(gDstMac, ehdr->s_addr.addr_bytes, RTE_ETHER_ADDR_LEN); rte_memcpy(&gSrcIp, &iphdr->dst_addr, sizeof(uint32_t)); rte_memcpy(&gDstIp, &iphdr->src_addr, sizeof(uint32_t)); rte_memcpy(&gSrcPort, &udphdr->dst_port, sizeof(uint16_t)); rte_memcpy(&gDstPort, &udphdr->src_port, sizeof(uint16_t)); //待发送的负载数据封装成UDP数据包 struct rte_mbuf *txbuf = ng_send(mbuf_pool, (uint8_t *)(udphdr+1), length - sizeof(struct rte_udp_hdr)); //发送数据报 rte_eth_tx_burst(gDpdkPortId, 0, &txbuf, 1); rte_pktmbuf_free(txbuf); #endif rte_pktmbuf_free(mbufs[i]); } } } }
四、dpdk一些基本函数接口
rte_eal_init()
初始化环境抽象层,在DPDK应用程序中使用DPDK库之前,必须首先调用rte_eal_init函数进行初始化。
#include <rte_eal.h> int rte_eal_init(int argc, char **argv);
argc和argv参数分别表示应用程序的命令行参数数量和参数列表
在初始化完成后,它会返回0以表示初始化成功,或返回负值以表示初始化失败。
rte_pktmbuf_pool_create()
创建和初始化一个内存池,用于管理mempool对象中的mempool元素,同时用于存储和分发网络数据包的缓冲区。
#include <rte_mbuf.h> struct rte_mempool* rte_pktmbuf_pool_create ( const char *name, unsigned n, unsigned cache_size, uint16_t priv_size, uint16_t data_room_size, int socket_id );
其中,各参数的含义如下:
1)name: 内存池的名称
2)n: 内存池中最多可以包含的元素数量
3)cache_size: 每个CPU缓存队列的大小,用于提高缓存命中率和性能
4)priv_size: 每个元素的私有数据大小,通常为0或包含一些与网络协议相关的元数据
5)data_room_size: 每个元素中数据缓冲区的大小,用于存储接收到的网络数据包或构建要发送的数据包
6)socket_id: 内存池分配的NUMA节点,-1表示由系统自动选择
rte_socket_id()
获取当前线程或者任务所在的CPU socket的ID
int rte_socket_id(void);
获取当前线程或者任务所在的CPU socket的ID
rte_eth_dev_configure()
配置和初始化指定的以太网设备及其相关参数。
int rte_eth_dev_configure( uint16_t port_id, uint16_t nb_rx_queue, uint16_t nb_tx_queue, const struct rte_eth_conf * eth_conf );
其中,各参数的含义如下:
1)port_id: 待配置的以太网设备端口号
2)nb_rx_queue: 接收队列数量
3)nb_tx_queue: 发送队列数量
4)eth_conf: 以太网设备配置参数,包括MAC地址、速率、MTU等
rte_eth_rx_queue_setup()
配置和启动指定以太网设备的接收队列.
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 * mb_pool );
其中各参数的含义如下:
1)port_id:待配置的以太网设备端口号。
2)rx_queue_id:待配置的接收队列编号。
3)nb_rx_desc:接收队列中缓冲区描述符的数量,决定了队列的深度和性能。
4)socket_id:所属的NUMA节点,用于内存分配。如果为RTE_ETH_DEV_NO_NUMA_SOCKET,则表示不分配NUMA节点。
5)rx_conf:接收队列相关的配置参数。
6)mb_pool:接收队列使用的内存池,用于存储接收到的数据包。
rte_eth_tx_queue_setup ()
配置和启动指定以太网设备的发送队列
int rte_eth_tx_queue_setup ( uint16_t port_id, uint16_t tx_queue_id, uint16_t nb_tx_desc, unsigned int socket_id, const struct rte_eth_txconf * tx_conf );
其中各参数的含义如下:
1)port_id:待配置的以太网设备端口号。
2)tx_queue_id:待配置的发送队列编号。
3)nb_tx_desc:发送队列中缓冲区描述符的数量,决定了队列的深度和性能。
4)socket_id:所属的NUMA节点,用于内存分配。如果为RTE_ETH_DEV_NO_NUMA_SOCKET,则表示不分配NUMA节点。
5)tx_conf:发送队列相关的配置参数。
rte_eth_dev_socket_id()
获取指定以太网设备所使用的 NUMA节点编号
int rte_eth_dev_socket_id( uint16_t port_id )
rte_eth_dev_start()
启动以太网设备。
int rte_eth_dev_start(uint16_t port_id);
rte_eth_rx_burst()
从指定的以太网设备的接收队列中获取数据包。
static uint16_t rte_eth_rx_burst ( uint16_t port_id, uint16_t queue_id, struct rte_mbuf ** rx_pkts, const uint16_t nb_pkts );
其中各参数的含义如下:
1)port_id:待读取数据包的以太网设备端口号。
2)queue_id:待读取数据包的接收队列编号。
3)rx_pkts:用于存储读取数据包的缓冲区数组指针。
4)nb_pkts:缓冲区数组中可存储的最大数据包数量。
返回实际检索到的数据包数
rte_eth_tx_burst()
用于发送数据报.
static uint16_t rte_eth_tx_burst( uint16_t port_id, uint16_t queue_id, struct rte_mbuf ** tx_pkts, uint16_t nb_pkts );
1)port_id: 要发送数据报的端口 ID。
2)queue_id: 要发送数据报的队列 ID。
3)tx_pkts: 一个指向 rte_mbuf 结构体指针数组的指针,数组中包含要发送的数据报。
4)nb_pkts: 要发送的数据报的数量。
返回实际发送的数据包数
rte_pktmbuf_mtod_offset()
将struct rte_mbuf类型的数据包缓冲区对象转换为指向偏移量处的缓冲区数据的指针。
#define rte_pktmbuf_mtod_offset (m,t,o) ((t)(void *)((char *)(m)->buf_addr + (m)->data_off + (o)))
其中各参数的含义如下:
1)m:待转换为指针的rte_mbuf结构体对象。
2)t:指向缓冲区数据的指针类型,即要强制转换的类型。
3)o:偏移量。
rte_pktmbuf_mtod()
#define rte_pktmbuf_mtod(m,t ) rte_pktmbuf_mtod_offset(m, t, 0)
rte_pktmbuf_alloc()
用于分配一个rte_mbuf类型的缓冲区,可以用来存储网络数据包。需要注意的是,rte_mbuf缓冲区分为两部分:一个是元数据,主要用于存储关于包的信息,比如包的长度、IP地址等;另一个是包的实际数据,主要用于存储网络数据包的内容。
static struct rte_mbuf* rte_pktmbuf_alloc(struct rte_mempool *mp);
rte_memcpy()
用于将一个内存区域的数据内容复制到另一个内存区域。该函数与标准库函数 memcpy() 的语义相同,但是对于 DPDK 的应用场景做了一定的优化,可以提高拷贝性能。
void *rte_memcpy(void *dst, const void *src, size_t n);
其中,dst 是目标内存区域的指针,src 是源内存区域的指针,n 是拷贝的字节数。
rte_eth_dev_info_get()
获取指定 Ethernet 设备的设备信息。
void rte_eth_dev_info_get(uint16_t port_id, struct rte_eth_dev_info *dev_info);
该函数需要两个参数:
1)port_id:表示需要获取设备信息的端口 ID。
2)dev_info:一个指向 rte_eth_dev_info 结构体的指针。rte_eth_dev_info 结构体中包含了各种设备信息的字段,例如设备名称、设备类型、最大传输单元(MTU)大小等。
在调用 rte_eth_dev_info_get 函数之前,需要先声明一个 struct rte_eth_dev_info 类型的变量,以便将获取到的设备信息存储在其中。在调用函数时,需要将该变量的指针作为第二个参数传递给函数,以便函数可以将获取到的信息存储在该结构体中。
以太网协议的头部 rte_ether_hdr
#define RTE_ETHER_ADDR_LEN 6 typedef uint16_t rte_be16_t struct rte_ether_addr{ uint8_t addr_bytes [RTE_ETHER_ADDR_LEN]; }; struct rte_ether_hdr{ struct rte_ether_addr dst_addr; struct rte_ether_addr src_addr; rte_be16_t ether_type; };
1)src_addr:一个长度为 6 字节的数组,表示源 MAC 地址;
2)dst_addr:一个长度为 6 字节的数组,表示目的 MAC 地址;
3)ether_type:一个 16 位无符号整数,表示以太网数据帧的类型,常见的有 IP 数据包、ARP 请求和响应等。
IPv4 协议的头部 rte_ipv4_hdr
/** * An IPv4 header. */ struct rte_ipv4_hdr { uint8_t version_ihl; /**< version and header length */ uint8_t type_of_service;/**< type of service */ uint16_t total_length; /**< length of packet including header */ uint16_t packet_id; /**< unique packet ID */ uint16_t fragment_offset;/**< fragment offset field */ uint8_t time_to_live; /**< time to live */ uint8_t next_proto_id; /**< protocol ID */ uint16_t hdr_checksum; /**< header checksum */ uint32_t src_addr; /**< source address */ uint32_t dst_addr; /**< destination address */ } __attribute__((__packed__));
1)version_ihl:一个 8 位无符号整数,表示 IPv4 头部的版本和长度信息;
2)type_of_service:一个 8 位无符号整数,表示服务类型信息;
3)total_length:一个 16 位无符号整数,表示整个 IPv4 数据包的长度;
4)packet_id:一个 16 位无符号整数,表示数据包的唯一标识符;
5)fragment_offset:一个 16 位无符号整数,表示分段信息,常用于分片后的数据包重组;
6)time_to_live:一个 8 位无符号整数,表示数据包的生存时间;
7)next_proto_id:一个 8 位无符号整数,表示下一层协议的 ID,例如 TCP、UDP 等;
8)hdr_checksum:一个 16 位无符号整数,表示 IPv4 头部的校验和;
9)src_addr:一个 32 位无符号整数,表示源 IP 地址;
10)dst_addr:一个 32 位无符号整数,表示目的 IP 地址。
UDP协议的头部 rte_udp_hdr
/** * A UDP header. */ struct rte_udp_hdr { uint16_t src_port; /**< UDP source port. */ uint16_t dst_port; /**< UDP destination port. */ uint16_t dgram_len; /**< Length of UDP datagram including header. */ uint16_t dgram_cksum; /**< UDP datagram checksum (0 if not used). */ } __attribute__((__packed__));
1)src_port:一个 16 位无符号整数,表示 UDP 数据包的源端口号。
2)dst_port:一个 16 位无符号整数,表示 UDP 数据包的目的端口号。
3)dgram_len:一个 16 位无符号整数,表示 UDP 数据报的总长度(包括头部和负载数据)。
4)dgram_cksum:一个 16 位无符号整数,用于存储 UDP 数据包的校验和。如果该字段为 0,则表示数据包没有启用校验和。
小问题
uint8_t *和char *的区别和适用场景
uint8_t *
和 char *
都是 C 语言中常用的指针类型,它们之间的主要区别在于指向的对象类型。uint8_t *
表示一个指向 unsigned char
类型数据的指针,而 char *
则表示一个指向 char
类型数据的指针。两者都占用1个字节(即8位)的内存空间,因此可以将它们视为等价的数据类型。
char *
指针适用于字符串操作、文件读写等普遍场景,而 uint8_t *
指针则适用于二进制数据处理、网络编程、加密解密等专用领域。