目录
由联合体epoll_data引出类Channel
在之前使用epoll时,有使用到一个结构体epoll_event
cpp
// 这是联合体,多个变量共用同一块内存
typedef union epoll_data {
void *ptr; // 指针类型,可以指向任意数据类型,通常用于存储自定义数据结构的指针
int fd; // 文件描述符,常用于指代与 epoll_ctl 操作相关的文件描述符
uint32_t u32; // 32 位无符号整形,可用于存储无符号整数类型的用户数据
uint64_t u64; // 64 位无符号整形,适合存储较大的整数或数据
} epoll_data_t;
// epoll_event 结构体用于描述要监控的事件及其相关的数据
struct epoll_event {
uint32_t events; /* epoll 事件类型 */
epoll_data_t data; /* 用户数据变量 */
};
// 调用 epoll_ctl 函数来控制与 epoll 相关的事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
结构体epoll_data_t
epoll_event中有个成员变量data,这是一个联合体。初学情况下,我们会使用data的成员变量fd。
回想一下,我们使用epoll_event的时候
cpp
// 调用 epoll_wait,等待事件发生
// epfd: epoll 实例的文件描述符
// events: 用于存储被激活事件的数组
// MAX_SIZE: 事件数组的最大容量
// -1: 阻塞模式,直到有事件发生
int nums = epoll_wait(epfd, events, MAX_SIZE, -1);
// 遍历所有返回的事件
for (int i = 0; i < nums; ++i) {
// 检查当前事件的文件描述符是否为监听文件描述符 (lfd)
if (events[i].data.fd == lfd) {
handl1(); // 处理连接操作,例如接受新的客户端连接
}
// 检查是否为可读事件
else if (events[i].events & EPOLLIN) {
handl2(); // 处理通信操作,例如读取客户端发送的数据
}
}
这样我们只使用联合体中成员fd,只能得到文件描述符。
当时当我们想要获取关于该fd更多的信息时候,就不妥了。比如想获取该用户连接成功的时间。这时可能有朋友想到可以使用std::unordered_map来存储,但这就需要借助外部的变量。
那么,我们换一种思路,联合体epoll_data中还有个变量ptr,其是个void*指针。那就可以指向任何一块地址空间,也可以指向一个类对象,这就可以包含关于这个文件描述符的更多信息。
按照上面的思路,我们可以把firstTime和fd构成一个结构体,之后就使用联合体epoll_data的ptr,不使用fd。
cpp
// 用户自定义的数据结构,用于存储与文件描述符相关的详细信息
struct moreMsg {
int fd; // 文件描述符,标识特定的套接字或文件
int64_t firstTime; // 连接成功的时间,通常用于记录客户端的连接时间
int events; // epoll_wait 返回的活跃事件,用于存储事件类型(如可读、可写等)
};
在新用户连接成功的时候,就创建moreMesg结构体,给fd和firstTime和events都赋值。这个epoll_data 会随着 epoll_wait 返回的 epoll_event 一并返回。那附带的自定义消息,就是ptr指向这个自定义消息。
那么我们可以把一个文件描述符封装成一个类,这个类里面有更多的关于这个文件描述符的信息,我们把他叫做Channel类,用这个void*指针指向Channel类对象。
Channel类
cpp
class Channel {
public:
// 构造函数,初始化与 Epoll 对象的指针和文件描述符
Channel(Epoll* ep, int fd);
// 设置关注的事件类型
void setEvent(uint32_t events);
// 获取关注的事件类型
uint32_t Event() const;
// 设置从 epoll_wait 返回的事件类型
void setRevent(uint32_t revents);
// 获取从 epoll_wait 返回的事件类型
uint32_t Revent() const;
// 检查通道是否在 epoll 中
bool isInepoll();
// 设置通道是否在 epoll 中的状态
void setInEpoll(bool in);
// 获取该通道关联的文件描述符
int fd();
// 启用读取事件
void enableReading();
private:
Epoll* ep_; // 指向关联的 Epoll 对象的指针,用于更新事件
int fd_; // 文件描述符,表示该通道关联的文件或 socket
uint32_t events_; // 用户关心的事件类型,例如可读或可写
uint32_t revents_; // 从 epoll_wait 返回的活跃事件类型
bool isInEpoll_; // 标记该通道是否在 epoll 的红黑树中
};
成员变量:
- 那一定会有文件描述符fd,每个Channel对象自始至终只负责一个fd,但是它不拥有这个fd,即也不会在析构的时候关闭这个fd;
- 接着有Epoll类对象,我们要通过epoll把fd添加到epoll上,一个channel只跟着一个Epoll,通过enableReading()函数添加fd到该epoll上。
- events_是channel关心的IO事件,由用户设置( 即用户想监听该fd的哪类事件,如读事件**)**
- **revents_是通过epoll_wait()返回的目前该fd活动的事件(**即我们要根据这个类型执行相对的操作,返回读事件就执行读操作)。
- isInEpoll_是用于判断该fd是否在epoll红黑树上,进而判断是使用EPOLL_CTL_ADD还是使用EPOLL_CTL_MOD。
一个channel就对应了一个客户端,或者服务器端。
Channel类的使用
cpp
// 创建一个 Socket 对象,用于网络通信
Socket serv_socket;
// 创建一个 Epoll 对象,用于管理多个文件描述符的事件
Epoll ep;
// 创建一个 Channel 对象,关联先前创建的 Epoll 对象和 Socket 的文件描述符
Channel* ch = new Channel(&ep, serv_socket.fd());
// 说明:Channel 的构造函数需要传入一个指向 Epoll 对象的指针
// 以及要监视的文件描述符(即 serv_socket.fd()),
// 这样 Channel 才能在事件被触发时有效地与 Epoll 进行交互。
那接下来我们需要把fd添加到epoll红黑树上,之前是使用ep.update(fd,EPOLLIN,EPOLL_CTL_ADD);
那现在是通过Channel去把fd添加到红黑树上
cpp
// 启用读取事件的成员函数
void enableReading() {
// 设置关注的事件类型为可读事件(EPOLLIN)
setEvent(EPOLLIN);
// 更新 epoll 中的通道,将这个通道的新事件添加到 epoll 中
ep_->updateChannel(this);
}
// 设置关注的事件类型
void setEvent(uint32_t events) {
// 将传入的事件类型存储到成员变量 events_ 中
events_ = events;
}
其主要是做了两件事,将要监听的事件events
设置为读事件,然后用成员变量ep_去更新channel。
Epoll类的改变
Channel::enableReading()函数内部调用了Epoll::updateChannel(Channel*)。updateChannel成员函数的参数类型是Channel*,猜想函数内部是对channel做了一些操作的。
EpoLL类的updateChannel()的实现如下:
cpp
// 更新与 epoll 关联的通道
void updateChannel(Channel* ch) {
// 获取通道的文件描述符
int fd = ch->fd();
// 创建 epoll_event 结构体,用于描述要修改的事件
struct epoll_event ev;
// 清空结构体
memset(&ev, 0, sizeof(ev));
// 将通道的指针存储在 epoll_event 结构中
ev.data.ptr = ch; // 这里很重要,我们使用指针而不是文件描述符
ev.events = ch->Event(); // 获取通道当前设置的事件类型
// 检查通道是否已在 epoll 树中
if (ch->isInepoll()) {
// 如果通道已经在 epoll 树中,修改其事件
epoll_ctl(epfd_, EPOLL_CTL_MOD, fd, &ev);
} else {
// 如果通道不在 epoll 树中,添加该通道
epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev);
ch->setInEpoll(true); // 设置通道已在 epoll 树上
}
}
// 注意:原函数省略了错误判断,实际使用中应该检查
// epoll_ctl 的返回值,处理可能的错误情况
EPOLL的epoll_wait()函数代码也有修改,有了Channel之后,返回的就是vector<Channel*>。
cpp
// 使用 Channel 类的 Epoll_wait 函数
void Epoll_wait(vector<Channel*>& active, int timeout = 10) {
// 调用 epoll_wait,等待事件发生
int nums = epoll_wait(epfd_, events_, SIZE, timeout);
// 遍历返回的事件数量
for (int i = 0; i < nums; ++i) {
// 从 events_ 中获取 Channel 对象的指针
Channel* ch = static_cast<Channel*>(events_[i].data.ptr);
// 设置从 epoll_wait 返回的事件类型
ch->setRevents(events_[i].events);
// 将活跃的通道添加到 active 列表中
active.emplace_back(ch);
}
}
// 以前的写法
/*
void Epoll_wait(vector<epoll_event>& active, int timeout = 10) {
// 调用 epoll_wait,等待事件发生
int nums = epoll_wait(epfd_, events_, SIZE, timeout);
// 遍历返回的事件数量
for (int i = 0; i < nums; ++i) {
// 将 events_ 中的每个事件添加到 active 列表中
active.emplace_back(events_[i]);
}
}
*/
那么在编写服务端程序的时候,我们不再用epoll_events结构体,而是使用Channel。
cpp
// 创建一个 Epoll 对象
Epoll ep;
// 事件循环,持续等待和处理 I/O 事件
while (true) {
// 创建一个用于存储活动通道的向量
vector<Channel*> activeChannel;
// 调用 Epoll 的 Wait 函数,获取活跃的通道
ep.Epoll_Wait(activeChannel);
// 获取活跃通道的数量
int nums = activeChannel.size();
// 遍历所有活跃通道
for (int i = 0; i < nums; ++i) {
// 获取当前活跃通道关联的文件描述符
int fd = activeChannel[i]->fd();
// 如果文件描述符是监听文件描述符 (lfd),处理连接请求
if (fd == lfd) {
handle1(); // 处理新连接
}
// 检查当前通道是否有可读事件
else if (activeChannel[i]->Event() & EPOLLIN) {
handle2(); // 处理通信操作
}
}
}
这一节我们还没有使用回调函数,这不符合我们的要求的,这将在下一节中进行修改。
通过这一节的修改,我们可以获得关于epoll返回的活跃的文件描述符的更多信息,添加了Channel类。接下来也会逐渐实现Reator模式。