一、为什么需要?
看上一篇文章
二、基本思想
#先构造一张有关描述符的表,然后调用一个函数,这些文件描述符的一个或者多个已准备好进行IO操作时的函数才返回;
函数返回时告诉进程那个描述符已就绪,可以进行IO操作。
三、select函数
1.头文件
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
2.函数体
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval
*timeout);
3.参数
nfds:最大的文件描述符+1
readfds:读事件的表
writefds:写事件的表
exceptfds:异常事件的表
timeout:超时检测 //填NULL表示一直阻塞,直到文件描述符准备好为止
4.宏选项
用于设置文件描述符
void FD_CLR(int fd, fd_set *set);//把表中的一个文件描述符删除
int FD_ISSET(int fd, fd_set *set);//检测文件描述符是否准备好了,准备好了返回1,否则返回0
void FD_SET(int fd, fd_set *set);//加入到表中
void FD_ZERO(fd_set *set);//清空表
5.返回值
成功:准备好的文件描述符个数
失败:-1;
6.优缺点分析
select基本原理:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>
int main(int argc, const char *argv[])
{
int fd1 = open("./f1", O_RDWR);
int fd2 = open("./f2", O_RDWR);
int fd3 = open("./f3", O_RDWR);
//1.创建一张文件描述符表
fd_set readfds, tmpfds;
FD_ZERO(&readfds); //2.清空表
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
FD_SET(fd3, &readfds);
int maxfd = fd3;
tmpfds = readfds;
char buf[64] = {0};
int ret, i;
while(1)
{
readfds = tmpfds;
ret = select(maxfd+1, &readfds, NULL, NULL,NULL);
if(ret == -1)
{
perror("select");
return -1;
}
for(i=fd1; i<maxfd+1; i++)
{
if( FD_ISSET(i, &readfds) )
{
read(i, buf, sizeof(buf));
printf("%s\n", buf);
memset(buf, 0, sizeof(buf));
}
}
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
案例二
利用复用IO,解决多个客户端对服务器进行收发数据阻塞的问题
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>
int main(int argc, const char *argv[])
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror("sockfd");
return -1;
}
printf("sockfd = %d\n", sockfd);
//端口复用函数:解决端口号被系统占用的情况
int on = 1;
int k = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if(k == -1)
{
perror("setsockopt");
return -1;
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0 ,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(8888);
serveraddr.sin_addr.s_addr = inet_addr("0"); //自动路由,寻找IP地址
int len = sizeof(serveraddr);
int ret = bind(sockfd, (struct sockaddr *)&serveraddr, len);
if(ret == -1)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 5);
if(ret == -1)
{
perror("listen");
return -1;
}
//select机制
fd_set readfds,tmpfds;//readfds读事件,tmpfds是用来修正表的
FD_ZERO(&readfds);//清空表
FD_SET(sockfd, &readfds);//加入表
int maxfd = sockfd;//把文件描述符3设置为当前最大的文件描述
tmpfds = readfds;//用于修正读事件表
char buf[64] = {0};
int i;
while(1)
{
readfds = tmpfds;//修正表
ret = select(maxfd+1, &readfds, NULL, NULL, NULL);
if(ret == -1)
{
perror("select");
exit(-1);
}
for(i=sockfd; i<maxfd+1; i++)
{
if(FD_ISSET(i, &readfds))
{
if(i== sockfd)//先处理sockfd也就是3号文件描述符
{
printf("%d 已经准备好了\n", i);
//建立连接,用accept函数
int connfd = accept(i, NULL, NULL);
printf("%d is link\n", connfd);
FD_SET(connfd, &tmpfds);
if(maxfd < connfd)
{
maxfd = connfd;
}
}
else{
memset(buf, 0, sizeof(buf));
//处理connfd
ret = recv(i, buf, sizeof(buf), 0);
if(ret == -1)
{
close(i);
perror("recv");
return -1;
}
else if(ret == 0)
{
printf("%d is unlink\n", i);
FD_CLR(i, &tmpfds);
close(i);
break;
}
else{
printf("%d:message=%s\n", i,buf);
}
}
}
}
}
close(sockfd);
return 0;
}
四、理解事件表的修正
文件描述符 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
事件响应标志位 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
注:文件描述符的0 、1 、2 指的是标准输入、输出、错误输出
所以这里的读事件表表示的就是 4号 5号 6号文件描述符代表的客户端已连接上了 3号文件描述符代表的服务器,并且已加入读事件表,事件响应标志位的0代表的是该客户端并未就绪
----------------------------------------------------------------------------------------------------------------------------
当4号文件描述符所代表客户端向服务器发送消息时,select函数会轮询检测到该文件句柄发生变化(就绪态),就把该客户端的事件响应标志位置为1,如下图所示
文件描述符 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
事件响应标志位 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
———————————————————————————————————————————
当后续操作(服务器收到消息并打印出来)执行完时;需要把其对应的事件响应标志位置为初始状态,也就是0(上述代码中是用tmpfds保存的读事件表的初始状态)
保存读事件表的初始状态
———————————————————————————————————————————
把读事件表置为初始状态