【Linux网络编程】数据链路层 高级IO模型
文章目录
- 一、数据链路层:
- 1、数据链路层报文解析:
- 2、局域网通信:
- 3、MSS与MTU:
- 4、ARP协议:
- 工作原理:
- ARP报头解析:
- 模拟一次ARP协议:
- 5、其他重要协议或技术
- DNS:
- 域名:
- ICMP:
- NATPT:
- 代理服务器:
- 二、高级IO:
- 1、重新理解IO:
- 2、五种IO模型:
- 阻塞式:
- 非阻塞轮询:
- 信号驱动IO:
- 多路转接:
- 异步IO:
- 3、非阻塞代码理解:
一、数据链路层:
IP层解决在公网中的不同主机之间的数据交付问题,本次所讲的数据链路层所解决的就是在同一个局域网中,直接相连的主机(不仅仅是电脑和电脑直接,也可是电脑和路由器之间)之间的,进行数据交付的问题
Mac地址:区分在同一个局域网中,区分特定主机的地址
Mac帧:带着Mac地址的数据包
1、数据链路层报文解析:
对于一个协议,那么理解这个协议需要理解两点重要的
- 如何进行解包
- 如何进行分用
如何解包:
定长报文,当数据链路层得到一个报文后,直接读取前面的字节数量就能够直接得到报头了,然后就能够直接进行报文和有效载荷的分离了
如何分用:
当读到报头后,在报头中有一个字段叫做类型,通过这个类型进行分用,判断是何种,比如说:0800就是IP数据包,0806就是ARP请求/应答,8035就是RARP请求/应答
2、局域网通信:
每台主机在局域网上都要有自己唯一的一个标识
当老师在讲台上进行讲话的时候,对下面的人说:张三,你怎么没交作业,此时是所有人都会听到这个信息,拿到这个报文,然后进行报头的比对发现说的不是我,就将整个报文的丢弃,但是张三进行报文解析的时候发现讲的就是我,那么就会做出反应,就会对老师说:我昨天给你了啊,那么之后老师就会给予回应而不是别人,但是别人会听到张三的回答,但是并不会对其进行反应,因为张三是对老师进行回答的
如上这种向当前局域网所有主机发送消息,但是会进行比对主机这种局域网通信就是以太网通信,所以说:局域网通信的过程,其实是有很多吃瓜群众的
数据碰撞
如果在一个局域网中有多个主机在进行发送数据,那么就会在这个局域网中进行数据碰撞,这样其中的光电信号,波信号就会混乱了,就会导致接收的数据出现问题
但是一个主机怎么知道什么时候会发生数据碰撞呢?当在一个教室里面进行讲话,我自己也会听到我自己的声音的,所以一个主机将数据发出了后也会收到自己的数据的,那么就能够知道当前局域网有没有发生数据碰撞
在系统中有发送主机都要执行的碰撞避免的算法,这个算法是由以太网的驱动程序自己定的,当发生了数据碰撞后,就延迟发送,错峰发送
正确看待局域网:
局域网内的数据是能够直接通信的
任何主机都可以向局域网里发消息,但任何一时刻只允许一台主机向一个局域网中发送消息,所以局域网是所有主机的共享资源,所以要保证所有主机在使用这个共享资源的互斥访问,这个操作并不是通过加锁完成的,是一旦发生错误,就重新发送资源,相当于这个主机在发送资源的时候没有其他主机使用这个网络资源,出问题再说,所以局域网可以看做多台主机共享的临界资源
交换机:
交换机是一种用于连接多台设备,并让它们在局域网内高速交换数据的网络硬件,交换机将局域网一分为二,然后当左侧发生碰撞后,交换机能够检测到发生了碰撞,此时就拦截左边的数据继续往右传播,所以就不会影响右边了,此时右边还是能够进行数据的转发的,所以交换机能够:划分碰撞域,减少局域网碰撞
3、MSS与MTU:
-
MTU(最大传输单元)
层级:工作在数据链路层,针对的是数据帧
定义:指数据链路层帧结构中,数据部分(即封装的网络层数据包)的最大字节数,不包含链路层自身的头部(如以太网头部 14 字节、CRC 校验 4 字节)
以太网:默认 MTU=1500 字节 -
MSS(最大分段大小)
层级:工作在传输层(仅 TCP 协议使用,UDP 不涉及),针对的是TCP 分段
定义:指 TCP 分段中,应用数据部分的最大字节数,不包含TCP头部(通常 20 字节,若带选项则更多)和IP头部(通常 20 字节,若带选项则更多)
核心作用:TCP 通过MSS限制单个分段的应用数据大小,确保封装后的IP数据包不超过底层链路的MTU(避免 IP 分片,减少网络开销)
计算示例:
假设 IP 头部和 TCP 头部均为最小长度(无选项,各20字节),则:MSS = MTU - IP头部长度(默认20)- TCP头部长度(默认20),以最常见的以太网 MTU=1500 为例:MSS = 1500 - 20 - 20 = 1460字节这也是互联网中TCP连接的默认MSS值
4、ARP协议:
在之前的学习中,我们了解到了:所谓的数据发送到网络,本质上是通过无数个子网实现的,在发送报文中,通过子网进行跳跃到路由器中,然后路由器在网络层根据ipdst判断是不是在当前局域网的,如果不是就重新封包(此时ipsrc被更改为当前路由器的ip)然后继续路由,直到交到路由器的目标主机所在的同一个局域网
当目标主机所在的局域网中的路由器拿到报文后,在网络层发现目标主机是和我在同一个局域网的,那么路由器重新封装Mac帧,然后在局域网中进行广播,接着就是局域网的通信原理了
但是此时有个问题:在填目的地址的时候,路由器怎么知道目标主机的Mac地址呢?——此时就轮到arp协议登场了
ARP:在 TCP/IP 协议中,数据包的传输需要知道目标主机的 MAC 地址,而 IP 地址只是逻辑地址,不能直接用于数据包的传输。ARP 协议在网络层和数据链路层之间起着桥梁作用,确保数据包能够正确地传输到目标主机
工作原理:
当主机A要向主机B发送数据时,会先查看本地的ARP缓存表,看是否有主机B的IP地址对应的MAC地址。如果有,就直接使用该MAC地址进行数据封装和发送;如果没有,主机 A 会发送一个ARP请求广播包,该报文中包含源主机A的MAC地址和IP地址,以及目标主机B的IP地址,目的MAC地址为全F为广播,局域网上的所有主机都会收到该ARP请求,只有主机B会匹配自己的IP地址,然后将自己的MAC地址封装在ARP响应报文中,以单播方式发送给主机A。主机A收到ARP响应后,会将主机B的IP地址和MAC地址添加到自己的ARR缓存表中,以便后续通信
ARP报头解析:
ARP协议是属于MAC帧上层的,但是是归属到数据链路层的
报头解析:
- 硬件类型指链路层网络类型,1为以太网
- 协议类型指要转换的地址类型,0x0800为IP地址
- 硬件地址长度对于以太网地址为6字节
- 协议地址长度对于和IP地址为4字节
- op字段为1表示ARP请求,op字段为2表示ARP应答
- 发送端和目的就是我们的路由器和目标主机
模拟一次ARP协议:
首先,当路由器R接收到了别的路由器跳转来的报文,然后进行解析,此时比对报文中的目的IP地址,发现目的IP地址的网络号是当前子网,那么就证明要找的主机B就在当前子网,此时在本地的ARP缓存表进行查看,发现没有,那么就会进行一次广播:
前三个是按照数据链路层的报文来填写,当后面的数据填完后再添加前面三个Mac帧
目的地址不知道填全F表示广播
原地址填写当前路由器的Mac地址
帧类型填写0806表示ARP请求
硬件类型填1,表示以太网
协议类型是IP地址转化为MAC地址,所以填0800
硬件长度就是6,协议地址长度就是4
然后OP字段是1
发送端的以太网地址是谁呢? 就是MacR
发送端的IP地址就是IPR
目的以太网地址不知道,填写广播为全F
目的IP就是IPB
填写完后就在局域网中进行广播,此时每台主机都会收到当前报文,在数据链路层中收到后,查看帧类型,发现是0806为ARP报文后向上交付给ARP层,接着查看OP字段,发现是1是请求,此时在看IP地址,发现如果不是当前IP,就将报文丢弃,如果是当前IP,如这里的主机B,就进行应答:(此时就会将自己的Mac地址返回)
之后就将这个报文定向发送给路由器,这样当路由器拿到当前报文后,就能够拿到MacB的Mac地址了
在ARP的过程中,收到的任何ARP报文,都是先看OP,OP决定了ARP的类型:请求或者应答,如果是请求,我们看的是目的MAC地址和目的IP;如果是应答,我们看的是发送端的MAC地址和发送端的IP,看看这两个是否相等
主机的mac地址和ip地址,会被主机临时缓存起来
- ARP只有在缓存失效的时候,才会进行
- 我可以通过我的IP和子网掩码,得到我的网络号,然后拼接IP地址,ping所有的主机,得到所有主机的IP和mac
- 如果我收到多次同样的arp应等我会以最新的为准
5、其他重要协议或技术
DNS:
这是域名解析服务:我们访问网站本质上是通过一串数字访问的,但是这样不方便访问,那么就有了域名,自然也就有了域名解析服务,这就叫做DNS
域名:
这里以www.baidu.com为例:
.com:一级域名,表示一个企业域名,同级的还有“net”(网络提供商),“org”(非盈利组织)等
baidu:二级域名一般都叫做公司名
www:习惯用法,可以不写
有个经典面试题:
当在浏览器中输入一个URL后按回车会发生什么
- 输入 URL ——>浏览器解析 URL 并校验;
- DNS 解析 ——>域名转 IP 地址;
- TCP 三次握手 ——>建立可靠连接;
- 进行协商加密
- 发送 HTTP 请求 ——>浏览器向服务器要资源;
- 服务器处理 ——>返回 HTTP 响应(如 HTML);
- 浏览器渲染 ——>解析 HTML/CSS/JS,生成可视化页面;
- 网络四层的作用
以上是粗略地进行讲解,还需要一个步骤一个步骤更细分
ICMP:
这是网络层的协议:主要是用来确认报文是否丢失,是在IP协议之上的,当存在ICMP协议的时候,如果主机A发送数据给主机B,但是数据不可达,那么路由器就会返回给主机A一条信息,告诉我们信息不可达
作用:
确认IP包是否成功到达目标地址
通知在发送过程中IP包被丢弃的原因
ICMP也是基于IP协议工作的.但是它并不是传输层的功能,因此人们仍然把它归结为网络层协议
ICMP只能搭配IPv4使用,如果是IPv6的情况下,需要是用ICMPv6
其中,网络命令ping就是基于ICMP的,不光能验证网络的连通性,同时也会统计响应时间和TTL
这里也有一个面试题:
telnet对应的端口号是23,ssh 对应的端口号是22,那么ping对应的端口号是多少呢?
ping 命令基于ICMP,是在网络层,而端口号是传输层的内容,在 ICMP 中根本就不关注端口号这样的信息,所以 ping 根本没有端口号,ping 命令实际是绕过了传输层的直接访问底层 ICMP 协议的一种做法
NATPT:
在理解这个之前首先要知道什么是NAT技术,这部分在上一章IP协议中有讲到IP协议
但是还有一点未讲到:
当公网返回私网报文的时候,此时肯定会有很多报文,这些响应报文的目的IP都是当前路由器的WAN口IP,我们怎么将这些报文和那些主机对应起来呢?
在路由器中有一张表,叫做NAT转换表:当有报文转发出去的时候,会将内网中的源主机的IP和端口,目标主机的IP和端口和路由器的WAN口IP和端口,目标主机的IP和端口进行映射形成KV式的哈希表
因为IP和端口号的唯一性,所以这个映射是唯一的,并且即使主机B也发送报文,那么映射后的WAN口IP的端口号一定是不一样的,这样就能够保证映射的唯一性
NAT在替换的时候,不仅仅会替换IP,也会替换端口号,这样就保证了映射表中的键值对是双向的,即映射双方互为键值,所以,当数据回来的时候,我们的数据就知道是给哪个目的IP,哪个目的端口号,去查键值对,就能锁定内网当中的目标主机了
代理服务器:
代理服务器的功能就是代理网络用户去取得网络信息,代理服务器又分为正向代理和反向代理
正向代理:
客户端并不直接访问目标服务器,而是先访问代理服务器,由代理服务器代替客户端去访问对应的目标服务器,并将目标服务器的响应结果返回给客户端
当多台主机都要访问外网的同一个资源,那么正向代理服务器就可以将对应的资源缓存到本地,此时当其他主机要访问该资源时,直接在正向代理服务器就可以获取,而不需要再次进行外网访问
反向代理:
当反向代理服务器收到客户端的数据请求后,就会将我们的数据请求转发给其所代理的某台服务器进行数据处理,然后再将数据处理的结果返回给客户端,它不做任何业务的处理,只负责将请求推送到后端的指定主机,用户不需要知道目标服务器的地址,用户只需要访问反向代理服务器就可以获得目标服务器提供的服务
正反向代理服务器的区别
正向代理是客户端的代理,帮助客户端访问其无法访问的服务器资源的,而反向代理则是服务器的代理,帮助服务器做负载均衡、安全防护等工作的
正向代理中,服务器不知道真正的客户端到底是谁,服务器认为正向代理服务器就是真实的客户端,而反向代理中,客户端不知道真正的服务器是谁,客户端认为反向代理服务器就是真实的服务器
NAT和代理服务器的区别
NAT 和代理服务器都是代替我们向服务器发起数据请求的:
- 从应用上讲,NAT 设备是网络基础设备之一(必须的),解决的是 IP 不足的问题。代理服务器则是更贴近具体应用,比如通过代理服务器进行翻墙,另外像迅游这样的加速器,也是使用代理服务器
- 从底层实现上讲,NAT 工作在网络层,直接对 IP 地址进行替换,而代理服务器往往工作在应用层
- 从使用范围上讲,NAT 一般在局域网的出口部署,代理服务器可以在局域网做,也可以在广域网做,也可以跨网
- 从部署位置上看,NAT 一般集成在防火墙,路由器等硬件设备上,代理服务器则是一个软件程序,需要部署在服务器上
二、高级IO:
1、重新理解IO:
IO也就是Input和Output
这里通过read和write来理解:
当调用read的时候,如果接收缓冲区中没有数据,那么read就会进行阻塞
当调用write的时候,如果发送缓冲器写满了,那么write也会阻塞
当这些读和发送这种IO的函数不是直接将数据发送到网络上的,而是拷贝到发送或者接收缓冲区,在由TCP自主发送,这些本质是拷贝函数,但是这些拷贝函数大部分时间都在等,所以IO本质是 等+拷贝,为什么要等呢?对于read,需要等待接收缓冲区中有数据,对于write,需要等待发送缓冲区中有空位,所以在拷贝之前需要条件成立
对于高效IO:单位时间内,IO中等的比例越小,IO的效率就越高,所以几乎所有提高IO效率的本质都是让等的比重减小
2、五种IO模型:
哪五种:
- 阻塞式等待
- 非阻塞式等待,非阻塞轮询查看是否读写事件就绪
- 信号驱动,就是当读写事件就绪的时候通知我们
- 多路转接,多路复用
- 异步IO
这里通过一个故事来理解:
- 张三喜欢钓鱼,张三钓鱼的时候,抛竿后一直看着鱼竿进行等待,直到鱼咬钩后提鱼竿,这就是阻塞式
- 李四钓鱼的时候,抛竿后就在一边搞其他事,每过一定时间后就看鱼竿有没有鱼咬钩,这就是非阻塞式
- 王五钓鱼的时候,在鱼竿上加上一个铃铛,然后进行抛竿,之后不用每过一定时间去看,而是直到铃铛响后就直接钓起来,这就是信号驱动
- 赵六钓鱼的时候,搞多个鱼竿,然后将鱼竿都抛进海里,然后在岸上找了个视野开阔的位置坐着,眼睛同时盯着这几根鱼竿的动静。他既不用像张三那样紧盯着一根竿子啥也干不了,也不用像李四那样频繁起身挨个检查,而是就坐在原地,一旦看到某一根鱼竿的鱼线往下沉、浮漂有动静 —— 也就是这根鱼竿 “有鱼咬钩” 的信号就绪了,就立刻起身过去提这根竿子;等处理完这根竿子的情况,无论是钓上鱼还是重新抛竿,他又坐回原位,继续同时盯着所有鱼竿,等待下一根有动静的竿子,这就是多路转接
- 田七钓鱼的时候,叫了一个小王,给了小王桶,电话,钓竿,然后跟小王说:你钓鱼,有了之后直接给我送过来,其中田七是钓鱼的发起者,并没有进行参与,只需要向 “执行者”(小王,对应操作系统内核或异步 IO 框架)提交 “钓鱼任务”(IO 请求),并告知结果交付方式(对应回调函数或通知机制),之后就能去处理其他事务,完全不用等待钓鱼过程,这就是异步IO
阻塞式:
当recvfrom读取数据的时候,如果数据没有准备好,那么就会阻塞在系统中,本质是OS把PCB从运行队列中拿下来,当有数据了之后,把进程唤醒,在进行内核到用户的数据拷贝
非阻塞轮询:
recvfrom可以设置成非阻塞的,此时每过一定时间都会去问有没有数据,不会一直阻塞等待,而是返回EWOULDBLOCK,当检测成功的时候,就和阻塞式一样了进行内核到用户的数据拷贝
信号驱动IO:
内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作
多路转接:
之前有讲过IO = 等+拷贝,在这里由select系统调用进行等,等待就绪后通知recvfrom,recvfrom就只需要进行读即可,这里最大的特点就是他们会一次性等待多个文件描述符
异步IO:
调用接口后直接返回,应用层做自己的事 OS等待数据,当OS等待成功后帮我把数据从内核拷贝到用户,之后再进行数据处理
3、非阻塞代码理解:
接下来实现一下网络套接字版本的非阻塞IO:
fd:要操作的文件描述符(必须是已打开的有效文件描述符,如通过 open 或 socket 获得)
cmd:操作命令(核心参数,决定 fcntl 执行的具体操作)
对于cmd:
F_GETFL:获取当前文件状态标志
返回值包含文件的访问模式(O_RDONLY、O_WRONLY、O_RDWR)和其他标志(如 O_NONBLOCK、O_APPEND 等)
注意:访问模式(O_RDONLY 等)需通过 O_ACCMODE 掩码提取(直接判断可能不准确)
F_SETFL:设置文件状态标志
可修改的标志:O_NONBLOCK(非阻塞模式)、O_APPEND(追加模式)、O_ASYNC(异步 I/O 通知)等
不可修改的标志:访问模式(O_RDONLY 等)、O_CREAT 等(需通过 open 重新打开文件修改)
F_GETLK:检查是否存在与目标锁冲突的锁
第三个参数:struct flock *lock(传入要检查的锁信息,返回冲突锁的信息)
F_SETLK:设置锁(非阻塞)
若无法获取锁(如被其他进程占用),直接返回 -1 并设置 errno = EAGAIN 或 EACCES
F_SETLKW:设置锁(阻塞)
若无法获取锁,会阻塞等待直到锁可用(或被信号中断)
F_DUPFD:
复制一个现有的描述符
如下是非阻塞轮询的代码示例:
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include<cstring>using namespace std;void SetNoBlock(int fd)
{int fl = fcntl(fd,F_GETFL);if(fl < 0){perror("fcntl");exit(1);}fcntl(fd,F_SETFL,fl | O_NONBLOCK);
}int main()
{SetNoBlock(0);char buffer[1024];while(true){cout << "Please Enter# ";fflush(stdout); ssize_t n = read(0,buffer,sizeof(buffer)-1);if(n > 0){// 把读上来的数据当做字符串buffer[n] = 0;cout << "读上来的数据:" << buffer << endl;}else if(n == 0){cout << "读取结束" << endl;}else{if (errno == EWOULDBLOCK){cout << "还没有数据,重新尝试" << endl;sleep(1);}else{cout << "读取失败 n = " << n << endl;cout << "错误码: " << errno << " " << "错误描述: " << strerror(errno) << endl;}}}return 0;
}