首先必须谈谈epollin和epollout事件触发的条件 没看这个 整个epoll就是懵的
1、EPOLLOUT事件触发的条件?
(1)、一次write操作,写满了发送缓冲区,返回错误码为EAGAIN(11)。
(2)、对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。
简单地说:内核缓冲区由不可写变为可写时会触发,才会触发一次,所以叫边缘触发。
暴力方法:直接调用epoll_ctl()重新设置一下event就可以了, event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。
2、EPOLLIN事件触发的条件?
对端有数据写入时才会触发。
epoll还分为LT和ET 即水平触发和边缘触发
LT就是只要满足条件我就会一直触发事件 而ET是你满足一次条件我触发一次事件
LT模式会一直触发EPOLLOUT,当缓冲区有数据时会一直触发EPOLLIN
ET模式会在连接建立后触发一次EPOLLOUT,当收到数据时会触发一次EPOLLIN
LT模式触发EPOLLIN时可以按需读取数据,残留了数据还会再次通知读取
ET模式触发EPOLLIN时必须把数据读取完,否则即使来了新的数据也不会再次通知了
LT模式的EPOLLOUT会一直触发,所以发送完数据记得删除,否则会产生大量不必要的通知
ET模式的EPOLLOUT事件若数据未发送完需再次注册,否则不会再有发送的机会
通常发送网络数据时不会依赖EPOLLOUT事件,只有在缓冲区满发送失败时会注册这个事件,期待被通知后再次发送
下面就是epoll服务器的代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <errno.h>
#include <sys/epoll.h>
struct sockitem {
int sockfd;
int (*callback)(int fd, int events, void *arg);`
}
//等会epoll传入的指针需要用到的结构体 用来回调
// ./epoll 8080
int recv_cb(int fd, int events, void *arg) {
}
//具体的回调函数
int accept_cb(int fd, int events, void *arg) {
int clientfd = accept();
struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
si->sockfd = clientfd;
si->callback = recv_cb;
//
epoll_ctl()
}
//具体的回调函数
int main(int argc, char *argv[]) {
if (argc < 2) {
return -1;
}
int port = atoi(argv[1]);//
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
return -1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
return -2;
}
if (listen(sockfd, 5) < 0) {
return -3;
}
//上面都是最基本的socket流程 不想过多讲述
// 从这里开始epoll
//首先创建一个epoll 参数无所谓01
int epfd = epoll_create(1);
//接下来创建epoll需要的结构体 该结构体用来像epoll内注册io事件和获取一些信息
struct epoll_event ev, events[512] = {0};
//ev用来临时加入epoll events用来获取已经被写入或被读取数据的io
ev.events = EPOLLIN;//帮我监听epollin事件
ev.data.fd = sockfd; //文件描述符是sockfd
//epoll_event有个最大的好处就是他允许你带一个参数进去 那么这里我们就传入我们自定义的结构体 等下就能自动调用回调函数
struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
si->sockfd = sockfd;
si->callback = accept_cb;
//上面是填写结构体的值 没啥好说的
//注意第二个值是一个回调函数 准备回调的
ev.data.ptr = si;
//将指针也写入epoll_event结构体
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
//将最开始用来listen的结构体传入epoll中开始监测
while (1) {
//写一个循环不断的去epoll中拿被改写过的IO
int nready = epoll_wait(epfd, events, 512, 50);
//这里的意思就是我去epfd对应的epoll中去拿 最多拿512个 给我放到首地址为events的地方 最长等待时间是50ms
//此时间将四舍五入为系统时钟的粒度,并且内核调度延迟意味着阻塞间隔可能会少量溢出。
if (nready < -1) {
break;
}
//没收到的话 直接break掉
int i = 0;
for (i = 0;i < nready;i ++) {
if (events[i].events & EPOLLIN) {
//如果是epollin事件
struct sockitem *si = (struct sockitem*)events[i].data.ptr;
si->callback(events[i].data.fd, events[i].events, si);
}
if (events[i].events & EPOLLOUT) {
//如果是epollout事件
//代码就不写了 跟上面流程是一样的
}
}
}
}
但是以上代码明显有几个思考的点 比如多线程下如何同时使用epoll 文件描述符之间如何传递?