后端必读:传说中的网络I/O复用,到底是怎么回事?

大家好呀,我是码财同行。

在网络开发中,I/O复用是经常被提及的一个技术名词。今天,我们就来聊聊它。

来看一组I/O模型的示意图:

上图中,I/O 模型有3种,他们是

  • 阻塞 I/O
  • 非阻塞 I/O
  • I/O 复用

无论哪种类型,都分为2个阶段:

  1. 等待网卡数据到达
  2. 将数据从内核空间拷贝到程序调用方的用户空间

现在思考一下,这里的阻塞是阻塞谁?

答案是调用方的线程或进程。

假如有个客户端连接过来,看看调用方的线程或进程是什么表现。

第一种,阻塞I/O,全程阻塞,线程只要调用 socket 的I/O 函数(如read),线程或进程就卡住了,其他什么事情都干不了:

  • 做不了其他逻辑,如协议包的逻辑处理、发送Ack包等;
  • 服务不了其他连接,读不了连接数据。这对于一个服务器而言是不可接受的,服务器的基本宗旨是服务大量的连接(玩家),不能让一个玩家做无效的独占。

那这个卡住(阻塞)会持续多久?

不知道。可能很快,如这条连接上立即有新的数据过来;也可能很慢,客户端用户半天没什么操作,也就没有交互数据。

实际上,这种情况一个进程或线程只能为一个连接服务,是对系统资源的很大浪费。

事实上一个连接上,大部分时间是没有数据需要处理的。

因此,一个线程或者进程资源,服务多个连接完全没有问题。这样,也能节约服务器的系统资源。这相当于通信上的时分复用,一段时间服务这个连接,一段时间服务那个连接,交替进行,看起来像是同时(并发)服务多个连接。

要达到时分复用的效果,I/O方案就必须是:

第二种,非阻塞I/O。这个时候,如果线程调用 socket 的I/O 函数(如read),有数据就读取,没有数据就返回。

这种方式能解决卡住(阻塞)的问题么?

可以。因为读不到数据就返回了嘛,调用方线程可以处理其他逻辑,如读取其他连接上的数据或者做逻辑处理。

现在,假如有多个连接,都被设置成了非阻塞I/O,服务器同时服务这些连接。

那应该采取什么样的方式及时得读取所有连接上过来的请求数据呢?

这时候,有几种方案可以解决这个问题:

1)轮询

第一种方法,对多个连接进行人工轮询,读到数据就处理,读不到就遍历下一个。

那这种方式最大的问题是什么?

效率低。

效率为何低呢?因为I/O操作(如read)一般是系统调用,轮询时候需要不停的在用户空间和内核空间进行切换;此外,轮询本身消耗 CPU,连接数如果成千上万,依次遍历的效率可想而知。

所以非阻塞I/O一般不单独使用。

2)I/O复用

第二种方法是让操作系统帮我们探测有数据的连接,通知我们,然后我们去处理。这就是I/O 复用,也是服务器主流的处理高并发的方案。

I/O复用本质上,是让操作系统对多个连接上I/O事件的并发探测,所谓复用,就是复用同一个调用方资源(如同一个线程或进程)来并发服务多个连接,也类似时分复用的思想。 如果不复用,像前面说的阻塞I/O模式,那只能让一个调用方资源对应一个连接了。

这里的处理只是数据上的并发通知,实际数据的读取还不是并发的,当然我们可以用多个工作线程或工作进程来并发读数据和处理消息。

最后再提一点,回过头看文章一开始的图,细心的同学可能发现了:

I/O复用虽然从使用上是两个阶段(探测有无数据,读取数据),但也都是阻塞的啊?如果一直阻塞,什么时候做逻辑处理?

其实,这里的阻塞是对所有连接的阻塞,当所有连接上都没有数据时,才是阻塞的。

即使所有连接上都没有新数据传输过来,也有几种方案可以解决这个问题:

  1. 设置依次I/O复用的超时,短时间I/O超时(没有数据)之后,就可以做逻辑处理了;
  2. 可以把一些逻辑转化成I/O的形式,例如linux上定时器就可以用文件fd来实现(muduo);
  3. 可以把I/O操作放在单独的一个网络线程中,逻辑处理再开一个线程或进程处理,这就是大名鼎鼎的 Reactor 模式;

这篇文章到这里就结束啦。

好了,看了这么多,一定很费脑力吧。来个笑话放松一下 :)

【笑话一则】我每天都会做仰卧起坐,每天晚上一个仰卧,每天早上一个起坐。

感谢您花时间阅读这篇文章!如果觉得有趣或有收获,请来个关注、评论、点赞吧,您的鼓励是我持续创作的动力,蟹蟹!

| 往期推荐

# 协程没有秘密(二):几张图文快速入门协程,推荐收藏

基于本地知识库,定制一个私有GPT助手,不能再简单了

【建议收藏】服务注册与发现原理+踩坑,一文包教会

【技术·真相】谈一谈游戏AI - 真的搞懂寻路(一)

【技术·真相】谈一谈K8S的存储(一)

相关推荐
程序猿DD5 小时前
OctaFuse Gateway 2.15.0:MCP 聚合、向量 Embeddings 与官方目录同步
后端·agent
程序员Sunday7 小时前
TypeScript 的 any、unknown、never 有什么区别?
前端·面试·typescript
余槐i7 小时前
PostgreSQL 函数索引报错 must be immutable:3种绕法与代价
数据库·后端·sql·postgresql·性能优化
需要8268 小时前
Spring Bean 生命周期:从实例化到销毁的完整钩子
java·后端·spring
小 黄 鸡8 小时前
HashMap 面试必会知识点详解(JDK 8)
java·面试·职场和发展
凤山老林9 小时前
Spring Boot 接入国密 SM2/SM3/SM4 的一些实践记录
spring boot·后端·python
伯恩bourne10 小时前
Spring Boot Bean 注册的两种方式:@Component 家族 vs @Configuration + @Bean
java·spring boot·后端
张彦峰ZYF10 小时前
停顿短不等于延迟低:G1、ZGC 与低延迟服务的目标分野、代价账本与选型标定
后端·回收暂停、应用停止总时长·到达安全点、分配停滞·屏障吃吞吐、并发线程吃核心·并发窗口吃堆余量·分区式靠挑回收集兑现目标·染色指针式不支持压缩引用
pride.li11 小时前
第03章 Python 教程-基础语法:注释与变量
服务器·前端·后端·python·算法·计算机视觉
upala11 小时前
【仓颉语言入门 · 第二篇 第 6 课】
后端