那我们先呢跟大家解释这个协议栈这个东西啊协议栈这个东西呢或多或少啊各个朋友应该都听过,我们站在一个设计者的角度,站在一个设计者的角度,站在tcpip的个人的角度,我们怎么去设计这个协议的?
设计队的角度来设计这个网络协议战。
就是各位朋友们你想一下这个网络协议战,
有很多朋友就会想到一个点,那为什么我们还需要去设计一个网络协议栈,我们不是去学这个东西就可以吗?其实我也跟大家解释一下,
其实你在那把网络协议上理解的很透彻的话,你一定要站在一个设计者的角度,
就是你抛开所有的东西,抛开所有的那些框架性的东西,就是你自己去想这个两台PC机之间他们如何通信的,他们通信是a发一段数据b如何收到?
收到完了之后如何去想这个过程是怎么样的,以及发送的数据格式是怎么样的,啊那我们今天就站在这么一个角度来跟大家讲。
当然在讲这个的时候,
我们还会引入一个概念,站在一个设计者的角度去设计一个协议栈的话那怎么去设计?呢那当然我们就跟大家讲到这个用户态协议栈,
因为用户态的协议栈呢他是把协议账当做一个应用程序
来运行,就好比我们很多时候我们写了一个服务器,我们写了一个代码,好,我们跑的时候我们调用网络的接口是调用send,recive,
这个我们在之前讲网络编程的时候给大家讲过,就是我们调用的调用的connect Listen accept
啊这些接口我们是调用的这些接口,这些接口呢是我们系统早就已经帮忙完成的这些接口。
那如果做一个用户态的协议栈呢请大家注意,就是把网络的这一层把我们网络协商对于网络数据解析的这一层,
把它重新拎出来,跟我们的应用程序坐在一起,就把这个网络协议的解析放到进程里面的一部分。
就这么个意思好吧?
就是
好,把协议栈这是协议栈这是我们的应用程序,如果不是这么做,呢
本来的做法呢是这样的,是把网络协议,但它是在操作系统的,把这一部分跟我们的应用程序分开,
放到操作系统里面就这么一种情况,现在用户态协议栈就是把这个协议栈放到应用程序跟应用程序放在一起,这么明显的这个能理解吧。
好,那我们现在再来分析一下,
为什么会要有这个用户态协议栈,呢啊为什么会要有这个用户态协议栈?呢
好,我在这里问一下大家,好减少拷贝,我在这里问一下大家大家有没有接触过或者有用过用户态协议栈的
好,这里有朋友说应该是很多朋友是没有用的,没有用过是很正常的,啊串口通信串口通信不是走的网络吧,应该绝大多数朋友是没有用过的。
那我们接着来跟大家讲一讲,如果没有在这种场景下面,但是你是很难用得上的,就是跟大家解释一下,为什么会有用户态协议栈。
就是
啊各公司私有协议算了,
呃私有协议这是应该是属于用户的协议,就是类似于在TCP的上面去定义的协议,好吧?
好减少CPU上下文切换,那好,
我们来给大家解解释一下为什么是这么个说法,我们这里要跟大家解释一下,首先第一个这里是一个网卡,这里是一个网卡,然后在对应上
中间这一层是我们的协议栈。
好,还有这边是我们的应用程序,应用程序就是我们自己编译出来的进程这么一个概念。
这三个点
第一个数据是从哪里来的?这三个这三个我们用一个框把它框加入,就是我们
在服务端也好或者在客户端也好,就是我们能够进行网络通信的也好,就这样一个一个应用程序就是把它放到了一起,就这一段。
好,现在比如说我们应用程序通过一个客户端一个应用程序就是一个进程,没错,应用程序就是一个进程进程是运行阶段,
现在一个客户端也好,然后我们通过来通信就是一个客户端,就是我pc机现在给百度访问百度这个过程也好,或者去访问淘宝这个过程也好,就这样的。
访问先数据是先到达网卡,先到达网卡,这边是客户端也好,就是对端的一台对端的机器,然后发送数据先经过网卡网卡先接收到这个数据,
然后网卡把它处理完了之后,再把这个数据然后copy到协议栈里面,然后协议栈再把这个数据
我们通过系统调用都从协议栈里面copy到我们应用程序上面来。
好,这个过程能不能理解?这个过程应该能理解,就是
我们先把数据先到了网卡那这里,我们问一下网卡的作用用来做什么?
好,
网卡资源大家打开这个7层模型啊打开这个7层模型,
网卡是属于哪一层的,大家看吧打开这7层模型,大家可能很多朋友说的网卡属于哪一层,其实我跟大家讲一下
就是在光纤里面传的时候,在光纤里面传的时候传的是
光信号在双绞线里面传的是电信号,这个物理层的什么物理层就是所说的我们光信号或者电信号
网卡的作用呢就是把这个光电信号转换为数字信号,
转化为数字信号,也就是说一个AD在三个的过程中呢就是把数字信号转化为
我们的模拟信号转化为光电信号,光电就是模拟信号,那也就是说一个AD转化和da转换作用。
好,那地方它所以网卡它不是在任何一层,
它既不是在物理层,也不是在数据链路层,它是在物理层数据链数据链路层之间做这个物理层转化为数据链路层这么一个概念,等于说就是把这个模拟信号转化为数字信号数字信号转化为模拟信号这么一个概念
来理解,如果大家能理解这个网卡的作用
那我们网卡对象在这一版对端机器接收完之后,我们不管是网卡,不管是光纤还是双绞线
网卡接收完这个数据之后,通过AD转化
把模拟信号转为数字信号,然后把这个数据放到协议上,怎么把这个数据从网卡迁移到协议上,请大家注意这里有个东西也要跟大家解释一下,这有个工具
不是叫工具,有一个东西叫做 SK Buffer叫做SK buffer。
好sk_buff这个东西就是就是用来从网卡里面
数据运到协议栈里面,协议栈里面主要对网卡数据进行解析的。
这就是网卡的数据,这就是协议栈
协议栈把网卡数据解析完之后,把SK包的数据解析完之后,对应的这一帧一帧的数据,
然后放到这个recv缓冲区里面,然后我们通过系统调用调用receive这个函数
要用receive,好,然后从协议栈里面把数据从协议栈上发到我们的应用程序,所以我们就能够读到这个数据来理解,这就是中间经过这么两个方面,
就是从网卡copy到协议栈,再从协议站copy到我们的应用程序,这是我们现在操作系统,他工作方式就是这样的,
来理解一个网卡对应一个Mac地址没错,一个网卡对应一个Mac。
好,就是以我们现在的linux我们为例,他就是这么工作的,
就是我们现在每接收一堆数据,就是你现在写的服务器也好,你写的客户端也好,你把数据发送出去接收数据也好,
它都是这么一个过程,每一帧从网卡里面需要copy到协议栈上,再从协议栈上copy再用程序每一次都用,所以很多朋友就在想一个方法,想一个方法,
就是这里面从网卡里面copy到协议栈再从协议栈copy到应用程序,这个过程它有两次拷贝,两次copy,
两次拷贝来理解,这两次拷贝分别是从网卡copy到协议栈,再从协议栈copy到应用程序,其他的我们还没算,就是大家你是因为里面我们copy多,
消息队里面他的消消息对列把copy数据库它的数据库里面拿出来,那这个东西没算,就是每一次系统调用都需要经过这么两次,所以很多问题就在考虑一个问题,
就是我们能不能简化一下,好,后面就出现了一个新的方法,就是这样的。
好,这里是一块内存,这是一块内存,然后通过这么一个方法,
就是通过网卡的这个 dma的方式,就是将网卡映射到内存中间。
好,将网卡映射到内存中间,就是网卡里面解析完的数据这里有块存储,把这块存储的空间映射到内存中间,跟内存的空间是一一对应的,
也就是说接收数据网卡解析完之后,数据就直接映射到内存中间这一个方法来理解,这个方法呢就是跟大家讲到的一个叫做内存映射叫m map的方式,
它底层是走的一种叫做dma的方式,叫做内存直接从直接通道这么一个方法。
这里跟大家讲的大家可以看到从网卡里面的数据到达内存中间,然后应用程序是直接在内存中间可以直接读取这个数据的直接读取
映射过来的这一块数据了,所以在这个过程中间就减少了这么一次拷贝,但是过程中间有可能会,说这不是减少一次吗?有这么斤斤计较吗?请大家注意。
这一次dma的方式,他从严格意义上来说,它不叫copy,什么叫做拷贝
好,首先我们把这数据
数据的通道大家能够理解,就是现在通过网卡映射需要到内存中间,如果大家能理解这个方式,那我们接着再跟大家讲一讲。
就是第一个上面这一条路是有两次拷贝,
下面这一次是采用一种DMA的方式,也就是说这个 DNA的方式什么意思?可以跟大家讲它是没有拷贝的。
没有copy,那很多朋友说那这个网格数据怎么到内存里难道没有拷贝吗?请大家注意拷贝是什么?就是复制什么意思?
或者是通过我们CPU执行的这,要通过 CPU指令的啊是通过CPU指令才能够做得到的,
其他注意那这个 dma的方式它是自己操作的,是CPU是不需要去干预的,
CPU不需要干预的,啊就是说网卡的数据直接到达内存中间,
能理解这一个请大家注意,所以这里两次拷贝,但是这个网卡的数据到达内存中间它是没有拷贝的,我们应用程序是可以直接去取这个数据的,所以在这个过程它是没有拷贝的,也叫做零拷贝,
这个地方不是有一次拷贝的,这个零拷贝是怎么理解?就是我刚才讲零拷贝就是利用dma CPU是没有干预的,CPU是没有操作的,所以它叫做零拷贝。
好,如果大家能理解这个零拷贝这个词没有,
那我们在这里跟大家延伸一下延伸一下,有很多朋友问到这个 mmap的原理好,m map的原理
没错,有那么问到mmap里面的那 mmap,不对应,啊我可以跟大家解释一下,mmap我们可以这么做,我们可以从磁盘中间对
对磁盘中的一个文件我们可以映射,我们同样也可以对网卡进行映射,
也可以同样大家可以对一个USB或者对一个U盘,你也可以叫做dma叫做mmap你也可以把它映射出来,包括有一些蓝牙的设备,包括WiFi的设备,
都是可以直接通过m map映射到内存中间来进行操作的,能理解那 m map它的原理是怎么样?
好这一步也跟大家解释一下,
也跟他解释讲要依赖dma对应来说它是一个总线,它是一个总线,请大家注意啊这一步我们就没有深入进去跟大家讲计算机系统体系结构了,因为这东西就比较多了,请大家注意。
像这种dma的方式,mmap包括对于磁盘操作,它也是一个dma的方式,也是绕开了这个 CPU去复制,
包括包括我们对于蓝牙操作或者外设或者USB操作它都是这样的,通过一个DNA的方式,然后直接把对应的存储映射到内存中间,
也就是数据是直接过来的,请大家注意这里有一个前提是需要有一条总线的,
好,请大家注意这一点就可以了,也就是mmap它底层实现的东西,它是由于底层是有DMA的这种方式的支持才可以做到的,好吧?
好CPU如何知道就这样吧这里有个情况就是当做数据,
映射完了之后,这时候会给CPU触发一个中断,就是数据已经就绪了,这一步这是体系结构里面已经讲过,这个这是DMA的方式,数据已经运行,转过来之后,dma
这边传输完了之后会给CPU引发一个中断,好吧?接着跟大家讲网卡上面网卡上面是直接芯片的,好,这个我给大家解释一下。
好,讲到这里,我相信大家可能还有一些概念上的原理,概念上的原理在这里我也问大家一下,就是既然大家有问到这个问题,啊我问一下大家就是关于这个网卡,
网卡的驱动它是运行在哪里好?
答案一是运行在网卡上面的,
第二运行在CPU上面的,运行到我们的操作系统里面的,运行到CPU,就跟我们操作系统运行在一起的,内核里面的啊是选择一还是选择二?
没错,请大家注意,请大家注意。
这里有一个概念都有吧,请大家注意这里就有一个概念的问题,请大家注意,这里有一步就以CPU
好,这个我们把它简化一下我们这两部分,一部分这两个框这一部分是操作系统,也就是我们内核,好,这是网卡。
好,这是网卡。
好,请大家注意啊这个网卡中间它也有芯片,请大家注意这个芯片上面运行的东西叫做物检?,它是本来在芯片出厂的时候,在网卡出厂的时候早就已经做好了,
那这往返驱动请大家注意它是内核里面的一部分是去驱动
使得这个网卡进行正常工作的,所以很多时候跟网卡的驱动,包括我们后面跟大家讲的这个 Nic系统的这种
它是运行在内核里面的,它是去兼容使网卡能够正常工作的人,那就它是取使网卡正常工作的,
这么一个东西叫做驱动叫做网卡驱动,能理解这个概念,一定跟大家讲的网卡驱动是运行在内核里面的,它是使得网卡能够正常工作的,以及能够去接受网卡的数据,使得网卡能够发送数据这样一个正常工作,这是网卡系统
好,网卡上面是,芯片芯片上面它也是需要有程序的,以及包括这个网卡上面跟大家解释一下,比如这个网卡上面它接收数据的处理,它的怎么一种处理方法,也就是说对于这个
模模数转换这高低电瓶怎么去以多少作为一个参数,它也是需要有代码的,也是需要有程序收入进去的。
好,
这是关于底层的原理这一部分,我们就关于网络底层这个东西给大家讲到这里。
因为这里面还有一部分就是关于这个网卡它的作用还有很大一部分的作用,包括我们对于这个网卡可以做虚拟化,对于它的一些功能我们还可以做,对于有用户态协议栈这个东西之后,
我们的对于网卡的想象空间就会更大,有了用户态协议栈,我们对网卡的想象空间会更大。
举个例子跟大家解释一下举个例子跟大家解释一下,第一个比如说比如说沟通,比如说我们现在一台PC机,
一台pc机,如果我们能够把网卡自己能够通过代码去控制的话,那我们是不是可以把我们的做成一个交换机也可以,或者说我们做了一个路由器也是可以,
或者说我们包括像一些数据的过滤我们也是ok的,就是一些数据我们不去处理也是ok的,所以对于这个网卡我们的可操作性它就会变得更强。
好,
讲这个讲这里那我们核心的原理还是跟大家来讲,这个协议上这底层原理是依赖这样一个动作,
那对于网卡我们接收完一帧完整的数据,我们怎么处理好吧?
那我们接着来跟大家讲,假设现在假设有一个前提,
假设我们能够取到一帧完整的网络数据,一个完整的数据包。
取得一个完整的数据包,这个完整的数据包呢就是包括网卡里面接收什么数据,我们就能够用什么数据。
好那在这个基础上面我们就有的去实现这个协议栈的这个前提,有了实现就有了实现这个协议上的最基础的东西。
好,
在这里我也跟大家分享,我们怎么去取到一张网络的数据,啊得到一个完整的网络数据包的数据,这里有这么几个方法,
第一个方法,
第一个方法我们可以利用原生的sock的好如何取到一定完整数据,这是第一个方法。
第二个方法,
我们可以利用一些开源的框架比如netmap。第三个我们也可以用一些成熟的,啊我们可以用比如说一些商业的框架比如dpdk,
大概是这么三种方法,当然还有一些其他的,比如像这种pfl这种方向都是可以的,大概能够在网卡中间取一点完整的数据的,有这么三种方法。
好吧,那我们今天在这个基础上面,
我们来跟大家先来封装一下,今天的代码量会比较多,代码量会比较多,我们今天要跟大家封装几个协议,为了我们后面就跟大家去实现TCP有关系。
现在我们在我们实现的这个前提下面,在这个底层的框架的基础上面我们实现一个协议栈,那也就是说我们应用的这个框架利用netmap,因为netmap开源的
dbdk我们后面会有专门的主题,专门的内容来给大家讲这一部分。
好,如果各部门现在没有,那地方可以现在在github里面搜一下这个框架,然后把它放下来,然后编译一下然后就可以了,我们在这就跟大家实现这个协议栈
这里是个服务器,这里是个客户端,现在客户端给服务器发送一份数据好。
还是,我们还是采用用一个udp的系统,先从udp开始封装起,比如客户端现在发一些数据到服务器,服务器接收这个数据接上一个完整的udp的数据包,然后怎么处理呢?
我们对一个udp的数据包啊这里要从头开始跟大家讲,就是以一个udp的数据包为例,啊 udp的数据包大概分为这几个方面
第一个
以太网头,第二个IP头,第三个udp头,第四部分才是我们的用户数据,才是我们接收到,我们调用recvfrom接收到的那一段的数据。
也就是说一针udp的数据包分为这么几个层次,几个包,第一个以太网的头,第二个IP头,第三个udp头,第四个是我们的用户数据。
也就是说以太网络头是对应的在数据链路层的,然后IP头是在网络层的传输层,udp是在传输层的,然后以及用户层的数据,
每一个我们对应的来跟大家来封装一下,大家可以大家就可以对应的代码,并且我们要把它跑起来。
这里我有必要再打开这个情节给大家解释一下,就是本身这个题目的话,今天我们是讲这个
滑动窗口,我认为在讲滑动窗口之前,啊
然后包括像TCP协议上的实现之前,我们先把这个环境先把它跑起来,先在后面的时候我们再去跟大家去讲到TCP协议具体实现的时候,再去跟大家讲这个关于滑动窗口具体是怎么做的。
第一个就是以太网头怎么封装,呢
以太网的头包括14个字节的以太网的头,
前面6个字节是目的地址,后面6个字节是原地址以及2个字节的类型,请大家注意这个目的地址六个字节什么意思?
就是所谓的那个
MAC地址
请他认为这个MAC地址这个东西
每个网卡都有一个MAC地址,网卡出场时的那个MAC地址那你可以改的,因为我们在每发送一帧数据包的时候,在软件上面我们是可以对这个数据进行修改的,那就是关于这个 MAC地址,
原厂出的mac地址是可以改的,也就是说大家你所接到的MAC地址也好,IP地址也好,端口也好,请大家注意,
我们没有在计算机没有哪个物件,它叫做MAC地址,没有哪个物件叫IP地址,也没有哪个固体的东西叫做端口都没有,请大家注意。
所谓的MAC地址也好,ID地址要端口也好,全是协议栈里面一个字段名,那地方全是这样一个字段名而已,它并不是一个固体的部件。
好6个字节目的地址,再加上6个字体的原地址,再加上1个协议,
这就是关于以太网的头
6个字节的目的地址,6个字节原地址,请大家注意这1个数组包
大家等一下我们去取的时候
这个我们取出来这么一个一帧数据包,这个数据包按照我们这里应该用数组吧 ;注意这个内存啊这个内存在排布的时候,
结构体的使用它跟数组是一样的,是一样,就是对于一块内存的使用,比如同样是8个字节,
同样是14个字节,那我们是用数组去存也是可以的,我们用结构体去存它也是ok的,好会不会有对齐的问题肯定有。
好,这是关于以太网头,
然后第二个就是IP地址
好在这呢关于这个长度我有必要再跟大家聊一聊,就是关于这个长度,啊大家可以看到这16位的这个长度它有多少?
16位,一个udp的是4,节数,16位总长度它有多少?65535,好。
65535,也就是从理论上面来讲,一个IP包它有多长,一个IP包最长它能够有多长?各位最长它能有多长?
也就是说最大的传输单元是1500,最大的传输单元是1500,那一IP包它最大可以传65535,
但是很多人就不太理解,那653这里面不是已经规定了最大只能传1500,嘛为什么这个IP波还能做64k还能做64k?
这 Mtu那个东西它是以太网的限制,你比如说我们一个64k的数据,
一个IP的64k的包,就一个流媒体的数据包,我们发过去了,一个大64k满包发过去,发过去之后请他注意在以太网这层在网卡传出去的时候,它会分片
分成。
这么大一个1500一个一个包把它发出去,连续发多个把它发出去
我们都会有问过这个 mtu和这个最大传输是不是会有些冲突?
比如说如果避免分包分包的过程是避免不了的比如说你去访问百度的过程中间或者访问一个公共IP,你中间要访问的时候,你要经过那些路由器那些路由器或者网关,它也是一个网卡的设备,它也是要协商进行解析,对数据进行分析,请大家注意,
他也会去分包,它在关于分包的这个避免的情况它是避免不了,除非你传输的包特别小,然后每一个包中间的时间间隔足够长,
然后就是协议。
可以看到以太网这里有一个proto,这呢关于IP里面呢也有一个协议,
为什么每一层都会有一个协议,
这是标志着让数据链来传输的时候,从这头里面能够解析出来网络层是用的什么协议,通过网络层我们能够解析出来传输层用的什么协议对不对?IP头里面的这一个proto是用来去形容传输层我们用什么协议
UDP包里面对于每一个包它有没有一个ID?
IP包里面没有,
它没有这个每一个包它是哪一个包发的它是没有的,那这就造成了一个现象,请大家注意就是这个 ud p在发送的时候,
他的协议上面是不可能去实现,从协议本身它不可能去实现。
对于包的定义的就是一个udp,发数据它是没有边界的,你是很难去给udp这个数据包定义一个包的,就是udp协议本身它就没有这个包的概念能理解。
所以各位在这里讲的是udp研发店所有的udp的包,你发现它的包的头
唯一的就是通过这个 check能够效去检验这个包对不对,有没有丢失,但是我们很难去把它去看出来,这里总管一起我们发了多少个包,从它的定义的格式上面,从这个逻辑上面它就应该体会出来,
udp它是没有数据包的概念,所以说啊所以说我们后面会去以一个ud的包给他发送出去,但是请到你udp的头在定义的时候,他压根就没有这个包的概念。
那有一个包肯你知道这个包括有个ID吗?它至少会有一个分割会知道的,包括是有个ID,我知道这个包收到了,UDP是没有
udp只有8个字节协议头。
得出这样一个结论,就是关于IP层为什么没有为什么没有这个端口,
MAC地址它是以太网是数据电路层的产物,
IP地址它是网络层的产物,端口号它是传输层的产物,所以各方面在IP层在网络上它是没有端口这个概念的,所以在另外一个层次我们也可以帮助大家更好去理解就是
如果没有MAC没有IP地址的话,换一句话说换句话说没有IP地址,也就是说哪些如果路由器路由器它是工作在网络层的,
如果没有路由器没有交换机的话,从一定程度上交换机本身就是二层的交换机,二层交机它只适合在局域网内工作,那就它只适合在局域网内工作,
如果要跨网络的话,那就需要借助路由器来调节,或者三层调换器三层调节三层调换器就是它能够工作在网络上来理解,
就是交换机他只能工作在局域网内,但是如果要跨网络,从a网的话请不引这里一定要引入一个路由器或者三层交换机在内。
网好。对于这个端口后呢
很多时候再来问大家一个问题,再问大家一个问题,大家有没有听过NAT的东西?网络地址映射网络地址映射它是什么?将端口
和IP地址做映射的,它是需要工作的传输层,它是要对传输层进行解析的工作,
他需要对传输层的协议进行解析,所以很多时候我们听到一些东西,工作上2层3层4层5层,
工作在哪一层,请大家注意,你就可以看到它是对哪一层产物进行解析的,你就能够判断出来它能够在哪一层,比如说交换机它只对MAC地址进行处理,
所以交换机是二层的产物
nginx工作到应用层,他是对应用层协议进行解析的东西。
haproxy它是对TCP端口他是传输层的,
lvs它是对IP地址也是网络层的,
f5数据链路层
关于负载均衡每一层工作的概念,能理解他工作在哪一层,
这是跟大家讲那个分层的时候,他每一层工作在哪个产品上面,大家能够理解上他,你也能够从他这个工作在哪个层面,你自己应该也能想明白他原理。
第一部分它是需要有一个以太网的,第二个就是IP,
第三个就是udp的,还有一段用户数据就是data,那这个这个用户数据我们怎么定义?
那个用户数据我们怎么地,因为这里面有几个概念,刚才说我想到用数据,
好,那用数组可以,那是我的长度怎么定,第二个有什么数组好像这个长度不那么低,就想到我们用指针指针可不可以用?指针好像不太对,那我来给大家解释一下。
大家看到这里面我们定一下这个长度我们怎么定?好这个 data,
大家呢有两个情况,我们第一个用数组,数组的长度不太好定义,因为我们不知道用户数据有多少,第二个用指针的话就出现一个现象,我比如用指针去定义,你会发现这4个字节的指针会在哪,,
也就是说这个数据包是在这里截止的,后面这个是没有也是这4个字节,它是1个指针指向另外一块内存的,指针肯定是不合适的,
这里给大家介绍叫做一个柔性数组,也叫做零长数组。
柔性速度用在两个方面,有两种前提它是可以用的,
第一个内存是已经分配好的,
第二个跟这个柔性数组它的长度我们是可以通过其他方法来计算出来的,这两种条件下面我们是可以用柔性数组
我们怎么去抓到udp数据包?
我们就用netmap方案来跟大家讲。
柔性数组仅此它是有一个标签而已,
大家看到就是对于这样一个数据包,
前面这4个字节是以太网头,20个字节是IP头,8个字节是udp的头那这个payload就是1个标签,指向的
是这个udp8个字节头后面的这个位置,就是指向这个位置,至于后面多长,拿着payload这个标签加1,
这里有两个前提就在手里头,第一个内存是先分配好的,
第二个就是我们可以通过一某种计算能够得出这个数组的长度我们才可以用,不然的话很容易会造成这个类型越界。
第一个这里画三个东西,第一个这里是一个网卡,这里是个CPU,CPU上面在这里有一个不等同的概念,就是可以在这里理解为那个内核
被理解为我们所有在CPU上面执行的东西需要通过CPU,这里面,包括内部包括我们的应用程序,包括我们自己写的代码应用程序就在CPU上执行的。
好还有一个板块就是内存将网卡映射到内存中间,内存将网卡映射到内存中间,
现在我们的应用程序是直接在网卡里面取数据的,直接在网卡里面取数据,所以在这里面我们写的第一个,
首先第一个先让neymap先工作起来,那就是直接发出去没错,直接接收数据和发送数据,
有的网卡安装出来之后叫eth0也有的叫ens33,
ens33是虚拟机的网卡,然后eth0是物理网卡
这个网卡接下来它的数据就被接管了,请大家注意这一行之后,这个网卡的所有数据就被映射到内存中间,
用你的虚拟机开始启动两个网卡来做,那例如也就是说我们用SSH连接的是eth0,
但是我们接管的是eth1来理解这个做法就不会去影响,有时候你发现一开始工作的时候,如果你只有一个网卡,工作之后你会发现断网了,发现SSH连不上了,为什么?
就是因为你这个所有的数据都被让他们去接管了
好,现在跟大家再解决一个问题,就是我们怎么知道这个网卡来数据,怎么知道这个网卡里面有数据来了?
好,今天表这里也有一个方法,就这个网卡数据来,它会有个中断,这里有一个东西这是那个 map实现的东西,就是他把这个数据接收完的这个方法,他把它跟我们的io多路复用,也是我们open出来之后。
我们借助一个poll能够去知道,有一个fd能够去知道这里面有数据来了,如果映射完之后tcpdump是抓不到数据。
接下来我们就开始去处理它对应的数据。
这里有一个东西要给大家解释一下,大家看到这个摆完之后写完之后,这个poll就判断它对应有没有数据了,就是对应它就是这里有个fd,
natmap在工作的时候,他把这个网卡比较做到一好的一点,他就直接把这个网卡做成了/dev下的一个文件,就做成一个设备文件,
比如说这个设备文件里面,网卡里面所有的数据,所有数据都会到这设备文件里面,也就是natmap里面,这个fd是监测这个设备文件,
我们判断它里面有数据就有数据,这个fd里面有数据来了,我们就可以去读取它。
这里也有一个情况,
读这个动作大家有没有去想过这个关于read或者write什么叫读?
读这个动作,那一定是从外存读到内存,这从包括我们读文件,
读文件或者说我们去读一个设备,包括我们读数据库这个也是ok的,他从外面读到内存中间,从外层读到内存中间叫读,
内存的操作,我们不叫内存叫操作内存,请大家注意这个读这个动作,它叫做这里面一旦我们检测有数据中,这里面我们去操作,那时候我们不叫做读,叫做nm_nextpkt()
叫做去操作获取下一包,这个怎么理解呢?
这个给大家讲这也是netmap实现的,就是网卡里面过来的数据的时候,网卡里面处理完数据之后,把数据映射到内存中间,这个映射的过程中间,
一个包一个包的映射,映射的这个包叫package,如果你再来一个包再来一包, n个客户端连接的网络,n个客户端给这个网卡发送数据的话,
那就有n个包,那内存中间这是n个包是如何组织起来了?
其他的东西这里用的一个东西叫做循环队列,叫做ring_buffer。
就是来一个包映射过来的时候,把这包加入这个循环队列里面,所以我们在取的时候,只要记住它头在哪个地方,就是取下一包,拿出下一包我们再使用,
就是过来的包这里有一个循环的队列,
好,映射一个包到我们在这里,读的时候这里叫做nm_nextpkt,读出下一个包出来。
这个地方为什么叫做nm_nextpkt,为什么这个地方不叫read。
**就是零拷贝应用在哪些场景?**其实大家可以看到,包括大家你能够想到去做持久化的部分,做日志操作,我们调用的都是像fwrite或者调用write这两个函数,我们在操作日志的时候能够操作文件,那我们如果用mmap这个方法零拷贝就可以使用,我们可以去open一个文件,把它映射到内存中间,然后我们的日志在落盘的时候,我们直接写到这个内存区域中间,然后由他同步过去,不用经过文件,映射到内存就同步过去
知道长度不用担心“/0”
这里还有一个前提也跟大家讲一下就是关于这个
这一步强转可能很多朋友也不太理解,就是我们针对这个数组针对于这一块,stream,你不要简单的把它理解为字符数组,
请你把它理解为单一的一块内存,一块内存的一个开口位置,我们需要对它单独的每一个字节进行操作,所以我们利用的是这个无符号的数组,请注意理解它是指向一块内存,而这个内存多少我们是可以自己去计算的。
有人说这个方法是不是阻塞的,这个nm_nextpkt是不是阻塞的?
这个nm_nextpkt是在内存中间,它是内存中间一个循环的
一个环形队列,每一个快给每个包是在内存中间的,但内存操作的时候,我们去操作内存的时候,就在这个点上面,我们没有所谓的阻塞和非阻塞,就是因为这个数据包早就已经准备就绪了,已经有这个数据了。
现在我们调用的是一个poll,他通知到我们应用程序,现在这里已经有包了,我们现在再来取,再来把那个nm_nextpkt包取出来,请注意它是取,但是我们没有做copy。
这个方法,它不能够阻塞和非阻塞来讲,它是存内存操作,
接着我们看一下定义几个东西,这些东西呢在各方面这不是我要定义的,而是这个文档里面有
我们先把这些数据包给抓起来,
它能够执行而已,我们也可以采用循环啊我们可以采用循环,
对于这个 Udp,这里我们取出的是一个以太网的帧,以太网的帧,这里当然会有一些特殊情况的存在,就比如说你如果发这个包很大的话,
他不一定那个就是有一些数据是在后面的,就是你如果挖这个数据包很大,那这个nm_nextpkt取得这种包,它可能还有一部分在后面,
这个以太网的帧接受这个数据包,也就是说在我们小于1500的数据的时候,你不用担心,
就是我们如果发送数据小于这个1460的时候,我们根本就不用去担心啊它的数据,比如一半到前面,一半到后面,你的担心是多余的,它都不会存在的好吧?
如果超过1640,那就可能它就会有一个分包现象,就有可能我们一个包在下一个包后面。
接着我们跟大家讲这里面还有一个字节对齐问题
这里这个以太网的头是多少?14个字节,然后 IP头现在是多少?20个字节,然后udp的头是多少?8个字节。
但是现在这个 sizeof(udppkt)有多少?
在我们现在这个情况下面等于多少?
等于44号,为什么?
这里会有自己对齐的问题,这前面是4字节,14字节我们一块内存以4个字节
对齐为例,前面4个字节再4个字节,再4个字节,再加上前面2个字节,这里面合到一起14个字节,也就是这一块。
这里面在紧接IP包的时候它是顶齐,从这里开始再分配,这里就留下了两个字节的一个空窗期,这中间会有小窗口,这两个字节里面是没有数据的,所以在 sizeof(udppkt)后
里面是等于44的,不是42,
因为这地方有一个小窗口,为了保证就是我们接收的是一个完整的数据包,,一个packet它中间是没有这两个之间的空空格期的,所以在这里我们要对它加上一个对齐,以一个字节的方式对齐,netmap编译的时候,请代表要加上这个东西,就是
刚开始是可以运行的,是可以接收数据的,
但是你要过一段时间,这不行了,好这是第一个问题。
第二个问题刚刚我们是可以拼这个 IP地址,我们现在拼一下,这个我把它看到了我们再跑一下,刚开始我们跑起来时候我们再比较有没有发现这个点?
就是我们刚刚我们在没启动之前它是可以拼的,但你发现我们现在启动了之后现在是不可以拼的,这是第二个问题。
这里请大家注意第一个问题为什么它不行?
这里要给大家引入一个概念叫做arp,
因为我们现在做的这个协议栈跑的这个东西压根就没有去实现arp的协议,
只是简单的把这个 udp的数据包能够接受而已,那为什么最开始它又可以?
arp的工作是这样的,arp的工作是在局域网内全部进行广播,
比如每一台机器都会对局域网内从
一段一开始1~255中间每一个区某每一台机器都会去广播,
就是我是192.168点多少,你的MAC地址是多少?
紧接着这台机器接收到这个数据包之后接收到这个arp请求之后,
这台机器就会返回我是某某某我的MAC地址是多少,然后收到响应会在本地建立一个叫做**ARP的表,这里面包含一下IP地址是多少,MAC地址是多少,**每一台都有,我们可以看一下,
过一段时间是没了,是因为我们现在这台物理机的这个 arp表里面已经丧失了已经没有了,它已经超时了,这个 IP地址和MAC地址它的这个 arp表的这条信息已经超时了,
好,还有几个问题就是为什么不能这里面也有一个协议,叫做acmp协议。
这里面除了arp和acmp,还有一些其他的协议,比如一些广播上其他的给他发过来,我们这里识别不了你,可是乱码请注意这里面一个数据包都是有自己协议的,
它的表实效是因为现在windows电脑的arp表失效,
他发出的请求还在给他发,但是我现在时间接管网卡的这个应用程序接收完之后没有给他回响应,来理解这个打印
就在这地方挺重要,这一层是说的网络层的协议,IP层如果我们在这个 Ip的这个协议地方,我们引入一个else if这个包等于arp的协议,好我们可以对它进行arp的处理,这是这第一个情况。
第二个情况,如果在这里面判断它是不是acmp协议
**arp是跟IP是一层的,icmp是跟udp一层的,它是传输层的,**也就是大家可以看到这里面所谓的网络分层,其实对于我们来说是先发的那一层协议,是一个谁先谁后的问题。
ICMP它是传输层的协议,就是在这个定义的时候是在IP头里面定义的,是在IP头里面。
udp有这么几个特点,
第一个它的实时性比较强,
第二个就是他不带拥塞控制。
传输速度要比TCP快。
是在udp的基础上面,我们是需要封装一层应用协议的,如果不封装应用协议的话,那 udp它是没办法用的对吧?那封装的观点它也是个一对一的传承也是一个一对一的传输好吧?
udp协议它是用于哪一些场景?
nginx
#include
#include
#include
#include
#include
#include
#define NETMAP_WITH_LIBS
#include
#pragma pack(1)
#define ETH_ALEN 6
#define PROTO_IP 0x0800
#define PROTO_ARP 0x0806
#define PROTO_UDP 17
#define PROTO_ICMP 1
#define PROTO_IGMP 2
struct ethhdr {
unsigned char h_dest[ETH_ALEN];
unsigned char h_source[ETH_ALEN];
unsigned short h_proto;
};
struct iphdr {
unsigned char version;
unsigned char tos;
unsigned short tot_len;
unsigned short id;
unsigned short flag_off;
unsigned char ttl;
unsigned char protocol;
unsigned short check;
unsigned int saddr;
unsigned int daddr;
};
struct udphdr {
unsigned short source;
unsigned short dest;
unsigned short len;
unsigned short check;
};
struct udppkt {
struct ethhdr eh;
struct iphdr ip;
struct udphdr udp;
unsigned char body[128];
};
struct arphdr {
unsigned short h_type;
unsigned short h_proto;
unsigned char h_addrlen;
unsigned char protolen;
unsigned short oper;
unsigned char smac[ETH_ALEN];
unsigned int sip;
unsigned char dmac[ETH_ALEN];
unsigned int dip;
};
struct arppkt {
struct ethhdr eh;
struct arphdr arp;
};
struct icmphdr {
unsigned char type;
unsigned char code;
unsigned short check;
unsigned short identifier;
unsigned short seq;
unsigned char data[32];
};
struct icmppkt {
struct ethhdr eh;
struct iphdr ip;
struct icmphdr icmp;
};
void print_mac(unsigned char *mac) {
int i = 0;
for (i = 0;i < ETH_ALEN-1;i ++) {
printf("%02x:", mac[i]);
}
printf("%02x", mac[i]);
}
void print_ip(unsigned char *ip) {
int i = 0;
for (i = 0;i < 3;i ++) {
printf("%d.", ip[i]);
}
printf("%d", ip[i]);
}
void print_arp(struct arppkt *arp) {
print_mac(arp->eh.h_dest);
printf(" ");
print_mac(arp->eh.h_source);
printf(" ");
printf("0x%04x ", ntohs(arp->eh.h_proto));
printf(" ");
}
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 *hmac) {
memcpy(arp_rt, arp, sizeof(struct arppkt));
memcpy(arp_rt->eh.h_dest, arp->eh.h_source, ETH_ALEN);
str2mac(arp_rt->eh.h_source, hmac);
arp_rt->eh.h_proto = arp->eh.h_proto;
arp_rt->arp.h_addrlen = 6;
arp_rt->arp.protolen = 4;
arp_rt->arp.oper = htons(2);
str2mac(arp_rt->arp.smac, hmac);
arp_rt->arp.sip = arp->arp.dip;
memcpy(arp_rt->arp.dmac, arp->arp.smac, ETH_ALEN);
arp_rt->arp.dip = arp->arp.sip;
}
void echo_udp_pkt(struct udppkt *udp, struct udppkt *udp_rt) {
memcpy(udp_rt, udp, sizeof(struct udppkt));
memcpy(udp_rt->eh.h_dest, udp->eh.h_source, ETH_ALEN);
memcpy(udp_rt->eh.h_source, udp->eh.h_dest, ETH_ALEN);
udp_rt->ip.saddr = udp->ip.daddr;
udp_rt->ip.daddr = udp->ip.saddr;
udp_rt->udp.source = udp->udp.dest;
udp_rt->udp.dest = udp->udp.source;
}
unsigned short in_cksum(unsigned short *addr, int len)
{
register int nleft = len;
register unsigned short *w = addr;
register int sum = 0;
unsigned short answer = 0;
while (nleft > 1) {
sum += *w++;
nleft -= 2;
}
if (nleft == 1) {
*(u_char *)(&answer) = *(u_char *)w ;
sum += answer;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
answer = ~sum;
return (answer);
}
void echo_icmp_pkt(struct icmppkt *icmp, struct icmppkt *icmp_rt) {
memcpy(icmp_rt, icmp, sizeof(struct icmppkt));
icmp_rt->icmp.type = 0x0; //
icmp_rt->icmp.code = 0x0; //
icmp_rt->icmp.check = 0x0;
icmp_rt->ip.saddr = icmp->ip.daddr;
icmp_rt->ip.daddr = icmp->ip.saddr;
memcpy(icmp_rt->eh.h_dest, icmp->eh.h_source, ETH_ALEN);
memcpy(icmp_rt->eh.h_source, icmp->eh.h_dest, ETH_ALEN);
icmp_rt->icmp.check = in_cksum((unsigned short*)&icmp_rt->icmp, sizeof(struct icmphdr));
}
int main() {
struct ethhdr *eh;
struct pollfd pfd = {0};
struct nm_pkthdr h;
unsigned char *stream = NULL;
struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
if (nmr == NULL) {
return -1;
}
pfd.fd = nmr->fd;
pfd.events = POLLIN;
while (1) {
int ret = poll(&pfd, 1, -1);
if (ret < 0) continue;
if (pfd.revents & POLLIN) {
stream = nm_nextpkt(nmr, &h);
eh = (struct ethhdr*)stream;
if (ntohs(eh->h_proto) == PROTO_IP) {
struct udppkt *udp = (struct udppkt*)stream;
if (udp->ip.protocol == PROTO_UDP) {
struct in_addr addr;
addr.s_addr = udp->ip.saddr;
int udp_length = ntohs(udp->udp.len);
printf("%s:%d:length:%d, ip_len:%d --> ", inet_ntoa(addr), udp->udp.source,
udp_length, ntohs(udp->ip.tot_len));
udp->body[udp_length-8] = '\0';
printf("udp --> %s\n", udp->body);
#if 1
struct udppkt udp_rt;
echo_udp_pkt(udp, &udp_rt);
nm_inject(nmr, &udp_rt, sizeof(struct udppkt));
#endif
#if 0
} else if (udp->ip.protocol == PROTO_ICMP) {
struct icmppkt *icmp = (struct icmppkt*)stream;
printf("icmp ---------- --> %d, %x\n", icmp->icmp.type, icmp->icmp.check);
if (icmp->icmp.type == 0x08) {
struct icmppkt icmp_rt = {0};
echo_icmp_pkt(icmp, &icmp_rt);
//printf("icmp check %x\n", icmp_rt.icmp.check);
nm_inject(nmr, &icmp_rt, sizeof(struct icmppkt));
}
#endif
} else if (udp->ip.protocol == PROTO_IGMP) {
} else {
printf("other ip packet");
}
#if 0
} 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.2.217")) {
echo_arp_pkt(arp, &arp_rt, "00:50:56:33:1c:ca");
nm_inject(nmr, &arp_rt, sizeof(struct arppkt));
}
#endif
}
}
}
}