谈到IO复用就不得不了解一下,都有哪些关于IO复用的方法
IO复用的方法分为3种,这篇重点介绍select,至于其余的两种方法将会在下一篇介绍,考虑到篇幅和问题研究的专一性,这篇的重点就是分析select接口和原理,以及select使用的实例,如果对于为什么要使用IO复用以及IO复用的好处,可以翻看上一篇博客。
建议在了解这篇博客之前,先了解这些概念:阻塞、非阻塞、以及文件描述符的就绪状态、IO复用(以上这些可以在上一篇IO中已有一些介绍)。
IO复用的三种方法:select、poll、epll,这几种方式均为(同步IO处理)
select:
IO复用在设计上遵守一下规则:
1.I/O多路复用:当一个文件描述符I/O就绪时进行通知。
2.都不可用?在有用的文件描述符到来之前处于睡眠状态。
3.唤醒:哪个文件描述符可用了?
4.处理所有I/O就绪的文件描述符,没有阻塞。
5.返回第1步,重新开始。
select()系统调用:
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数说明:
nfds:这个值对应于所要监控的所有描述符中描述符值最大的加1
readfds:是一个fd_set类型,fd_set是一个结构体数组仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符数量有FD_SETSIZE决定,这个限制了select同时能处理的文件描述的总量。Linux下默认是1024。readfds可以看作一个位图,其中加入的文件描述符,我们关心的是它们的可读事件。
writefds:可以对照readfds理解。
exceptfds:在这个位图中加入的文件描述符,我们只关心他们的可写事件。
timeout:前面我们说过,当没有就绪的文件描述符到来时select一直处于阻塞状态,而有了这个timeout之后,程序会在调用处select阻塞timeout时间段,直到有就绪的文件描述符时才返回,或者超出这段时间时返回。
注意: select调用之后,readfds、writefds、exceptfds均会被改变,因此我们在每次使用之前需要将它们重置。
协助select工作的宏函数:
void FD_CLR(int fd, fd_set *set);//将一个文件描述符从set中清除
int FD_ISSET(int fd, fd_set *set);//判断某一个文件描述是否在set中
void FD_SET(int fd, fd_set *set);//将某一个文件描述加入到set
void FD_ZERO(fd_set *set);//将set全置为0
下面是一段是关于select使用的详细实例:
实例一:监视标准输入
#include<unistd.h>
#include<stdio.h>
#include<fcntl.h>
#include<sys/select.h>
int main()
{
//要监控的描述符
int fd = STDIN_FILENO;
struct timeval s;
s.tv_sec = 2;
//select使用之前的fd_set
fd_set read_set;
fd_set temp_set;
FD_ZERO(&read_set);
int nfds = fd;
//将标准输入加入到read_set中
FD_SET(fd,&read_set);
temp_set = read_set;
//持续监控stdin状态是否发生变化
while(1)
{
//因为select每次都会对read_set进行修改,所以每次调用select时,进行重置
read_set = temp_set;
s.tv_sec = 2;
int ret = select(nfds+1,&read_set,NULL,NULL,&s);
if(ret<0)
{
printf("select!\n");
return 1;
}
else if(ret == 0)
{
printf("time out!\n");
//continue;
}
char buf[1024] = {0};
if(FD_ISSET(fd,&read_set))
{
int rz = read(fd,buf,1023);
if(rz<0)
{
printf("read!\n");
return 3;
}
if(rz == 0)
{
printf("断开该文件描述符!\n");
close(fd);
}
printf("read:%s",buf);
}
}
return 0;
}
实例二:通过select实现tcp服务端
server.cc:
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/select.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
int main(int argc,char* argv[])
{
if(argc<3)
{
printf("./server [ip] [port]\n");
return 1;
}
//1.创建socket
int listen_sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(listen_sock<0)
{
printf("socket\n");
return 2;
}
printf("%d\n",listen_sock);
//2.绑定
struct sockaddr_in listen_addr;
listen_addr.sin_family = AF_INET;
listen_addr.sin_addr.s_addr = inet_addr(argv[1]);
listen_addr.sin_port = htons(atoi(argv[2]));
socklen_t len = sizeof(listen_addr);
if((bind(listen_sock,(const struct sockaddr*)&listen_addr,len))<0)
{
perror("bind!\n");
return 3;
}
//3.改套接字状态为监听状态
if(listen(listen_sock,5)<0)
{
printf("listen!\n");
return 4;
}
//这里才是select的重点部分
//4.多路复用部分
fd_set read_set;
fd_set temp_set;//因为select每次都会修改read_set,通过temp_set备份
int max = 0;
int a_size = 0;
//添加listen套接字到read_set中
FD_ZERO(&read_set);
FD_ZERO(&temp_set);
FD_SET(listen_sock,&read_set);
max = listen_sock;
a_size++;
temp_set = read_set;
//文件描述符表
int a[1024] = {-1};
a[0] = listen_sock;
//5.循环处理业务
while(1)
{
read_set = temp_set;
sockaddr_in peer_addr;
FD_SET(listen_sock,&read_set);
struct timeval t;
t.tv_sec = 2;
int ret = select(max+1,&read_set,NULL,NULL,&t);
if(ret<0)
{
printf("select\n");
}
if(ret == 0)
{
printf("time out!\n");
}
if(FD_ISSET(listen_sock,&read_set))
{
int connect_fd = accept(listen_sock,(struct sockaddr*)&peer_addr,&len);
if(connect_fd<0)
{
printf("accept!\n");
return 5;
}
//判断fd是否加入到文件描述符表
a[a_size++] = connect_fd;
//将connect_fd加入到temp_set中
FD_SET(connect_fd,&temp_set);
//判断是否需要更新max
if(connect_fd>max)
max = connect_fd;
}
//处理connect_fd
for(int i = 0;i<a_size;i++)
{
if(FD_ISSET(a[i],&read_set))
{
char buf[1024] = {0};
int rz = read(a[i],buf,1023);
if(rz<0)
{
printf("read!\n");
continue;
}
if(rz == 0)
{
printf("peer close!!\n");
close(a[i]);
}
printf("peer say:%s\n",buf);
write(a[i],buf,strlen(buf));
}
}
}
return 0;
}
客户端就是我们之前写过的现在拿来直接有就可以:
client.cc:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
int main(int argc,char* argv[])
{
if(argc<3)
{
printf("./client [IP] [port]\n");
return 1;
}
//创建套接字
int cli_sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(cli_sock<0)
{
printf("socket!\n");
return 2;
}
//连接服务端
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(atoi(argv[2]));
socklen_t len = sizeof(addr);
int ret = connect(cli_sock,(const struct sockaddr*)&addr,len);
if(ret<0)
{
printf("connect!\n");
return 0;
}
while(1)
{
char buf[1024]={0};
printf("client[enter]:");
fflush(stdout);
scanf("%s",buf);
write(cli_sock,buf,1024);
read(cli_sock,buf,1023);
printf("server[say]:%s\n",buf);
}
close(cli_sock);
}
实例运行效果:
客户端1:
客户端2:
服务端: