一、IO多路转接
1、IO操作方式
- 阻塞等待
好处:不占用CPU的宝贵时间片
缺点:同一时刻只能处理一个操作,效率低 - 非阻塞,忙轮询
好处:提高了程序的执行效率
缺点:需要占用更多的CPU和系统资源
2、解决方案:使用IO多路转接技术select/poll/epoll
- 第一种:select/poll 【线性表】
select代收员比较懒,他只会告诉你几个快递到了,但是哪个快递,你需要挨个遍历一遍。【委托内核做的】 - 第二种:epoll 【红黑树】
epoll代收员很勤快,不仅概诉你有几个,还会告诉你是哪几个(如对应的客户端)
3、什么是I/O多路转接技术
-
先构造一张有关文件描述符的列表,将要监听的文件描述符添加到该表中
-
然后调用一个函数,监听该表中的文件描述符,知道这些描述符表中的一个进行I/O操作时(即查看读的内核缓冲区),该函数才返回。
• 该函数为阻塞函数
• 函数对文件描述符的检测操作是由内核完成的 -
在返回时,它告诉进行有多少(哪些)描述符要进行I/O操作
二、select
struct timeval{
long tv_sec;//秒
long tv_usec;//微秒
//tv_sec+tv_usec
};//在settimer()使用
int select(int nfds,
fd_set *readfds,//传入传出参数,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
fd-读、写、异常
参数:
nfds:要检测的文件描述符中最大的fd+1 - 1024(fd_set最大存储的文件描述符个数为1024,为什么是1024:fd_set用数组实现)
readfds:读集合 检测读文件描述符对应的读缓冲区
writefds:写集合 NULL(写是主动的,一般不去检测)
exceptfds:异常集合 NULL
timeout:
1)NULL:永久阻塞,当检测到文件描述符fd发生变化时返回
2)经过10s返回
timeval a;
a.tv_sec=10;
a.tv_usec=0;
文件描述符集类型:fd_set rdset
//文件描述符操作函数
/*全部清空,所有标志位清空为0*/
void FD_ZERO(fd_set *set);
/*从集合中删除某一项 1->0*/
void FD_CLR(int fd,fd_set *set);
//FD_CLR(2,&reads);
/*将某个文件描述符添加到集合 0->1*/
void FD_SET(int fd,fd_set *set);
/*判断某个文件描述符是否在集合中*/
int FD_ISSET(int fd,fd_set *set);
1、select 工作过程分析
客户端A,B,C,D,E,F连接到服务器,分别对应文件描述符3,4,100,101,102,103
- A,B,C发送了数据 【3->1 4->1 100->1 101->1 102->1 103->1】
- 用户区的表拷贝到内核区
- 内核检测完后,若无数据写入内核核缓冲区,则将无数据的标志位修改为0【101->0 102->0 103->0】
- 然后,内核区的表拷贝回用户区,覆盖原先的表
fd_set reads,temp; //-用户空间
FD_SET(3,&reads);....
temp=reads;
select(103+1,&temp,NULL,NULL,NULL);
/*reads这张表,内核会拷贝一份(从用户区到内核区),
传递的是地址,修改后原先的数据被覆盖了,
因此需要传入一个临时的变量*/
2、select伪代码
int main()
{
int lfd=socket();
bind();
listen();
//创建一文件描述符
fd_set reads,temp;
//初始化
FD_ZERO(&reads);
//监听的lfd加入到读集合
FD_SET(lfd,&reads);
int maxfd=lfd;
while(1)
{
//委托检测
temp=reads;
int ret=select(maxfd,&temp,NULL,NULL,NULL);
//是不是监听的
if(FD_ISSET(lfd,&temp))
{
//接受新连接
int cfd=accept();
//cfd加入到读集合
FD_SET(cfd,&reads);
//更新maxfd
maxfd=maxfd<cfd?cfd:maxfd;
}
//客户端发送数据
for(int i=lfd+1;i<maxfd+1;++i)
{
if(FD_ISSET(i,&temp))
{
int len=read();
if(len==0)
{
//cfd从读集合中删除
FD_CLR(i,&reads);
}
write();
}
}
}
}
3、select代码实现
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/select.h>
int main(int argc,const char* argv[])
{
if(argc<2)
{
printf("eg: ./a.out port\n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len=sizeof(serv_addr);
int port=atoi(argv[1]);
//创建套接字
int lfd=socket(AF_INET,SOCK_STREAM,0);
//初始化服务器sockaddr_in
memset(&serv_addr,0,serv_len);
serv_addr.sin_family=AF_INET;//地址族
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);//监听本机所有IP
serv_addr.sin_port=htonl(port);
//绑定IP和端口
bind(lfd,(struct sockaddr*)&serv_addr,serv_len);
//设置同时监听的最大个数
listen(lfd,36);
printf("Start accept.....\n");
struct sockaddr_in client_addr;
socklen_t cli_len=sizeof(client_addr);
//最大的文件描述符
int maxfd=lfd;
//文件描述符读集合
fd_set reads,temp;
//初始化
FD_ZERO(&reads);
FD_SET(lfd,&reads);
while(1)
{
//委托内核做IO检测
temp=reads;
int ret=select(maxfd+1,&temp,NULL,NULL,NULL);
if(ret==-1)
{
perror("select error");
exit(1);
}
//客户端发起了新连接
if(FD_ISSET(lfd,&temp))
{
//接受连接请求 - accept 不阻塞
int cfd=accept(lfd,(struct sockaddr*)&client_addr,&cli_len);
if(cfd==-1)
{
perror("accept error");
exit(1);
}
char ip[64];
printf("new client IP:%s,Port:%s\n",inet_ntop(AF_INET,
&client_addr.sin_addr.s_addr,ip,sizeof(ip)),
ntohs(client_addr.sin_port));
//将cfd加入到待检测的读集合中 - 下一次就可以检测到了
FD_SET(cfd,&reads);
//更新最大的文件描述符
maxfd=maxfd<cfd?cfd:maxfd;
}
//已经连接的客户端有数据到达
int i;
for(i=lfd+1;i<maxfd+1;++i)
{
if(FD_ISSET(i,&temp))
{
char buf[1024]={0};
int len=recv(i,buf,sizeof(buf),0);
if(len==-1)
{
perror("recv error");
exit(1);
}
else if(len==0)
{
printf("客户端已经断开了连接\n");
close(i);
//从读集合中删除
FD_CLR(i,&reads);
}
else
{
printf("recv buf:%s\n",buf);
send(i,buf,strlen(buf)+1,0);
}
}
}
}
close(lfd);
return 0;
}
4、优缺点
优点:
- 跨平台
缺点:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时会很大
- select支持的文件描述符数量太小了,默认是1024【可扩展,修改配置文件】