Linux网络编程|UNP学习_I/0复用之epoll函数实现回射服务器

UNP学习_I/0复用之epoll函数实现回射服务器
一、函数原型

#includeint epoll_create(int size); int epoll_ctl(int epfd,int op, int fd, struct epoll_event* event); int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); struct epoll_event{ uint32_t events; epoll_data_t data; } typedef union epoll_data{ void* ptr; int fd; uint32_t u32; uint64_t u64; } epoll_date_t;

(1)epoll_create函数生成一个epoll专用的文件描述符,size参数表示通知内核epoll监听数目的大致数量,可拓展。
(2)epoll_ctl是epoll的注册函数,用于控制某个epoll文件描述符下监听的事件,可以注册、修改、删。返回0表示成功,否则返回–1,此时需要根据errno错误码判断错误类型。
epfd:epoll_create生产的epoll文件描述符
op:
EPOLL_CTL_ADD --注册
EPOLL_CTL_MOD --修改
EPOLL_CTL_DEL --删除
fd:关联的文件描述符
event:告诉内核要监听的事件
EPOLLIN(读)、EPOLLOUT(写)、EPOLLERR(异常)
(3)epoll_wait函数用来等待IO事件发生,可以设置阻塞。
epfd:要检测的epoll文件描述符
events:回传待处理事件的数组
maxevents:告诉内核这个events的大小
timeout:-1(永久阻塞)、0(立即返回)、>0(没有检测到事件发生时最多等待的时间(单位为毫秒))
二、epoll工作模式
1、水平触发 LT
只要fd对应的缓冲区有数据epoll_wait就会返回,返回的次数和发送数据的次数没有关系,这是epoll默认的工作模式。
2、阻塞边沿触发 block-ET
fd文件属性默认为阻塞,client给server发一次数据,server的epoll_wait就返回一次,无论缓冲区是否还有数据,都只返回一次,此时如果使用while(recv())可以读完数据,但是读到最后一次recv()会阻塞。
3、非阻塞边沿触发nonblock-ET
这个就是在阻塞的基础上更改fd的属性,可以利用open函数或者fcntl函数更改
int flag = fcntl(fd,F_GETFL); flag |=O_NONBLOCK; fcntl(fd,F_SETFL,flag)

三、非阻塞边沿触发的回射服务器代码实现
// File Name: epoll_server.c // Author: AlexanderGan // Created Time: Wed 31 Jul 2020 04:20:11 PM CST#include #include #include #include #include #include #include #include #include #include #includeint main(int argc, char* argv[]){ if(argc < 2) { printf("eg: ./a.out IP port\n"); } int port = atoi(argv[1]); struct sockaddr_in serv_addr, client_addr; socklen_t serv_len = sizeof(serv_addr); socklen_t cli_len = sizeof(client_addr); //创建套接字 int lfd = socket(AF_INET,SOCK_STREAM,0); printf("lfd = %d\n",lfd); //初始化服务器 memset(&serv_addr,0,serv_len); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(port); //绑定ip和端口 bind(lfd,(struct sockaddr*)&serv_addr,serv_len); //设置同时监听的最大个数 listen(lfd,36); printf("Start accept !\n"); //epoll根结点创建 int epfd = epoll_create(2000); //初始化epoll树 struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = lfd; epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev); struct epoll_event all[2000]; while(1) { //使用epoll通知内核进行检测 int ret = epoll_wait(epfd,all,sizeof(struct epoll_event),-1); printf("============= epoll_wait! ============\n"); //遍历数组 for(int i = 0; i < ret; i++){int fd = all[i].data.fd; //判断是否有新连接 if(fd == lfd){ int cfd = accept(lfd,(struct sockaddr*)&client_addr,&cli_len); //接受连接请求 if(cfd == -1){ perror("accept error!\n"); exit(1); } //设置文件为非阻塞模式 int flag = fcntl(cfd,F_GETFL); flag |= O_NONBLOCK; fcntl(cfd,F_SETFL,flag); //将新的fd挂到树上 struct epoll_event temp; //设置边沿触发 temp.events = EPOLLIN | EPOLLET; temp.data.fd = cfd; epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&temp); //打印客户端信息 char ip[64]; printf("New Client IP: %s,Port:%d\n", inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,ip,sizeof(ip)), ntohs(client_addr.sin_port)); } else{ //处理已经连接的客户端发过来的消息if(!all[i].events & EPOLLIN) continue; //如果没有EPOLLIN事件则跳一次循环 //循环读数据 char buf[5] = {0}; int len; while((len = recv(fd,buf,sizeof(buf),0)) > 0) { //打印到标准输出 write(STDOUT_FILENO,buf,len); //发送给客户端 send(fd,buf,len,0); } if(len == 0) { printf("客户端已主动断开连接。\n"); //从树上删除节点 int ret = epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL); if(ret == -1){ perror("epoll_ctl error!\n"); exit(1); } close(fd); } else if(len == -1){ if(errno == EAGAIN){ printf("缓冲区数据已经读取完。\n"); } else{ printf("recv error!\n"); exit(1); } }} } } close(lfd); return 0 ; }

4、epoll的优缺点
epoll建立了一个红黑树用于存放socket,另外维护了一个链表用来存放准备就绪的事件。执行epoll_ create时,创建了红黑树和就绪链表,执行epoll_ ctl时,向红黑树添加节点,然后向内核注册回调函数。当监控事件发生时向就绪链表中插入数据,执行epoll_wait时立刻返回准备就绪链表里的数据。
优点:
(1)epoll只返回触发事件的描述符链表,不需要再遍历大量的描述符来寻找触发事件的那个描述符。
(2)epoll将事件的注册和监控分离,可以在任何时候添加套接字或从移除,即使另一个线程在epoll_wait函数中。还可以修改描述符事件,一切都会正常工作,而且这种行为是被支持和记录的。
(3)使用epoll_wait()可以让多个线程在同一个epoll队列中等待,这在select/poll中是做不到的。事实上,这不仅在epoll中可以实现,而且是边沿触发模式下的推荐方法。
【Linux网络编程|UNP学习_I/0复用之epoll函数实现回射服务器】缺点:
(1)改变事件标志(即从read到write)需要epoll_ctl的syscall,而使用poll时,这是一个完全在用户空间完成的简单的位掩码操作。用epoll将5000个套接字从读切换到写,需要5000次syscall,从而进行上下文切换(截至2014年,对epoll_ctl的调用仍然不能批量化,每个描述符必须单独更改),而在poll中,则需要在pollfd结构上进行一次循环。
(2)每个accept()套接字都需要添加到集合中,和上面一样,用epoll必须通过调用epoll_ctl来完成–这意味着每个新的连接套接字需要两个系统调用,而不是poll的一个。如果你的服务器有很多发送或接收流量很少的短连接,epoll可能会比poll花费更多的时间来服务它们。
(3)跨平台支持不好,epoll是Linux的专有,虽然其他平台也有类似的机制,但它们并不完全相同–例如,边缘触发是非常独特的(虽然FreeBSD的kqueue也支持)。
(4)高性能的处理逻辑比较复杂,因此调试起来比较困难,尤其是边缘触发,如果错过了额外的读/写,很容易出现死锁。

    推荐阅读