本篇是第三篇,主要用来讲解作为服务器的机器是如何管理多个socket的客户端连接的,毕竟recv只能监视单个socket。
一、背景介绍
在此之前,我们先来看下"操作系统是如何区分网络收到的数据是属于那一个socket的?"
答案:socket与端口号是一一对应的,操作系统会维护端口号到socket的索引结构,以快速读取,所以操作系统可以很方便的找到收到的网络数据属于那一个socket。
基于前面第2篇的知识,如果我们能够做到传递一个socket的列表,并且能够做到在socket列表没有数据的时候挂起进程,只要有一个socket有数据就唤醒这个进程貌似就可以解决这个问题。而这个也恰恰就是select的实现思路。
二、select介绍
我们通过使用select的代码来分析select的过程
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...)
int fds[] = // 用于存放需要监听的socket
while(1){ // 死循环,利用操作系统的进程阻塞和唤醒来工作
int n = select(..., fds, ...) // 传入fds
for(int i=0; i < fds.count; i++){ // 唤醒进程之后,遍历所有的socket
if(FD_ISSET(fds[i], ...)){ // 利用FD_ISSET来判断对应的socket是否有数据
// fds[i]的数据处理
}
}
1.调用select之后,操作系统把进程A分别加入这三个socket的等待队列中。
2.当任何一个socket收到数据后,中断程序将唤起进程。下图展示了sock2接收到了数据的处理流程。
3.所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面。
当进程A被唤醒后,它知道至少有一个socket接收了数据。程序只需遍历一遍socket列表,就可以得到就绪的socket。
三、select的不足之处
其一,每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
其二,进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。
补充说明:本节只解释了select的一种情形。当程序调用select时,内核会先遍历一遍socket,如果有一个以上的socket接收缓冲区有数据,那么select直接返回,不会阻塞。这也是为什么select的返回值有可能大于1的原因之一。如果没有socket有数据,进程才会阻塞。
参考文档:
https://zhuanlan.zhihu.com/p/64138532