网络服务器

如果要让服务器服务于多个客户端

多线程服务器

  • 最简单的想法就是多线程:为每一个连接创建线程。
    • 创建进程也可以,但是线程更加轻量级。
    • 现在也可以用协程实现服务器。
  • 进一步:多线程变成线程池, 减少线程不停创建销毁的开销,资源复用
    • 问题出现? 一个线程需要处理多个连接的业务。怎么才能让线程高效的处理多个连接的业务?
      • 多线程时:线程一般采用read -> 业务处理 -> send的处理流程。如果当前没有数据可读,线程会阻塞在read上,这种阻塞不会影响其他线程。
      • 当线程池存在时:线程read,如果没有数据,不能阻塞,如果这个时候阻塞,线程就没办法处理其他连接的业务了。
      • 解决这个问题的方案:
        • 最简单的方式:socket设置为非阻塞,线程不断地轮询调用 read 操作来判断是否有数据。缺点是比较粗暴,轮询非常消耗cpu,随着线程处理连接的变多,轮询的效率就会变低。线程并不知道当前连接是否有数据可读,需要每次通过read去试探
        • IO多路复用IO多路复用则只有当连接上有数据的时候,线程才会发起请求

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单线程

  • Reactor 对象的作用是监听和分发事件,通过IO多路复用接口监听事件,dispatch是分发事件操作。具体分发给Acceptor对象还是Handler对象,还要看收到的事件类型。
  • Acceptor对象的作用是获取连接,Acceptor对象会通过accept方法获取连接,并创建一个Handler对象来处理后续的响应事件。回调方法。
  • Handler 对象的作用是处理业务,完成完整的业务流程。

缺点:

  • 无法利用CPU多核优势。
  • Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟。

单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。

Redis 是由 C 语言实现的,它采用的正是「单 Reactor 单进程」的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案。

单Reactor多线程

单Reactor多线程

  • Reactor 对象的作用是监听和分发事件,通过IO多路复用接口监听事件,dispatch是分发事件操作。具体分发给Acceptor对象还是Handler对象,还要看收到的事件类型。
  • 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
  • 如果不是连接建立事件, 则交由当前连接对应的Handler对象来进行响应;只不过,Handler对象不再处理具体业务,而是开辟新的线程去处理业务逻辑。

因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。

多Reactor多线程

alt text

  • 主线程中的 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++的库,用起来更爽。

参考链接

https://zhuanlan.zhihu.com/p/368089289

https://blog.csdn.net/yand789/article/details/10906329


网络服务器
https://cauccliu.github.io/2024/03/25/网络服务器/
Author
Liuchang
Posted on
March 25, 2024
Licensed under