并发服务器的实现方式一般有三种:多进程服务器,多线程服务器,多路复用服务器
多进程服务器
实现原理:当父进程 accept 一个请求之后,立即 Fork 出一个子进程去处理请求。而父进程则继续循环等待 accept 接受到新的请求。没有请求的情况下,父进程处于阻塞状态。
- 创建套接字
- 绑定(bind)服务器端口
- 监听(listen)端口
- 受理(accept)连接请求
- 给获取到新的请求创建网络套接字传递(fork)给子进程
- 子进程处理连接
- 继续(accept)等待新的连接
子进程会复制父进程的所有资源,多个子进程之间相互独立,互不影响。
多进程服务器的优点
- 由操作系统进行调度,运行相对稳定健壮
- 通过操作系统可以方便的进行监控和管理
- 比较好的隔离性,每个进程相互独立,不影响主程序的稳定性。
- 充分利用多核 CPU , 实现并行处理
多进程服务器的缺点
- 进程的创建和销毁比较消耗资源,每个进程都独立加载完整的应用环境,内存消耗比较大。
- CPU 消耗高,高并发下,进程之间频繁的进行调度切换,需要大量的内存操作
- 进程数量限制了并发处理数,使得 I/O 的并发处理能力比较低
多线程服务器
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
多线程服务器的实现:
- 创建套接字
- 绑定(bind)服务器端口
- 监听(listen)端口
- 受理(accept)连接请求
- 服务器通过 accept 受理连接请求
- 每当有新连接时,创建新的线程来处理用户请求
- 处理完成后,销毁线程
- 继续(accept)接收新的连接请求
多线程服务器的优点
- 对内存消耗小,线程之间共享进程的堆内存和数据,每个线程的栈都比较小,不超过 1M
- CPU 上下文切换比较快
- 线程的切换开销远低于进程,I/O 的并发能力强
多线程服务器的缺点
- 不方便操作系统的管理
- 由于线程存在对资源的共享操作,一旦出现死锁和线程阻塞,使得影响整个应用的稳定性
多路复用服务器
多路复用即 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 的几大缺点:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小了,默认是1024
利用 poll 实现 I/O 复用服务器
实现过程和 select 函数大致相同,区别在于 select 使用的结构是集合 fd_set 结构,poll 使用的结构是 pollsd 结构。
利用 epoll 实现 I/O 复用服务器
基于 select 的 I/O 复用服务器,有比较明显的不合理:
- 每次调用 select 函数后针对所有文件描述符分循环
- 每次调用 select 函数都需要向该函数传递监视对象的信息(fd_set)
每次调用 select 函数时是向操作系统传递监视对象信息,那必然会发生系统调用,需要把 fd 集合从用户态拷贝到内核态。开销太大
Linux 下的 epoll 具有如下优点:
- 无需编写以监视状态变化为目的针对所有文件描述符的循环语句
- 调用对应于 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。