select 实现与应用
select 的原理
在网络IO一篇中我们讲到了5种的IO网络模型。而select则是多路复用中的一种。它把等待数据就绪和读取数据区分开,实现了单线程操作多个网络IO的功能。
select如何实现单线程中对多个网络IO读取操作的呢?
内核根据io的3种状态,将各个状态准备好的io放入对应的位图中去。而后,我们根据对应的位图,去遍历io获取或写入数据。由此实现同时操作多个io。
重点小黑板:
- io的3种状态 可读、可写、是否出错
- 位图 fd_set,最大为1024位
- socket创建出来的fd是一个int型,而且是逐次递增的。如果中间关闭了一个fd,则下次创建的时候,新建的fd为之前关闭的fd值。
基本函数
fd_set: 该类型可以简单的理解为按 bit 位标记句柄的队列,例如标记一个值为8的句柄,则该fd_set的第8位标记为1.
头文件#include <arpa/inet.h>
select
- 原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,struct timeval *timeout)
- 参数介绍
参数名 | 说明 |
---|---|
nfds | 遍历到的最大socketfd |
readfds | 获取到的可读的io列表 |
writefds | 获取到的可写的io列表 |
exceptfds | 获取到的出错的io列表 |
timeout | 读取io状态的间隔时间 |
timeout为NULL时,表示置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
为0时,表示不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
大于0时,该值为等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回,否则在超时后一定返回,文件无变化返回0,有变化返回一个正值。
-
函数功能
用来获取出可读、可写、出错的io列表 -
注意事项
当新客户端connect的时候也会触发select的可读事件,故也需要处理
对fd_set 操作
函数名 | 函数原型 | 函数作用 |
---|---|---|
FD_ISSET | FD_ISSET(int fd, fd_set* fds) | 判断fd是否存在于fds |
FD_SET | FD_SET(int fd, fd_set* fds) | 在fds中标记fd的句柄 |
FD_CLR | FD_CLR(int fd, fd_set* fds) | 将fd的标记从fds集合中清除 |
FD_ZERO | FD_ZERO(fd_set* fds) | 清空fds里面所有的标记 |
用select搭建一个简单的服务端
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define BUFFER_LEN 1024
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
printf("create socket error!\n");
return -1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(4018);
addr.sin_addr.s_addr = INADDR_ANY;
if(bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0)
{
printf("bind socket error! \n");
return -1;
}
if (listen(sockfd, 5) < 0)
{
printf("listen socket error! \n");
return -1;
}
fd_set rReadset,rfds;
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
int maxfd = sockfd;
while (1)
{
rReadset = rfds;
// 这里只获取可读状态的io
int nready = select(maxfd + 1, &rReadset, NULL, NULL, NULL);
if (nready < 0)
{
printf("select error!\n");
continue;
}
if (FD_ISSET(sockfd, &rReadset))
{
struct sockaddr_in clientAddr;
memset(&clientAddr, 0, sizeof(struct sockaddr_in));
int cliLen = sizeof(struct sockaddr_in);
int clientfd = accept(sockfd, (struct sockaddr*)&clientAddr, (socklen_t *)&cliLen);
if (clientfd < 0)
{
printf("accept client error\n");
continue;
}
char str[INET_ADDRSTRLEN] = {0};
printf("recv from %s at port %d, sockfd:%d, clientfd:%d\n", inet_ntop(AF_INET, &clientAddr.sin_addr, str, sizeof(str)),ntohs(clientAddr.sin_port), sockfd, clientfd);
if (maxfd == FD_SETSIZE)
{
printf("clientfd out of range\n");
break;
}
FD_SET(clientfd, &rfds);
maxfd = clientfd > maxfd ? clientfd : maxfd;
printf("sockfd:%d, max_fd:%d, clientfd:%d\n", sockfd, maxfd, clientfd);
if (--nready == 0)
{
continue;
}
}
for (int i = sockfd + 1; i < maxfd + 1; i++)
{
if (!FD_ISSET(i, &rReadset))
{
continue;
}
char recvBuf[BUFFER_LEN] = {0};
int ret = read(i, recvBuf, BUFFER_LEN);
if (ret < 0)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
printf("read all data early\n");
}
FD_CLR(i, &rfds);
close(i);
}else if (ret == 0)
{
printf("connet is close %d\n", i);
FD_CLR(i, &rfds);
close(i);
}
else
{
printf("Recv: %s, %d Bytes\n", recvBuf, ret);
}
if (-- nready == 0)
{
break;
}
}
}
return 0;
}
总结
fd_set 数据结构中只有1024位,故通常说法是select最高支持1024个连接。
select在select调用的时候也是阻塞的,因为kernel 会“监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。
优点:只用单线程执行,占用资源少,不消耗太大的cpu资源,能够同时为多个客户端提供服务
缺点:当句柄值太大的时候,本身需要消耗大量的时间去轮询。很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll