一.IO复用
io复用使得程序能够监听多个文件描述符。
以下情况下需要使用io复用:
1.客户端同时处理多个socket(如:非阻塞的connect技术);
2.客户端程序需要同时处理用户输入和网络连接(如:聊天室程序);
3.TCP服务器要同时处理监听socket和连接socket;
4.服务器要同时处理TCP请求和UDP请求(如:回射服务器);
5.服务器同时监听多个端口,或者处理多种服务(xinetd服务器);
二.select函数
select函数原型如下:
#include<sys/select.h>
int select(int fd,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
//第一个参数:fd,指被监听的文件描述符的总数
//第二个参数:readfds,指向可读事件对应的文件描述符的集合
//第三个参数:writefds,指向可写事件对应的文件描述符的集合
//第四个参数:exceptfds,指向异常事件对应的文件描述符的集合
//第五个参数:timeout,指定select函数的超时时间
(1).通过修改2,3,4参数传入自己感兴趣的wenjianmiaoshuf,select函数调用返回时,内核修改,它们来通知应用程序那些文件描述符已经就绪。
(2).fd_set是一个结构体指针,结构体体内包含一个整形数组,该数组的每一个元素的每一位都会标记一个文件描述符,fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制select能同时处理的文件描述符的总量。FD_ISSET是用来测某一位是否被标记为1.
(3).用以下宏来访问fd_set结构体中的位:
#include<sys/select.h>
FD_SETint (fd,fd_set *fdset);//设置fdset的位fd
FD_CLR(int fd,fd_set *fdset);//清除fdset的位fd
FD_ISSET(int fd,fd_set *fdset);//测试fdset的位fd是否被设置
FD_ZERO(fd_set *fdset);//清除fdset中的所有值
(4).timeout是一个timeval结构类型的指针,timeval结构体的定义如下:
struct timeval
{
long tv_sec;//秒数
long tv_usec;//微秒数
}
(5 ). select成功时返回就绪(可读,可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0.select失败时返回-1.
三.文件描述符就绪的条件
以下情况下socket可读:
1.socket内核接收缓存区中的字节数大于或等于其低水位标志SO_RCVLOWAT。
(重要)2.监听socket上有新的连接请求。
3.socket上有未处理的错误。
4.socket通信的双方关闭连接。
以下情况下socket可写:
1.socket内核接收缓存区中的字节数大于或等于其低水位标志SO_SNDLOWAT。
2.socket的写操作关闭。
3.socket使用非阻塞connect连接成功或者失败(超时)之后。
4.socket上有未处理的错误。
四.select的缺点
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024
五.代码实现
服务器端:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<string.h>
#include<sys/time.h>
#include<assert.h>
#define MAX 10
int create_socket()//创建套接字
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);//tcp字节流式服务
if(sockfd==-1)//等于-1,创建失败
{
return -1;
}
struct sockaddr_in saddr;//专用的地址结构
memset(&saddr,0,sizeof(saddr));//将saddr置为0
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6666);
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));//命名函数,将saddr的地址分配给未命名的sockfd文件描述符
listen(sockfd,5);//监听sockfd,5指内核监听的最大长度
return sockfd;
}
int fds_init(int fds[])//初始化文件描述符集合
{
int i=0;
for(;i<MAX;i++)
{
fds[i]=-1;//将集合内的值都置为-1
}
}
void fds_add(int fds[],int fd)//添加文件描述符
{
int i=0;
for(;i<MAX;i++)
{
if(fds[i]==-1)
{
fds[i]=fd;
break;
}
}
}
void fds_del(int fds[],int fd)//删除文件描述符
{
int i=0;
for(;i<MAX;i++)
{
if(fds[i]==fd)
{
fds[i]=-1;
break;
}
}
}
int main()
{
int sockfd=create_socket();//创建sockfd
int fds[MAX];//设置一个数组fds
fds_init(fds);//初始化数组fds
fds_add(fds,sockfd);//将socffd添加到数组中
fd_set fdset;
while(1)
{
FD_ZERO(&fdset);//清除fdset的所有位
int maxfd=-1;
int i=0;
for(;i<MAX;i++)//循环遍历找到最大的文件描述符
{
if(fds[i]!=-1)//遍历找到fds数组中不是初始值-1的位
{
FD_SET(fds[i],&fdset);//设置fdset的fds[i]位
if(maxfd<fds[i])
{
maxfd=fds[i];
}
}
}
struct timeval tv={5,0};//设置超时时间为5秒
int n=select(maxfd+1,&fdset,NULL,NULL,&tv);//用n来接收select成功时返回就绪文件描述符的总数
if(n==-1)//返回失败
{
perror("select error");
break;
}
else if(n==0)//超时时间内没有文件描述符就绪
{
printf("timeout\n");
continue;
}
else
{
int i=0;
for(;i<MAX;i++)//循环遍历
{
if(fds[i]==-1)
{
continue;
}
if(FD_ISSET(fds[i],&fdset))//fdset的fds[i]位已经被测试
{
if(fds[i]==sockfd)
{
struct sockaddr_in caddr;
int len=sizeof(caddr);
int c=accept(sockfd,(struct sockaddr*)&caddr,&len);//接收一个套接字已建立的连接
if(c<0)
{
continue;
}
printf("accept :%d\n",c);
fds_add(fds,c);
}
else
{
char buffer[128]={0};
if(recv(fds[i],buffer,127,0)<=0)//接收服务器端的数据是小于等于零,说明客户端已经关闭
{
close(fds[i]);
fds_del(fds,fds[i]);//清除fds数组
printf("一个客户端关闭\n");
continue;
}
printf("buffer %d =%s\n",fds[i],buffer);
send(fds[i],"ok",2,0);
}
}
}
}
}
}
客户端:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建套接字(tcp流式服务)
assert(sockfd != -1);//断言创建不成功
struct sockaddr_in saddr;//专用的地址结构
memset(&saddr,0,sizeof(saddr));//将saddr置为0
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6666);//端口要与服务器端口一致
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//本地ip
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//connect触发tcp三次握手发起连接
//connect成功时返回0
assert(res != -1);
while(1)//循环传输
{
char buff[128] = {0};
printf("input:\n");
fgets(buff,128,stdin);//从键盘接收数据
if(strncmp(buff,"end",3) == 0)//如果接收到的是end,就跳出循环,close sockfd
{
break;
}
send(sockfd,buff,strlen(buff),0);//向已连接的sockfd发送数据
memset(buff,0,128);//将数组buff的128位置为0
recv(sockfd,buff,127,0);//从服务器端接收数据
printf("buff = %s\n",buff);//打印数据
}
close(sockfd);//close sockfd
}
六.结果展示