select系统调用的用途是:在指定一段时间内,监听用户感兴趣的文件描述符的可读、可写和异常等事件。
select的原型如下:
int select(int maxfd, struct fd_set *readfds,struct fd_set *writefds, struct fd_set *exceptfds, struct timeval *timeout)
maxfd:监听的最大文件描述符的值+1(原因后面详述)
readfds,writefds, exceptfds:分别关注可读、可写、异常三种事件文件描述符
调用select时,三个结构体变量回想内核传递用户关注的文件描述符
select返回时,三个结构体变量会向用户传递有事件发生的文件描述符总数
Readfds writefds exceptfds 内核会在线修改三个结构体,每次调用select,都必须重新设置三个值
timeout:设置select一次监听的时间,当时间到达没有文件描述符就绪,则返回0,有就绪文件描述符返回>0,出错返回-1。
fd_set结构如下:
typedef struct
{
long int fds_set[32];
}fd_set;
由定义可见,fd_set结构体仅包含一个整形数组,该数组每一个元素的每一位(bit)标记一个文件描述符。fd_set的文件描述符数量由 宏 FD_SETSIZE(源码中)指定,限制了select能同时处理的文件描述符的总量。
实现select需要解决的问题:
1.如何将用户关注的文件描述符设置到fd_set结构变量上
2.select返回之后如何判断那些文件描述符有事件发生
对于位运算操作,系统提供了4个宏函数分别是
FD_ZERO(fd_set *fds);初始化 清空
FD_SET(int fd, fd_set *fds); 将fd设置到fds上
FD_CLR(int fd, fd_set *fds); 清楚fds上的fd
FD_ISSET(int fd, fd_set *fds) 判断fds上的fd文件描述符是否有事件发生
由于select返回的是就绪文件描述符的总数,所以我们需要去用FD_ISSET()遍历所有返回的文件描述符。
解决方法:
1.使用宏函数
2.使用FD_ISSET()遍历返回的所有文件描述符
Select缺点:
1.只能关注三种事件类型
2.select的三个fd_set参数是在线修改的,每次都必须重新设置
3.fd_set 最多纪录1024个文件描述符,最大值1023
4.仅仅返回了就绪文件描述的个数,用户需要探测就绪的文件描述符的时间复杂度O(n)
5.内核使用轮询的方式检测就绪文件描述符,内核时间复杂度 O(n)(将maxfd+1,传入select是为了优化内核检索文件描述符,不需要每次都检索1024(文件描述符的容量),检索至maxfd+1的位置即可 )
具体实现代码如下:
定义全局数组,用来存放文件描述符以及对应链接的IP地址和端口号
#define FDMAX 100
typedef struct ClientInfo
{
int fd;//存放接收到的文件描述符
struct sockaddr_in addrInfo;//存放该链接的信息
}ClientInfo;
ClientInfo fdall[FDMAX];
//初始化函数,文件描述符不会为-1,故将所有用户数组中的文件描述符置-1
void InitFd()
{
int i = 0;
for(; i<FDMAX; ++i)
{
fdall[i].fd = -1;
}
}
//将获取到的文件描述符添加到用户数组中等待设置到相应的事件结构上
void AddFd(int fd, struct sockaddr_in ser)
{
int i = 0;
for(; i<FDMAX; ++i)
{
if(fdall[i].fd == -1)
{
fdall[i].fd = fd;
fdall[i].addrInfo = ser;
return;
}
}
}
//检索用户数组中的文件描述符,使用FD_SET()将获取的文件描述符添加到该事件上
void SetFd(fd_set *fds)
{
int i = 0;
for(; i<FDMAX; ++i)
{
if(fdall[i].fd != -1)
{
if(fdall[i].fd > maxfd)
maxfd = fdall[i].fd;
FD_SET(fdall[i].fd, fds);
}
}
}
//当一个连接断开时需要删除用户数组中存放的相应文件描述符
void DelFd(int fd)
{
int i = 0;
for(; i<FDMAX; ++i)
{
if(fdall[i].fd == fd)
{
fdall[i].fd = -1;
return;
}
}
}
//当获取的文件描述符为一个新的客户端链接时,接收(accept())新的链接,并将该文件描述符存入用户数组中等待设置到事件上
void GetClientLink(int listenfd)
{
struct sockaddr_in cli;
int len = sizeof(cli);
int n = accept(listenfd, (struct sockaddr*)&cli, &len);
if(n == -1)
{
return ;
}
AddFd(n, cli);
}
//当获取的文件描述符是客户端的信息时,去处理接收到的信息
void DealClientData(int fd, struct sockaddr_in cli)
{
char buff[128] = {0};
int n = recv(fd, buff, 127, 0);
if(n <= 0)
{
DelFd(fd);
return;
}
printf("%s:%d %s\n", inet_ntoa(cli.sin_addr), ntohs(cli.sin_port), buff);
send(fd, "OK", 2, 0);
}
//处理已经就绪的文件描述符,检索用户数组中的文件描述符是否有对应的事件发生,对不同类型的文件描述符进行不同处理
void DealFinshEvent(int listenfd, fd_set *fds)
{
int i = 0;
for(; i<FDMAX; ++i)
{
if(fdall[i].fd != -1)
{
int fd = fdall[i].fd;
if(FD_ISSET(fd, fds))
{
if(fd == listenfd)
{
GetClientLink(fd);
}
else
{
DealClientData(fd, fdall[i].addrInfo);
}
}
}
}
}
int main()
{
//首先进行TCP连接
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
assert(listenfd != -1);
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(6000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(listenfd, (struct sockaddr*)&ser, sizeof(ser));
listen(listenfd, 5);
//初始化存放文件描述符的用户数组
InitFd();
//将listenfd连接文件描述符添加到用户数组中
AddFd(listenfd, ser);
//定义一个关注读事件结构
fd_set read;
while(1)
{
//将用户数组中的文件描述符设置到read结构中
SetFd(&read);
//因为只关注了读事件,故其他时间为NULL,超时时间为NULL,无文件描述符会阻塞
int n = select(maxfd+1, &read, NULL, NULL, NULL);
if(n <= 0)
{
exit(0);
}
//处理已经就绪的文件描述符
DealFinshEvent(listenfd, &read);
}
}