1、原始的epoll模型
socket、bind、listen创建socket套接字--->epoll_create创建监听红黑树
--->返回监听文件红黑树文件描述符epfd--->epoll_ctl()向树上添加一个监听fd
--->while(1)--->epoll_wait监听--->对应监听fd有事件产生--->返回监听满足数组
--->判断返回数组元素--->lfd满足--->Accept--->cfd 满足--->read()--->小->大--->write回去。
2、epoll反应堆模型
epoll ET模式 + 非阻塞、轮询 + void *ptr。
不但要监听 cfd 的读事件、还要监听cfd的写事件。【自动回调】
socket、bind、listen创建socket套接字--->epoll_create创建监听红黑树
--->返回监听文件红黑树文件描述符epfd--->epoll_ctl()向树上添加一个监听fd
--->while(1)--->epoll_wait监听--->对应监听fd有事件产生--->返回监听满足数组
--->判断返回数组元素--->lfd满足--->Accept--->cfd 满足--->read()--->小->大
--->cfd从监听红黑树上摘下--->EPOLLOUT--->回调函数--->epoll_ctl函数
--->EPOLL_CTL_ADD重新放到红黑树上监听写事件--->等待epoll_wait返回
--->说明cfd可写--->write回去--->cfd从监听红黑树上摘下--->epoll_ctl
--->EPOLL_CTL_ADD重新放到红黑树上监听读事件--->epoll_wait监听。
反应堆的理解:加入IO转接之后,有了事件,server才去处理,这里反应堆也是这样,由于网络环境复杂,服务器处理数据之后,可能并不能直接写回去,比如遇到网络繁忙或者对方缓冲区已经满了这种情况,就不能直接写回给客户端。反应堆就是在处理数据之后,监听写事件,能写会客户端了,才去做写回操作。写回之后,再改为监听读事件。如此循环。
/*************************************************************************
> File Name: epoll_reactor.c
> Author: Winter
> Created Time: 2022年07月08日 星期五 14时48分21秒
************************************************************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_EVENTS 1024 // 监听上限
#define BUF_LEN 4096 // 缓冲区大小
#define SERVER_PORT 9527 // 端口号
void recv_data(int fd, int events, void* arg);
void send_data(int fd, int events, void* arg);
// 就绪文件描述符相关信息
struct MyEvent {
int fd; // 要监听的文件描述符
int events; // 对应的监听事件
void* arg; // 泛型参数
void (*call_back)(int fd, int events, void* arg); // 回调函数
int status; // 是否监听,1表示在红黑树上,0表示不在
char buf[BUF_LEN]; // 缓冲区
int len; // 缓冲区大小
long last_active; // 记录每次加入到红黑树g_efd的时间
};
// 两个全局变量
int g_efd; // 全局变量,保存红黑树的根节点,即epoll_create的返回值
struct MyEvent g_events[MAX_EVENTS + 1]; // 自定义结构体类型数组 +1------>listen fd
// 将结构体MyEvent的成员变量初始化
void event_set(struct MyEvent* ev, int fd, void (*call_back)(int, int, void*), void* arg) {
ev->fd = fd;
ev->arg = arg;
ev->events = 0;
ev->call_back = call_back;
ev->status = 0;
memset(ev->buf, 0, sizeof(ev->buf));
ev->len = 0;
ev->last_active = time(NULL);
}
// 向epoll监听红黑树上添加一个文件描述符
void event_add(int efd, int events, struct MyEvent* ev) {
struct epoll_event epv = {0, {0}};
int op;
epv.data.ptr = ev;
epv.events = ev->events = events; // events是EPOLLIN或者EPOLLOUT
// 不在红黑树上的了
if (ev->status == 0) {
op = EPOLL_CTL_ADD; // 将其添加到红黑树上
ev->status = 1;
}
// 实际添加/修改
if (epoll_ctl(efd, op, ev->fd, &epv) < 0) {
printf("event add falied [fd = %d], event[%d]\n", ev->fd, events);
} else {
printf("event add ok [fd = %d], op = %d, event[%0x]\n", ev->fd, op, events);
}
return;
}
// 从epoll监听红黑树上删除一个文件描述符
void event_del(int efd, struct MyEvent* ev) {
struct epoll_event epv = {0, {0}};
// 不在红黑树上
if (ev->status == 0) {
return;
}
epv.data.ptr = NULL;
// 修改状态
ev->status = 0;
// 将ev->fd从红黑树上摘下
epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv);
return;
}
// 有文件描述符就绪,epoll返回,调用该函数与客户端建立连接
void accept_conn(int lfd, int events, void* arg) {
// 客户端地址结构
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int cfd, i;
char client_ip[BUF_LEN];
if ((cfd = accept(lfd, (struct sockaddr*)(&client_addr), &client_addr_len)) == -1) {
if (errno != EAGAIN && errno != EINTR) {
// 不做处理
}
printf("%s:accept:%s\n", __func__, strerror(errno));
return;
}
do {
// 找到全局数组中最小空闲元素
for (i = 0; i < MAX_EVENTS; i++) {
if (g_events[i].status == 0) {
break;
}
}
// 代码健壮性处理
if (i == MAX_EVENTS) {
printf("%s:max connect limit [%d]\n", __func__, MAX_EVENTS);
break;
}
int flag = 0;
// 将cfd设置成非阻塞
if ((flag == fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) {
printf("%s:fcntl nonblocking falied:%s\n", __func__, strerror(errno));
break;
}
// 给cfd设置设置回调函数recv_data
event_set(&g_events[i], cfd, recv_data, &g_events[i]);
// 将读事件添加到红黑树上
event_add(g_efd, EPOLLIN, &g_events[i]);
}while (0);
// 打印客户端信息
printf("new connect[%s:%d][time:%ld], pos[%d]\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
ntohs(client_addr.sin_port), g_events[i].last_active, i);
return;
}
// 接收数据
void recv_data(int fd, int events, void* arg) {
struct MyEvent* ev = (struct MyEvent*)arg;
// 读文件描述符,数据存储到MyEvent的buf中
int len = recv(fd, ev->buf, sizeof(ev->buf), 0);
// 将该节点从红黑树上摘下
event_del(g_efd, ev);
if (len > 0) {
ev->len = len;
ev->buf[len] = '\0';
printf("client %d:%s\n", fd, ev->buf);
// 设置该fd的回调函数是send_data
event_set(ev, fd, send_data, ev);
// 将写事件添加到红黑树上
event_add(g_efd, EPOLLOUT, ev);
} else if (len == 0) {
close(ev->fd);
printf("[fd = %d], pos[%ld], closed\n", fd, ev - g_events);
} else {
close(ev->fd);
printf("recv[fd = %d], error[%d]: %s\n", fd, errno, strerror(errno));
}
return;
}
// 发送数据
void send_data(int fd, int events, void* arg) {
struct MyEvent* ev = (struct MyEvent*)arg;
// 直接将数据回写给客户端,未做处理
int len = send(fd, ev->buf, ev->len, 0);
// 从红黑树g_efd中删除
event_del(g_efd, ev);
if (len > 0) {
printf("send [fd = %d], [%d]%s\n", fd, len, ev->buf);
// 将该fd的回调函数改为reccv_data
event_set(ev, fd, recv_data, ev);
// 重新添加到红黑树上,设置为监听读事件
event_add(g_efd, EPOLLIN, ev);
} else {
close(ev->fd);
printf("send[fd = %d], error = %s\n", fd, strerror(errno));
}
return;
}
// 初始化监听socket
void init_listen_socket(int efd, short port) {
// 服务器地址结构
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 1创建socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1) {
perror("socket error");
exit(1);
}
// 将lfd设置成非阻塞
int flag = fcntl(lfd, F_GETFL);
flag = flag | O_NONBLOCK;
fcntl(lfd, F_SETFL, flag);
// 2绑定服务器地址结构
int res = bind(lfd, (struct sockaddr*)(&server_addr), sizeof(server_addr));
if (res == -1) {
perror("bind error");
exit(1);
}
// 3设置监听上限
res = listen(lfd, 128);
if (res == -1) {
perror("bind error");
exit(1);
}
// 为lfd设置回调函数accept_conn
// 这里lfd是监听文件描述符
event_set(&g_events[MAX_EVENTS], lfd, accept_conn, &g_events[MAX_EVENTS]);
// 将EPOLLIN事件添加到红黑树上
event_add(efd, EPOLLIN, &g_events[MAX_EVENTS]);
}
int main(int argc, char* argv[])
{
unsigned short port = SERVER_PORT;
// 使用用户指定的参数
if (argc == 2) {
port = atoi(argv[1]);
}
// 创建红黑树
g_efd = epoll_create(MAX_EVENTS + 1);
if (g_efd <= 0) {
printf("create_epoll in %s error:%s\n", __func__, strerror(errno));
}
// 初始化监听socket
init_listen_socket(g_efd, port);
// 保存已经满足就绪事件的文件描述符数组
struct epoll_event events[MAX_EVENTS + 1];
printf("server running : port[%d]\n", port);
int check_pos = 0, i;
while (1) {
// 超时验证,每次测试100个连接,不测试listenfd,当客户端60s没有与服务器通信,则关闭客户端连接
long now = time(NULL);
for (i = 0; i < 100; i++, check_pos++) {
if (check_pos == MAX_EVENTS) {
check_pos = 0;
}
// 不在红黑树上
if (g_events[check_pos].status == 0) {
continue;
}
// 客户端不活跃时间
long duration = now - g_events[check_pos].last_active;
if (duration >= 60) {
// 关闭连接
close(g_events[check_pos].fd);
printf("[fd = %d], timeout\n", g_events[check_pos].fd);
// 将该客户端从红黑树上删除
event_del(g_efd, &g_events[check_pos]);
}
}
// 监听红黑树g_efd将满足事件的文件描述符添加到events数组中,1s没有事件满足,就返回0
int nfd = epoll_wait(g_efd, events, MAX_EVENTS + 1, 1000);
if (nfd < 0) {
printf("epoll_wait error,exit\n");
break;
}
for (int k = 0; k < nfd; k++) {
// 使用自定义结构体MyEvent类型指针,接收联合体data中的ptr成员
struct MyEvent* ev = (struct MyEvent*)events[k].data.ptr;
// 读事件
if ((events[k].events & EPOLLIN) && (ev->events & EPOLLIN)) {
ev->call_back(ev->fd, events[k].events, ev->arg);
}
// 写事件
if ((events[k].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {
ev->call_back(ev->fd, events[k].events, ev->arg);
}
}
}
return 0;
}
笔者最近把epoll反应堆的代码看了几遍,有了新的感悟,整理一下思路。
(1)有一个自定义的结构体,里面有【监听文件描述符fd、对应的监听事件events、泛型参数arg、回调函数call_back、监听状态status、缓冲区buf、缓冲区大小len、记录每次加入到红黑树的时间】;两个全局变量【红黑树的根节点、自定义结构体数组】
这个结构体非常重要,回调函数的泛型参数是比较重要的信息。
(2)从主函数开始梳理代码思路:首先是处理端口port,再创建监听红黑树,到这里都好理解;接下来在主函数中调用的init_listen_socket这个函数,传参是红黑树的根节点g_efd和端口port,接下来分析init_listen_socket这个函数;
(3)这个函数直到调用event_set之前的代码都好理解,就是常规的创建监听文件描述符、将监听文件描述符设置成非阻塞的方式、绑定服务器地址结构、设置监听上限。到event_set函数就开始难理解了,先看event_set的原型:
// 将结构体MyEvent的成员变量初始化
void event_set(struct MyEvent* ev, int fd, void (*call_back)(int, int, void*), void* arg) {
ev->fd = fd;
ev->arg = arg;
ev->events = 0;
ev->call_back = call_back;
ev->status = 0;
memset(ev->buf, 0, sizeof(ev->buf));
ev->len = 0;
ev->last_active = time(NULL);
}
可见,这个函数是为自定义的结构体赋值的。再看init_listen_socket函数中event_set是怎么用的:
event_set(&g_events[MAX_EVENTS], lfd, accept_conn, &g_events[MAX_EVENTS]);
首先,类型上都没有问题。将结构体数组的最后一个数据的地址传入,将监听文件描述符lfd传入、回调函数是accept_conn,最后一个泛型参数也是自定义结构体的最后一个数据的地址(这很有意思)。下一句是代码是:
event_add(efd, EPOLLIN, &g_events[MAX_EVENTS]);
先啃event_add函数源码
// 向epoll监听红黑树上添加一个文件描述符
void event_add(int efd, int events, struct MyEvent* ev) {
struct epoll_event epv = {0, {0}};
int op;
epv.data.ptr = ev;
epv.events = ev->events = events; // events是EPOLLIN或者EPOLLOUT
// 不在红黑树上的了
if (ev->status == 0) {
op = EPOLL_CTL_ADD; // 将其添加到红黑树上
ev->status = 1;
}
// 实际添加/修改
if (epoll_ctl(efd, op, ev->fd, &epv) < 0) {
printf("event add falied [fd = %d], event[%d]\n", ev->fd, events);
} else {
printf("event add ok [fd = %d], op = %d, event[%0x]\n", ev->fd, op, events);
}
return;
}
可以看出event_add函数是将events事件添加到监听红黑树efd上,
(4)分析accept_conn函数。这个函数到do语句执行之前也都好理解,即服务器阻塞监听客户端。看看do{}while(0)语句中的代码。首先是遍历自定义数组,找到全局最小空闲元素,数组越界处理(用户多了,终止程序,这是代码健壮性处理)、将通信文件描述符cfd设置成非阻塞方式,接下来又是event_set函数:
event_set(&g_events[i], cfd, recv_data, &g_events[i]);
将找到的最小空闲元素传入,处理事件是通信文件描述符cfd,回调函数是recv_data(看函数名是发送数据),泛型参数也是找到的最小空闲元素。把(3)、(4)画个图会看的更清楚
也就说:先在自定义数组下标最大的位置将监听文件描述符lfd及其回调函数accept_conn填入、而监听文件描述符lfd的回调函数accept_conn,先处理通信事件,得到通信文件描述符cfd,然后遍历自定义数据,找到数组最小的空闲元素,将通信文件描述符cfd及其回调函数recv_data填入。
那么,通信文件描述符cfd的功能是什么呢?
当时是从客户端读取数据了,即接收数据。
再将EPOLLIN事件添加到监听红黑树efd上
event_add(g_efd, EPOLLIN, &g_events[i]);
后面的代码是输出客户端的信息。
(5)分析recv_data函数代码。接收数据,再将文件描述符fd的回调函数改成send_data并将其挂到红黑树上,将事件设置成写事件
// 设置该fd的回调函数是send_data
event_set(ev, fd, send_data, ev);
// 将写事件添加到红黑树上
event_add(g_efd, EPOLLOUT, ev);
即fd处理完接收数据后,要开始发送数据了。
(6)分析send_data函数代码。发送数据,发送成功后,再将文件描述符fd的回调函数改成recv_data并将其挂到红黑树上,将事件设置成读事件
// 将该fd的回调函数改为reccv_data
event_set(ev, fd, recv_data, ev);
// 重新添加到红黑树上,设置为监听读事件
event_add(g_efd, EPOLLIN, ev);
即fd处理完发送数据后,要开始读取数据了。
如此往复循环。。。。。。
socket、bind、listen创建socket套接字--->epoll_create创建监听红黑树
--->返回监听文件红黑树文件描述符epfd--->epoll_ctl()向树上添加一个监听fd
--->while(1)--->epoll_wait监听--->对应监听fd有事件产生--->返回监听满足数组
--->判断返回数组元素--->lfd满足--->Accept--->cfd 满足--->read()--->小->大
--->cfd从监听红黑树上摘下--->EPOLLOUT--->回调函数--->epoll_ctl函数
--->EPOLL_CTL_ADD重新放到红黑树上监听写事件--->等待epoll_wait返回
--->说明cfd可写--->write回去--->cfd从监听红黑树上摘下--->epoll_ctl
--->EPOLL_CTL_ADD重新放到红黑树上监听读事件--->epoll_wait监听。
最终运行的时候,似乎只能有一次数据的传输???