网络编程-IO多路复用

IO多路复用

允许同时对多个IO事件进行控制 同时监控多个“文件描述符”
这种方式就相当于你去钓鱼 你钓鱼的方式就是准备很多根鱼竿(同时监控多个文件描述符)
当没有鱼上钩的时候 你就去睡觉 当其中一根或者多跟鱼竿上钩了 你就醒过来起竿。
那么这种方式虽然也是属于阻塞IO 但是可以对多个文件描述符同时进行阻塞监听 所以效率较阻塞IO高。
IO多路复用的实现的机制是通过select/poll/epoll函数来实现的

与传统的多进程/多线程模型相比 IO多路复用的最大优势是系统开销小 系统不需要额外创建新的进程或线程。
也不需要维护这些进程/线程的允许 降低了系统的维护工作 节省了系统资源

主要应用场景:
服务器需要同时处理多个处于监听状态(listen)或多个连接状态(accept)的套接字

多路复用的实现(select/poll/epoll)
1.select

	实现原理: 
	
	①将所有需要监听的文件描述符放在一个监听集合中 将及这个集合拷贝到内核中 
	②在内核中创建一个线程 由这个线程去轮询所有的文件描述符 这个线程处于内核空间 所以这个cpu占用内核的执行时间 
	 而不是占用用户的执行时间。当一个或者多个文件描述符就绪(比如:可以读了 可以写了 出错了)时,就将就绪的文件
	 描述符集合拷贝到用户空间去
	③用户去处理就绪的文件描述符集合

	缺点:
		1.每一次select的时候  文件描述符都要copy两次 
		2.有事件就绪的时候 只能告诉你有事件就绪了 并不能确定具体是哪一个文件描述符就绪
  1. 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;
		}
	}
}

猜你喜欢

转载自blog.csdn.net/weixin_46836491/article/details/127140655