在Linux上,为我们提供了三种IO多路复用的函数供我们使用,select函数是网络通信编程中很常用的一个函数。select函数一般用于检测在一组socket中是否有事件准备就绪。
select的声明:
#include <sys/time.h> //for struct timeval
#include <unistd.h> //for select
/**
* return 状态变化的文件描述符的个数
* @param nfds: linux上的socket也是一种fd(文件描述符),将这个参数的值设置为所有需要使用select函数检测事件的fd的最大值加1
* @param readfds:需要监听可读事件的fd集合
* @param writefds:需要监听可写事件的fd集合
* @param exceptfds: 需要监听的异常事件的fd集合
* @param timeout:超时时间,即在这个参数设定的时间内检测这些fd的事件,超过这个时间后,select函数立即返回。
**/
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
fd_set是一个结构体信息,其定义位于/usr/include/sys/select.h 中,其定义如下:
typedef struct
{
long int __fds_bits[16]; //可以看作128bit的数组
} fd_set
在将一个fd添加到fd_set这个集合中时需要使用FD_SET宏,其定义如下:
void FD_SET(int fd, fd_set* set);
将一个fd从fd_set中删除需要使用FD_CLR,其定义如下:
void FD_CLR(int fd, fd_set* set);
如果需要将fd_set中所有fd全都清除,则使用FD_ZERO,其定义如下:
void FD_ZERO(fd_set* set);
当select函数返回时,我们使用FD_ISSET宏判断在某个fd是否有我们关心的事件,FD_ISSET宏的定义如下:
int FD_ISSET(int fd, fd_set* set);
select函数使用的基本流程
示例代码
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <sys/time.h>
#include <vector>
#include <errno.h>
static const int INVALID_FD = -1; //自定义代表无效fd的值
int main(int argc, char** argv)
{
//创建一个监听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd == INVALID_FD)
{
std::cout << "create listen socket error." << std::endl;
return -1;
}
//初始化服务器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if(bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
{
st::cout << "bind listen socket error." << std::endl;
close(listenfd);
return -1;
}
//启动监听
if(listen(listenfd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
close(listenfd);
return -1;
}
//存储客户端socket的数组
std::vector<int> clientfds;
int maxfd;
while(true)
{
fd_set readset;
FD_ZERO(&readset);
//将监听socket加入待检测的可读事件中
FD_SET(listenfd, &readset);
maxfd = listenfd;
//将客户端的fd加入待检测的可读事件中
int clientfds_length = clientfds.size();
for(int i = 0; i < clientfds_length; ++i)
{
if(clientfds[i] != INVALID_FD)
{
FD_SET(clientfds[i], &readset);
if(maxfd < clientfds[i])
{
maxfd = clientfds[i];
}
}
}
timeval tm;
tm.tv_sec = 1;
tm.tv_usec = 0;
//暂且检测可读事件,不检测可写和异常事件
int ret = select(maxfd + 1, &readset, NULL, NULL, &tm);
if(ret == -1)
{
if(errno != EINTR)
{
break; //出错,退出程序
}
}
else if(ret == 0)
{
//超时,继续
continue;
}
else
{
//检测到某个socket有事件
if(FD_ISSET(listenfd, &readset))
{
//监听socket的可读事件,表明有新的连接到来
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
//接收客户端的连接
int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &clientaddrlen);
if(clientfd == INVALID_FD)
{
//接受连接出错,退出程序
break;
}
//只接受连接,不调用recv收取任何数据
std::cout << "accept a client connection, fd:" << clientfd << std::endl;
clientfds.push_back(clientfd);
}
else
{
//假设对端发来的数据长度不超过63个字符
char recvbuf[64];
int clientfdslength = clientfds.size();
for(int i = 0; i < clientfdslength; ++i)
{
if(clientfds[i] != INVALID_FD && FD_ISSET(clientfds[i], &readset))
{
memset(recvbuf, 0, sizeof(recvbuf));
//非监听socket,接收数据
int length = recv(clientfds[i], recvbuf, 64, 0);
if(length <= 0)
{
//收取数据出错
std::cout << "recv data error, clientfd:" << clientfds[i] << std::endl;
close(clientfds[i]);
//不直接删除该元素,将该位置的元素标记为INVALID_FD
clientfds[i] = INVALID_FD;
continue;
}
std::cout << "clientfd:" << clientfds[i] << ", recv data:" << recvbuf << std::endl;
}
}
}
}
}
//关闭所有客户端socket
int clientfdslength = clientfds.size();
for(int i = 0; i < clientfdslength; ++i)
{
if(clientfds[i] != INVALID_FD)
{
close(clientfds[i]);
}
}
//关闭监听socket
close(listenfd);
return 0;
}
关于以上代码,在实际开发中有几个需要注意的点,如下:
- select 函数在调用前后可能会修改readfds, writefds和exceptfds这三个集合中的内容,如果想在下次调用select函数时复用这些fd_set变量,则要在下次调用前使用FD_ZERO将fd_set清零,然后调用FD_SET将需要检测事件的fd重新添加到fd_set中。
- select函数也会修改timeval结构体的值,如果想复用这个变量,则必须给timeval变量重新设置值。
- select函数的timeval结构体的tv_sec和tv_usec如果都被设置为0,即检测事件的总时间被设置为0,其行为是select检测相关集合中的fd,如果没有需要的事件,则立即返回
select 函数的缺点
- 每次调用select函数时,都需要把fd集合从用户态复制到内核态中,这个开销在fd较多时会很大,同时每次调用select函数都需要在内核中遍历传递进来的所有fd,这个开销在fd较多时也很大。
- 单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过先修改宏定义然后重新编译内核来调整这一限制,但这样非常麻烦且效率低下。
- select函数在每次调用之前都要对传入的参数进行重新设定,这样做也比较麻烦。
- 在Linux上,select函数的实现原理是其底层使用了poll函数
|