I/O复用
I/O复用的作用:使得程序能同时监听多个文件描述符,提高程序的性能
I/O复用的应用场景
- cli程序需要处理多个socket.
- cli程序要同时处理用户连接和网络连接。
- TCP服务器要同时处理监听socket和连接socket.
- 服务器要同时处理TCP请求和UDP请求
- 服务器要同时监听多个端口,处理多种服务。
select系统调用
作用:在一段指定的时间内,监听用户感兴趣的文件描述符上的就绪事件。
就绪条件
-
读就绪
-
socket内核接收缓冲区中的字节数>=其低水位标记SO_RCVLOWAT,此时该socket刻度,并且读操作返回的字节数>0
-
socket通信的对方关闭连接。socket的读操作返回的字节数=0
-
监听socket上由新的请求
-
socket上由未处理的错误。可调用getsockopt来读取和清除该错误。
-
写就绪
-
socket内核发送缓冲区中的可用字节数>=其低水位标记SO_SNDLOWAT,该socket可写,写操作返回的字节数>0
-
socket的写操作关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号
-
socket使用非阻塞connect连接成功或失败(超时)后
-
socket由未处理的错误。此时可以使用getsockopt来读取和清除该错误。
select系统调用API
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开始计数的。
- readfds、writefds、exceptfds参数分别指向可读、可写和异常事件对应的文件描述符集合。select返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。
- fd_set结构体
#include<typesizes.h>
#define _FD_SETSIZE 1024
#include<sys/select.h>
#define FD_SETSIZE _FD_SETSIZE
typedef long int _fd_mask;
#undef _NFDBITS
#define _NFDBITS (8*(int) sizeof(_fd_mask))
typedef struct
{
#ifdef _USE_XOPEN
_fd_mask fds_bits[_FD_SETSIZE/ _NFDBITS]; //32个元素,每个元素是long int 类型
#define _FDS_BITS(set) ((set)->fds_bits)
#else
_fd_mask _fds_bits[_FD_SETSIZE/ _NFDBITS];
#define _FDS_BITS(set) ((set)->_fds_bits)
#endif
}fd_set;
fd_set结构体包含一个整型数组,数组中每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了能同时处理的文件描述符的总量。
使用下述宏来访问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是否被设置
timout参数用来设置select函数的超时时间。他是一个timeval 结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。但调用失败时timeout值是不确定的。
timeval 结构体
struct timeval
{
long tv_sec; //秒数
long tv_userc; //微秒数
}
如果给timeout变量的tv_sec成员和tv_usec成员都传递0,则select将立即返回。如果给timeout变量传递NULL,则select将一直阻塞,直到某个文件描述符就绪。
select成功时返回就绪文件描述符的个数。如果在指定时间内没有任何文件描述符就绪,select将返回0
select应用举例
ser
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/time.h>
#define MAXFD 10
void fds_add(int fds[],int fd)//将文件描述符添加进fds数组中
{
int i=0;
for(;i<MAXFD;++i)
{
if(fds[i]==-1)
{
fds[i]=fd;
break;
}
}
}
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
printf("sockfd=%d\n",sockfd);
struct sockaddr_in saddr,caddr;
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));
assert(res!=-1);
listen(sockfd,5);
fd_set fdset;//定义fdset集合
int fds[MAXFD];
int i=0;
for(;i<MAXFD;++i)
{
fds[i]=-1;
}
fds_add(fds,sockfd);
while(1)
{
FD_ZERO(&fdset);//清0
int maxfd=-1;
int i=0;
for(;i<MAXFD;i++)
{
if(fds[i]==-1)
{
continue;
}
FD_SET(fds[i],&fdset);
if(fds[i]>maxfd)
{
maxfd=fds[i];
}
}
struct timeval tv={5,0};//设置超时时间
int n=select(maxfd+1,&fdset,NULL,NULL,&tv);//select系统调用,在这里我们只关注读事件
if(n==-1)//失败
{
perror("select error");
}
else if(n==0)//没有任何文件描述符返回
{
printf("time out\n");
}
else//有就绪事件产生
{
/*
由于我们只能通过select的返回值知道就绪事件的个数,而无法知道具体是哪些事件就绪
因此,需要遍历每一个文件描述符进行判断
*/
for(i=0;i<MAXFD;++i)
{
if(fds[i]==-1)
{
continue;
}
if(FD_ISSET(fds[i],&fdset))//该文件描述符对应的事件就绪
{
//对文件描述符分两种情况进行判断
if(fds[i]==sockfd)//文件描述符是套接字,表示有新的客户端连接
{
//accept
struct sockaddr_in caddr;
int len=sizeof(caddr);
int c=accept(sockfd,(struct sockaddr *)&caddr,&len);
if(c<0)
{
continue;
}
printf("accept c=%d\n",c);
fds_add(fds,c);//将连接套接字添加进存放文件描述符的数组中
}
else //已经存在的客户端发送数据
{
//recv
char buff[128]={0};
int res=recv(fds[i],buff,127,0);//接收数据
if(res<=0)
{
close(fds[i]);
fds[i]=-1;
printf("one client over\n");
}
else
{
printf("recv(%d)=%s\n",fds[i],buff);
send(fds[i],"OK",2,0);//回复
}
}
}
}
}
}
}
cli
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in saddr,caddr;
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=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res!=-1);
while(1)
{
printf("input:\n");
char buff[128]={0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
send(sockfd,buff,strlen(buff),0);
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("buff=%s\n",buff);
}
close(sockfd);
}