简介
epoll反应堆实际上就是将文件描述符、监听的事件、回调函数用结构体封装在一起,发生事件时触发对应的回调函数即可。下面举一个简易版的例子。
在epoll_ctl()函数中,我们需要操作一个struct epoll_event *ev对象,结构体如下:
struct epoll_event
{
uint32_t events; // epoll事件
epoll_data_t data;
};
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
epoll反应堆的核心是使用data中的ptr成员,而非fd成员,因为ptr是一个void类型指针,我们可以定义一个结构体来保存所有的信息,包括回调函数地址,当对应事件发生,就可以自动触发这个回调函数即可,在这里,结构体可定义为如下形式:
// 事件驱动结构体
// 主要是前三个成员
typedef struct xx_event {
int fd; // 文件描述符
int events; // 需要监听的事件
void (*call_back)(int fd, int events, void* arg); // 回调函数
void* arg;
char buf[1024];
int buflen;
int epfd;
}xevent;
下面给出完整的简易版epoll反应堆代码:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include"wrap.h"
#include<unistd.h>
#define _BUF_LEN_ 1024
#define _EVENT_SIZE_ 1024
// 全局epoll树的根
int gepfd = 0;
// 事件驱动结构体
typedef struct xx_event {
int fd;
int events;
void (*call_back)(int fd, int events, void* arg);
void* arg;
char buf[1024];
int buflen;
int epfd;
}xevent;
xevent myevents[_EVENT_SIZE_ + 1];
void readData(int fd, int events, void* arg);
// 添加事件
void eventadd(int fd, int events, void (*call_back)(int, int, void*), void* arg, xevent* ev)
{
ev->fd = fd;
ev->events = events;
ev->call_back = call_back;
struct epoll_event epv;
epv.events = events;
epv.data.ptr = ev; // 核心
epoll_ctl(gepfd, EPOLL_CTL_ADD, fd, &epv); // 上树
}
// 修改事件
void eventset(int fd, int events, void (*call_back)(int, int, void*), void* arg, xevent* ev)
{
ev->fd = fd;
ev->events = events;
// ev->arg = arg;
ev->call_back = call_back;
struct epoll_event epv;
epv.events = events;
epv.data.ptr = ev;
epoll_ctl(gepfd, EPOLL_CTL_MOD, fd, &epv); // 修改
}
// 删除事件
void eventdel(xevent* ev, int fd, int events)
{
printf("begin call %s\n", __FUNCTION__);
ev->fd = 0;
ev->events = 0;
ev->call_back = NULL;
memset(ev->buf, 0x00, sizeof(ev->buf));
ev->buflen = 0;
struct epoll_event epv;
epv.data.ptr = NULL;
epv.events = events;
epoll_ctl(gepfd, EPOLL_CTL_DEL, fd, &epv); // 下树
}
// 写数据
void senddata(int fd, int events, void* arg)
{
printf("begin call %s \n", __FUNCTION__);
xevent* ev = arg;
Write(fd, ev->buf, ev->buflen);
eventset(fd, EPOLLIN, readData, arg, ev);
}
// 读数据
void readData(int fd, int events, void* arg)
{
printf("begin call %s\n", __FUNCTION__);
xevent* ev = arg;
ev->buflen = Read(fd, ev->buf, sizeof(ev->buf));
if (ev->buflen > 0) // 读到数据
{
// void eventset(int fd, int events, void (*call_back)(int, int, void*), void *arg, xevent *ev);
eventset(fd, EPOLLOUT, senddata, arg, ev); // 修改成写事件
}
else if (ev->buflen == 0) // 对方关闭连接
{
close(fd);
eventdel(ev, fd, EPOLLIN);
}
}
// 新连接处理
void initAccept(int fd, int events, void* arg)
{
printf("begin call %s, gepfd = %d\n", __FUNCTION__, gepfd);
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int cfd = Accept(fd, (struct sockaddr*)&addr, &len);
//查找myevents数组中可用的位置
int i;
for (i = 0; i < _EVENT_SIZE_; i++)
{
if (myevents[i].fd == 0)
{
break;
}
}
// 设置读事件
eventadd(cfd, EPOLLIN, readData, &myevents[i], &myevents[i]);
}
int main()
{
// 创建socket
int lfd = Socket(AF_INET, SOCK_STREAM, 0);
// 端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8888);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(lfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
// 监听
Listen(lfd, 128);
// 创建epoll树根节点
gepfd = epoll_create(1024);
printf("gepfd = %d\n", gepfd);
struct epoll_event events[1024];
// 添加最初始事件,将监听套接字上树
eventadd(lfd, EPOLLIN, initAccept, &myevents[_EVENT_SIZE_], &myevents[_EVENT_SIZE_]);
while (1)
{
int nready = epoll_wait(gepfd, events, 1024, -1);
if (nready < 0) // 调用epoll_wait失败
{
perror("epoll_wait error");
}
else if (nready > 0)
{
int i = 0;
for (i = 0; i < nready; i++)
{
xevent* xe = events[i].data.ptr; // 取ptr指向结构体地址
printf("fd = %d\n", xe->fd);
if (xe->events & events[i].events)
xe->call_back(xe->fd, xe->events, xe); // 调用事件对应的回调
}
}
}
return 0;
}
上述代码的缺点:
回调函数运行时,如果此时来了新的客户端请求连接,会导致不能及时处理请求,这在高并发的环境中可能会导致有很大的延迟。所以可以使用多线程的方式,具体是连接任务和客户端任务分别用不同的线程来处理,可以使用线程池的方式来解决,在下一篇文章中写具体如何实现。