IO多路复用
允许同时对多个IO事件进行控制 同时监控多个“文件描述符”
这种方式就相当于你去钓鱼 你钓鱼的方式就是准备很多根鱼竿(同时监控多个文件描述符)
当没有鱼上钩的时候 你就去睡觉 当其中一根或者多跟鱼竿上钩了 你就醒过来起竿。
那么这种方式虽然也是属于阻塞IO 但是可以对多个文件描述符同时进行阻塞监听 所以效率较阻塞IO高。
IO多路复用的实现的机制是通过select/poll/epoll函数来实现的
与传统的多进程/多线程模型相比 IO多路复用的最大优势是系统开销小 系统不需要额外创建新的进程或线程。
也不需要维护这些进程/线程的允许 降低了系统的维护工作 节省了系统资源
主要应用场景:
服务器需要同时处理多个处于监听状态(listen)或多个连接状态(accept)的套接字
多路复用的实现(select/poll/epoll)
1.select
实现原理:
①将所有需要监听的文件描述符放在一个监听集合中 将及这个集合拷贝到内核中
②在内核中创建一个线程 由这个线程去轮询所有的文件描述符 这个线程处于内核空间 所以这个cpu占用内核的执行时间
而不是占用用户的执行时间。当一个或者多个文件描述符就绪(比如:可以读了 可以写了 出错了)时,就将就绪的文件
描述符集合拷贝到用户空间去
③用户去处理就绪的文件描述符集合
缺点:
1.每一次select的时候 文件描述符都要copy两次
2.有事件就绪的时候 只能告诉你有事件就绪了 并不能确定具体是哪一个文件描述符就绪
-
poll
poll和select的功能类似 只不过在内核轮询的时候 poll使用链表保存需要轮询的文件描述符 而select使用数组存储文件描述符(默认情况下最多只能监听1024个文件描述符) poll在监听的所有的文件描述符都没有就绪的情况下 poll也是阻塞的
3.epoll
epoll改进了select和poll的两个缺点
epoll不会随着需要监听的文件描述符的数量增多而降低效率(函数返回之后不需要轮询)
epoll支持边缘触发<----
使用select函数 实现IO多路复用(省略.h文件)
1.使用链表 把客户端信息存储在链表中
#include "LinkList.h"
//初始化头结点
LIST* Create_Head()
{
LIST *h=(LIST*)malloc(sizeof(LIST));
h->first=NULL;
h->last=NULL;
h->num=0;
return h;
}
//增加结点
int InsertClient(LIST *h,CLI_INFO *node)
{
if(h==NULL)
{
printf("插入信息为空\n");
return -1 ;
}
NODE * p=malloc(sizeof(NODE));
p->prev=NULL;
p->next=NULL;
p->data.fd=node->fd;
p->data.fp=node->fp;
p->data.port=node->port;
strcpy(p->data.ip,node->ip);
if(h->num==0)
{
h->first=p;
h->last=p;
}
else
{
h->last->next=p;
p->prev=h->last;
h->last=p;
}
h->num++;
return 0;
}
//查询节点 h是头结点 根据传过来的p结点中的fd查询
NODE * SeleteClient(LIST *h,int fd)
{
if(h==NULL || h->first==NULL)
{
printf("链表为空\n");
return NULL ;
}
NODE *p=h->first;
while (p!=NULL)
{
if(p->data.fd == fd)
{
return p;
}
else
{
p=p->next;
}
}
return NULL;
}
//删除节点 h是头结点 p是想删除的p结点
int DeleteClient(LIST*h,NODE *p)
{
//查询节点 px就是要删除的结点
NODE *px=SeleteClient(h,p->data.fd);
NODE * pr=NULL;
p->prev->next=p->next;
if(px==NULL)
{
printf("删除的结点为空\n");
return -1;
}
if(h->num==1)
{
h->first=NULL;
h->last=NULL;
}
else if(h->first==px)
{
h->first=px->next;
px->next->prev=NULL;
}
else if(px==h->last)
{
h->last=px->prev;
h->last->next=NULL;
}
else
{
px->prev->next=px->next;
px->next->prev=px->prev;
}
free(px);
h->num--;
return 0;
}
void print_list(LIST *h)
{
if(h==NULL)
{
printf("NO Client\n");
return;
}
printf("Client num==%d\n",h->num);
NODE * p=h->first;
while (p!=NULL)
{
printf("Client IP:%s PORT:%d\n",p->data.ip,p->data.port);
p=p->next;
}
}
2.绑定服务器
#include "maketcp.h"
int TcpInit(const char * ip ,unsigned short port)
{
int sock_fd=socket(AF_INET,SOCK_STREAM,0);
if(-1==sock_fd)
{
perror("socket error\n");
return -1;
}
int on=1;
//设置端口复用
int ret=setsockopt(sock_fd,SOL_SOCKET,SO_REUSEPORT,(void *)&on,sizeof(on));
if(ret==-1)
{
perror("setsockopt PORT error\n");
return -1;
}
//设置ip复用
ret=setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR,(void *)&on,sizeof(on));
if(ret==-1)
{
perror("setsockopt ADDR error\n");
return -1;
}
struct sockaddr_in serv;
serv.sin_family=AF_INET;
serv.sin_port=htons(port);
serv.sin_addr.s_addr=inet_addr(ip);
ret=bind(sock_fd,(struct sockaddr*)&serv,sizeof(serv));
if(-1==ret)
{
perror("bind error\n");
return -1;
}
//开启对套接字的监听
ret =listen(sock_fd,256);
if(ret==-1)
{
perror("listen error\n");
return -1;
}
printf("Init Tcp Server Success!\n");
return sock_fd;
}
3.main函数
#include "maketcp.h"
#include"LinkList.h"
#include <time.h>
#include <pthread.h>
typedef struct cli_message
{
time_t time;
char buf[1024];
int len;
int fd; //文件描述符
}CLI_MES;
LIST *h;
int shut=0;
void * writefile(void *arg)
{
//设置分离属性
pthread_detach(pthread_self());
CLI_MES *mes=(CLI_MES*)arg;
//查找对应的客户端套接字
NODE *p=SeleteClient(h,mes->fd);
if(p==NULL)
{
printf("Client is closed\n");
return NULL;
}
//保存时间
char t[256]={0};
ctime_r(&mes->time,t); //把时间转换为字符串
printf("thraed:time=%s\n",t);
fwrite(t,sizeof(char),strlen(t),p->data.fp);
fwrite(mes->buf,sizeof(char),strlen(mes->buf),p->data.fp); //把消息写到日志中
fprintf(p->data.fp,"\n");
printf("服务器接收:%s",mes->buf);
fflush(p->data.fp);
write(mes->fd,"Hello!",6);
//消息处理完毕 释放掉这个结构体
free(mes);
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
printf("arg num error\n");
return -1;
}
int sockfd=TcpInit(argv[2],atoi(argv[1]));
if(sockfd==-1)
{
perror("TcpInit error\n");
return -1;
}
h=Create_Head();
//使用IO多路复用去监所有客户端和服务器
int max_fd=0;
fd_set rfds;
max_fd=sockfd;
//当服务器不关闭的时候 一直监听
while (shut==0)
{
FD_ZERO(&rfds);
FD_SET(sockfd,&rfds);
NODE *p=h->first;
//链表中没有结点就跳过
while (p!=NULL)
{
FD_SET(p->data.fd,&rfds);
max_fd=max_fd>p->data.fd ? max_fd :p->data.fd;
p=p->next;
}
//调用select监听
int ret=select(max_fd+1,&rfds,NULL,NULL,NULL);
if(ret <=0)
{
continue;
}
//如果是服务器就绪
if(FD_ISSET(sockfd,&rfds))
{
//接收客户端的连接
struct sockaddr_in Client;
socklen_t len=sizeof(Client);
int confd=accept(sockfd,(struct sockaddr*)&Client,&len);
if(confd<0)
{
perror("accept error\n");
continue;
}
//打印客户端的信息
printf("NewClient IP:%s PORT:%d\n",inet_ntoa(Client.sin_addr),ntohs(Client.sin_port) );
//保存客户端的相关信息
CLI_INFO new;
new.fd=confd;
new.port=ntohs(Client.sin_port);
strcpy(new.ip,inet_ntoa(Client.sin_addr));
//创建一个日志文件(文件名为端口号+IP)
char filename[512]={0};
sprintf(filename,"./log/%s_%d.txt",new.ip,new.port);
new.fp=fopen(filename,"a+"); //需要创建log文件夹 只会创建txt文件
if(new.fp==NULL)
{
perror("fopen error\n");
return -1;
}
int retInsert=InsertClient(h, &new);
if(retInsert!=0)
{
printf("添加链表失败\n");
}
print_list(h);
}
//如果是客户端就绪 查找哪个客户端就绪
p=h->first;
while (p!=NULL)
{
NODE * temp=p->next;
if(FD_ISSET(p->data.fd,&rfds))
{
//读消息 把读到的写到日志中
CLI_MES *mes=malloc(sizeof(CLI_MES));
ret=read(p->data.fd,mes->buf,1024);
if(ret<=0)
{
perror("read error\n");
close(p->data.fd);
fclose(p->data.fp);
DeleteClient(h, p);
free(p);
free(mes);
}
else
{
mes->len=ret;
mes->fd=p->data.fd;
mes->time=time(NULL);
//用线程把读到的写到日志中
pthread_t tid;
int ret=pthread_create(&tid,NULL,writefile,(void *)mes);
if(ret!=0)
{
perror("pthread create error\n");
return -1;
}
}
}
p=temp;
}
}
}