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)高性能的处理逻辑比较复杂,因此调试起来比较困难,尤其是边缘触发,如果错过了额外的读/写,很容易出现死锁。
推荐阅读
- Linux|109 个实用 shell 脚本
- linux笔记|linux 常用命令汇总(面向面试)
- Linux|Linux--网络基础
- linux|apt update和apt upgrade命令 - 有什么区别()
- 网络|网络编程释疑(TCP连接拔掉网线后会发生什么)
- linux|2022年云原生趋势
- Go|Docker后端部署详解(Go+Nginx)
- 开源生态|GPL、MIT、Apache...开发者如何选择开源协议(一文讲清根本区别)