0%

并发服务器的实现方式


并发服务器的实现方式一般有三种:多进程服务器,多线程服务器,多路复用服务器

多进程服务器

实现原理:当父进程 accept 一个请求之后,立即 Fork 出一个子进程去处理请求。而父进程则继续循环等待 accept 接受到新的请求。没有请求的情况下,父进程处于阻塞状态。

  • 创建套接字
  • 绑定(bind)服务器端口
  • 监听(listen)端口
  • 受理(accept)连接请求
  • 给获取到新的请求创建网络套接字传递(fork)给子进程
  • 子进程处理连接
  • 继续(accept)等待新的连接

子进程会复制父进程的所有资源,多个子进程之间相互独立,互不影响。

多进程服务器的优点

  1. 由操作系统进行调度,运行相对稳定健壮
  2. 通过操作系统可以方便的进行监控和管理
  3. 比较好的隔离性,每个进程相互独立,不影响主程序的稳定性。
  4. 充分利用多核 CPU , 实现并行处理

多进程服务器的缺点

  1. 进程的创建和销毁比较消耗资源,每个进程都独立加载完整的应用环境,内存消耗比较大。
  2. CPU 消耗高,高并发下,进程之间频繁的进行调度切换,需要大量的内存操作
  3. 进程数量限制了并发处理数,使得 I/O 的并发处理能力比较低

多线程服务器

通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

多线程服务器的实现:

  • 创建套接字
  • 绑定(bind)服务器端口
  • 监听(listen)端口
  • 受理(accept)连接请求
  • 服务器通过 accept 受理连接请求
  • 每当有新连接时,创建新的线程来处理用户请求
  • 处理完成后,销毁线程
  • 继续(accept)接收新的连接请求

多线程服务器的优点

  1. 对内存消耗小,线程之间共享进程的堆内存和数据,每个线程的栈都比较小,不超过 1M
  2. CPU 上下文切换比较快
  3. 线程的切换开销远低于进程,I/O 的并发能力强

多线程服务器的缺点

  1. 不方便操作系统的管理
  2. 由于线程存在对资源的共享操作,一旦出现死锁和线程阻塞,使得影响整个应用的稳定性

多路复用服务器

多路复用即 I/O 多路复用,是指内核一旦发现进程指定的一个或者多个 I/O 条件准备读取,它就通知该进程。
I/O复用原理:让应用程序可以同时对多个I/O端口进行监控以判断其上的操作是否可以进行,达到时间复用的目的。

select 模型

使用 select 函数时,可以将多个文件描述符集中到一起进行监视:

  • 是否存在套接字接受数据
  • 无需阻塞传输数据的套接字有哪些
  • 哪些套接字发生了异常

利用 select 函数实现 I/O 复用服务器

实现过程描述:

  • 创建套接字
  • 绑定(bind)服务器端口
  • 监听(listen)端口
  • 注册服务端套接字到 fd_set 变量
  • while 循环
    • 调用 select 函数监听 fd_set 里的套接字
    • 监听发生状态变化的(有接受数据的)网络套接字
    • 首先发生变化的是否是验证服务端套接字,如果是,则说明有新的连接请求,accept 新的请求,并将客户端连接的套接字注册到 fd_set 变量中
    • 如果发生变化不是服务端套接字,则说明是客户端连接套接字,则读取客户端数据
    • 读取的数据是 EOF,则证明套接需要关闭套接字,并从 select 注册的套接字中删除该套接字

select 的几大缺点:

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  3. select支持的文件描述符数量太小了,默认是1024

利用 poll 实现 I/O 复用服务器

实现过程和 select 函数大致相同,区别在于 select 使用的结构是集合 fd_set 结构,poll 使用的结构是 pollsd 结构。

利用 epoll 实现 I/O 复用服务器

基于 select 的 I/O 复用服务器,有比较明显的不合理:

  1. 每次调用 select 函数后针对所有文件描述符分循环
  2. 每次调用 select 函数都需要向该函数传递监视对象的信息(fd_set)

每次调用 select 函数时是向操作系统传递监视对象信息,那必然会发生系统调用,需要把 fd 集合从用户态拷贝到内核态。开销太大

Linux 下的 epoll 具有如下优点:

  1. 无需编写以监视状态变化为目的针对所有文件描述符的循环语句
  2. 调用对应于 select 函数的 epoll_wait 函数无需每次都传递监视对象信息

epoll 提供了三个函数:

  • epoll_create:是创建一个 epoll 句柄
  • epoll_ctl:是注册要监听的事件类型
  • epoll_wait:则是等待事件的产生

实现过程描述:

  • 创建套接字
  • 绑定(bind)服务器端口
  • 监听(listen)端口
  • epoll_create 创建 epoll 例程
  • epoll_ctl(add) 注册事件到 epoll 句柄
  • while 循环
    • 调用 epoll_wait 函数监听套接字
    • 监听发生状态变化的(有接受数据的)网络套接字
    • 首先发生变化的是否是验证服务端套接字,如果是,则说明有新的连接请求,accept 新的请求,并调用 epoll_ctl 将客户端连接的套接字注册到 epoll 句柄
    • 如果发生变化不是服务端套接字,则说明是客户端连接套接字,则读取客户端数据
    • 读取的数据是 EOF,则证明套接需要关闭套接字,调用 epoll_ctl(del) 注册事件到 epoll 句柄

epoll 和 selectp/poll 最大的区别:

  • epoll_ctl 函数,每次注册新的事件到 epoll 句柄时,会把所有的 fd 拷贝进内核,而不是在 epoll_wait 的时候重复拷贝。epoll 保证了每一个 fd 在整个过程中只会拷贝一次
  • epoll 的解决方案不像 select 或 poll 一样每次都把需要监听的套接字加入 fd 对应的设备等待队列中,而只在 epoll_ctl 时挂载一遍,并为每个 fd 设置一个回调函数,当设备就绪,唤醒等待队列的等待者时,就会调用这个函数,而这个毁掉函数就会把 fd 加入一个有序链表。
  • epoll_wait 的工作实际上就是在这个就绪的链表中查看有没有就绪的 fd。

总结