IO多路复用机制
问题引入
如果一个服务器需要同时监听处理多个连接请求,你需要如何设计?
1. 多进程
当我们听到多个请求,同时处理这些字段时很容易想到使用多线程或者多进程来进行监听,当请求数量比较少时,看起来还算OK,但是当出现大量连接时,会带来一些问题:
- 线程资源是有限的,这就导致了我们的服务器连接数也是有限的,过多的请求导致没有足够的线程资源进行处理。
- 当线程读取未准备完成的数据时,会被阻塞,浪费线程资源同时也会浪费连接资源。
- 线程之间的切换也会带来一定的开销,降低服务器性能甚至导致宕机。
2. 单线程
更加上面的分析我们发现,每个请求设置一个线程是不可取的,那么我们是否可以使用单线程来轮询监听所有的请求,当有请求到来时,我们再创建一个新的线程来处理这个请求呢? 这样既解决了连接数目的问题,也解决了创建大量的监听线程的问题。
上面的这种方式就是Select模型所采用的思想
Select模型
Select的原理就是创建一个轮询线程对监听的Socket进行轮询,当接收到请求时,对这个socket进行处理
// C语言伪代码
while(true) {
for(fd in fds) { // 不断轮询所有的文件描述符
if(fd.receivedData()) { // 当接收到数据时,进行处理
handle(fd);
}
}
}
让我们看一下Linux中的Select示例:
int main() {
fd_set rfds;
int retval, nfds;
// 如果用作服务器监听程序,一般将下面代码放入while循环中
FD_ZERO(&rfds);
FD_SET(0, &rfds);
// select会监听一系列文件描述符(rfds),当任意文件描述符接收到数据的,会返回1,并将有数据的文件描述符对应的bitmap进行set
retval = select(nfds, &rfds, NULL, NULL, NULL); // 会阻塞当前线程
if (retval == -1)
perror("select() error");
else if (retval) {
// 当有数据到来时,遍历所有的文件描述符,处理数据
for (int i = 0; i < nfsd; i++) {
if (FD_ISSET(i, &rfds)) handle(i, rfsd);
}
} else
printf("No data received.\n");
return 0;
}
void handle(int i, fd_set fd) {
// 处理逻辑
}
select函数是Linux系统调用,是Linux系统Select模型的实现,他会监听传入的文件描述符(Linux系统中一切皆文件,每个Socket连接表示为一个文件描述符) 当有数据到来时,会将对应的文件描述符的bitmap进行set,同时返回接收数据的文件描述符个数,当没有数据到来时,会阻塞当前线程,直到有数据到来。 要注意的是,select()只能监听不超过FD_SETSIZE(1024)个文件描述符。
当接收到数据时,会遍历所有的文件描述符,找到接收到文件的描述符,然后处理数据。
Select看似很美好,我们可以使用一个线程来监听所有的请求,但是同样还算不算完美:
- select()只能监听不超过FD_SETSIZE(1024)个文件描述符,限制了服务器的连接数目。
- 当我们用作服务器监听程序时,在while循环调用select()会有用户态和内核态的切换产生开销。
- select()会遍历所有的文件描述符,找到接收到文件的描述符,然后处理数据,这样会带来一定的开销。