select模型支持IO多路复用,select函数如下
int select ( IN int nfds, //windows下无意义,linux有意义 IN OUT fd_set* readfds, //检查可读性 IN OUT fd_set* writefds, //检查可写性 IN OUT fd_set* exceptfds, //例外数据 IN const struct timeval* timeout); //函数的返回时间
逐个解释每个参数意义:
nfds:一个整型变量,表示比最大文件描述符+1
readfds: 这个集合监测读事件的描述符,将要监听
读事件的文件描述符放入readfds中,通过调用select,
readfds中将没有就绪的读事件文件描述符清除,留下
就绪的读事件描述符,可以通过read或者recv来处理
writefds:这个集合监测写事件的描述符,将要监听的
写事件的文件描述符放入writefds中,通过调用select,
writefds中没有就绪的写事件文件描述符被清除,留下
就绪的写事件描述符,可以通过write或者send来处理。
execptfds:这个集合在调用select后会存有错误的文件
描述符。根据Linux网络网络编程第二版中介绍,可以
监视带外数据OOB,带外数据使用MSG_OOB标志发送
到套接字上,当select()函数返回的时候,readfds将清除
其中的其他文件描述符,留下OOB数据
函数返回值:
当返回0时表示超时,-1表示有错误,大于0表示没有错误。
当监视文件集中有文件描述符符合要求,即读文件描述符集
合中有文件可读,写文件描述符集合中有文件可写,或者
错误文件描述符集合中有错误的描述符,都会返回大于0的数。
timeval结构体解释
struct timeval { long tv_sec; //秒 long tv_usec; //毫秒 };
timeval指针为NULL,表示一直等待,直到有符合条件的描述符
触发select返回
如果timeval中个参数均为0,表示立即返回,否则在select没有
符合条件的描述符,等待对应的时间和,然后返回。
另外需要了解一些select的操作宏函数
fd_set是一个SOCKET队列,以下宏可以对该队列进行操作:
FD_CLR( s, fd_set *set) 从队列set删除句柄s;
FD_ISSET( s,fd_set *set) 检查句柄s是否存在与队列set中;
FD_SET( s, fd_set *set )把句柄s添加到队列set中;
FD_ZERO( fd_set *set ) 把set队列初始化成空队列.
看一个select的使用示例
//前面是服务器socket的创建,绑定和监听 int listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // Bind local.sin_family = AF_INET; local.sin_addr.S_un.S_addr = htonl(INADDR_ANY); local.sin_port = htons(PORT); bind(listenSocket, (sockaddr*)&local, sizeof(SOCKADDR_IN)); // Listen listen(listenSocket, 3); //设置非阻塞 fcntl(listenSocket, F_SETFL, O_NONBLOCK ); //这个使用来统计所有已经连接上来的文件描述符, //也可以用数组表示,只是遍历的时候要根据数据结构进行更改 FD_SET socketSet; FD_SET writeSet; FD_SET readSet; //清空 socketSet FD_ZERO(&socketSet); //将文件描述符放入 socketSet, //用于accept FD_SET(listenSocket,&socketSet); //统计最大的socket int maxfd = listenSocket; int conNum = 1; //数组存储连接的socket int connectArray[1024]={0}; while(true) { //清空读写集合 FD_ZERO(&readSet); FD_ZERO(&writeSet); //读写都监听 readSet=socketSet; writeSet=socketSet; //同时检查套接字的可读可写性。 //为等待时间传入NULL,则永久等待。传入0立即返回。不要勿用。 int ret=select(maxfd,&readSet,&writeSet,NULL,NULL); if(ret==-1) { return false; } sockaddr_in addr; int len=sizeof(addr); //是否存在客户端的连接请求。 //在readset中会返回已经调用过listen的套接字 if(FD_ISSET(listenSocket,&readSet)) { acceptSocket=accept(listenSocket,(sockaddr*)&addr,&len); if(acceptSocket==INVALID_SOCKET) { return false; } else { //大于我们最大的监听数量了 if(conNum > 1024) { return false; } //更新数组 connectArray[conNum] = acceptSocket; //设置非阻塞 fcntl(connectArray[conNum], F_SETFL, O_NONBLOCK ); //加到socketset里,以后赋值给读写集合 FD_SET(connectArray[conNum],&socketSet); if(acceptSocket > maxfd) { maxfd = acceptSocket; } conNum++; } } for(int i=0;i<conNum;i++) { //判断是否有读事件 if(FD_ISSET(connectArray[i],&readSet)) { //调用recv,接收数据。 //判断recv结果,为0则客户端断开, //那么调用FD_CLR并关闭对应的socket } //判断是否有写事件 if(FD_ISSET(connectArray[i],&writeSet) { //调用send,发送数据。 //判断send结果,为0客户端断开, //那么调用FD_CLR并关闭对应的socket } } }
上面的例子结合了网上提供的一些demo,其实writeSet不一定要放入socket,当某个socket需要send内容时
再调用FD_SET(socket,&writeSet),写成功后再调用FD_CLR(socket,&writeSet);避免造成busyloop,
因为当缓冲区非空时,写事件是一直就绪的。
我的微信公众号,定期推送一些技术总结,一起努力吧。