目录
多路复用,引入了对多个文件的同时等待,那么对这多个文件的管理,就又可以引入我们的数据结构啦
1.NAT技术
NAT技术背景
之前我们讨论了, IPv4协议中, IP地址数量不充足的问题
NAT技术当前解决IP地址不够用的主要手段, 是路由器的一个重要功能;
- NAT能够将私有IP对外通信时转为全局IP. 也就是就是一种将私有IP和全局IP相互转化的技术方法:
- 很多学校, 家庭, 公司内部采用每个终端设置私有IP, 而在路由器或必要的服务器上设置全局IP;
- 全局IP要求唯一, 但是私有IP不需要; 在不同的局域网中出现相同的私有IP是完全不影响的;
NAT IP转换过程
之前说过,当进行数据跨网络传输时,构建一个报文向下交付,到网络层后要经过路由选择,发现要去目标主机和我并不再同一个子网,所以向下交付封装MAC帧交付给出口路由器 ,家用路由器的IP地址肯定知道因为你连接过路由器,如果不知道MAC地址就做ARP。
- 然后出口路由器经过同样的策略交给下一跳路由器直到交给目标主机。
- 在整个交付过程中,路由器不仅仅是把报文交给下一跳。
- 更重要的在交付的时候把源IP地址不断的做替换。
- 每一个路由器不仅有LAN口IP,还要WAN口IP,当做数据包转发时,是把源IP替换成路由器的WAN口IP。
一路替换然后数据包经过目的主机处理然后返回接下来怎么走呢?
下面我们具体说一下整个过程。
- 客户端A、B、C都有自己的私有IP,每台主机我们要透过现象看本质,每台机器都有TCP/IP协议栈。
- 客户端A将数据经过路由器发送给服务器。
- 其中路由器本质天然的就包含了NAT地址转化的功能,所以在数据在刚开始转发时源IP地址是它自己私有IP,然后经过路由选择发现要去路由器,到路由器之后就把当前IP报头中源地址替换成路由器自己的WAN口IP,然后让路由替这个主机发起网络请求,最终经过内网和公网转发这个数据到了服务器。
- 服务器处理好了根据得到的源IP构建响应返回给NAT路由器,那此时NAT路由器中报文中目的IP目前是202.244.174.37,接下来问题是怎么从这个路由器回到主机。
有人可能说转化时建立一个映射关系,然后回到NAT路由器在把目的IP转化一次变成源IP地址不就可以了吗,但如果是一个局域网主机都访问同于一个服务器呢?
NAPT
那么问题来了, 如果局域网内, 有多个主机都访问同一个外网服务器, 那么对于服务器返回的数据中, 目的IP都是相同的. 那么NAT路由器如何判定将这个数据包转发给哪个局域网的主机?
- 这时候NAPT来解决这个问题了. 使用IP+port来建立这个关联关系
- (还是 我们之前提到的,一层区分不了,那就再加一层😋
- 当数据包回到NAT路由器的时候,目前并不能确认这个报文应该转给主机A、B、C当中哪一个。
- 但我们要求它必须分清楚,是谁的数据就给谁返回,就决定了当前路由器就必须自己维护从外网到内网之间数据包和主机之间的对应关系。
- 学了这么久实际上我们知道主机A、B、C三台主机IP地址是不一样的,所以对应每台主机都会有特定的标识符来告诉NAT路由器我们各自是谁。
- 可是有一个细节我们从开始到现在一直没说,实际上通信的并不全是客户端A、B、C这些主机,其实本质是户端A、B、C上特定的进程和服务器上某个特定的进程在通信,而进程都是自己的端口号的。并且同时可能会存在一台主机上有不同进程可能都要去访问外网。
所以NAT路由器除了要区分客户端A、B、C之间差别,也要有能力区分同一台主机不同的进程!
- 说白了就是NAT路由器不仅仅要区分客户端A、B、C,然后把响应报文要转给不同的主机。
- 还有同一台主机不同进程发给其他服务器,服务器处理之后把响应报文都发到NAT路由器,NAT路由器要把响应给同一台主机的不同进程,所以还需要端口号来区分同一台主机不同进程!
所以数据包在经过NAT路由器进行转发出去的时候,不仅要考虑源IP地址替换的问题,源端口号也要被替换。
因此路由器还要在自身内部维护一张NAT转换表! 其实这张转换表是KV式的转换表。
(于是 又回到了我们的算法问题上面
- 公网IP肯定是不一样的,局域网内不同主机IP地址也是不一样的。
- 可能是同一台主机不同进程发送请求,也有可能是不同主机但是端口号相同的进行发送请求
但是因为有IP和端口号的存在这个组成四元组的请求在局域网内一定是唯一四元组。
(☺️理解:虽然客户端发的可能是去 同一个主机+port,但因为是不同客户端发的,我们之后可能还涉及到了一个应答的问题,所以在 nat 转发的时候,替换为相同的 转发的源 ip 地址时,port 端口要设置的不同
- 把请求报文经过NAT路由器源IP地址的转换,源port也可能转换(可能一台主机不同进程都访问外网)。
- 替换之后这个四元组请求在公网内也是唯一的。
所以NAT路由表做了源IP替换和可能源port的替换之后变成了新的源IP和或者新的源port,因为一个是私网IP一个是公网IP,这两种IP一定是不一样的,也就是说替换完了之后两个四元组的源IP一定是不一样的 - 就注定了这两个唯一的四元组合起来整体一定是不一样的并且是唯一的。
所以在NAT路由器中就构建了互为键值的映射表 并且都是唯一的。
也就是说从左到右,左侧的值是具有唯一性的,从右到左,右侧的值也是具有唯一性的。
所以数据包回来到NAT路由器后,虽然NAT路由器看到目的IP都是一样的,但是曾经端口号可能做了替换,因此看到的整体一定是不一样的。
- 然后再根据映射,所以最终就可以区分出那个数据包是那个主机的进程的。
- 所以不用担心数据包回来的问题,因为路上的路由器都会建立NAT转换表。这个NAT转换表可以从左到右查,也可以从右到左查。(有不同的端口,可以实现来区分返回)
- 经过刚才的分析,如果主机上从来没有访问外网,那外网能不能之间通过路由器访问这台主机呢?
不能!因为路上的路由器并没有建立对应的映射关系。 - 所以也不能把数据反向的从外网推送给该主机。除非该主机访问外网然后路上路由器记录对应的映射关系,外网才能访问该主机。
这种互为映射的关联关系也是由NAT路由器自动维护的.
- 例如在TCP的情况下, 建立连接时, 就会生成这个表项;
- 在断开连接后, 就会删除这个表项
所以有NAT这种技术的存在可以让我们构建各自各样的子网,然后通过转发访问公网 - 只要保证最终的出口路由器(连接公网的路由器)它所面对公网的IP地址是唯一的就可以了,内网环境可以使用NAT进行各自转发。
NAT技术的缺陷
由于NAT依赖这个转换表, 所以有诸多限制:
- 无法从NAT外部向内部服务器建立连接; (NAT 只能从内向外)
- 转换表的生成和销毁都需要额外开销;
- 通信过程中一旦NAT设备异常, 即使存在热备, 所有的TCP连接也都会断开;(这是一个很危险的事情)
2.NAT和代理服务器
代理服务器又分为正向代理和反向代理.
举个例子
花王尿不湿是一个很经典的尿不湿品牌, 产自日本.
我自己去日本买尿不湿比较不方便, 但是可以让我在日本工作的表姐去超市买了快递给我. 此时超市看到的买家是我表姐, 我的表姐就是 “反向代理”;(代表的是日本超市)
就好比我自己在访问时看不到真正服务器,我只知道要什么东西就找这个代理服务器要。这就是反向代理。
后来找我表姐买尿不湿的人太多了, 我表姐觉得天天去超市太麻烦, 干脆去超市买了一大批尿不湿屯在国内家里, 如果有人来找她代购, 就直接把屯在国内家里的货发出去, 而不必再去超市. 此时我表姐就是 "正向代理(代表的是客户)
代理客户端的是 正向代理
代理服务端的是 反向代理
(相当于 这个正 是以客户为正)
一般我们在公司层面,现在有很多机器,这些机器通过网络访问公司内部的服务器。
- 公司内部一定是有多种服务器的,可是有这么多服务器都暴露在公网里面, 每一台服务器也都得有自己公网IP的话,那么最终我们应该访问那一台服务器呢?其实是比较尴尬的。
- 所以提供服务的一方呢,可能会给我们提供一个入口服务器。这个入口服务器一般会部署某些网络服务,现在就变成了只有这一台入口服务器有自己的公网IP,暴露在公网里,所有人都访问这台入口路由器,那么这一台机器最终就可以把所有人请求转发到它内网中某台服务器来对外提供服务。
我们就把这台入口机器称之为代理服务器严格来说称之为反向代理服务器。
- 假设现在有4千万个客户端,有大量的请求都过来的,反向代理服务器收到大量的请求,如果它内部没有做任何处理,把请求全都给一台机器或者少量几台机器
- 最终会导致这几台机器压力非常大,其他机器很闲,最终导致这个集群承受压力能力变得非常低。
- 所以反向代理为了解决这个问题,在自己内部中当有请求到来它内部有策略的把请求均衡的分散到整个集群所有主机上,这种策略我们称之为负载均衡
一般作为反向代理服务器配置都比较高,上面通常充当反向代理服务的一般有一些软件服务,比如说Nginx,它是一款web服务器也可以充当代理服务器。- 一般把结果返回给客户端有两种做法,一种是把对应的处理结果返回给代理,然后由代理通过网络转发给客户端。
另一种把对应的处理结果由内网机器直接通过网络访问客户端。
反向代理通常作为机房入口机器,来实现分发和负载均衡
反向代理服务器在学校网络环境中的应用
- 分发和负载均衡:反向代理通常作为机房或网络入口,用于实现流量的分发和负载均衡,优化资源使用并提高访问效率。
- 访问控制与缓存:当学生尝试通过校园网访问外网时,实际上请求首先发送到学校的反向代理服务器。学校可以通过这道“门”限制学生的访问,例如阻止访问某些不被允许的内容。此外,如果多个用户请求相同的内容(如观看同一部电影),服务器可以缓存这些内容,并在后续请求中直接提供缓存版本,减少重复的公网访问,提高响应速度。
- 禁止访问:对于不允许访问的公网资源,学校可以直接在反向代理服务器上丢弃这些请求,从而阻止学生访问特定的外网内容。
- 上网认证与收费:为了管理校园网资源,学校可能会要求学生通过登录页面进行身份验证和缴费确认后才能上网。所有网络请求都需经过这台服务器,确保只有通过认证且付费的用户才能获得公网访问权限。这种方式不仅帮助学校管理网络资源,还能保证用户的身份安全和服务质量。
- 像这种服务器替用户去访问我们称之为正向代理。它可以缓存资源,然后从学校层面可以根据正向代理服务器对学生所访问的各种资源进行限定,同理也可以对学生做入网许可的管理,这就是正向代理。正向代理服务器归根结底其实就是把所有请求收集在一起方便对请求本身做管理。
上面内容好像和刚才说的NAT有些类似,NAT是在做由路由器替用户去进行请求,这不就是正向代理,那么是不是这样呢?
- 路由器往往都具备NAT设备的功能, 通过NAT设备进行中转, 完成子网设备和其他子网设备的通信过程.
- 代理服务器看起来和NAT设备有一点像. 客户端像代理服务器发送请求, 代理服务器将请求转发给真正要请求的服务器; 服务器返回结果后, 代理服务器又把结果回传给客户端.
- 看起来它们俩工作原理好像有一点像,但其他它们俩是不同的东西。
那么NAT和代理服务器的区别有哪些呢?
(即 如何理解 NAT 的转发,和代理服务器的转发 之间的区别
从应用上讲,
- NAT设备是网络基础设备之一, 解决的是IP不足的问题.
- 代理服务器则是更贴近具体应用, 比如通过代理服务器进行翻墙, 另外像迅游这样的加速器, 也是使用代理服务器.
从底层实现上讲,
- NAT是工作在网络层, 直接对IP地址进行替换.
- 代理服务器往往工作在应用层.
从使用范围上讲,
- NAT一般在局域网的出口部署,
- 代理服务器可以在局域网做, 也可以在广域网做, 也可以跨网.
从部署位置上看,
- NAT一般集成在防火墙, 路由器等硬件设备上,
- 代理服务器则是一个软件程序, 需要部署在服务器上.代理服务器是一种应用比较广的技术.
- 翻墙: 广域网中的代理.
- 负载均衡: 局域网中的代理.
3.网线通信各层协议总结
以下是对网络通信各层协议的整理表格,按层级结构分类展示关键知识点:
协议层级 |
核心协议/概念 |
主要特点 |
关键知识点 |
数据链路层 |
以太网(Ethernet) |
实现同一局域网内设备间的直接通信 |
▪ 包含物理层规范(拓扑结构、传输速率) |
ARP协议 |
动态维护IP地址与MAC地址映射关系 |
▪ 通过广播请求获取目标MAC |
|
网络层 |
IP协议(IPv4/IPv6) |
实现跨网络的数据路由与寻址 |
▪ IP地址逻辑标识设备 |
ICMP协议 |
网络状态诊断与控制 |
▪ Ping命令基于ICMP回显请求 |
|
路由协议(如OSPF、BGP) |
动态选择最优传输路径 |
▪ 路由表维护目标网络与下一跳关系 |
|
传输层 |
TCP协议 |
提供可靠、面向连接的端到端传输 |
▪ 三次握手建立连接/四次挥手断开 |
UDP协议 |
提供高效、无连接的简单传输服务 |
▪ 无连接、不可靠但延迟低 |
|
应用层 |
HTTP/HTTPS |
万维网数据通信基础 |
▪ 请求-响应模型(GET/POST等) |
DNS协议 |
域名与IP地址的映射系统 |
▪ 分层解析(根域名→顶级→权威) |
|
自定义应用协议 |
满足特定业务需求 |
▪ 需明确定义报文格式(头部+载荷) |
补充说明
- 层级关联:
▪ 数据链路层MAC地址用于局域网通信 → 网络层IP地址实现跨网寻址 → 传输层端口号定位具体应用 → 应用层协议处理具体业务数据 - 典型交互流程:
- 协议设计原则:
▪ 下层为上层提供服务(如IP层依赖数据链路层传输)
▪ 协议开销与效率的平衡(TCP可靠性 vs UDP高效率)
▪ 安全性分层实现(链路层加密/WiFi WPA2、传输层TLS、应用层HTTPS)
4.五种 IO 模型
1.什么是IO?什么是高效的IO?
在之前我们都知道的input,output不就是IO吗
- 站在冯诺依曼体系角度我们知道从外设把数据搬到内存这不就是Input吗
- 把数据从内存拷贝到外设中这不就是output吗。
这不就是传说中的IO吗。没错,但是这种理解还不够深刻!
- 当我们在网络中发送数据的时候是使用write发生,read读取。
- 当我们在进行write写入的时候曾经说过,我们在应用层调用write本质并不是把数据发送到网络中,其实只是把数据从应用层拷贝到传输层的发送缓冲区,所有write本质就是拷贝。
- 当我们调用read读取数据时,其实并不是从网络中读取,而是从传输层的接收缓冲区中把数据从内核中拷贝到应用层,所以read也是拷贝函数。可是你想拷贝就能给你拷贝吗?
你想write,有没有可能发送缓冲区因为流量控制的问题发送缓冲区已经被写满了数据,你想write但当前缓冲区没有空间让你write了。
- 那么此时write操作默认就是阻塞在哪里,直到缓冲区有空间了
- write我们写代码到现在见到很少。但是当我们read读取数据的时候,我们被阻塞的情况是非常常见的。
以读为例,说读取就是拷贝这句话没错,但是当你想拷贝就能拷贝吗?
万一人家接收缓冲区就没有数据呢?你的read只能阻塞住。所以要记住read、write本质就是拷贝,但是拷贝是有条件的。
所以不用考虑操作系统,就站在read接口使用角度,调用read/recv… 有两种情况
- 没有数据,就会阻塞住
- 有数据,read/recv… 会在拷贝完成之后进行返回
这个阻塞不就是在等待资源就绪吗。
所以不能简单认为read/recv… 只有拷贝。这是不全面的认识。
read/recv… 读取的本质应用要分成两种东西。
- 站在我们角度read/recv…就是input。
- 读取也是同样如此要风两种东西,write/send…就是output。
IO本质:
IO = 等 + 数据拷贝
在系统层面和网络层面IO都叫数据拷贝,就比如写文件的时候,把数据写到文件的过程我们根本不知道,调用write也只是把文件写到操作系统里,然后由操作系统把数据刷新到文件里。
- 同理,我们也没有资格把数据直接写到网络里,只是把数据交给了操作系统,由操作系统帮我们发送。
- 所以我们发现系统和网络在IO的处理上是一至的。
- 在系统的时候我们不说IO=等+数据拷贝,是因为在系统层面等这个事情不直观,访问一个本地文件很快就写完成,很快就读完成了。
- 看不到等。其实有没有等呢?一定要等!今天就知道了,你要读取数据,但数据可能并不在内存中,你必须要等,因为操作系统首先要把数据从外设(磁盘)搬到内存里。
- 而磁盘是外设,所以操作系统要给磁盘下达指令把数据从磁盘中拷贝到内存等工作做完了,然后你才把数据从操作系统拷贝到用户,只不过这个过程太快了,你感受不到。
今天就不一样,在网络通信距离变长了,还要流量控制、拥塞控制等,所以距离一长等的比重就显得明显了,就能感觉到IO=等+数据拷贝了。
什么是高效的IO?
你经常会听别人说我们要高效的IO,凭什么?你IO高效的提高究竟是在做哪方面的提高?
- 首先数据拷贝这件事情,它的效率是固定的。
- 因为数据拷贝的的本质是从硬件到硬件,该花多少时间就花多少时间,要么就是由你主机上的总线的位宽决定的,要么就是由你网络的带宽决定的。
- 所以这个东西本身就是确定的,只要你能保证你在拷贝的时候它在100%一直在拷贝,它的效率就已经到达上限了。
既然IO = 等 + 数据拷贝,那什么叫做高效IO呢?
其实,只要减少 等待 的比重,即可!
- 想象一下调用read只花1秒,可是其中有99%的时间都在等待,等待的事件永远是主要矛盾,那么只有1%的时间花在拷贝上,拷贝本身就是从操作系统拷贝到用户,它是从内核到用户。
- 站在硬件角度上就是从内存到内存,这个时间本身就是一个固定时间,站在操作系统角度把数据从外设搬到内存,把硬件上速度拉满它能拷贝多少就是多少。
- 可是在IO大部分时间在等,如果把等和数据拷贝时间反过了,99%在拷贝,1%在等
- 我调用read很快就能够或者等的比重降的非常低,一调用read就直接返回,那这就叫做高效IO。
而在 等待 这件事情上,我们是需要从软件策略完成的。
read/recv它们策略很简单粗暴,没有数据就等,有数据就拷贝。
2.有那些IO的方式?这么多的方式,有那些是高效的?
下面讲个小故事理解IO的过程。
我们可能见过别人钓鱼或者自己钓鱼,那么把钓鱼步骤化繁为简,钓鱼分两步
钓鱼 = 等 + 钓
钓鱼:
- 张三:专注死盯鱼漂,不达目的不罢休 → 阻塞IO
- 李四:三心二意,频繁查看鱼漂 → 非阻塞IO
- 王五:挂铃铛通知,轻松做其他事 → 信号驱动IO
- 赵六:百竿齐发,来回巡检 → 多路复用(select/epoll)
- 田七:完全委托小王,自己专注工作 → 异步IO
效率分析:
- 直观效率:赵六 > 王五 > 张三 > 李四 > 田七(但田七实现了零等待)
- 理论极限:田七的异步模式效率最高(系统代为完成所有操作)
- 现实折中:赵六的多路复用是性价比最高的方案
关键对应关系:
| 故事元素 | 计算机概念 | 性能瓶颈 |
|---------------|-----------------------|----------------------|
| 鱼漂动静 | 数据就绪事件 | 事件检测延迟 |
| 100根鱼竿 | 多文件描述符 | 系统监控上限 |
| 来回巡检 | select轮询 | O(n)时间复杂度 |
| 铃铛响动 | SIGIO信号 | 信号处理延迟 |
| 小王代劳 | aio_read异步调用 | 系统回调机制 |
现实工程启示:
- 阻塞IO:简单但浪费线程资源(如传统socket编程)
- 非阻塞IO:CPU空转严重(需配合超时机制)
- 信号驱动:适用于低频事件(如串口通信)
- 多路复用:高并发基石(nginx/epoll模型)(可以一次等待多个)
- 异步IO:未来方向但实现复杂(如Windows IOCP)
💡 在Linux中,
epoll_wait
就是赵六的"巡检优化版",用红黑树管理鱼竿(文件描述符),事件通知复杂度降为O(1)
所以,钓鱼的人,等的比重比较低,单位时间,钓鱼的效率就高!
- 其次,张三,李四,王五,赵六,田七(小王)谁钓鱼效率最高?
- 首先张三、李四、王五、田七(小王)它们只有一人一竿,只有赵六是一人多竿。鱼竿多就是了不起。假设赵六100条竿,加上其他的人4条竿。
- 站在鱼的角度头顶上有着104个诱饵,咬到任何一个诱饵概率是一样的,要是咬的话,赵六钓鱼成功概率就是100/104,其他人只是1/104,所以赵六钓鱼时任一鱼竿就绪概率概率就100/104。
- 所以单位时间内任何一个鱼竿就绪概率就是比其他人大。所以站在旁观者看赵六就可能一直有鱼咬竿的事情。
- 所以单位时间内,赵六这种钓鱼方式等的比重比较低,所以赵六钓鱼的效率比较高。
我们把这种一次可以等待多个鱼竿的钓鱼方式叫做多路转接/多路复用
张三 ------> 阻塞IO
李四 ------> 非阻塞IO
王五 ------> 信号驱动式IO(还没有钓鱼就知道铃铛响了,鱼就咬钩了)
赵六 ------> 多路转接/多路复用
田七 (小王) ------> 异步IO
- 张三、李四,王五、赵六、田七 ----> 进程/线程
小王 ----> OS
鱼 ----> 数据
河 ----> 内核空间
鱼鳔 ----> 数据就绪的事件
鱼竿 ----> 文件描述符
钓鱼的动作 ----> read/recv…钓鱼
理解:
- 当张三这个进程去读数据时,只要底层数据没有就绪,就要一直等待将自己挂起。只有数据就绪了,才会被唤醒然后读到数据在返回。
- 李四这个进程去读数据时,当底层数据没有就绪,李四并不会因为read/recv…而被阻塞,而是立马返回,在自己的while循环中去做其他事情。然后再去读(可以设定询问间隔时间)。
- 王五这个进程在进行IO之前,一旦IO了操作系统会给进程推送SIGIO信号(需要特定接口去设置),王五在进行调用recv之前,他只是注册一下SIGIO的方法,然后王五继续向后执行做自己的事情,一旦有IO就绪了,王五的信号捕捉方法里直接调用recv,然后把数据从内核拷贝到用户空间,这叫做信号驱动。
- 赵六这个进程拿着多个文件描述符,一次等待多个,具体怎么等后面说。
- 田七这个进程,通过异步IO的接口直接将数据读取的工作交给操作系统,除了把任务交给操作系统同时他还给了操作系统一个缓冲区(鱼桶),以及给了操作系统一个通知(电话),比如是某些回调方法或者某些回调策略。
- 让操作系统在读取数据时直接把数据全部从内核中读取到缓冲区,然后用告诉操作系统的方法,来告知田七数据准备好了让田七直接用就好了,这就叫做异步IO。
所以我们把上面对IO的方式,我们称之为五种IO模型。所有IO都隶属于上面模型,目前大部分使用的文件接口用的是阻塞IO。
而多路转接/多路复用是比较高效的
对比五种IO模型的差别
张三、李四、王五在效率上有差别吗?
没有!因为他们在整个IO过程,该等多少时间就等了多少时间。效率上是没有差别的。都只有一个鱼竿,而鱼钓上来的概率是一样的。
但是在其他方面有差别!阻塞式什么事都不干,只进行IO,所以其他方面没有优势。
- 而非阻塞式IO,它可以轮询式的方法检测底层数据是否就绪,在检测没有数据就绪时还可以在等的时间做其他事情。
- 信号驱动也是一样的,在等待数据就绪时,也同样在等的时间做其他事情。
- 所以张三、李四、王五在IO上效率是一样的,但是整体上李四,王五可以做其他事情,表现上他们好像多做了事情然后更高效一点,但是这高效没有体现在IO上。
王五(信号驱动)究竟有没有等待呢?
他一定等了,要不然王五早就走了,为什么还要待在岸边呢?所以本质上还是等了。
只不过等的方式有些差别,别人是主动去检测,而他变成了你好了,你来叫我。信号驱动是采用回调的方式来进行等待的。
- 张三、李四、王五、赵六他们其实每一个人都等了,当鱼咬钩时每一个人都钓了。每一个人都参与了IO的过程,我们把他们都可以称之为同步IO。
- 田七并没有等鱼咬钩,也没有当鱼咬钩时把鱼钓上来,他连河边都没有去过,他把任务交给小王,并没有参与IO的两个阶段中的任何一个阶段,我们把他称之为异步IO。
- (他都没有进行 IO,直接拿到了结果,我们之前写的 rpc 就是一个异步 IO,但是谨记,没有银弹,异步有时容易造成混乱,所以我们有时会采用协程)
阻塞式IO和非阻塞式IO有什么差别呢?
共同点:钓
不同点:等的方式不同!
异步这里好理解,但是同步这里就有一个问题了,我们曾经学过一个线程同步的概念。
- 现在又学了一个同步IO,那这两个同步是一样的吗?
它们之间的关系就和老婆和老婆饼一样,没有任何关系!当我们在网络中搜索同步的概念时一定要加前提条件。线程同步是让多线程执行具有一定的顺序性。还是说IO的同步允许参与IO的过程!
为什么多路转接/多路复用是高效的代名词?
因为 IO = 等 + 数据拷贝,多路转接/多路复用可以减少等的比重
同样等,但是一次可以等待多个文件描述符至少有一个就绪。调用read等的比重降低了,未来效率就高了。
异步 IO
异步I/O的一个缺点是在某些情况下可能会导致更多的上下文切换和更高的开销,因为操作系统需要频繁地在多个任务之间进行调度。
用之前钓鱼故事中的「田七委托小王」场景来解释异步IO的缺点:
🎣 关键缺陷类比
田七(应用程序) 小王(操作系统)
│ │
│ 委托钓鱼任务 │
│─────────────────────────>│
│ ├─ 需要准备鱼竿、鱼饵、水桶...
│ ├─ 必须完全信任小王的钓鱼能力
│ ├─ 无法实时查看钓鱼进度
│ │(突然下暴雨也不知道)
│ │
│ ←─── 桶满才通知 ──── │
📝 具体技术缺陷
- 开发复杂度剧增
-
- 需要设计复杂的回调机制(如同田七要给小王写详细钓鱼指南)
- 状态管理困难(无法直观看到鱼竿的实时状态)
- 示例代码复杂度对比:
// 同步模式(张三式)
fish = wait_and_catch(); // 简单直观
// 异步模式(田七式)
start_async_catch(callback_func);
while(1){ /* 处理其他事但需维护回调状态 */ }
- 系统支持碎片化
操作系统 |
实现方式 |
如同... |
Linux |
libaio/epoll |
小王只会传统钓鱼法 |
Windows |
IOCP |
小王会用智能钓鱼机器人 |
macOS |
kqueue |
小王擅长海钓 |
- 调试噩梦
-
- 回调链断裂时难以追溯(如小王钓到鱼但忘记打电话)
- 多线程+异步的竞态条件(多个小王同时操作鱼桶)
- 资源隐性消耗
-
- 每个异步操作需要维护上下文(如同每个鱼竿要配记录本)
- 事件循环本身消耗CPU(田七频繁看手机是否收到通知)
- 所以他哪怕去干别的事了,只等结果,但是他等的也不安心..(异步IO 存在的问题)
- 不适用场景
┌───────────────┬───────────────┐
│ 适合场景 │ 如同... │
├───────────────┼───────────────┤
│ 大规模并发请求 │ 赵六式百竿监控 │(多路复用)
├───────────────┼───────────────┤
│ 简单串行任务 │ 张三式专注单竿 │
└───────────────┴───────────────┘
⚠️ 实践中的坑
- 缓冲区管理失控:如同小王钓的鱼太多,桶溢出导致鱼逃跑(内存泄漏)
- 超时机制缺失:若小王永远钓不满桶,田七会永久等待(死锁)
- 优先级反转:紧急钓鱼需求可能被普通请求阻塞
建议在需要处理 10,000+ 并发连接 的场景(如高频交易系统)才考虑纯异步方案,其他情况可结合多路复用(赵六模式)+线程池优化。
下篇文章我们将继续详细讲解代码实现~