什么是协议栈? 用户态协议栈设计(udp协议栈)

news/2024/11/19 12:22:00/

什么是协议栈呢?

(协议栈(Protocol Stack)是计算机网络和通信系统中的一个重要概念,它指的是一组协议层的层次结构,这些协议层一起协同工作,以便在不同计算机或设备之间实现数据通信和交换。每个协议层都有特定的功能和责任,从物理层到应用层,每一层都在不同的抽象级别上处理数据和通信任务)友情提示,请阅读代码的注释

通过mmap可以将网卡里的数据映射到内存中去
这里是零拷贝,指的是cpu指令没有参与,但并不是没有拷贝,这是一种DMA的方式

实现协议栈有几种方式,如raw-socket、netmap、dpdk等,这里用netmap实现

如果不这样实现的话,会多拷贝一次,下面看原理图

. 获取原始数据

获取原始数据的三种方法介绍

不经过网络协议栈解析,拿到原始数据sk_buff;

  1. 使用原始套接字raw socket , tcpdump和wireshark就是使用这个做的,raw socket主要用来抓包。
  2. dbdk
  3. netmap是用于用户层应用程序收发原始网络数据的高性能框架,本文使用netmap进行数据的收发。
1、netmap 原理

网卡即不在物理层,也不在数据链路层,是在这两层之间做转换。

数据传输的流程

网卡将物理层的光电信号转换为数字信号(0101010)。给到网卡驱动,然后把这个数据(通过sk_buff(搬运工)) 拷贝迁移到协议栈。 然后协议栈解析完数据之后将数据拷贝放入recv buffer,然后应用程序通过系统调用就能得到这个数据。
 

netmap 采用 mmap 的方式,将网卡驱动的 ring 内存空间映射到用户空间。这样用户态可以直接操作内存,获取原始的数据,避免了内核和用户态的两次拷贝(网卡 -> 内核协议栈 -> 内存)

 

如果不用netmap走内核协议栈的话,我们在驱动和协议栈之间拷贝一次,在协议栈和应用层拷贝一次,那么就走了两次,当大量数据到来的话就会造成 传输速度下降,因为我们的IO操作

其实是很费时间的,所以我们就拷贝一次,大大的缩短了时间

2、netmap 环境搭建

安装 netmap

# 安装 netmap
git clone https://github.com/luigirizzo/netmap.git
cd netmap/LINUX
./configure
make && make install# 将头文件拷贝到 /usr/include/net
cd ./netmap/sys/net/ # netmap 头文件位置
cp * /usr/include/net  

 启动 netmap

# 开启 netmap
insmod netmap.ko 
ls /dev/netmap -l
# 关闭 netmap
rmmod netmap.ko
 3、udp 协议栈的实现

3.1.以太网协议头格式
struct ethhdr {unsigned char h_dst[ETH_ALEN];//源mac,6字节unsigned char h_src[ETH_ALEN];//目的mac,6字节unsigned short h_proto;//协议类型,2字节
};
3.2 ip协议头格式

struct iphdr {unsigned char hdrlen:4,  //为什么跟报头看着不一样呢,那是因为我们的网络字节序是大端的version:4; // 0x45     //我们的协议报头的时候,低位是版本号,高位是报头长度//那么编程的时候,低位的报头长度,高位是版本号//那么在转到网络上去之后就是低位是版本号			  //字节序问题,请去百度一下大小端问题unsigned char tos;//type of serviceunsigned short totlen;//total length//ip包总大小 - 首部大小等于数据大小unsigned short id;//16位标识//标识分片的包,因为网络层向下传的时候//会受mtu的大小,进行分片//所以要想确保数据是正常的,就需要设置一个标识,标识完整的数据包//也以便ip上传到传输层后续重组unsigned short flag_offset; //3位标志+13位片偏移//3位标识一个是df为1表示数据包不可以分片,0表示告知可以分片,//mf标识是否有更多分片,为0就表示最后一个分片了//那么我们在收到包的时候可以根据这个标志位和16位标识以及片偏移量重组数据了//unsigned char ttl; //time to live 生存周期(比如:每经过一个网关ttl-1)// 0x1234// htonsunsigned char type;//8位协议  用于指明IP的上层协议.传输层的报头没有协议unsigned short check;//16位首部校验和unsigned int sip;//源ip,标识发送方主机unsigned int dip;//目的ip,标识接收方主机}; // 20字节

3.3 udp协议头

//udp协议头
struct udphdr {unsigned short sport;//源端口unsigned short dport;//目的端口unsigned short length;//udp长度unsigned short check;//校验值}; // 8字节
3.4 arp协议头

struct arphdr {unsigned short h_type;//硬件类型unsigned short h_proto;//协议类型//真正的地址是mac地址,//ip地址是逻辑地址,mac地址是物理地址,唯一标识一台主机的unsigned char h_addrlen;unsigned char h_protolen;unsigned short oper;//操作码,在发送arp包的时候,会//用到操作码,arp响应2和arp请求1//有了这个操作码,我们就知道是请求获取我的mac地址还是//我的arp请求已经到了(响应)//因为刚开始发arp包的时候,只携带自身的mac地址和arp请求//发送过后,再回发arp响应,将mac地址填上,此时收到的//arp包的源mac地址就是我们之前广播的主机的mac地址//然后做一个映射unsigned char smac[ETH_ADDR_LENGTH];unsigned int sip;unsigned char dmac[ETH_ADDR_LENGTH];unsigned int dip;
};

ICMP协议头我就不实现了,主要是用来进行ping命令的

3.5 各层数据包格式

我们还得定义一下OSI七层模型的数据包,因为网络层的数据包从上到下是解包和封包的过程

 越下面的层,会封装上面的层的协议头

struct udppkt {struct ethhdr eh;struct iphdr ip;struct udphdr udp;unsigned char data[0];//用户数据//柔性数组,这样就可以在结构体末尾动态地分配内存空间。//不会发生越界情况
};//定义完以太网包,ip包和udp包之后
//我们还需要定义一个arp包
//为什么呢,因为arp缓存
//在我们xshell连接上之后会将
//eh0这张网卡的mac地址和ip地址做一个映射关系
//过一段时间之后这个mac和ip地址的映射关系就会消失
//所以我们需要自己搞一个arp包或者自己设置arp缓存
//或者静态的//没有设置也没有静态arp缓存的话客户端就会发一次arp包
//那么既然我们是用netmap的方式接收的包,那么就需要自己接收
//到包,封装包,struct arppkt {struct ethhdr eh;struct arphdr arp;};

其他的包就不写了,其实就是在上层的包那里,添加下当前网络层的协议头


#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>#include <sys/poll.h>
#include <arpa/inet.h>#define ETH_ADDR_LENGTH		6
#define NETMAP_WITH_LIBS#include <net/netmap_user.h> 
#pragma pack(1)
//内存对齐设置为1,如果不设置为1的话
//会出问题,参考我的博客里的内存对齐篇#define ETH_ALEN	6
#define PROTO_IP	0x0800
#define PROTO_ARP	0x0806#define PROTO_UDP	17
#define PROTO_ICMP	1
#define PROTO_IGMP	2struct ethhdr {unsigned char h_dst[ETH_ALEN];//源mac,6字节unsigned char h_src[ETH_ALEN];//目的mac,6字节unsigned short h_proto;//协议类型,2字节
};struct iphdr {unsigned char hdrlen:4,  //为什么跟报头看着不一样呢,那是因为我们的网络字节序是大端的version:4; // 0x45     //我们的协议报头的时候,低位是版本号,高位是报头长度//那么编程的时候,低位的报头长度,高位是版本号//那么在转到网络上去之后就是低位是版本号			  //字节序问题,请去百度一下大小端问题unsigned char tos;//type of serviceunsigned short totlen;//total length//ip包总大小 - 首部大小等于数据大小unsigned short id;//16位标识//标识分片的包,因为网络层向下传的时候//会受mtu的大小,进行分片//所以要想确保数据是正常的,就需要设置一个标识,标识完整的数据包//也以便ip上传到传输层后续重组unsigned short flag_offset; //3位标志+13位片偏移//3位标识一个是df为1表示数据包不可以分片,0表示告知可以分片,//mf标识是否有更多分片,为0就表示最后一个分片了//那么我们在收到包的时候可以根据这个标志位和16位标识以及片偏移量重组数据了//unsigned char ttl; //time to live 生存周期(比如:每经过一个网关ttl-1)// 0x1234// htonsunsigned char type;//8位协议  用于指明IP的上层协议.传输层的报头没有协议unsigned short check;//16位首部校验和unsigned int sip;//源ip,标识发送方主机unsigned int dip;//目的ip,标识接收方主机}; // 20字节//udp协议头
struct udphdr {unsigned short sport;//源端口unsigned short dport;//目的端口unsigned short length;//udp长度unsigned short check;//校验值}; // 8字节struct udppkt {struct ethhdr eh;struct iphdr ip;struct udphdr udp;unsigned char data[0];//用户数据//柔性数组,这样就可以在结构体末尾动态地分配内存空间。//不会发生越界情况
};//定义完以太网包,ip包和udp包之后
//我们还需要定义一个arp包
//为什么呢,因为arp缓存
//在我们xshell连接上之后会将
//eh0这张网卡的mac地址和ip地址做一个映射关系
//过一段时间之后这个mac和ip地址的映射关系就会消失
//所以我们需要自己搞一个arp包或者自己设置arp缓存
//或者静态的//没有设置也没有静态arp缓存的话客户端就会发一次arp包
//那么既然我们是用netmap的方式接收的包,那么就需要自己接收
//到包,封装包,struct arphdr {unsigned short h_type;//硬件类型unsigned short h_proto;//协议类型//真正的地址是mac地址,//ip地址是逻辑地址,mac地址是物理地址,唯一标识一台主机的unsigned char h_addrlen;unsigned char h_protolen;unsigned short oper;//操作码,在发送arp包的时候,会//用到操作码,arp响应2和arp请求1//有了这个操作码,我们就知道是请求获取我的mac地址还是//我的arp请求已经到了(响应)//因为刚开始发arp包的时候,只携带自身的mac地址和arp请求//发送过后,再回发arp响应,将mac地址填上,此时收到的//arp包的源mac地址就是我们之前广播的主机的mac地址//然后做一个映射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;};
//icmp我就不封装了,有icmp协议我们才能用ping命令,否则用ping命令是
//ping不通的,可以用wireshark抓包看一下有没有icmp协议int str2mac(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;
}void echo_arp_pkt(struct arppkt *arp, struct arppkt *arp_rt, char *mac) {//把源和目的 的ip换一下就行了,然后补个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;}
//就是解析完arp包后再发过去,目的mac和源mac什么的变一下
// void echo_arp_pkt(struct arppkt *arp, struct arppkt *arp_rt, char *mac):
//这是一个函数定义,它接受三个参数,其中 arp 是指向输入ARP数据包的指针,arp_rt 
//是指向输出ARP数据包的指针,mac 是一个字符数组,可能用于存储MAC地址。// // memcpy(arp_rt, arp, sizeof(struct arppkt)):
// 这行代码将从输入ARP数据包 arp 复制整个数据包的内容到输出ARP数据包 arp_rt 中,
// 复制的字节数为 sizeof(struct arppkt)。// // memcpy(arp_rt->eh.h_dst, arp->eh.h_src, ETH_ADDR_LENGTH):
// 这行代码将输入ARP数据包中的目标MAC地址(eh.h_dst)复制到输出ARP数据包的源MAC地址(eh.h_src),
// 以交换它们的值。 ETH_ADDR_LENGTH 可能是一个常量,表示MAC地址的长度。// // str2mac(arp_rt->eh.h_src, mac):
// 这行代码似乎是将 mac 中的MAC地址数据复制到输出ARP数据包的源MAC地址字段(eh.h_src)中。// // arp_rt->eh.h_proto = arp->eh.h_proto:
// 这行代码将输出ARP数据包的以太网协议类型字段(eh.h_proto)设置为与输入ARP数据包相同的值,以保持协议类型不变。// // arp_rt->arp.h_addrlen = 6 和 arp_rt->arp.h_protolen = 4:
// 这两行代码设置输出ARP数据包的地址长度字段和协议地址长度字段。// // arp_rt->arp.oper = htons(2):这行代码将输出ARP数据包的操作码字段(oper)设置为2,这表示ARP响应。// // str2mac(arp_rt->arp.smac, mac):这行代码将 mac 中的MAC地址数据复制到
// 输出ARP数据包的发送方MAC地址字段(smac)中。// // arp_rt->arp.sip = arp->arp.dip:这行代码将输出ARP数据包的发送方IP地址字段(sip)
// 设置为输入ARP数据包的目标IP地址字段(dip)的值。// // memcpy(arp_rt->arp.dmac, arp->arp.smac, ETH_ADDR_LENGTH):这行代码将输入ARP数据包的源MAC地址(smac)
// 复制到输出ARP数据包的目标MAC地址字段(dmac),以交换它们的值。// // arp_rt->arp.dip = arp->arp.sip:这行代码将输出ARP数据包的目标IP地址字段(dip)
// 设置为输入ARP数据包的发送方IP地址字段(sip)的值。// // 总的来说,这个函数接受一个ARP请求数据包,将其内容复制到一个ARP响应数据包中,
// 同时交换了源和目标的MAC地址和IP地址,以制作一个相应的ARP响应数据包,用于回应原始ARP请求。
// 这种操作通常用于网络通信中,以满足地址解析的需求。函数的实现可能依赖于其他未提供的函数或数据结构,
// 如 struct arppkt 和 str2mac。//
int main() {struct nm_pkthdr h;//ringbuffer的头struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);if (nmr == NULL) return -1;//把fd放入pollfd中,如果fd可读,就去操作数据,不可读就不操作struct pollfd pfd = {0};pfd.fd = nmr->fd;pfd.events = POLLIN;while (1) {int ret = poll(&pfd, 1, -1);//第一个参数:pollfd,第二个参数:fd个数,第三个参数:-1代表一直阻塞,直到数据过来if (ret < 0) continue;if (pfd.revents & POLLIN) {//有数据来了unsigned char *stream = nm_nextpkt(nmr, &h);//取数据(因为已经在内存中了,不能用读,由于是环形ringbuffer,因此取数据叫next package)struct ethhdr *eh = (struct ethhdr *)stream;//把stream中的第一个部分转换为以太网头if (ntohs(eh->h_proto) ==  PROTO_IP) { //取出来的上层协议是IP协议struct udppkt *udp = (struct udppkt *)stream;//转化为udp帧数据格式if (udp->ip.type == PROTO_UDP) { //udp包int udplength = ntohs(udp->udp.length);udp->data[udplength-8] = '\0'; //udp总长度-8个字节长度的udp头  就是upd数据部分的长度。  末尾加上字符串结尾'\0'printf("udp --> %s\n", udp->data);} else if (udp->ip.type == PROTO_ICMP) {}} else if (ntohs(eh->h_proto) ==  PROTO_ARP) {//ARP包struct arppkt *arp = (struct arppkt *)stream;struct arppkt arp_rt;//eth0的ip地址,eth0是网卡接口if (arp->arp.dip == inet_addr("10.0.4.12")) { //如果接受到的广播arp是本机的就回复 (如果不进行判断就是ARP攻击了,不管是什么arp请求,都回复,会导致它们的arp表更新错误的信息)//eth0的mac地址echo_arp_pkt(arp, &arp_rt, "52:54:00:d5:c3:82");//创建一个arp回复的包(源和目的互换,补充上mac地址(ifconfig可以查看))nm_inject(nmr, &arp_rt, sizeof(arp_rt));//发送arp应答printf("arp ret\n");}}}}}

使用nm_open()函数时,需要指定的是物理网卡名。eth0是物理显卡名,ens33是虚拟网卡名。
修改网卡名字:

sudo vim /etc/default/grub//修改GRUB_CMDLINE_LINUX为如下,主要是增加 net.ifnames=0 biosdevname=0 这句
GRUB_CMDLINE_LINUX="find_presend=/presend.cfg noprompt net.ifnames=0 biosdevname=0 default_hugepagesz=1G hugepagesz=2M hugepages=1024 isolcpus=0-2"

启动程序后刚开始可以接收udp包,过一段时间后就接受不到了

1.原因:程序把网卡的数据发送到了共享内存,不经过协议栈。而局域网内所有机器每隔一段时间会发送arp协议告知局域网内其他机器自己的IP和MAC地址,如果一段时间内没有收到对方的arp协议,那么本机就会把arp表对应的arp协议信息(IP和MAC地址)删掉。
因此,因为一开始发送udp包对方的时候,还知道对方的IP和MAC地址。对方因为没有走协议栈,对方就会不发arp协议给我,那么过段时间后,我的arp表就会把对方的IP和MAC地址信息删掉,我就没办法知道对方的IP和MAC地址,因此后面就无法发送upd包给到对方了。

没开启进程前,可以ping通进程所在的机器,过段时间后无法ping通。
2.原因:程序把网卡的数据发送到了共享内存,不经过协议栈。而ping协议的反馈是走ICMP协议的
因此,因为ping对方的时候,对方因为没有走协议栈,对如果对方处理网卡信息的时候,没有实现对ICMP协议的解析和回复,那么我ping对方就没办法收到对方的反馈。

解决方法:

1.ping命令只需要实现一下icmp包就行
2.怎么保存arp缓存呢

   2.1 自己添加一下arp,设置成静态的,那么数据包就知道发到局域网的哪台主机了,

   因为路由器保存着局域网内的arp缓存表,arp缓存表的ip地址是局域网内部的私有ip

   地址,添加了之后,路由器就有一条arp缓存,当数据包到来的时候,路由器识别到 

   的是公网ip地址,然后路由器收到数据包之后,根据arp缓存表,找到对应的mac地址

   和,先去发到mac层,判断是否符合数据包的mac地址,再发到网络层,判断ip是否

   是数据包的ip,是的话,就向传输层传输

  2.2 自己写arp包,收到arp请求的时候,填充我们的eth0的ip地址和mac地址,重新发送

  过去


http://www.ppmy.cn/news/1198103.html

相关文章

【入门Flink】- 06Flink作业提交流程【待完善】

Standalone 会话模式作业提交流程 代码生成任务的过程&#xff1a; 逻辑流图&#xff08;StreamGraph&#xff09;→ 作业图&#xff08;JobGraph&#xff09;→ 执行图&#xff08;ExecutionGraph&#xff09;→物理图&#xff08;Physical Graph&#xff09;。 作业图算子链…

【Spring源码分析】BeanFactory系列接口解读

认识Bean工厂 一、认识Bean工厂BeanFactoryListableBeanFactoryHierarchicalBeanFactoryAutowireCapableBeanFactoryConfigurableBeanFactoryConfigurableListableBeanFactory 二、总结 一、认识Bean工厂 Spring Bean 工厂是Spring框架提供的一种机制&#xff0c;用于创建和管理…

第四章IDEA操作Maven

文章目录 创建父工程开启自动导入配置Maven信息创建Java模块工程创建 Web 模块工程 在IDEA中执行Maven命令直接执行手动输入 在IDEA中查看某个模块的依赖信息工程导入来自版本控制系统来自工程目录 模块导入情景重现导入 Java 类型模块 导入 Web 类型模块 创建父工程 开启自动导…

Redis中的List类型

目录 List类型的命令 lpush lpushx rpush lrange lpop rpop lindex linsert llen lrem ltrim lset 阻塞命令 阻塞命令的使用场景 1.针对一个非空的列表进行操作 2.针对一个空的列表进行操作 3.针对多个key进行操作. 内部编码 lisi类型的应用场景 存储(班级…

【CIO人物展】黄淮学院副CIO周鹏:构建数智化平台赋能学校高质量发展

周鹏 本文由黄淮学院副CIO周鹏投递并参与《2023中国数智化转型升级优秀CIO》榜单/奖项评选。丨推荐企业—锐捷网络 大数据产业创新服务媒体 ——聚焦数据 改变商业 黄淮学院是2004年经教育部批准成立的一所省属全日制普通本科高校。学校位于素有“豫州之腹地、天下之最中”之美…

C# DateTime类型 直接使用Proto、Bson 问题

Proto问题 默认这里的时区为UTC、DateTimeOffset 为0&#xff0c;参考下面文档可找到&#xff1b;这就会导致设置的时区信息丢失,如下 eg. // json "VitalityUpdateTime":"2023-10-31T15:19:25.15050508:00", // proto, 没有带时区 "VitalityUpdateT…

多线程JUC 第2季 多线程的内存模型

一 内存模型 1.1 概述 在hotspot虚拟机里&#xff0c;对象在堆内存中的存储布局可以划分为3个部分&#xff1a;对象头&#xff1b;实例数据&#xff0c;对齐填充。如下所示&#xff1a;

逆向学习记录(2)windows常用基本操作及用环境变量配置上多个python版本

1、如何打开cmd 第一种方法&#xff1a;按下winr&#xff0c;运行cmd 第二种方法&#xff1a;进入一个目录&#xff0c;点击路径处&#xff08;显示蓝色背景&#xff09;&#xff0c;然后直接键盘输入cmd&#xff0c;回车&#xff0c;运行cmd并直接进入此目录。 2、命令dir&am…