5.实现 Channel 类,Reactor 模式初步形成

目录

由联合体epoll_data引出类Channel

结构体epoll_data_t

Channel类

Channel类的使用

Epoll类的改变


由联合体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模式。

相关推荐
敲上瘾10 分钟前
高并发内存池(二):Central Cache的实现
linux·服务器·c++·缓存·哈希算法
一只努力学习的Cat.10 分钟前
Linux:环境变量
linux
EasyNVR18 分钟前
视频分析设备平台EasyCVR视频结构化AI智能分析:筑牢校园阳光考场远程监控网
网络·音视频
iOS技术狂热者22 分钟前
Flutter 音视频播放器与弹幕系统开发实践
websocket·网络协议·tcp/ip·http·网络安全·https·udp
安顾里27 分钟前
Linux命令-tar
linux·运维·服务器
jinan88632 分钟前
企业的移动终端安全怎么管理?
大数据·网络·安全·数据分析·开源软件
W说编程37 分钟前
《UNIX网络编程卷1:套接字联网API》第5章 TCP客户服务器程序示例
c语言·网络·网络协议·tcp/ip·unix·tcp
不爱敲代码的阿玲41 分钟前
西门子s7协议
服务器·网络·tcp/ip
有莘不破呀1 小时前
服务器磁盘卷组缓存cache设置介绍
linux·运维·服务器
栗筝i1 小时前
Spring 核心技术解析【纯干货版】- XVII:Spring 网络模块 Spring-WebFlux 模块精讲
java·网络·spring