网络服务器
如果要让服务器服务于多个客户端
多线程服务器
- 最简单的想法就是
多线程
:为每一个连接创建线程。- 创建进程也可以,但是线程更加轻量级。
- 现在也可以用
协程
实现服务器。
- 进一步:多线程变成
线程池, 减少线程不停创建销毁的开销,资源复用
。- 问题出现? 一个线程需要处理多个连接的业务。怎么才能让线程高效的处理多个连接的业务?
- 多线程时:线程一般采用read -> 业务处理 -> send的处理流程。如果当前没有数据可读,线程会阻塞在read上,这种阻塞不会影响其他线程。
- 当线程池存在时:线程read,如果没有数据,不能阻塞,如果这个时候阻塞,线程就没办法处理其他连接的业务了。
- 解决这个问题的方案:
- 最简单的方式:socket设置为非阻塞,线程不断地轮询调用 read 操作来判断是否有数据。缺点是比较粗暴,轮询非常消耗cpu,随着线程处理连接的变多,轮询的效率就会变低。
线程并不知道当前连接是否有数据可读,需要每次通过read去试探
。 - IO多路复用:
IO多路复用则只有当连接上有数据的时候,线程才会发起请求
。
- 最简单的方式:socket设置为非阻塞,线程不断地轮询调用 read 操作来判断是否有数据。缺点是比较粗暴,轮询非常消耗cpu,随着线程处理连接的变多,轮询的效率就会变低。
- 问题出现? 一个线程需要处理多个连接的业务。怎么才能让线程高效的处理多个连接的业务?
IO多路复用
IO多路复用技术:用一个系统调用函数来监听我们关心的连接,也就说可以在一个监控线程里面监控很多的连接。select/poll/epoll 就是内核提供给用户态的多路复用系统调用,线程可以通过一个系统调用函数从内核中获取多个事件。
select poll epoll是如何获取网络事件的?
获取事件时,把关心的连接传给内核,再由内核检测:
- 如果没有事件发生,线程只需阻塞在这个系统调用,而无需像前面的线程池方案那样轮训调用 read 操作来判断是否有数据。
- 如果有事件发生,内核会返回产生了事件的连接,线程就会从阻塞状态返回,然后在用户态中再处理这些连接对应的业务即可。
IO多路复用就是高性能网络的基础,单纯的使用IO多路复用API,实际上使用的面向过程的方法去写代码。
IO多路复用下,内核不断地去巡检事件,然后通知处理,比较理想的方式是以一种回调
的方式去处理事件,事件做完以后通知主线程。Reactor和Proactor本质都是回调,区别是回调的方式是异步还是同步的。
Reactor反应堆模式
Reactor:IO多路复用监听事件,收到事件后,根据事件类型分配给某个进程/线程。来了一个事件,Reactor 就有相对应的反应/响应。是同步IO
的处理方式。
Reactor 模式主要由 Reactor 和处理资源池这两个核心部分组成:
- Reactor 负责监听和分发事件,事件类型包含连接事件、读写事件;
- 处理资源池负责处理事件,如 read -> 业务逻辑 -> send;
Reactor和业务处理都可以是多个,也就有了多种处理方案:
- 单 Reactor 单进程 / 线程;
- 单 Reactor 多进程 / 线程;
- 多 Reactor 单进程 / 线程;不常用,相比于单Reactor单线程,复杂且没有性能优势。
- 多 Reactor 多进程 / 线程;
单Reactor单线程
- Reactor 对象的作用是监听和分发事件,通过
IO多路复用接口监听事件
,dispatch是分发事件操作。具体分发给Acceptor对象还是Handler对象,还要看收到的事件类型。 - Acceptor对象的作用是获取连接,Acceptor对象会通过
accept方法获取连接,并创建一个Handler对象来处理后续的响应事件
。回调方法。 - Handler 对象的作用是处理业务,完成完整的业务流程。
缺点:
- 无法利用CPU多核优势。
- Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟。
单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。
Redis 是由 C 语言实现的,它采用的正是「单 Reactor 单进程」的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案。
单Reactor多线程
- Reactor 对象的作用是监听和分发事件,通过
IO多路复用接口监听事件
,dispatch是分发事件操作。具体分发给Acceptor对象还是Handler对象,还要看收到的事件类型。 - 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
- 如果不是连接建立事件, 则交由当前连接对应的Handler对象来进行响应;只不过,Handler对象不再处理具体业务,而是开辟新的线程去处理业务逻辑。
因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
多Reactor多线程
- 主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;
- 子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入select继续进行监听,并创建一个Handler用于处理连接的响应事件。
- 如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。
Proactor反应堆模式
Proactor是异步网络模式。
阻塞、非阻塞、同步、异步IO
- 阻塞:程序执行 read ,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read 才会返回。需要等待
内核数据准备好
和数据从内核态拷贝到用户态
两个过程。 - 非阻塞:
read 请求在数据未准备好的情况下立即返回
,可以继续往下执行,此时应用程序不断轮询内核
,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read 调用才可以获取到结果。所以说非阻塞等待的是数据从内核态拷贝到用户态
。 - 同步 I/O:无论阻塞还是非阻塞都是同步调用,Reactor模式就是同步非阻塞IO方式。
- 异步 I/O:
内核数据准备好
和数据从内核态拷贝到用户态
两个过程都不等待。内核自动将数据从内核空间拷贝到用户空间,这个拷贝过程同样是异步的,内核自动完成的。
你去打印店,打印东西,一个个排队打印,是同步阻塞。
同步非阻塞是什么时候有时间,店家给你打电话。
异步操作:店家打印好给你打电话去取。同步非阻塞是半托管,异步非阻塞是全托管。
看起来Proactor这种方式比Reactor这种模式更好。
- 在 Linux下的异步I/O是不完善的, aio系列函数是由 POSIX 定义的异步操作接口,不是真正的操作系统级别支持的,而是在用户空间模拟出来的异步,并且仅仅支持基于本地文件的 aio 异步操作,网络编程中的socket是不支持的,这也使得
基于Linux的高性能网络程序都是使用Reactor方案。
- Windows 里实现了一套完整的支持socket的异步编程接口,这套接口就是
IOCP,是由操作系统级别实现的异步I/O,真正意义上异步I/O,因此在Windows里实现高性能网络程序可以使用效率更高的Proactor方案
。
从技术上看:IOCP比EPOLL更智能,全包了。但是Epoll的效率高于IOCP?IOCP确实看上去更智能,啥事都干完了,但同异步情况来看,并不见得就比Epoll要好。无非就是收包这步系统做了。linux内核的协议栈实现要优于windows,因为linux本身就是服务器的架构。
现阶段网络上比较流行的网络库中:libevent和boost.asio,相对轻量级一点。
- libevent基于Reactor。libevent是一个C语言写的网络库,官方主要支持的是类linux操作系统,最新的版本添加了对windows的IOCP的支持。在跨平台方面主要通过select模型来进行支持。
- Boost.asio基于Proactor。Boost.Asio类库,其就是以Proactor这种设计模式来实现,是C++的库,用起来更爽。
参考链接