7、epoll边沿触发与反应堆

epoll边沿触发

1、epoll事件模型:

​ epoll监听的是文件描述符,也可以监控进程间通信的事件。

#include 
#include 
#include 
#include 
#include 

#define MAXLINE 10

int main(int argc, char *argv[])
{
    int efd, i;
    int pfd[2];
    pid_t pid;
    char buf[MAXLINE], ch = 'a';

    pipe(pfd);//创建一个管道
    pid = fork();//创建一个主进程

    if (pid == 0) 
    //pid = 0说明是子进程,子进程完成写操作
    {          
        close(pfd[0]);//关闭子进程读端
        while (1)
        {
            //aaaa\n
            for (i = 0; i < MAXLINE/2; i++)
                buf[i] = ch;
            buf[i-1] = '\n';
            ch++;
            //bbbb\n
            for (; i < MAXLINE; i++)
                buf[i] = ch;
            buf[i-1] = '\n';
            ch++;
            //aaaa\nbbbb\n
            write(pfd[1], buf, sizeof(buf));
            //向管道里写buf
            sleep(5);
        }
        close(pfd[1]);
    } 
    else if (pid > 0)
    { 
        struct epoll_event event;
        struct epoll_event resevent[10];        //epoll_wait就绪返回event
        int res, len;

        close(pfd[1]);
        efd = epoll_create(10);//创建一个文件描述字来进行监听cfd

        event.events = EPOLLIN | EPOLLET;     // ET 边沿触发
       // event.events = EPOLLIN;                 // LT 水平触发 (默认)
        event.data.fd = pfd[0];
        epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);
        /*
        event.events = EPOLLIN | EPOLLET;:这行代码设置了event结构体的events字段,指示关		联的文件描述符(pfd[0])应该以边沿触发(Edge Triggered,ET)模式监视可读事件			 (EPOLLIN)。在ET模式下,只有当文件描述符上的状态从不可读变为可读时,才会触发事件,因此需要		 确保在处理可读事件后,读取所有可用数据,直到再次返回EAGAIN或EWOULDBLOCK。
		event.data.fd = pfd[0];:这行代码设置了event结构体的data.fd字段,将文件描述符		  pfd[0]与这个事件关联起来。
		epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);:这行代码将event结构体中定义的事	   件添加到epoll实例(由efd表示)。这将告诉epoll开始监视pfd[0]上的可读事件,并在该事件发生	  时通知应用程序。
        */	
        //LT为水平触发,只要缓冲区里还有就会触发
        //ET为边沿触发,只有缓冲区中内容在增加时才会触发
        while (1) 
        {
            res = epoll_wait(efd, resevent, 10, -1);
            //epoll_wait函数来等待事件的发生。efd是epoll实例的文件描述符,resevent是用于存			 //储就绪事件的数组,10表示resevent数组的大小,-1表示等待时间无限长
            printf("res %d\n", res);
            if (resevent[0].data.fd == pfd[0]) 
          //检查就绪事件数组中的第一个事件是否与文件描述符pfd[0]相关。data.fd字段存储了与事件			  //相关的文件描述符。
            {
                len = read(pfd[0], buf, MAXLINE/2);
                write(STDOUT_FILENO, buf, len);
            }
        }

        close(pfd[0]);
        close(efd);

    } else {
        perror("fork");
        exit(-1);
    }

    return 0;
}

ET模式:边沿触发

​ 缓冲区剩余未读尽的数据不会导致epoll_wait返回。新的事件满足才会触发。

LT模式:水平触发(默认)

​ 缓冲区未读尽的数据会导致epoll_wait返回

2、epoll中的ET非阻塞模式

readn:读够一定数量的字节才会返回

**只需要三行代码:**文件描述字设置非阻塞

flag = fcntl(cfd,F_GETFL);
flag |= 0_NOBLOCK;
fcntl(cfd,F_SETFL,flag);

缺点:不能跨平台,只能在Linux上使用

epoll反应堆模型

1、概述:

​ 有n个客户端,服务器就会有n个连接(n个客户端和连接端)。

客户端
连接端
epoll检测是可读事件还是可写事件
服务器

​ n:有可读事件(包括lfd)和可写事件。

/*一个事件其是可以看成下面结构体的三个成员*/
struct event
{
    int cfd;
    read_cb();
    write_cb();
}

​ 反应堆:一个IO对应多个事件。

2、代码实现(检测业务的实现)

lfd采用accept处理
cfd采用recv和send处理
#define EVENT_LENGTH 1024
unsigned char buf[];
int init_server
{

}
struct item
{
	int cfd;
	unsigned char rbuffer[1024];//读缓冲区
	int rlen;
	unsigned char wbuffer[4096];
	int wlen;//将要写的长度
	int wsize;//已经写的长度
};
struct item *get_item_by_clientfd(int clientfd)
{
	//通过cfd来找item数组
};


int main()
{
	int epfd = epoll_create(1);
	int sockfd = init_server();
	//最开始的时候,红黑树只有一个lfd
	epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev);
	struct epoll_event events[EVENT_LENGTH] = {0};
	while(1)
	{
		int nready = epoll_wait(epfd,events,EVENT_LENGTH,-1);
		int i = 0;
		for(i = 0; i < nready; i++)
		{
			//第一层if用来判断是lfd还是cfd
			//第二层if用来判断是读cfd,还是写cfd
			if(events[i].data.fd == sockfd)
			{
				struct sockaddr_in client;
				socklen_t len = sizeof(client);
				int cfd = accept(sockfd,(struct sockaddr*)&client,len);
				//来一个客户端连接一个服务器
				struct epoll_event ev = {0};
				ev.event = EPOLLIN;
				epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
			}
			else
			{
				struct item *it = get_item_by_clientfd(events[i].data.fd);
				if(event[i].event & EPOLLIN)
				{
					//读客户端
					
					it->len = recv(events[i].data.fd,buffer,1024,0);//只读一半,没有读完
				}
				if(event[i].event & EPOLLOUT)
				{
					//写客户端
					send(events[i].data.fd,it->wbuffer+it->wsize,it->wlen-it.size,0);
					//当一次性没有写完时,要放在写缓冲区里面
					//对于一次性没有读完时,要放在读缓冲区
					//这里的缓冲区指的是内核的缓冲区
					it->wsize += ret;
					
				}
			}
		}
	}
}

那么对于以上代码,buffer的处理业务在哪里呢?

对于数据来说,有三层操作:

检测IO事件
read/write
解析

​ 为什么不能直接将解析代码放到recv和send后面呢?因为如果这样写的话,代码的可复用性较差。所以使用回调函数。

​ 反应堆主要是用来检测fd是读还是写。

错误说法:reactor是epoll加上了回调函数: reactor的是用来形容一个事件是否触发

对于下面一个结构体的两种封装方式,哪一个好:

struct reactor
{
	int epfd;
	struct item *items;
	int count;
	int ucount;
};

封装方式一:

struct reactor *Init_reactor(int size)
{
	struct reactor *t = malloc(sizeof(struct reactor));
	
}
struct reactor *del_reactor(struct reactor *r)
{
	free(r);
}

封装方式二:

struct reactor *Init_reactor(struct reactor *r)
{
	r->epfd = epoll_create(1);
	r->items = malloc(1024*sizeof(struct reactor *r));
	r->count = 1024;
	r->ucount = 0;
}
struct reactor *Init_reactor(struct reactor *r)
{
	close(r->epfd);
}

封装方式二更好,因为其避免了出现返回值

之后需要封装下面几个函数

int accept_callback(int fd, int events, void *arg)
{   
}
int recv_callback(int fd, int events, void *arg)
{    
}
int send_callback(int fd, int events, void *arg)
{ 
}
int set_events(int fd, int events, void *arg)
{
}

你可能感兴趣的:(网络编程黑马程序员,数据库)