Linux 网络编程 全解(七)--------epoll的ET和LT触发模式以及epoll反应堆

写在前面:本篇介绍epoll的ET和LT两种触发模式,和epoll反应堆,epoll反应堆是实现libevent原理的组成部分。可能代码部分也是会多一点。

Linux 网络编程 全解(一)--------网络基础协议

Linux 网络编程 全解(二)--------套接字socket

Linux 网络编程 全解(三)--------TCP三次握手、数据传输、四次挥手、滑动窗口

Linux 网络编程 全解(四)--------多进程并发服务器和多线程并发服务器

Linux 网络编程 全解(五)--------TCP状态切换

Linux 网络编程 全解(六)--------多路IO转接服务器

正文:

一、epoll的ET和LT触发模式

1、ET和LT简介
  ET 模式即 Edge Triggered 工作模式,边沿触发,缓冲区剩余未读尽的数据不会导致epoll_wait返回,新的事件满足才会导致epoll_wait返回,边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
  
  LT 模式即 Level Triggered 工作模式,水平触发,默认,水平触发只要有数据都会触发,缓冲区剩余未读尽的数据会导致epoll_wait返回。默认情况下为LT触发模式。

2、代码实现,具体实验目的和实现方式都在代码注释中了,已经非常详细。

#include 
#include 
#include 
#include 

#define MAXLEN (10)

char w_buf[MAXLEN];
char r_buf[MAXLEN];
char ch = 'a';




/*pipe创建一个管道,fork创建子进程,子进程往管道中写数据,父进程从管道中读数据
 *子进程每次发10个字节的数据,然后延时5s,父进程每次收5个字节的数据,
 *当设置为ET模式时,因为边缘触发只有数据到来才触发,不管缓存区中是否还有数据,这样父进程每个
 *5s内只会收到5个字节数据,当下个5s到来时,父进程才会收到上个5s子进程发的10个字节中的剩余5个字节数据。
 *当设置为LT模式时,默认情况下就是这个模式,只要有数据都会触发,所以每次5s中都会将缓冲区中的数据
 *读完,即都会将10个Bytes的数据读完。
 *
 */
int main(void)
{
	pid_t pid;
	int pipefd[2] = {-1,-1};
	int ret = -1;
	
	ret = pipe(pipefd); //创建管道成功后,pipefd[0]为读端,pipefd[1]为管道的写入端
	printf("pipefd[0] = %d, pipefd[1] = %d\n",pipefd[0],pipefd[1]);
	if(ret < 0)
	{
		printf("pipe error\n");
		
		return -1;
	}
	pid = fork();
	
	if(0 == pid)//子进程
	{
		int i = 0;
		
		close(pipefd[0]);
		
		printf(" clid process\n");
		while(1)
		{
			for(i = 0;i < MAXLEN/2;i ++)
			{
				w_buf[i] = ch;				
			}
			w_buf[i-1] = '\n';
			
			ch++;
			
			for(;i < MAXLEN; i++)
			{
				w_buf[i] = ch;
			}
			w_buf[i-1] = '\n';       //即把w_buf构造成"aaaa\nbbbb\n"这种格式
			ch ++;
			
			//printf("before write buffer:%s\n",w_buf);
			write(pipefd[1], w_buf, sizeof(w_buf));//即一次写10个Bytes
			
			printf("write buffer:%s\n",w_buf);
			sleep(5);
			//printf("clid send buffer\n ");

		}
		close(pipefd[1]);
		
	}else if(pid > 0)//父进程
	{
		sleep(1);
		struct epoll_event lepevt; //listen epoll event
		struct epoll_event ep_set[10];
		int ep_nums = -1;
		int i = 0;
		
		close(pipefd[1]);
		//创建一个监听树,监听树的最大节点设为50
		int efd = epoll_create(10);
		if(efd < 0)
		{
			printf("epoll_create error \n");
			
			return -1;
		}
		
		lepevt.events = EPOLLIN | EPOLLET ;//ET模式,即边缘触发只有数据到来才触发,不管缓存区中是否还有数据
		//lepevt.events = EPOLLIN; //LT模式,水平触发,默认就是LT模式,只要有数据都会触发
		lepevt.data.fd = pipefd[0];

		//将监听读事件添加到监听树上
		epoll_ctl(efd, EPOLL_CTL_ADD, pipefd[0], &lepevt);
		
		printf(" parents process\n");
		while(1)
		{
			//阻塞监听事件
			ep_nums = epoll_wait(efd, ep_set,10, -1);
			
			printf("ep_nums = %d\n",ep_nums);
			if(ep_nums > 0)
			{
				for(i = 0;i < ep_nums;i ++)
				{
					//如果监听到读端的数据
					if(ep_set[0].data.fd == pipefd[0])
					{
						//每次读5个Bytes的数据
						ret = read(pipefd[0], r_buf, MAXLEN/2);
						
						//printf("ret = %d\n",ret);
						
						if(ret > 0)
						{
							//write(STDOUT_FILENO,r_buf,ret);
							printf("receive buffer: %s\n",r_buf);
							memset(r_buf,0,sizeof(r_buf));
						}
						else
						{
							printf("read null\n");
						}
						break;
						

					}
				}
			}

		}

		close(pipefd[0]);
		close(efd);
		
	}

	
	
	
	
	return 0;
}

3、ET模式实验效果:从结果中可以看出,当配置为ET触发时,每次子进程写入管道时才会触发父进程中的epoll_wait返回。

Linux 网络编程 全解(七)--------epoll的ET和LT触发模式以及epoll反应堆_第1张图片

LT实验效果:从现象可以看出,当配置成LT模式时,只要缓冲区中有数据,就会触发epoll_wait的返回,一直将缓冲区中的数据读完。

Linux 网络编程 全解(七)--------epoll的ET和LT触发模式以及epoll反应堆_第2张图片

二、epoll反应堆

1、反应堆模型:epoll ET模式 + 非阻塞、忙轮询 + struct epoll_event中的data中的void *ptr

原来的流程:

      epoll_create();//创建监听红黑树
      epoll_ctl();//向书上添加监听fd
      epoll_wait();//监听
      有监听fd事件发送->返回监听满足数组,->判断返回数组元素->lfd满足accept-->cfd满足-->read()读数据--->write给对   端回应。

epoll反应堆流程:

      epoll_create();//创建监听红黑树
      epoll_ctl();//向书上添加监听fd
      epoll_wait();//监听
   
      有监听fd事件发送->返回监听满足数组,->判断返回数组元素->lfd满足accept-->cfd满足-->read()读数据-->cfd从监听  红黑树上摘下来--->event 中设置监听写事件 EPOLLOUT 和设置回调到 ptr中--->epoll_ctl() EPOLL_CTL_ADD重新放到监听红黑  树上监听写事件--->等待epoll_wait返回--->说明cfd可写--->write写到对端去--->再将cfd从监听红黑树上摘下--->修改成EPOLLIN---->epoll_ctl()  EPOLL_CTL_ADD重新放到监听红黑树上监听读事件--->wpoll_wait监听。

2、这里可能说的不是很明白,直接上代码吧,代码中尽可能详细的做了注释

 

#include 
#include 
#include 
#include 
#include           
#include 
#include 
#include 
#include 
#include 


#define MAX_EVENTS  (100) //最大的监听的个数
#define SER_PORT (6666)

typedef struct
{
	int fd ;//文件描述符
	int events;//事件
	void *args;
	void (*call_back)(int fd,int events,void *args);//事件回调函数,参数为上面三个
	int status; //0:没有添加到监听树;1:已添加到监听树
	char buff[1024]; //数据缓冲区
	int len;//数据长度
	long last_active;//上次的时间
	
}myevents_t;


myevents_t g_monitorEvs[MAX_EVENTS + 1];
int g_efd = -1;

int evtAdd(int efd,int events,myevents_t *p_evt);
int evtSet(myevents_t *p_evt,int fd, void (*call_back)(int fd,int events,void *args),void * arg);
int evtDel(int fd,myevents_t * p_evt);
void recvData(int fd,int events,void *args);

//发送给客户端数据
void sendData(int fd,int events,void *args)
{
	int ret = -1;
	
	myevents_t *p_evt = (myevents_t *)args;
	
	printf("p_evt ->len = %d\n",p_evt ->len);
	
	ret = send(fd, p_evt ->buff, p_evt ->len, 0);
	
	if(ret > 0)
	{
		printf("server send to client :%s\n",p_evt ->buff);
		//发送完数据之后,将fd从监听树中摘下,改为监听读事件,再放到监听树上去
		evtDel(fd,p_evt);
		evtSet(p_evt,fd, recvData,(void * )p_evt);
		evtAdd(g_efd,EPOLLIN,p_evt);
		
	}
	else
	{
		close(fd);
		evtDel(fd,p_evt);
	}

}


//读取客户端发过来的数据的函数
void recvData(int fd,int events,void *args)
{
	myevents_t *p_evt = (myevents_t *)args;
	
	memset(p_evt ->buff,0,1024);
	//读取客户端发过来的数据
	p_evt ->len = recv(fd, p_evt ->buff, 1024, 0);
	//将其从监听树中摘下
	evtDel(fd,p_evt);
	
	if(p_evt ->len  > 0)
	{
		printf("client send buffer:%s\n",p_evt ->buff);
		
		//再将此fd设置为监听写事件,再放到监听树上
		evtSet(p_evt,fd, sendData,(void * )p_evt);
		evtAdd(g_efd,EPOLLOUT,p_evt);
	}
	else
	{
		close(fd);
	}

}

//server跟client连接函数
void acceptConnection(int fd,int events,void *args)
{
	int cfd = -1;
	struct sockaddr_in cli_ip; 
	socklen_t cli_ip_len = sizeof(cli_ip);
	char cli_ip_addr[16] = {0};
	int i = 0;
	int flag = -1;
	
	cfd = accept(fd, (struct sockaddr *)&cli_ip,&cli_ip_len);
	
	if(cfd > 0)
	{
		memset(cli_ip_addr,0,16);
		inet_ntop(AF_INET, &cli_ip.sin_addr.s_addr, cli_ip_addr, 16);
		printf("client connected,client ip addr:%s\n",cli_ip_addr);
		
		//查找没有加入到监听树中的数组节点
		for(i = 0;i < MAX_EVENTS;i ++)
		{
			if(g_monitorEvs[i].status == 0)
				break;
		}
		
		if(i >= MAX_EVENTS)
		{
			printf("%s:g_monitorEvs if full \n",__FUNCTION__);
			return;
		}
		//将连接的cfd设置成非阻塞模式
		flag = fcntl(cfd, F_GETFL);
		flag |= O_NONBLOCK;
		fcntl(cfd, F_SETFL, flag);

		//找到合适的节点之后,将其添加到监听树中,并监听读事件
		evtSet(&g_monitorEvs[i],cfd, recvData,(void *) &g_monitorEvs[i]);
		evtAdd(g_efd,EPOLLIN,&g_monitorEvs[i]);
	}
	
	
}
//初始化myevents_t 类型的一个变量
int evtSet(myevents_t *p_evt,int fd, void (*call_back)(int fd,int events,void *args),void * arg)
{
	p_evt ->fd = fd;
	p_evt ->events = 0;
	p_evt ->args = arg;
	p_evt ->call_back = call_back;
	p_evt ->status = 0;
	//memset(p_evt ->buff,0,1024);
	//p_evt ->len = 0;
	p_evt ->last_active = time(NULL);
	
	return 0;
	
}
//将fd添加到监听树上,设置监听事件并设置相应的回调函数,和参数
int evtAdd(int efd,int events,myevents_t *p_evt)
{
	struct epoll_event ep_evt;
	int ret = -1;
	int op;
	
	memset(&ep_evt,0,sizeof(ep_evt));
	ep_evt.events = events; //设置相应的监听事件
	p_evt ->events = events;//监听数组中的元素也要设置
	ep_evt.data.ptr = (void *)p_evt; //这里将myevents_t 类型的结构体给了ptr指针
	                                 //这里应该就是epoll反应堆的精髓了
	if(1 == p_evt ->status)//监听树中已经有了这个fd 
	{
		op = EPOLL_CTL_MOD; //EPOLL_CTL_MOD,linux中man手册的解释是Change  the event event associated with the target file descrip‐
                            //tor fd,应该就是只更新这个fd对应的监听事件
	}else//监听树中没有这个fd
	{
		op = EPOLL_CTL_ADD;
		p_evt ->status = 1; //status 置1
	}
									 
	ret = epoll_ctl(efd, op, p_evt ->fd, &ep_evt);
	if(ret < 0)
	{
		printf("%s:epoll_ctl error\n",__FUNCTION__);
		return -1;
	}
	
	return 0;
	
}

int evtDel(int fd,myevents_t* p_evt)
{
	int op = EPOLL_CTL_DEL;
	int ret = -1;
	struct epoll_event ep_evt;
	
	//如果fd没有添加到监听树上,就不用删除,直接返回
	if(p_evt ->status == 0)
	{
		return 0;
	}
	//如果fd在监听树上
	p_evt ->status = 0;
	ep_evt.data.ptr = (void *)p_evt;
	
	ret = epoll_ctl(g_efd,op,fd, &ep_evt);
	if(ret < 0)
	{
		printf("%s:epoll_ctl error\n",__FUNCTION__);
		
		return -1;
	}
	
	return 0;
}

//初始化监听lfd,并将lfd添加到监听树上去
int lfdInit(int efd,myevents_t * p_evt)
{
	int lfd = -1;
	struct sockaddr_in ser_ip;
	
	//创建监听lfd
	lfd = socket(AF_INET,SOCK_STREAM,0);
	
	memset(&ser_ip,0,sizeof(struct sockaddr_in));
	ser_ip.sin_family = AF_INET;
	ser_ip.sin_port = htons(SER_PORT);
	ser_ip.sin_addr.s_addr = htonl(INADDR_ANY); 
	
	bind(lfd, (const struct sockaddr*)&ser_ip,sizeof(ser_ip));
	listen(lfd, 20);
	
	//设置端口复用
	int opt = 1;// 1:设置端口复用,0:设置端口不复用
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR,&opt, sizeof(opt));
	
	//初始化g_monitorEvs[MAX_EVENTS],并设置回调
	evtSet(p_evt,lfd, acceptConnection,(void *) p_evt);
	//将lfd添加到监听树上,监听读事件
	evtAdd(g_efd,EPOLLIN,p_evt);
}

int main(void)
{
	int ret = -1;
	int ep_wait_nums = -1;
	int i = 0;
	
	//创建监听树
	g_efd = epoll_create(MAX_EVENTS);
	if(g_efd < 0)
	{
		printf("epoll_create error\n");
	}
	
	//初始化监听lfd
	//将g_monitorEvs[MAX_EVENTS]作为监听连接的数组节点
	ret = lfdInit(g_efd,&g_monitorEvs[MAX_EVENTS]);
	
	//定义这个结构体数组,用来接收epoll_wait传出的满足监听事件的fd结构体
	struct epoll_event ep_evt[MAX_EVENTS];
	myevents_t *p_evt;
	
	while(1)
	{
		//调用eppoll_wait等待接入的客户端事件,再次声明,epoll_wait传出的是满足监听条件的那些fd的struct epoll_event
		//类型
		ep_wait_nums = epoll_wait(g_efd, ep_evt,MAX_EVENTS, -1);
		if(ep_wait_nums > 0)
		{
			for(i = 0;i < ep_wait_nums;i ++)
			{
				//注意这里了,epoll反应堆精髓部分
				//evtAdd()函数中,添加到监听树中监听事件的时候将myevents_t结构体类型给了ptr指针
				//这里epoll_wait返回的时候,同样会返回对应fd的myevents_t类型的指针
				p_evt = ( myevents_t *)ep_evt[i].data.ptr;
				if(NULL != p_evt)
				{
					//如果监听的是读事件,并返回的是读事件
					if((p_evt ->events & EPOLLIN)&&(ep_evt[i].events & EPOLLIN))
					{
						p_evt ->call_back(p_evt ->fd,ep_evt[i].events,p_evt ->args);
					}
					//如果监听的是写事件,并返回的是写事件
					if((p_evt ->events & EPOLLOUT)&&(ep_evt[i].events & EPOLLOUT))
					{
						p_evt ->call_back(p_evt ->fd,ep_evt[i].events,p_evt ->args);
					}
				}
			}
		}
		

		
	}
	
	return 0;
}






 

你可能感兴趣的:(Linux网络编程)