第一次仔细看epoll的一些资料,加个笔记慢慢更 有不对的求指正哈,抄录各种网上资料 加一些自己的笔记. 仅供参考~
主要参考来源:https://www.cnblogs.com/fnlingnzb-learner/p/5835573.html
epoll大体解决什么问题
epoll用于解决socket编程的一些问题,就是那一套创建套接字,bind,listen ,accept客户端链接过来的fd,然后send&recv这一套东东。然后为什么要有epoll呢,是因为你socket里调accept然后线程就阻塞了,你需要并行处理很多socket连接的时候就只能开多线程去处理这个问题,这样增加了很多的多线程切换的性能开销。于是epoll就来了,epoll用下面的三个api以一种注册&回调的方式处理了,裸的socket编程需要开多线程阻塞等待来作并发的问题。
先来看只用socket的方案(待补充)
epoll只有三个api
- int epoll_create(int size);
创建一个epoll专用的句柄,需要保存后面会用 ,这个size是你要默认监听的fd数量+1 ,因为这个方法创建好以后本身就占用一个fd,/proc/进程id/fd/ 下可以查到这个fd, 该epoll句柄被用完以后需要close 去释放这个fd,否则会导致fd用尽。(不过貌似一般的服务端epoll用完进程也就退出了,粗一点让操作系统自行回收也可不管) - int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
注册epoll事件用的,用epoll模型的处理中主要就是通过这个来改变fd监听的事件的状态来完成的
- 第一个参数是epfd 这个就是上面创建的epoll句柄
- 第二个参数op表示动作 包括下面几个
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd; - 第三个参数是需要监听的fd,就是服务端自己的socketfd 或者 从客户端那里accept()来的fd。
- 第四个参数就是事件的结构体了
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
2个部分组成 一个是uint32 events
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
先简单了解后面几个都不管,第一个in 就是fd里有数据了可以读, 第二个out就是这个东东可以写了
然后就是data字段,一般就是对应的fd 用来你之后收到event 事件的时候判断这个是那个fd的
- int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。(创建了epoll,注册了监听事件,就是用死循环epoll wait来等待事件发生了)
epoll的demo做笔记
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>
#define SERV_PORT 8802
int main()
{
int i,flag;
int sockfd,clntfd,newfd;
int epfd,nfds;
ssize_t n;
char buffer[1024];
int s = sizeof(struct sockaddr);
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
//定义epoll数据结构
struct epoll_event ev,events[20];
epfd = epoll_create(256);
//创建socket,并初始化事件ev
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket error!\n");
return -1;
}
ev.data.fd = sockfd;
ev.events = EPOLLIN|EPOLLET;
//注册epoll事件 这个fd是server端生成的fd 一开始添加的事件是读
//accept的时候会触发
flag = epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
if (flag < 0) {
perror("epoll_ctl error!\n");
return -1;
}
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl( INADDR_ANY );
// socket的bind 和epoll无关
flag = bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(struct sockaddr));
if (flag < 0) {
perror("bind error!\n");
return -1;
}
printf("bind\n");
// socket的listen 后面的20是请求队列长度,超出这个数就不给建立新连接了
// 每个fd只注册一个事件的话epoll wait 出来最多貌似就只有20个,复杂情况触发的事件太菜了还没研究。。
flag = listen(sockfd, 20);
if (flag < 0) {
perror("listen error!\n");
return -1;
}
printf("listen\n");
//开始循环
while (1) {
//等待事件发生,返回请求数目 20是你传进去events的长度, 500是timeout
nfds = epoll_wait(epfd, events, 20, 500);
//一次处理请求
for (i = 0; i < nfds; ++i) {
if (events[i].data.fd == sockfd){
// 收到了事件通知 且是server那个fd的通知
clntfd = accept(sockfd, (struct sockaddr*)&clnt_addr,(unsigned int*)&s);
if (clntfd < 0) {
perror("accept error");
continue;
}
printf("accept\n");
char *str = inet_ntoa(clnt_addr.sin_addr);
printf("accepnt the client ip : %s\n",str);
//设置文件标识符,设置操作属性:写操作
ev.data.fd = clntfd;
ev.events = EPOLLOUT | EPOLLET;
//向创建的的epoll进行注册写操作
//给accept到的客户端socket 加事件 out监听这个fd可以写入的时候通知,就是对面已经读完了缓冲区的时候
// 后续这个fd可以写入的时候这里会收到event(后续的epoll wait)
epoll_ctl(epfd, EPOLL_CTL_ADD, clntfd, &ev);
} else if (events[i].events & EPOLLOUT) {
printf("EPOLLOUT\n");
if ((newfd = events[i].data.fd) < 0)
continue;
bzero(buffer,sizeof(buffer));
strcpy(buffer,"welcome to myserver!\n");
flag = send(newfd, buffer, 1024, 0);
if (flag < 0) {
perror("send error");
continue;
}
//修改操作为读操作
// 因为你这个fd已经写过了fd里是有数据的,这里的流程是accept之后服务端发一条 client回一条
// 所以你这里要把这个fd的监听事件修改成in 就是这个缓冲区被对面写入了通知你 那时候你就可以处理回包
ev.data.fd = clntfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, newfd, &ev);
} else if (events[i].events & EPOLLIN) {
printf("EPOLLIN\n");
//一样的 这里对面写入了通知你读 你这个fd就可以读出来 然后类似上面一样绑定对应的事件做你想做的事情
//也可以什么都不做就打印出来
bzero(buffer,sizeof(buffer));
if ((newfd = events[i].data.fd) < 0)
continue;
if ((n = read(newfd, buffer, 1024)) < 0) {
if (errno == ECONNRESET){
close(newfd);
events[i].data.fd = -1;
printf("errno ECONRESET!\n");
} else {
perror("readbuffer error!\n");
}
} else if (n == 0) {//表示客户端已经关闭
close(newfd);
events[i].data.fd = -1;
printf("n为0\n");
}
if (buffer[0] != '0')
printf("have read: %s\n", buffer);
}
}
}
close(sockfd);
return 0;
}
总结
用了epoll这套api以后,原本你需要accept 然后开线程去处理和客户端的交互(read&send 好多好多次),然后开多线程处理你的具体请求业务, 现在你只需要一个线程就可以处理所有fd的read&send了, 开多线程或者别的去处理你的业务就好。
对比一下
1 纯socket
1个主线程accept + n个处理socket的线程(在双端通信的过程中要一直保持,处理双端的交互和具体的业务任务a,b,c)
2 epoll
1个主线程accept + n个工作线程(一次读取/返回+处理任务即可不用一直保持,一次只需处理一个任务a/b/c,剩下的等回调来了再继续处理)
总结中的总结
总之就是对于某个socket 两边来回send&recv 会有很多io 然后这个线程就只能阻塞等io,这样就会有好多线程在干这事,线程一多切换就多,性能就差了。epoll这种异步回调的处理就使得你不需要有任何一个线程在等待io(来回收发包)的过程中阻塞着(你自己的业务线程里写的代码除外)