图解常见网络I/O复用模型

模型1:单线程Accept(无I/O复用)

模型1是单线程的Server服务.并且不适用任何I/O复用机制.实现一个基本的网络服务器.如图所示.

1).首先启动一个Server服务器端进程.其中包括主线程main thread.一个基本的服务端Socket编程需要几个关键的步骤.创建一个ListenFd(服务端监听套接字)将这个LitenFd绑定到需要服务的Ip和端口上.然后执行阻塞Accept被动等待远程的客户端建立连接.每次客户端Connect连接过来.main thread中accept响应并建立连接.

2).这里第一个连接过来的Client1请求服务器端连接.服务器端Server创建连接成功.得到ConnFdl套接字后,依然在main thread串行处理套接字读写.并处理业务.

3).在2处理业务时.如果有新客户端Connect过来.Server无响应.直到当前套接字将全部业务处理完毕.

4).当前客户端处理完后.结束连接.处理下一个客户端请求.

优点:

模型1的Socket编程流程清晰简单.适合学习使用.

缺点:

非并发模型.而是串行服务器.同一时刻.监听并响应最大的网络请求量为1.即并发量为1.

模型2:单线程Accept+多线程读写业务(无I/O复用)

模型2是主进程启动一个main thread线程.其中main thread进行Socket初始化的过程和模型1是一样的.如果有新的Client建立连接请求进来.就会出现和模型1不同的地方.如图所示.

1).主线程main thread执行阻塞Accept.每次客户端Connect连接过来.main thread中accept响应并建立连接.

2).创建连接成功.得到Connfdl套接字后.创建一个新线程thread1.用来处理客户端的读写业务.main thread依然回到Accept阻塞.等待新的客户端.

3).thread1通过套接字ConnFdl与客户端进行通信读写.

4).Server在2处理业务中.如果有新客户端Connect过来.main thread中Accept依然响应并建立连接.重复2).过程.如图所示.

优点:

基于模型1.单线程Accept(无I/O复用)支持了并发的特性.使用灵活一个客户端对应一个线程单独处理.Server处理业务内聚程度高.客户端无论如何写.服务器端均会有一个线程做资源响应.

缺点:

随着客户端数量增多.需要开辟的线程也增多.客户端与Server线程数量的关系为1:1.对于高并发场景.线程数量受到硬件限制.对于长连接.客户端一旦无法业务读写.只要不关闭.Server的对应线程依然需要保持连接(心跳 健康监测等机制).占用连接资源和线程开销资源.从而造成资源浪费.仅适合客户端数量不大.并且数量可控的场景使用.

模型3:单线程多路I/O复用

模型3是单线程的基础上添加多路I/O复用机制.这样就减少了多开销线程的弊端.

1).主线程main thread创建ListenFd之后.采用多路I/O复用机制(如:select epoll)进行I/O状态阻塞监控.如有Client1客户端的Connect请求.并且I/O复用机制检测到ListenFd触发读事件.则进行Accept建立连接.并将新生成的ConnFdl加入监听I/O集合中.如图所示.

2).Client1再次进行正常读写业务请求.main thread的多路I/O复用机制阻塞返回.会触发该套接字的读/写事件等.如图所示.

3).对于Client1的读写业务.Server依然在main thread执行流程继续执行.此时如果有新的客户端Connect连接请求过来.Server将没有即时响应.如图所示.

4).等到Server处理完一个连接的Read + Write操作.继续回到多路I/O复用机制阻塞.其他连接过来时重复2 3流程.

优点:

单流程解决了可以同时监听多个客户端读写状态的模型.不需要1:1的客户端的线程数量关系.多路I/O复用阻塞.非忙询状态.不浪费CPU资源.CPU利用较高.

缺点:

虽然可以监听多个客户端的读写状态.但在同一时间内.只能处理一个客户端的读写操作.实际上读写的业务并发为1.多客户端访问Server.业务为串行执行.大量请求会有排队延迟现象.当Client3占据main thread流程时.Client1和Client2流程卡在I/O复用.等待下次监听事件触发.

模型4:单线程多路I/O复用+多线程读写业务(业务工作池)

模型4属于模型3的一种改进版.改进的地方是在处理应用层消息业务本身.将这部分承担的压力交给一个工作池来处理.执行流程如下.

1).主线程main thread创建ListenFd之后.采用多路I/O复用机制(如select epoll)进行I/O状态阻塞监控.如果有Client1客户端Connect请求.并且I/O复用机制检测到ListenFd触发读事件.则进程Accept建立连接.并将新生成的ConnFdl加入监听I/O集合中.如图所示.

当ConnFdl有可读消息时.触发读事件.并且进行读写消息.

2).main thread按照固定的协议读取消息.并且交给工作池.工作池在Server启动之前就已经开启固定数量的thread.里面的线程只处理消息业务.不进行套接字读写操作.如图所示.

3).工作池处理完业务.会触发ConnFdl写时间.将回执客户端的消息通过mian thread写给对方.如图所示.

接下来Client2的读写请求的逻辑就是重复上述1).~4).的过程.一般把这种基于消息事件的业务层处理的线程称为业务工作池.如图所示.

优点:

对于模型3.将业务处理部分通过工作池分离出来.减少多客户端访问Server.业务为串行执行.大量请求会有排队延迟时间.实际上读写业务并发为1.但是业务流程并发为Worker Pool线程数量.加快了业务处理并行效率.

缺点:

读写依然是由main thread单独处理.最高读写并行通道依然为1.虽然有多个worker线程处理业务.但是最后返回客户端时.也就需要排队.因为出口还是main thread的Read+Write.

模型5:单线程I/O+多线程I/O复用(连接线程池)

模型5在单线程I/O复用机制的基础上再加上多线程的I/O复用机制.

1).Server在启动监听之前.开辟固定数量(N)的线程/用Thread Pool管理.如图所示.

2).主线程main thread创建ListenFd之后.采用多路I/O复用机制(如selece epoll)进行I/O状态阻塞监控.如果有Client1客户端Connect请求.并且I/O复用机制检测到ListenFd触发读事件.则进行Accept建立连接.并将新生成的ConFdl分发给Thread Pool中的某个线程进行监听.

3).Thread Pool中的每个thread都启动多路I/O复用机制(select epoll).用来监听main thread是否建立成功及分发下来的Socket套接字.

4).如图所示.thread1监听ConnFd1 ConnFd2.thread2监听ConnFd3.thread3监听ConnFd4.当对应的ConnFd有读写事件时.对应的线程处理该套接字的读写业务时.

将这些固定承担epoll多路I/O监控的线程集合称为线程池.如图所示.

优点:

将main thread的单流程读写分散到多线程完成.这样增加了同一时刻读写并行通道.并行通道数量为N.N为线程池thread的数量.Server同时监听的ConnFd套接字数量几乎成倍增大.之前的全部监控数量取决于main thread的多路I/O复用机制的最大限值(select默认为1024,epoll默认与内存大小相关,3万~6万不等.)所以理论上单点Server最高响应并发数量为N*(3万~6万)(N为线程池数量.建议与CPU核心成比例1:1).如果良好的线程池数量和CPU核心数适配.则可以尝试将CPU核心与thread进行绑定.从而降低CPU的切换频率.提升每个thread处理合理业务的效率.降低CPU切换成本及开销.

缺点:

虽然监听的并发数量提升了.但是提高最高读写并行通道依然为N.而且多个身处同一个thread的客户端会出现读写延迟现象.实际上每个thread的模型特征与模型3的单线程多路I/O复用一致.

模型5(进阶):

模型5进程之间的资源都是独立的.所以当有客户端(如Client1)建立请求的时候.main process(主进程)的I/O复用会监听到ListenFd的可读事件.main process(主进程)的I/O复用会监听到ListenFd的可读事件.如果在线程模型中.则可直接Accept并以此将连接创建.并且将新创建的ConnFd交给线程某个I/O复用机制来监控.因为线程与线程中的资源是共享的.但是在多进程中则不能这么做.main process如果进行Accept得到的ConnFd并不能传递给子进程.因为它们都有各自的文件描述符序列.所以在多进程版本.主进程ListenFd触发读写事件.应该由主进程发送信号告知子进程目前有新的连接可以建立.最终应该由某个子进程进行Accept完成连接建立过程.同时得到与客户端通信的套接字ConnFd.最终用自己的多路I/O复用机制来监听当前进程创建的ConnFd.

1).进程和线程的内存不同导致.main process(主进程)不进行Accept操作.而是将Accept过程分散到各个子进程中.

2).进程的特性.资源独立.所以main process如果Accept成功的fd.则其他进程无法共享资源.所以需要各子进程自行Accept创建连接.

3).main process只是监听ListenFd状态.一旦触发读事件(有新连接请求).通过一些IPC(进程间通信.如信号 共享内存 管道)等.让各自子进程Process竞争Accept完成连接建立.并各自监听.

模型6:单线程多路I/O复用+多线程I/O复用+多线程

1).Server在启动监听之前.开辟固定数量(N)的线程.用Thread Pool管理.如图所示.

2).主线程main thread创建ListenFd之后.采用多路I/O复用机制(如select epoll)进行I/O状态阻塞监控.如果有Client1客户端Connect请求.并且I/O复用机制检测到ListenFd触发读事件.则进行Accept并以此建立连接.然后将新生成的ConnFd1分发给Thread Pool中的某个线程进行监听.如图所示.

3).Thread Pool中的每个thread都启动多路I/O复用机制(select epoll).用来监听main thread是否建立成功及分发下来的Socket套接字.一旦其中某个被监听的客户端套接字触发了I/O读写事件.就会立刻开辟一个新线程来处理I/O读写业务.如图所示.

4).当某个读写线程完成当前读写业务时.如果当前套接字没有被关闭.则将当前客户端套接字重新加回线程池的监控线程中.同时自身线程自我销毁.

优点:

除了能够保证同时响应的最高并发数.还能解决读写并行通道局限的问题.同一时刻的读写并行通道.达到最大化极限.一个客户端可以对应一个单独执行流程处理读写业务.读写并行通道与客户端数量为1:1关系.如图所示.

缺点:

该模型过于理想化.要求CPU核心数量足够大.如果硬件CPU数量(目前硬件情况)可数.则该模型将造成大量的CPU切换成本及浪费.为了保证读写并行通道与客户端1:1的关系.Server需要开辟的thread数量就与客户端一致.线程池中多多路I/O复用监听线程池绑定CPU数量将变得毫无意义.如果每个临时的读写thread都能够绑定一个单独的CPU.则此模型将是最优模型.但是目前CPU数量无法与客户端的数量达到一个量级.目前甚至是相差好几个量级.

语雀地址https://www.yuque.com/itbosunmianyi/xg8vfe?

《Go.》 密码:xbkk 欢迎大家访问.提意见.

朝圣的路是心路.

如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路

相关推荐
两个人的幸福2 天前
Windows 桌面应用自研 PHP 队列(下):完整代码与六大工程化优化
php
zzzzzz3102 天前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql
BingoGo4 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
JaguarJack4 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
用户3074596982075 天前
PHP 扩展——从入门到理解
php
鹏仔先生5 天前
拷贝漫画APP下载页PHP程序,后台带免费AI写作
php
大树885 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
小宇宙Zz5 天前
Maven依赖冲突
java·服务器·maven
网络研究院5 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智5 天前
ARP代理--工作原理
运维·网络·arp·arp代理