一、select函数
int select (
int __nfds, //指定被监听的文件描述符总数
fd_set *__readfds, //可读事件文件描述符集合
fd_set *__writefds, //可写事件文件描述符集合
fd_set *__exceptfds, //异常事件文件描述符集合
struct timeval *__timeout //该函数的超时时间
);
1.fd_set结构体
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
} fd_set;
从文档中复制下来的,删除了一些没有用的东西
虽然看起来复杂,但其实就是一个整型数组,该整形数组中的每一位(bit)标记一个文件描述符,且有最大数量,一般来说最好不要超过1024个
由于是位操作,很麻烦,所以官方提供了四个宏
FD_ZERO
fd_set se;
FD_ZERO(&se)
该宏用于清楚fd_set结构体中所有的位
FD_SET
fd_set se;
FD_SET(1,&se);
该宏用于设置某位
FD_CLR
fd_set se;
FD_CLR(1,&se); //清楚fs_set的位
该宏用于清除某位
FD_ISSET
fd_set se;
FD_ISSET(1,&se); //测试fs_set的位是否被设置
该宏用于测试位是否被设置
2.timeval 结构体
struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
该结构体有两个成员,一个为秒,一个为微妙
当这两个值均为0时,select函数将立即返回,如果传入NULL,则一直阻塞等待,直到某个文件描述符就绪
3.返回值
当select成功时,将返回就绪的文件描述符总数
如果超过时间没有任何文件描述符就绪,将返回0
如果失败,则返回-1,并设置errno
二、文件描述符就绪条件
网络编程中,下列情况被认为socket可读:
- socket内核接收缓存区中的字节数大于或等于其低水位标记SO_RECVLOWAT.此时可以无阻塞的读该socket,并且返回字节数大于0
- socket通信的对方关闭连接,此时对socket读操作将返回0
- 监听socket上有新的连接请求
- socket上有未处理的错误,此时可以使用getsockopt来读取和清除该错误
socket可写的情况:
- socket内核发送缓存区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时可无阻塞的写该socket,并且写操作返回字节数大于0
- socket的写操作被关闭,对关闭了写操作的socket执行写操作将触发SIGPIPE信号
- socket使用非阻塞connect连接成功或者失败之后
- socket上有未处理的错误,此时可以使用getsockopt来读取和清除该错误
socket异常:
- 只有当socket上接收到带外数据
三、使用
以下代码通过使用该函数实现了一个简陋的回声服务器
#include<iostream>
#include<unistd.h>
#include<assert.h>
#include<sys/wait.h>
#include<sys/select.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<string.h>
using namespace std;
int main(int arg, char* argv[])
{
int sock_listen,sock_cli; //创建监听socket和与客户端通信socket
sock_listen = socket(PF_INET,SOCK_STREAM,0); //创建socket
if (sock_listen == -1) return -1;
sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
int ret=bind(sock_listen,(sockaddr*)&addr,sizeof(addr));
if (ret == -1) return -1;
ret=listen(sock_listen,5);
if (ret == -1) return -1;
fd_set rfd;
FD_ZERO(&rfd);//清零
FD_SET(sock_listen, &rfd); //将监听socket置信
int maxFd = sock_listen; //记录当前最大文件标识值
fd_set copy_rfd; //rfd的副本,因为每次调用完select都会改变原值,所以需要用一个副本
int d = 0;
while (true) {
copy_rfd = rfd; //每次循环调用副本
int ret = select(maxFd + 1, ©_rfd, NULL, NULL, NULL); //根据副本等待读事件
if (ret == -1) { //返回-1则代表失败,打印错误
printf("%d:%s\n", errno, strerror(errno));
break;
}
//循环所有文件标识位
for (int i = 0; i <= maxFd + 1; i++) {
if (!FD_ISSET(i, ©_rfd)) continue; //遇到没有被设置的跳过,即该文件标识没有事件需要处理
if (i == sock_listen) { //如果有需要处理的事件是监听socket上的,则必然为有客户端连接
sockaddr_in tmpAddr;
socklen_t len = sizeof(tmpAddr);
sock_cli = accept(sock_listen, (sockaddr*)&tmpAddr, &len);
if (sock_cli==-1) {
printf("%d:%s\n", errno, strerror(errno)); //接收客户端连接出现错误,打印错误信息
break;
}
FD_SET(sock_cli, &rfd); //将客户端通信socket置信
if (maxFd < sock_cli) { //如果客户socket文件标识大于最大文件标识,则取代之
maxFd = sock_cli;
}
}
else {
char buf[0xFF]{};
size_t len = recv(sock_cli,buf,0xFF,0); //接收客户端数据
if (len == -1) { //发生错误
FD_CLR(sock_cli, &rfd); //清除客户端置位
continue;
}
else if (len == 0) { //客户端断开连接
FD_CLR(sock_cli, &rfd); //清除客户端置位
continue;
}
cout << buf << endl;
send(sock_cli,buf,len,0); //将接收到的数据发送回客户端
}
}
}
}
下面简述一下该代码的流程:
- 首先,先做好前置工作:创建、绑定监听
- 然后就是要创建一个
fd_set
变量,然后使用上面介绍的宏,对它进行操作 - FD_ZERO将所有标志置零,FD_SET将监听套接字的标志置1,并且创建了一个
fd_set
变量的副本,方便后面操作 - 然后进入循环,通过
select
函数,我们就可以很方便的监听所有套接字(目前只有一个监听套接字) - 一旦该函数返回了,就说明至少有一个套接字触发了事件,所以我们就需要对所有值进行遍历查找,是哪个套接字发生了变化
- 最后再根据具体情况对比,判断是哪个套接字,如果是监听套接字,那就只有一个别人连接上来的消息,所以就要在标志中新增一个客户端的标志
- 而如果是其它,那就是与客户端通信的套接字,又由于这里我们只进行监听了接收信息,那必然是收到消息了