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

0 阅读11分钟

模型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数量无法与客户端的数量达到一个量级.目前甚至是相差好几个量级.

语雀地址www.yuque.com/itbosunmian…?

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

朝圣的路是心路.

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

念何架构之路