前言
内核里面已经有网络协议栈了,为什么还要实现一遍用户态协议栈呢,主要是站在一个设计者的角度,自己去尝试实现一个协议栈,那么对协议栈的理解会比较透彻,这不比背八股文强?
获取原始数据
获取原始数据的三种方法介绍
1、使用原始套接字raw socket , tcpdump和wireshark就是使用这个做的,raw socket主要用来抓包。
2、dbdk,使用dbdk的话篇幅较长,这里就不展开了,有兴趣的可以看
Dpdk/网络协议栈/vpp/OvS/DDos/NFV/虚拟化/高性能专家-学习视频教程-腾讯课堂
3.NETmap是用于用户层应用程序收发原始网络数据的高性能框架,本文使用netmap进行数据的收发。
netmap
内核协议栈的数据到应用层的数据会经历两次拷贝,而netmap采用mmap的方式,直接将网卡的数据映射到一块内存中,应用程序可以直接通过mmap操作相应内存的数据。
零拷贝
其实通过上面的图我们就能看到,采用传统的内核协议栈的方式,会发生两次拷贝,一是网卡数据拷贝到内核协议栈;二是内核再拷贝到内存中去。而netmap采用的则是零拷贝。
所谓零拷贝,指的是不由CPU操作,copy这个动作是由cpu发出指令move实现的,所以零拷贝就是不由CPU管理,由DMA管理。DMA允许外设与内存直接进行数据传输,这个过程不需要CPU的参与。
更多的零拷贝相关内容看一下这个彻底搞懂零拷贝(Zero-Copy)技术
netmap安装与常用api介绍
安装netmap
单独写一篇安装教程,按照这个来即可手把手教你ubuntu18.04安装netmap
netmap的头文件#include<net/netmap_user.h>在 /netmap/sys/net/下
nm_open
调用 nm_open 函数时,如:nmr = nm_open("netmap:ens33", NULL, 0, NULL); nm_open()会对传递的 ifname 指针里面的字符串进行分析,提取出网络接口名。
nm_open() 会 对 struct nm_desc *d 申 请 内 存 空 间 , 并 通 过 d->fd =open(NETMAP_DEVICE_NAME, O_RDWR);打开一个特殊的设备/dev/netmap 来创建文件描述符 d->fd。
注意这个fd是/dev/netmap这个网卡设备,网卡只要来数据了,相应的这个fd就会有EPOLLIN事件,这个fd是检测网卡有没有数据的,因为是mmap,只要网卡有数据了,那么内存就有数据的。
fd是指向网卡,操作数据是操作内存,内存和网卡数据的同步的,而我们cpu只能操作内存,不能操作外设。
简而言之,struct nm_desc里面包含一个fd,这个fd指向/dev/netmap,用于poll、epoll等系统调用。
一旦调用 nm_open 函数,网卡的数据就不从内核协议栈走了,这时候最好在虚拟机中建两个网卡,一个用于netmap,一个用于ssh等应用程序的正常工作。
struct nm_desc *nm_open(const char *ifname, const struct nmreq *req, uint64_t new_flags, const struct nm_desc *arg);
struct nm_desc *nmr = nm_open("netmap:ens33", NULL, 0, NULL);
nm_nextpkt
nm_nextpkt是用来接收网卡上到来的数据包的函数。nm_nextpkt会将所有 rx 环都检查一遍,当发现有一个 rx 环有需要接收的数据包时,得到这个数据包的地址,并返回。所以 nm_nextpkt()每次只能取一个数据包。 因为接收到的数据包没有经过协议栈处理,因此需要在用户程序中自己解析。rx 环:想象成一个环形队列即可,每一项就是一个数据包。
stream即为数据在缓冲区中的首地址,struct nm_pkthdr为返回的数据包头部信息,不需要管头部的话直接从stream去取数据就行了。stream现在就是链路层的数据
static u_char *nm_nextpkt(struct nm_desc *d, struct nm_pkthdr *hdr);
unsigned* stream = nm_nextpkt(nmr, &nmhead);
nm_inject
nm_inject()是用来往共享内存中写入待发送的数据包数据的。数据包经共享内存拷贝到网卡,然后发送出去。所以 nm_inject()是用来发包的。
nm_inject()也会查找所有的发送环(tx 环),找到一个可以发送的槽,就将数据包写入并返回,所以每次函数调用也只能发送一个包。
static int nm_inject(struct nm_desc *d, const void *buf, size_t size);
nm_inject(nmr,&arp_rt,sizeof(arp_rt));
nm_close
nm_close 函数就是回收动态内存,回收共享内存,关闭文件描述符什么的了。
static int nm_close(struct nm_desc *d)
nm_close(nmr);
相关视频推荐
从netmap到dpdk,从硬件到协议栈,4个维度让网络体系构建起来
学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂
需要C/C++ linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
协议栈
协议栈的定义
所谓协议栈,“栈”怎么理解,先进后出,正如下图udp协议所示。在应用层我们调用sendto发送数据时,我们需要在用户数据前面加上udp的协议头,然后进入网络层加入ip头,进入链路层加入以太网的头,最后由网卡进行数模转换变成光电信号发送给对端。对端网卡接收到光电信号后进行模数转换,再依次拆包,最终到达应用层就是我们最初发送的数据了。这个过程就像栈一样,先进后出。
协议栈在一定意义是又可以称为协议族,“族”怎么理解,我们看到传输层有udp,tcp等协议,网络层有ip,icmp协议等等,这些协议形成了一个家族。
内核协议栈帮我们解析了传输层,网络层和数据链路层的协议,所以我们用户态协议栈正是去做这三层的协议。
链路层首部
以太网协议
以太网协议:两个地址皆为6字节的mac地址,后面2字节的类型区分是什么协议
#pragma pack(1) //一字节对齐
#define NETMAP_WITH_LIBS
#define ETH_ADDR_LENGTH 6
#define PROTO_IP 0x0800 //ip协议
#define PROTO_ARP 0x0806 //arp请求协议
#define PROTO_RARP 0x0835 //rarp应答协议
#define PROTP_UPD 17
struct ethhdr {
unsigned char h_dst[ETH_ADDR_LENGTH];
unsigned char h_src[ETH_ADDR_LENGTH];
unsigned short h_proto;
};
网络层首部
ip协议
关于ip协议里面第一个字节内的大小端转换不懂的请查看大端与小端概念、多字节之间与单字节多部分的大小端转换详解
struct iphdr {
unsigned char hdrlen: 4, //一字节,手动大小端转换
version: 4;
unsigned char tos;
unsigned short totlen;
unsigned short id;
unsigned short flag_offset;
unsigned char ttl;
unsigned char type;
unsigned short check;
unsigned int sip;
unsigned int dip;
};
struct ippkt {
struct ethhdr eh; //14
struct iphdr ip; //20
};
arp协议
struct arphdr {
unsigned short h_type;
unsigned short h_proto;
unsigned char h_addrlen;
unsigned char h_protolen;
unsigned short oper;
unsigned char smac[ETH_ADDR_LENGTH];
unsigned int sip;
unsigned char dmac[ETH_ADDR_LENGTH];
unsigned int dip;
};
struct arppkt {
struct ethhdr eh;
struct arphdr arp;
};
传输层首部
udp协议
这里传输层在本文中先只介绍udp,tcp下文再写
为什么在udppkt里面,定义了一个unsigned char data[0];,这个叫柔性数组,或者叫零长数组,是用来定义用户数据包的起始地址而不占用实际的结构体空间。具体内容自行百度。
struct udphdr {
unsigned short sport;
unsigned short dport;
unsigned short length;
unsigned short check;
};
struct udppkt {
struct ethhdr eh; //14
struct iphdr ip; //20
struct udphdr udp;//8
unsigned char data[0];
};
用户态协议栈设计实现
1. 实现udp协议
设计思路
我们使用udp工具,给我们的服务器发送udp包,看是否能解析出来,然后按再回发回去
如果要解析udp,那么协议顺序为 ether -> ip ->udp,所以我们按照这个顺序依次解析即可。
代码
//
// Created by 68725 on 2022/7/19.
//
#include <stdio.h>
#include <sys/poll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define NETMAP_WITH_LIBS
#include <net/netmap_user.h>
#include <string.h>
#pragma pack(1)
#define ETH_ADDR_LENGTH 6
#define PROTO_IP 0x0800
#define PROTO_ARP 0x0806
#define PROTP_UPD 17
struct ethhdr {
unsigned char h_dst[ETH_ADDR_LENGTH];
unsigned char h_src[ETH_ADDR_LENGTH];
unsigned short h_proto;
};
struct iphdr {
unsigned char hdrlen: 4,
version: 4;
unsigned char tos;
unsigned short totlen;
unsigned short id;
unsigned short flag_offset;
unsigned char ttl;
unsigned char type;
unsigned short check;
unsigned int sip;
unsigned int dip;
};
struct ippkt {
struct ethhdr eh; //14
struct iphdr ip; //20
};
struct udphdr {
unsigned short sport;
unsigned short dport;
unsigned short length;
unsigned short check;
};
struct udppkt {
struct ethhdr eh; //14
struct iphdr ip; //20
struct udphdr udp;//8
unsigned char data[0];
};
struct arphdr {
unsigned short h_type;
unsigned short h_proto;
unsigned char h_addrlen;
unsigned char h_protolen;
unsigned short oper;
unsigned char smac[ETH_ADDR_LENGTH];
unsigned int sip;
unsigned char dmac[ETH_ADDR_LENGTH];
unsigned int dip;
};
struct arppkt {
struct ethhdr eh;
struct arphdr arp;
};
void echo_udp_pkt(struct udppkt *udp, struct udppkt *udp_rt) {
memcpy(udp_rt, udp, sizeof(struct udppkt));
memcpy(udp_rt->eh.h_dst, udp->eh.h_src, ETH_ADDR_LENGTH);
memcpy(udp_rt->eh.h_src, udp->eh.h_dst, ETH_ADDR_LENGTH);
udp_rt->ip.sip = udp->ip.dip;
udp_rt->ip.dip = udp->ip.sip;
udp_rt->udp.sport = udp->udp.dport;
udp_rt->udp.dport = udp->udp.sport;
}
int main() {
struct nm_pkthdr h;
struct nm_desc *nmr = nm_open("netmap:ens33", NULL, 0, NULL);
if (nmr == NULL) {
return -1;
}
printf("open ens33 seccessn");
struct pollfd pfd = {0};
pfd.fd = nmr->fd;
pfd.events = POLLIN;
while (1) {
printf("new data coming!n");
int ret = poll(&pfd, 1, -1);
if (ret < 0) {
continue;
}
if (pfd.revents & POLLIN) {
unsigned char *stream = nm_nextpkt(nmr, &h);
//ether
struct ethhdr *eh = (struct ethhdr *) stream;
if (ntohs(eh->h_proto) == PROTO_IP) {
//ip
struct ippkt *iph=(struct ippkt *)stream;
if (iph->ip.type == PROTP_UPD) {
//udp
struct udppkt *udp = (struct udppkt *) stream;
int udplength = ntohs(udp->udp.length);
udp->data[udplength - 8] = '