select 实现与应用
- select 的原理
- 基本函数
- select
- 对fd_set 操作
- 用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 1024int 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;// 这里只获取可读状态的ioint 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