目录
一、概念
二、使用
1.select系统调用
代码实现
前言:
一般多客户端在和服务器通信时,服务器在执行recv时会先阻塞,然后按照顺序依次处理客户端,无论客户端有无数据都会被处理,这样大大降低了执行效率。此时就引入i/o复用技术,提高网络程序效率
io的处理方式是没有数据的客户端忽略不管,一旦接收就阻塞起来,有数据的客户端接收。
一、概念
- I/O复用可以同时监听多个文件描述符/多个套接字,以及检测套接字描述符内有无数据
- 通常,以下情况下需要使用i/o复用:
- 客户端程序要同时处理多个socket,虽然多线程也可以解决该问题,但是当客户端个数逐渐增大时,在多线程之间切换的开销会大大加大。
- 客户端程序要同时处理用户输入和网络连接
- TCP服务器要同时处理监听socket和连接socket。这是i/o复用使用最多的场合
- 服务器要同时处理TCP请求和UDP请求
- 服务器要同时监听多个端口,或处理多种服务
- Linux下实现i/o复用的系统调用主要有select、poll和epoll。
-
i/o复用操作流程:
-
先检查哪些描述符上有数据,哪些无数据
-
找到有数据的描述符,然后进行处理
-
需要指出的是,I/O 复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依处理其中的每一个文件描述符,这使得服务器看起来好像是串行工作的。如果要提高并发处理的能力,可以配合使用多线程或多进程等编程方法。
二、使用
1.select系统调用
- 用途:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。
- 函数原型:
#include<sys/select.h>
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
- 参数:
- 第一个参数nfds指定被监听的文件描述符总数。通常被设置为select监听的所有文件描述符中的最大值+1,因为文件描述符是从0开始计数的。
- 第二、三、四个参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用select函数时,通常从这三个参数中传入自己感兴趣的文件描述符。
- 第五个参数timeout用来设置select函数的超时事件。它是timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。
-
struct timeval { long tv_sec;//微秒 long tv_usec;//微秒数 }
select提供了一个微秒级的定时方式。如果timeout变量的tv_sec成员和tv_usec成员都传递0,则select将立即返回。如果timeout传递NULL,则select将一直阻塞,直到某个文件描述符就绪。
-
- 返回值:select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。这三个参数时fd_set结构指针类型。fd_set结构体定义在下面讲。
- 返回值:
- 成功时返回就绪(可读、可写和异常)文件描述符的总数(不会告知是哪个文件描述符,而是总数)。
- 返回值为0表示超时,在超时时间内没有任何文件描述符就绪
- 返回值为-1可能是失败,并设置srrno;如果在在select等待期间,程序收到信号,则select也会立即返回-1,并设置为EINTR。
fd_set结构体定义:
由以上定义可见,该结构体仅包含了一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符由FD_SETSIZE指定,可容纳文件描述符的位数是1024位,0-1023。按位偏移,0放在第一个位上。
由于位操作的繁琐(按位与规则:1&1=1. 1&0=0. 0&1=0. 0&0=0. 只要和1相与结果不为0则为真,也就是该文件描述符被设置过。1>>3 就是第三位和1按位与操作),我们使用下面的一系列宏来访问fd_set结构体中的位:
#include<sys/select.h>
FD_ZERO(fd_set *fdset); //轻触fdset中所有位
FD_SET(int fd,fd_set *fdset);//设置fdset的位fd
FD_CLR(int fd,fd_set *fdset);//清除fdset的位fd
int FD_ISSET(int fd,fd_set *fdset);//测试fdset的位fd是否被设置
什么叫事件就绪?
以读事件和写事件为例:
- 读事件:当执行recv()时,把数据发送到接收缓冲区,如果接收缓冲区满,此时recv就会被阻塞,读事件就没有就绪,反之,缓冲区未满,读事件就绪
- 写事件:当执行send()时,把数据写入到发送缓冲区,如果发送缓冲区满,此时send就会被阻塞,写事件就没有就绪,反之,缓冲区未满,写事件就绪
文件描述符就绪条件:
哪些情况下文件描述符可以被认为是可读、可写或异常情况,对于select的使用非常的关键。
- 在网络编程中以下情况socket可读:
- socket内核接收缓冲区中字节数大于或等于其低水位标记SO_RECVLOWAT。此时可以无阻塞的读该socket,并且读操作返回的字节数大于0.
- socket通信的对方关闭连接。此时对该socket的读操作返回0
- 监听socket上有新的连接请求
- socket上有未处理的错误。此时可以使用getsockopt来读取和清除该错误。
- 以下socket可写:
- socket内核发送缓冲区中的可用字节数>=其低水位标记SO_SNDLOWAT。此时我们可以无阻塞的写该socket,并且写操作返回的字节数>0
- soket的写操作被关闭。对写操作被关闭的socket执行写操作将出发一个SIGPOPE信号。
- socket使用非阻塞connect连接成功或失败(超时)之后
- socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误
- 网络编程中,select能处理的异常只要一种:socket上接收到带外数据
代码实现
i/o实现流程:
1.i/o函数检测描述符的数据有无情况。返回值会告知有几个
2.具体检测哪个文件描述符有数据
2.多个描述符内查找我们感兴趣的事件
下面用代码实现在键盘输入文件描述符,有数据打印出来,没数据就进行检测,最长等待5秒:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/select.h>
#include<sys/time.h>
#define STDIN 0
int main()
{
int fd=STDIN;
fd_set fdset;//收集描述符
//1.描述符中最大值+1
while(1)//如果有多个描述符就要循环存放
{
FD_ZERO(&fdset);//清空集合
FD_SET(fd,&fdset);//把键盘对应的描述符fdset添加到集合内
struct timeval tv={5,0};//超时时间
int n=select(fd+1,&fdset,NULL,NULL,&tv);//当前操作写事件
if(n==-1)
{
printf("select err\n");
}
else if(n==0)
{
printf("time out\n");
}
else//就绪状态,有用户输入
{
if(FD_ISSET(fd,&fdset))//fd_isset()判断当前描述符内是否有数据,有数据则返回为真,执行下面语句
{
char buff[128]={0};
read(fd,buff,127);//开始读取数据
printf("buff=%s\n",buff);//输出数据
}
}
}
}
运行结果:
如果超过5秒未输入或者输入的时间超过5秒还未发送,都会弹出“time out”超时的提示
如果上次的还没发出去(输入的太慢超过5秒)会在缓冲区内存着跟着下次一起发送
用select实现tcp服务器代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/select.h>
#include<sys/time.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define MAXFD 10
//初始化描述符
void fds_init(int fds[])
{
for(int i=0;i<MAXFD;i++)
{
fds[i]=-1;
}
}
//添加描述符
void fds_add(int fd,int fds[])
{
for(int i=0;i<MAXFD;i++)
{
if(fds[i]=-1)//说明未被使用
{
fds[i]=fd;
break;
}
}
}
//移除描述符
void fds_del(int fd,int fds[])
{
for(int i=0;i<MAXFD;i++)
{
if(fds[i]==fd)
{
fds[i]=-1;
break;
}
}
}
//创建tcp监听套接字
int socket_init()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
return -1;
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res==-1)
return -1;
res=listen(sockfd,5);
if(res==-1)
return -1;
return sockfd;
}
int accept_client(int sockfd)
{
struct sockaddr_in caddr;
int len=sizeof(caddr);
int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
return c;
}
int main()
{
int fds[MAXFD];//用于收集描述符
fds_init(fds);//初始化描述符
int sockfd=socket_init();//初始化套接字
if(sockfd==-1)
exit(0);
fds_add(sockfd,fds);//将套接字添加到描述符
fd_set fdset;//集合收集描述法
//找到描述符最大值,并将描述法都添加到集合内
while(1)
{
FD_ZERO(&fdset);
int maxfd=-1;
for(int i=0;i<MAXFD;i++)
{
if(fds[i]==-1)
continue;
FD_SET(fds[i],&fdset);//将数组中有效(>=0)的描述符添加到集合
if(fds[i]>maxfd)
{
maxfd=fds[i];//找到最大的描述符
}
}
struct timeval tv={5,0};
int n=select(maxfd+1,&fdset,NULL,NULL,&tv);//读
if(n==-1)
{
printf("select err\n");
}
else if(n==0)
{
printf("time out\n");
}
else
{
for(int i=0;i<MAXFD;i++)
{
if(fds[i]==-1)
{
continue;
}
if(FD_ISSET(fds[i],&fdset))//测试该监听套接字是否有数据
{
if(fds[i]==sockfd)//监听套接字,accept
{
int c=accept_client(fds[i]);
if(c!=-1)
fds_add(c,fds);//添加新接收的连接
}
else//连接套接字,recv处理
{
char buff[128]={0};
int num=recv(fds[i],buff,127,0);
if(num<=0)
{
close(fds[i]);
fds_del(fds[i],fds);
}
else
{
printf("recv:%s\n",buff);
send(fds[i],"ok",2,0);
}
}
}
}
}
}
}