网络编程模式
引入
服务器处理客户端请求,最直接的方式就是一对一即一个请求创建对应的线程或进程。(其中创建线程优于创建进程,线程的上下文切换较进程切换轻便,线程通信也要比进程通信简单)但这种方式是阻塞式的,也就是说若线程遇到无数据可读会阻塞当前线程,造成资源浪费。在高并发当道的今天,这种方式也是不可能的,不可能一万请求就创建一万线程。
进而考虑能否让请求资源准备好后再发起请求,实现这一技术的就是IO多路复用。IO多路复用会使用系统调用函数监听我们期望的连接,线程可以通过函数从内核中获取事件。获取事件时,先将连接传递给内核,由内核检测:没事件发生,线程阻塞该函数。有事件发生,内核返回事件的连接,线程从阻塞状态返回,进而用户态来处理对应的业务。这其中涉及到内核态,用户态的知识。IO多路复用接口写网络程序是面向过程写代码,效率较低。
Reactor
大佬基于面向对象的思想封装了IO多路复用,并起了名字叫 Reactor模式。Reactor翻译为响应器,意为对事件的响应,也就是说有事件发生的话,Reactor会有响应的反应。别名Dispatcher模式,即IO多路复用监听事件,根据收到的事件类型分配给某进程/线程。
Reactor模式由Reactor和处理池两核心部分组成:Reactor负责监听和分发事件,如连接事件、读写事件;处理池负责处理事件,如read-> 业务 -> send;Reactor可以有多个,处理资源池也一样,可以是单线/进程,或多线/进程。两两组合有四中方案:单Reactor单程、单Reactor多程、多Reactor单程、多Reactor多程,其中单Reactor单程较多Reactor多程简单省事,且性能一致,所以后者被pass了,故余下3个有实际应用:单Reactor单程、单Reactor多程、多Reactor多程。具体用进程还是线程看具体环境:Java语言一般用线程,如Netty;C语言进程线程都可以,如Nginx用进程、Memcache用线程
C语言实现单Reactor单进程、Java实现单Reactor单线程
单Reactor单程
构成:
- Reactor对象负责监听和分发事件、Acceptor对象负责获取连接、Handler对象负责处理业务。
- select、accept、read、send是系统调用函数,dispatch分发事件和业务处理是需要完成的操作,
说明:
- Reactor对象通过select(IO多路复用接口)监听事件,收到后根据事件类型,dispatch选择分发给acceptor或handler。
- 若为连接建立事件,有acceptor对象处理,对象会通过acceptor方法获取连接,并创建handler对象来处理后续响应的事件。
- 若为读写时间,交由当前连接对应的handler对象响应,handler对象通过read->业务处理->send流程完成完整业务。
总结:单Reactor单程的方案因为工作都在同进程内完成,实现较为简单,不用考虑进程通信和多线程竞争。但优于单进程无法利用多核cpu性能,且handler处理期间,整个进程无法处理其他连接事件,若业务较慢会造成响应慢。
应用:单Reactor单程不适于计算密集的场景,只适用于业务处理非常快的场景。Redis由C实现,采用的正式单Reactor单程模式,Redis业务处理主要在内存中,速度快且性能瓶颈不在cpu。
单Reactor多程
说明:
- Reactor对象通过select(IO多路复用接口)监听事件,收到后根据事件类型,dispatch选择分发给acceptor或handler。
- 若为连接建立事件,有acceptor对象处理,对象会通过acceptor方法获取连接,并创建handler对象来处理后续响应的事件。
- 若为读写时间,交由当前连接对应的handler对象响应,handler对象通过read->业务处理->send流程完成完整业务。(这几个步骤和上面一致)
- handler对象不再处理业务,仅负责数据的传输。对象read到数据后,会发给子线程processor进行业务处理
- 子线程的processor对象处理业务处理完后,将结果返给handler,再由handler通过send发送给client。
总结:
- 单Reactor多程优势在充分利用多核cpu性能,也导致多线程资源竞争问题。可以通过在共享资源上加互斥锁保证任意时刻仅单个线程在操作即可
- 实现麻烦,要考虑父子进程通信。多线程可以共享数据,且不进程通信简单多,实际应用中看不到多进程模式
- reactor存在性能瓶颈,一个Reactor监听所有事件且只在主进程运行,在瞬时高并发场景,存在瓶颈
多Reactor多程
说明:
- 主线程中MainReactor对象通过select监听连接事件,收到事件后通过Aceeptor对象中的accept获取连接,并将连接分发给子线程
- 子县城中SubReactor对象将MainReactor对象分配的连接加入select继续监听,并创建handler用于处理响应事件。
- 读写事件发生,调handler对象通过read、业务处理、send完成整体流程
总结:
-
多Reactor多程看的复杂,实现起来比单Reactor多程简单。主线程仅负责接受新请求,子线程负责处理后续、主线程将请求传递后,后续子线程会直接返回结果给客户端,不用主线程接手
-
Netty和Memcache都是多Reactor多线程
-
Ngnix用的多Reactor多进程,有小小差异,具体表现在主进程中仅仅用来初始化 socket,并没有创建 mainReactor 来 accept 连接,而是由子进程的 Reactor 来 accept 连接,通过锁来控制一次只有一个子进程进行 accept(防止出现惊群现象),子进程 accept 新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程。
Proactor
Reactor是同步非阻塞网络,Proactor是异步非阻塞网络
阻塞IO:当用户执行read,线程阻塞,一直到内核数据准备好,并将数据从内存缓冲区拷贝到应用程序的缓冲区,当拷贝完成完成,read才会返回。这里等待的是两个过程:1)内核数据准备。2)数据从内存缓冲区拷贝到应用程序缓冲区
非阻塞IO:非阻塞IO的read请求在数据未准备好时,线程不再等待,往下继续执行,但程序会不断轮训内核,直到数据就绪,内核将数据拷贝到应用程序缓冲区,read调用才获取结果。注意:这里的第二次等待是不可避免的即将数据拷贝至程序缓冲区
最后一次调用,指数据从内核缓冲区拷贝到程序缓冲区这个过程,是同步的。无论read、send的阻塞IO还是非阻塞IO这一步都不可避免 ,若内核的拷贝效率不高,read调用会在该过程阻塞较长事件。真正的异步IO是这两步都不用等待,即内核数据就绪、内核缓冲区拷贝到用户程序缓冲区都不需要等待。当发起aio_read(异步IO)后,立即返回,内核自动将数据从内核空间拷贝到用户空间,这个过程是异步的,内核自动完成,与前面的同步操作不同,应用程序并不需要主动拷贝。我理解是内核的限制被放开了,原来可能由于数据拷贝的问题导致内核不能有效利用,改成异步效率高
模式图
工作流程
- initiator负责创建proactor和handler对象,将两者通过processor注册到内核
- processor处理请求和IO操作
- processor在完成IO后会通知proactor
- proactor根据不同事件类型回调不同handler业务处理
- handler处理业务
linux下异步IO是不完善的,aio系列函数由POSIX定义的异步操作接口,不是系统级别支持,仅为用户空间模拟的异步,且仅支持本地文件的AIO异步操作,网络编程中socket是不支持的,使基于linux的高性能网络程序都用Reactor方案,windows实现完整的支持socket的异步编程接口IOCP,系统级别的实现异步IO,因此windows内实现的高性能网络程序可以使用更高效的proactor方案
总结
Reactor是同步非阻塞网络模式,感知的是就绪可读写事件。每次感知到有事件发生(如读写事件),要程序进程调用read方法完成数据读取,也就是要应用进程主动将socket接受缓存中的数据读到应用程序内,该过程是同步的,读完后应用进程才能处理数据
Proactor是异步非阻塞网络模式,感知的是已完成读写事件。发起异步读写请求时,要传入数据缓冲区地址(保存数据)等信息,然后系统内核会自动将数据的读写工作完成,不需要像Reactor那样要程序主动read、write来读写,内核完成后会通知应用程序直接处理数据的。
Reactor是事件来了系统通知程序来处理;Proactor是事件有系统处理,好了告诉程序。
这里的事件有:新链接、数据可读、数据可写
这里的处理有:从驱动读取到内核、从内核读取到用户空间
Reactor的实现方案
- 单Reactor单程:不考虑进程或线程通信,实现简单。但无法充分利用多核CPU性能且业务逻辑不能太长,不适于计算密集的场景。Redis即该模式
- 单Reactor多程:可以利用多核性能,但会受限于单Reactor的约束,单Reactor承担所有事件监听和响应。在瞬时高并发场景中,容易成为瓶颈
- 多Reactor多程:避免单Reactor的瓶颈问题,Netty和Memcache都使用该方案,Nginx使用类似的方案
Proactor是最猛的,异步IO实现网络模型,感知已完成事件,但linux没有实现