前期准备:
#pragma pack(1) 的意义是设置结构体的边界对齐为1个字节,也就是所有数据在内存中是连续存储的。这样能节约内存资源,但是会在效率上有所影响。虽说在效率上有一定的影响,不过,如果编写的是基于协议,如串口通讯的程序,那么必须严格按照一定的规则进行接收数据包。那么,只要#pragma pack(1),让数据在内存中是连续的,才好处理的。
柔性数组:详解
文章目录
用户态协议栈的意义
在内核实现协议栈往往存在两次拷贝过程,一是网卡中的数据通过sk_buff拷贝到内核,之后再从内核拷贝到进程用户空间。
于是我们考虑如何加快这个过程,减少拷贝次数。一种思路:通过DMA直接将数据从网卡拷贝到内存中,于是应用程序可以直接通过mmap从内存中取得数据。由于DMA的工作是不需要CPU干预的,所以对于CPU来说相当于没有做任何的拷贝操作,即所谓零拷贝。
那么应用进程如何通过这种方式从网卡直接取得数据?常用几种方法:
- 使用raw socket
- netmap框架
- dpdk框架
那还为啥还要使用内核协议栈
1,内核态协议栈可以为多个网络应用服务,用户态协议栈就不行。比如DPDK,它会将网卡从内核unbind,然后自己独占。
2,内核态协议栈提供了丰富的协议支持/调试手段,稳定性也经过时间的检验。反观用户态协议栈处于起步阶段,对网络协议的支持有限,大都是适配应用场景的二次开发。
如果是小型设备,通用需求,请使用内核协议栈
如果是大型设备/数据中心,需求固定,请使用DPDK等用户态协议栈,结合网络虚拟化技术,会获得最适合的性能。
netmap的原理分析
Netmap是基于零拷贝思想的高速网络I/O架构,它能够在千兆或万兆网卡上达到网卡的线速收发包速率。并且能够有效地节省cpu等计算机资源。
其基本原理可参考netmap原理分析。
常用接口说明
主要头文件:netmap.h 和 netmap_user.h,位于源码包的./netmap/sys/net/目录下。
netmap.h 被 netmap_user.h调用,里面定义了一些宏和几个主要的结构体。一般来说,如果仅仅只是想要收发数据,在上手时我们知道下面几个接口就可以了。
- nm_open
struct nm_desc *nm_open(const char *ifname, const struct nmreq *req,
uint64_t new_flags, const struct nm_desc *arg)
nm_open针对ifname指示的网卡接口启用netmap并返回针对该接口的描述符结构体。一般直接这样调用既可:
struct nm_desc *nmr = nm_open("netmap:eth1", NULL, 0, NULL);
struct nm_desc中包含一个fd指向/dev/netmap,可用于poll、epoll等系统调用。

- nm_nextpkt
nm_nextpkt()用于接收网卡上收到的数据包。它会将内部所有接收环检查一遍,如果有需要接收的数据包,则返回这个数据包。一次只能返回一个以太网数据包。因为接收到的数据包没有经过协议栈处理,因此需要在用户程序中自己解析。
读一个数据包时一般这样调用,stream即为数据在缓冲区中的首地址,struct nm_pkthdr为返回的数据包头部信息,不需要管头部的话直接从stream去取数据就行
struct nm_pkthdr nmhead = {
0};
char* stream = nm_nextpkt(nmr, &nmhead);
- nm_inject
nm_inject()是用于往共享内存中写入待发送的数据包,数据再被从共享内存拷贝到网卡,进而发送出去。它检查所有的发送环,找到一个可以发送的槽后将数据写入。一次只能发送一个包,包的长度由参数指定。一般的调用方式:
nm_inject(nmr, &datapack, packlen);
安装netmap
呵呵,我安装了半天呜呜呜。
这里推荐一个比较好的教程,照着按就行
netmap安装教程一
协议栈的数据结构定义
下图是实现一个UDP协议时的网络协议栈结构及其数据封装的简化形式,我们的用户态协议栈需要实现链路层、网络层以及传输层。按照图中数据包结构,我们需要依次提取链路层、网络层、传输的首部,并最终得到用户数据。
链路层
#pragma pack(1)
#define ETH_ADDR_LENGTH 6
#define PROTO_IP 0x0800
#define PROTO_ARP 0x0806
#define PROTO_UDP 17
#define PROTO_ICMP 1
/*以太网*/
struct ethhdr {
unsigned char h_dst[ETH_ADDR_LENGTH]; //目的地址mac地址
unsigned char h_src[ETH_ADDR_LENGTH]; //源地址
unsigned short h_proto; //类型
}; // 14
IP首部
/*ip头*/
struct iphdr {
unsigned char hdrlen:4,
version:4; // 0x45/*版本号和首部长度*/首部长度是低四位,版本长度是高四位
unsigned char tos; //服务类型
unsigned short totlen;//总长度
unsigned short id;//16位标识 取余更新
unsigned short flag_offset; //标志
unsigned char ttl; //time to live
// 0x1234// htons
unsigned char type;//8位协议
unsigned short check;//检验值
unsigned int sip;//源ip
unsigned int dip;//目的ip
}; // 20
UDP首部
struct udphdr {
unsigned short sport;//源端口
unsigned short dport;//目的端口
unsigned short length;//长度
unsigned short check;//校验值
}; // 8
用户态协议栈实现(一)
协议栈实现的第一部分,简单地实现UDP接收、ARP响应和ICMP响应。
实现一个简单的UDP协议数据接收
UDP数据包封装
首先定义UDP的整个数据包。这里涉及到了柔性数组(零长度数组)用以定义用户数据包的起始地址而不占用实际的结构体空间。
/*用来封装成数据包*/
struct udppkt {
struct ethhdr eh; // 14 以太网
struct iphdr ip; // 20 ip头
struct udphdr udp; // 8
unsigned char data[0];//用户数据(不能定义固定长度)
/*柔性数组?*/
}; // sizeof(struct udppkt) ==
代码展示
int main() {
struct nm_pkthdr h;
/*数据包*/
//struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
struct nm_desc *nmr = nm_open("netmap:ens33", NULL, 0, NULL);
if (nmr == NULL) return -1;
/*检测fd是否可读*/
struct pollfd pfd = {
0};
pfd.fd = nmr->fd;
pfd.events = POLLIN;
while (1) {
int ret = poll(&pfd, 1, -1);
if (ret < 0) continue;
if (pfd.revents & POLLIN) {
/*用来接收网卡上到来的数据包的函数*/
unsigned char *stream = nm_nextpkt(nmr, &h);
struct ethhdr *eh = (struct ethhdr *)stream;
if (ntohs(eh->h_proto) == PROTO_IP) {
struct udppkt *udp = (struct udppkt *)stream;
if (udp->ip.type == PROTO_UDP) {
//
int udplength = ntohs(udp->udp.length);
udp->data[udplength-8] = '\0';
printf("udp --> %s\n", udp->data);
} else if (udp->ip.type == PROTO_ICMP) {
}
}
/*此为arp响应部分可忽略*/
else if (ntohs(eh->h_proto) == PROTO_ARP) {
struct arppkt *arp = (struct arppkt *)stream;
struct arppkt arp_rt;
if (arp->arp.dip == inet_addr("192.168.112.119")) {
//
echo_arp_pkt(arp, &arp_rt, "00:0c:29:ae:a9:6e");
/*nm_inject()是用来往共享内存中写入待发送的数据包数据的。数据包经共享内存拷贝到
网卡,然后发送出去。所以 nm_inject()是用来发包的。*/
nm_inject(nmr, &arp_rt, sizeof(arp_rt));
printf("arp ret\n");
}
}
}
}
}
实现ARP协议
ARP全称“地址解析协议”,它为IP地址到对应的MAC地址之间提供动态映射。因为一台主机将以太网数据帧发送到同一局域网内的另一台主机时,是根据6字节的MAC地址来确定目的接口的。对应的还有RARP(逆地址解析协议),即从MAC地址找到IP地址。
设备驱动程序从不检查IP数据包中的目的IP地址。
当一台主机要向另一个IP地址发送数据时,发现自己的ARP表中并没有记录该IP对应的MAC地址,于是就广播这个ARP请求,然后后等待对方的响应。请求的大致意思就是:“如果你是这个IP地址的持有者,请告诉我你的MAC地址”。
ARP协议的报文格式
- op操作字段:指出4种操作类型:ARP请求(为1)、ARP应答(为2)、RARP请求(为3)、RARP应答(为4)
一个ARP请求的工作流程就是:
请求端发送一个ARP请求报文,op为1,发送端以太网地址和IP地址就是它自己的地址,目的IP地址为接收端的地址,目的以太网地址为空;
接收端收到ARP请求后,将两个发送端地址分别填到目的地址中,然后自己的以太网地址和IP地址填入两个发送端地址字段,记得将op改为2;同时别忘了把以太网首部的地址也做对应修改;最后将报文发送出去。
结构定义
/*qrp头*/
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];//mac地址
unsigned int sip;//ip地址
unsigned char dmac[ETH_ADDR_LENGTH];
unsigned int dip;
};
/*arp包以太网和arp*/
struct arppkt {
struct ethhdr eh;
struct arphdr arp;
};
代码展示
int str2mac(unsigned char *mac, char *str) {
char *p = str;
unsigned char value = 0x0;
int i = 0;
while (p != '\0') {
if (*p == ':') {
mac[i++] = value;
value = 0x0;
} else {
unsigned char temp = *p;
if (temp <= '9' && temp >= '0') {
temp -= '0';
} else if (temp <= 'f' && temp >= 'a') {
temp -= 'a';
temp += 10;
} else if (temp <= 'F' && temp >= 'A') {
temp -= 'A';
temp += 10;
} else {
break;
}
value <<= 4;
value |= temp;
}
p ++;
}
mac[i] = value;
return 0;
}
//arp响应
/*echo_arp_pkt(arp, &arp_rt, "00:0c:29:ae:a9:6e");*/
/*把源改成目的,把目的改成源*/
void echo_arp_pkt(struct arppkt *arp, struct arppkt *arp_rt, char *mac) {
memcpy(arp_rt, arp, sizeof(struct arppkt));
memcpy(arp_rt->eh.h_dst, arp->eh.h_src, ETH_ADDR_LENGTH);
str2mac(arp_rt->eh.h_src, mac);
arp_rt->eh.h_proto = arp->eh.h_proto;
arp_rt->arp.h_addrlen = 6;
arp_rt->arp.h_protolen = 4;
arp_rt->arp.oper = htons(2);//操作数改下
str2mac(arp_rt->arp.smac, mac);
arp_rt->arp.sip = arp->arp.dip;
memcpy(arp_rt->arp.dmac, arp->arp.smac, ETH_ADDR_LENGTH);
arp_rt->arp.dip = arp->arp.sip;
}
//
int main() {
struct nm_pkthdr h;
/*数据包*/
//struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
struct nm_desc *nmr = nm_open("netmap:ens33", NULL, 0, NULL);
if (nmr == NULL) return -1;
/*检测fd是否可读*/
struct pollfd pfd = {
0};
pfd.fd = nmr->fd;
pfd.events = POLLIN;
while (1) {
int ret = poll(&pfd, 1, -1);
if (ret < 0) continue;
if (pfd.revents & POLLIN) {
/*用来接收网卡上到来的数据包的函数*/
unsigned char *stream = nm_nextpkt(nmr, &h);
struct ethhdr *eh = (struct ethhdr *)stream;
if (ntohs(eh->h_proto) == PROTO_IP) {
struct udppkt *udp = (struct udppkt *)stream;
if (udp->ip.type == PROTO_UDP) {
//
int udplength = ntohs(udp->udp.length);
udp->data[udplength-8] = '\0';
printf("udp --> %s\n", udp->data);
} else if (udp->ip.type == PROTO_ICMP) {
}
} else if (ntohs(eh->h_proto) == PROTO_ARP) {
struct arppkt *arp = (struct arppkt *)stream;
struct arppkt arp_rt;
if (arp->arp.dip == inet_addr("192.168.112.119")) {
//
echo_arp_pkt(arp, &arp_rt, "00:0c:29:ae:a9:6e");
/*nm_inject()是用来往共享内存中写入待发送的数据包数据的。数据包经共享内存拷贝到
网卡,然后发送出去。所以 nm_inject()是用来发包的。*/
nm_inject(nmr, &arp_rt, sizeof(arp_rt));
printf("arp ret\n");
}
}
}
}
}
arp常见问题:arp欺骗(arp广播发送)
举例:
1.主机A要和主机C通信,主机A发出ARP包询问谁是192.168.1.3?请回复192.168.1.1。
2.这时主机B在疯狂的向主机A回复,我是192.168.1.3,我的地址是0A-11-22-33-44-02。
3.由于ARP协议不会验证回复者的身份,造成主机A错误的将192.168.1.3的MAC映射为0A-11-22-33-44-02。
本次arp响应中怎么能成arp欺骗呢?
将代码中判读响应主机ip匹配去掉,是此代码变成上述的主机B
if (arp->arp.dip == inet_addr("192.168.112.119")) //此判断去掉
ARP欺骗详细讲解:arp欺骗原理及实现
ICMP后续补充
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:服务器课程