目录
一、select模型的事件就绪条件
所谓的事件就绪,并非只有数据就绪这种情况,比如监听套接字listen_fd收到了新的连接请求,这也算 读事件就绪的一种,用户可以通过accpet函数将新连接拷贝到上层进行维护,所谓的读不正是将数据从内核拷贝到上层的过程吗?
1、读事件就绪
- 最典型的读事件是 缓冲区里的数据就绪。 此时缓冲区里数据所占字节数 大于 低水位标记SO_RCVLOWAT,然后上层便可以无阻塞的读该文件描述符。
- 监听套接字 listen_fd 收到了新的连接请求。这也算是读事件就绪的一种,然后上层便可以调用accept函数将新连接从内核拷贝到用户层,并返回新连接的 fd
- 对端的连接断开(recv/read的返回值为0)。
- socket上有未处理的错误
2、写事件就绪
- 最典型的写事件是 缓冲区里有足够的剩余空间。此时缓冲区里剩余空间所占字节数大小 大于 低水位标记SO_SNDLOWAT ,然后上层便可以无阻塞的写入内容。
- 调用 close 或者 shutdown 函数,此时写操作被关闭了。
- socket使用非阻塞connect 连接成功或者失败之后。
- socket上有未读取的错误。
二、select模型的执行过程
select的执行过程 以select函数调用为界,分为两部分。第一部分是告诉 select 哪些fd是需要被关注的;第二部分是 事件就绪以后的拷贝工作(调用read/recv/write/send函数)。
1、第一部分:select调用前
定义readfds_array数组的原因
在接收数据的时候,可能存在下面两种情况
- 一个文件描述符只是接收了一部分数据,此时数据就绪了,剩余尚未接收的数据本该留到下次接收,但是第二个参数readfds 输出时会清空所有的内容。
- 一个文件描述符读/写事件就绪后,第二个参数readfds 清空所有的内容,将就绪结果告知用户,但是下一次还想要继续写入内容,下一次又该怎么办呢??
根本原因在于 第二个参数 readfds 是一个输入输出型参数。输入时,用户告诉内核,哪些文件描述符需要被关注;输出时,readfds将原本的内容清空,换上输出结果,以此告诉用户,有哪些文件描述符上的事件已经就绪。
因此,我们需要一个数组来临时存储下一次仍然需要被关注的fd ,这个数组便是readfds_array
int main()
{
//创建套接字
int listen_fd = TcpSocket::CreateSocket();
//绑定端口号
TcpSocket::Bind(listen_fd, 8080);
//设为监听套接字
TcpSocket::Listen(listen_fd);
#define NUM 1024
int readfds_array[NUM]; //定义一个数组用于保存下一次需要关注的fd
for(int i=0; i< NUM ;i++){
readfds_array[i] = -1; //初始化数组,元素值为-1表示当前位置未被占用
}
readfds_array[0] = listen_fd; //新连接到来的时候,属于读事件就绪
fd_set *readfds = nullptr;
while (1)
{
int max_fd = readfds_array[0]; // select的第一个参数是所有文件描述符的最大值
FD_ZERO(readfds); //清空读事件集合
for (size_t i = 0; i < NUM; i++) //以循环的方式将数组里的fd添加到 读事件集合中
{
if (readfds_array[i] == -1)
continue;
//到这一步,readfds_array[i]一定存了文件描述符
FD_SET(readfds_array[i], readfds);
//获取到文件描述符的最大值
max_fd = max_fd < readfds_array[i] ? readfds_array[i] : max_fd;
}
//设置阻塞时间为5s
struct timeval timeout = {5,0};
int n = select(max_fd,readfds,nullptr,nullptr,&timeout);
switch (n)
{
case -1:
//select调用出错
break;
case 0:
//select阻塞超时(阻塞超时会进入非阻塞状态,这里不算调用出错)
break;
default:
/******************* 第二部分 *****************/
//说明有文件描述符上的事件就绪
break;
}
}
return 0;
}
2、第二部分:select调用后
这部分要重点了解的是事件就绪后如何处理的问题。(只不过我们这里只关注读事件就绪)。
事件就绪有可能是读事件、写事件就绪、异常事件就绪。此时我们并不知道是哪种事件的哪个fd就绪,所以要逐一去判断。如果 readfds_array[i] == -1,说明该位置没有存放文件描述符,直接进行下一次循环。
int main()
{
//创建套接字
int listen_fd = TcpSocket::CreateSocket();
//绑定端口号
TcpSocket::Bind(listen_fd, 8080);
//设为监听套接字
TcpSocket::Listen(listen_fd);
#define NUM 1024
int readfds_array[NUM]; //定义一个数组用于保存下一次需要关注的fd
for(int i=0; i< NUM ;i++){
readfds_array[i] = -1; //初始化数组,元素值为-1表示当前位置未被占用
}
readfds_array[0] = listen_fd; //新连接到来的时候,属于读事件就绪
fd_set *readfds = nullptr;
while (1)
{
int max_fd = readfds_array[0]; // select的第一个参数是所有文件描述符的最大值
FD_ZERO(readfds); //清空读事件集合
for (size_t i = 0; i < NUM; i++)
{
if (readfds_array[i] == -1)
continue;
//到这一步,readfds_array[i]一定存了文件描述符
FD_SET(readfds_array[i], readfds);
//获取到文件描述符的最大值
max_fd = max_fd < readfds_array[i] ? readfds_array[i] : max_fd;
}
//设置阻塞时间为5s
struct timeval timeout = {5,0};
int n = select(max_fd,readfds,nullptr,nullptr,&timeout);
switch (n)
{
case -1:
//select调用出错
break;
case 0:
//select阻塞超时(阻塞超时会进入非阻塞状态,这里不算调用出错)
break;
default:
/******************* 第二部分 *****************/
//说明有文件描述符上的事件就绪
for (size_t i = 0; i < NUM; i++)
{
if (readfds_array[i] == -1) continue;
//判断是不是读事件就绪(判断是否在读事件集合)
if(FD_ISSET(readfds_array[i],readfds)){
//说明该文件描述符上的读事件就绪
if(readfds_array[i] == listen_fd){
//收到了新的连接,此时可以调用accept
int sock = TcpSocket::Accept(readfds_array[i]);
if(sock > 0){
//说明获取成功,此时需要把这个文件描述符加入到 readfds_array[i]中
//因此需要遍历readfds_array数组,看看有哪个位置未被使用,第0个位置固定给监听套接字使用
int pos = 1;
for (; pos < NUM; pos++)
{
if (readfds_array[i] == -1) break; //-1表示该位置未被使用
}
//如果pos<NUM,说明有位置可以放;反之,没有位置可以放
if (pos < NUM)
{
readfds_array[pos] = sock;
}
else{
//没有位置可以放说明服务器满载了
close(sock);
}
}
}
else{
//收到了对方发来的数据,此时可以调用recv/read
char buffer[1024] = {0};
ssize_t s = recv(readfds_array[i],buffer,sizeof(buffer)-1,0);
if (s > 0)
{
// 将数据拷贝到上层,接下来可以处理数据了
}
else if (s == 0)
{
//对端关闭连接了
close(readfds_array[i]);
readfds_array[i] = -1; //空出位置给其他fd使用
}
else{
//读取失败
close(readfds_array[i]);
readfds_array[i] = -1; //空出位置给其他fd使用
}
}
}
//判断是不是写事件就绪
// ... ...
//判断是不是异常事件就绪
// ... ...
}
break;
}
}
return 0;
}
三、select的优缺点
1、优点
可以一次等待多个fd,可以让我们等待的时间重叠,一定程度上可以提高IO的效率。尽管多线程也可以实现,但是多线程的运行是受到CPU调度约束的,阻塞等待的时候会被加入到等待队列,事件就绪的时候再回到运行队列,频繁换队列存在一定的损耗。
2、缺点
缺点一
每次都要重新设置哪些文件描述符需要被关注。以第二个参数readfds为例,readfds是一个输入输出型参数,输入时告诉哪些文件描述符需要被关注;输出的时候,内核通知我们哪些文件描述符上的事件就绪了。
但是这样一来,我们最开始设置的输入就被输出结果给覆盖了,因此,下一次调用select的时候我们要重新设置第二个参数。
缺点二
每次调用select都需要把fd集合从用户层拷贝到内核,这个开销在fd较多时会很大。同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
缺点三
fd_set是一个位图结构,每次可以让select 关注的文件描述符数是有限的,只有1024个。