前面写了一个简单的echo服务器程序,这个程序同一时刻只能服务一个客户端,在实际应用中肯定是不行的。
在unix/linux系统上,提高服务器程序的并发量有很多方法,比如:
- 每一个客户端进来,服务器fork一个子进程专门处理这个客户端。这个问题是当客户太多时,需要fork很多进程,这对系统的压力很大。
- 预先fork多个子进程,在fork子进程之前,先创建监听fd,这样子进程就共享这个监听fd,然后每个子进程都开始accept客户端的连接。一个客户端连接进来,就可能被其中一个子进程accept然后处理。
- 预先创建一些线程,每accept一个客户端就扔给某个线程处理。
- 使用非阻塞(Nonblocking)的socket,然后定时检查fd是否可读写,这种定时轮询的方式经常要做无用功,比较浪费CPU。
- 最后一种就是我们要介绍的I/O模型:I/O多路复用,1个进程就可以同时处理大量客户端的请求。结合非阻塞socket,能最大程度的提高服务器的吞吐量。
这一篇主要研究select这个函数:
int
这个函数总是水平触发的模式(level-triggered),现在只要简单的理解成:只要监控的fd可以读写,就总是会返回。后面介绍到epoll再来理解这个模式,这个模式相对于边缘触发(edge-triggered)更容易处理。
这个函数的一些参数说明:
- readfds,writefds,exceptfds 这是我们要监控的fd集合,我们先设置好这三个集合,等函数返回时,内容会被修改成真正有IO的fd集合,三个参数并不用全部设置,比如不关心写和异常集合,可以传NULL
- readfds 代表想要读的的集合
- writefds 代表想要写的集合
- exceptfds 在Linux上只表示有带外数据(Out-of-band data)可读。
- fd_set 内部是一个位数组,一个fd占一个位,通过下面函数操作集合:
- FD_CLR(fd, fd_set) 清除某个fd
- FD_ZERO(fd_set) 清除整个集合
- FD_SET(fd, fd_set) 设置某个fd
- FD_ISSET(fd, fd_set) 判断fd是否在集合中
- fd_set是一个固定长度的位数组,常量FD_SETSIZE表示这个集合的大小,Linux下通常是1024。这表明fd的值不可以大于等于FD_SETSIZE,否则会出现不可预期的错误。
- nfds 是我们要监控的三个集合中最大的fd加1,为什么是这个值呢?原因是为了优化,如果我们不传这个nfds,那么内部实现只能从0到FD_SETSIZE判断fd是否存在,这个会影响select的效率。
- timeout 是select的超时,如果为NULL则一直阻塞到集中的IO事件到来。
- 如果select成功返回,返回值是三个集合的fd总数;如果返回0表示超时;如果返回-1表示出错,通过errno获得错误原因。
这个函数介绍完,我们来将之前的echo服务器改造成可以接收多个客户端请求的模式,这个例子没有使用非阻塞socket,因为那样会增加程序的复杂度:
#include