Muduo:(1) 文件描述符及其事件与回调的封装 (Channel)

1. 模块概述

1.1 设计目标

Channel 是 mymuduo 网络库中的事件分发器,它封装了文件描述符(fd)及其感兴趣的事件和事件回调。Channel 的核心设计目标包括:

  1. 事件抽象:将文件描述符和事件回调统一封装
  2. 回调管理:为不同类型的事件(读、写、错误、关闭)提供回调机制
  3. 生命周期管理:通过 tie 机制防止对象被提前销毁
  4. 事件状态跟踪:记录当前监听的事件和在 Poller 中的状态

1.2 Channel 的作用

Channel 为 事件(文件描述符)与回调的连接器

  1. 一对一关系 :在网络编程中,每一个 socket 连接(或者 timerfd 等)都有一个唯一的文件描述符 fd。一个 Channel 对象专门负责管理一个 fd。
  2. 承上启下
    • 向下:它告诉底层的 Poller(如 epoll):"我关注这个 fd 的读/写事件"。
    • 向上 :当 Poller 发现事件发生时,通知 EventLoop,EventLoop 找到对应的 Channel,Channel 再调用预先设置好的 回调函数(比如 onMessage, onConnection)。
  3. 不拥有 fd:Channel 只是管理 fd 的状态,它不负责创建或关闭 fd(通常由 TcpConnection 或 Socket 类负责),它只负责"监听"和"通知"。

2. 源码

2.1 Channel.h

cpp 复制代码
#pragma once
#include "NonCopyable.h" 
#include "Timestamp.h"
#include <functional>      // std::function 回调
#include <memory>          // 智能指针
namespace mymuduo {
class EventLoop;  // 前向声明,避免循环依赖

/**
 * Channel 类:封装一个fd、在Poller中的状态标识、所属的EventLoop、各种事件回调
 * 关键成员:
 * int fd_;                // 管理的文件描述符
 * int events_;            // 关注的事件类型(EPOLLIN等)
 * int revents_;           // 实际发生的事件类型
 * int index_;             // 在Poller中的状态标识
 * EventLoop* loop_;       // 所属的EventLoop
 * .....                   // 各种事件回调
 * 
 */
class Channel : NonCopyable {
public:
    // 定义回调类型
    using EventCallback = std::function<void()>;               // 通用事件回调(无参数)
    using ReadEventCallback = std::function<void(Timestamp)>;  // 读事件回调(带时间戳)

    Channel(EventLoop *loop, int fd);  // 构造函数(需传入所属 EventLoop 和 fd)
    ~Channel() = default;              // 默认析构

    // 核心方法:处理事件(由 EventLoop 调用)
    void handleEvent(Timestamp receiveTime);

    // 设置回调函数(读/写/关闭/错误),只转移指针,避免深拷贝
    void setReadCallback(ReadEventCallback cb) { readCallback_ = std::move(cb); }
    void setWriteCallback(EventCallback cb) { writeCallback_ = std::move(cb); }
    void setCloseCallback(EventCallback cb) { closeCallback_ = std::move(cb); }
    void setErrorCallback(EventCallback cb) { errorCallback_ = std::move(cb); }

    // 绑定对象生命周期(防止 Channel 被提前销毁)
    void tie(const std::shared_ptr<void> &);

    // 获取 fd 和事件状态
    int fd() const { return fd_; }          // 返回管理的文件描述符
    int events() const { return events_; }  // 返回当前监听的事件
    void set_revents(int revt) { revents_ = revt; }  // 提供给poller的接口,poller检测到了实际发生的事件,通知给channel

    // 事件监听控制
    void enableReading() { events_ |= kReadEvent; update(); }    // 启用读事件监听
    void disableReading() { events_ &= ~kReadEvent; update(); }  // 禁用读事件监听
    void enableWriting() { events_ |= kWriteEvent; update(); }   // 启用写事件监听
    void disableWriting() { events_ &= ~kWriteEvent; update(); } // 禁用写事件监听
    void disableAll() { events_ = kNoneEvent; update(); }        // 禁用所有事件监听

    // 事件状态检查
    bool isNoneEvent() const { return events_ == kNoneEvent; }  // 是否无监听事件
    bool isWriting() const { return events_ & kWriteEvent; }    // 是否监听写事件
    bool isReading() const { return events_ & kReadEvent; }     // 是否监听读事件

    // 用于 Poller 管理的索引(标识 Channel 在 Poller 中的位置)
    int index() { return index_; }
    void set_index(int idx) { index_ = idx; }

    // 获取所属 EventLoop
    EventLoop *ownerLoop() { return loop_; }

    // 从 EventLoop 中移除自己
    void remove();

private:
    // 内部方法
    void update();  // 更新事件监听状态(通知 EventLoop 调用 Poller)
    void handleEventWithGuard(Timestamp receiveTime);  // 带生命周期保护的事件处理

    // 静态常量(事件类型标志)
    static const int kNoneEvent;   // 无事件
    static const int kReadEvent;   // 读事件(EPOLLIN)
    static const int kWriteEvent;  // 写事件(EPOLLOUT)

    // 成员变量
    EventLoop *loop_;   // 所属的事件循环(一个 Channel 只属于一个 EventLoop)
    const int fd_;      // 管理的文件描述符(如 socket、timerfd)
    int events_;        // 当前注册的事件(用户设置)
    int revents_;       // Poller 返回的就绪事件(由 EventLoop 设置)
    int index_;         // 在 Poller 中的索引(方便快速查找)

    // 生命周期管理
    std::weak_ptr<void> tie_;  // 弱引用绑定(如 shared_ptr<TcpConnection>)
    bool tied_;                // 是否已绑定对象

    // 回调函数
    ReadEventCallback readCallback_;   // 读事件回调
    EventCallback writeCallback_;      // 写事件回调
    EventCallback closeCallback_;      // 关闭事件回调
    EventCallback errorCallback_;      // 错误事件回调
};  // Channel 类
}  // namespace mymuduo

2.2 Channel.cpp

cpp 复制代码
#include "LogStream.h"
#include "Channel.h"
#include "EventLoop.h"
#include <sys/epoll.h>

using namespace mymuduo;
const int Channel::kNoneEvent = 0; //空事件
const int Channel::kReadEvent = EPOLLIN | EPOLLPRI; //读事件, EPOLLPRI:紧急(带外)数据可读
const int Channel::kWriteEvent = EPOLLOUT; //写事件
 
Channel::Channel(EventLoop *loop, int fd)
    : loop_(loop)
    , fd_(fd)
    , events_(0)
    , revents_(0)
    , index_(-1)
    , tied_(false){}
 
void Channel::tie(const std::shared_ptr<void> &obj){  // TcpConnection* 转为 void*
    tie_ = obj;
    tied_ = true;  // 标记channel是否绑定了TcpConnection对象, weak_ptr 是否绑定到了TcpConnection对象
}
 
void Channel::update(){
    // 通过channel所属的eventloop,调用poller的相应方法,更改poller中fd的事件
    loop_->updateChannel(this);
}
 
void Channel::remove(){
    loop_->removeChannel(this);
}
 
void Channel::handleEvent(Timestamp receiveTime){
    if (tied_){   // 如果channel绑定了TcpConnection对象
        std::shared_ptr<void> guard = tie_.lock(); // 提升为shared_ptr, 检查TcpConnection对象是否存在
        if (guard){  // 如果提升成功了, 说明TcpConnection对象还存在
            handleEventWithGuard(receiveTime); // 处理事件
        }
        // 绑定了TcpConnection对象, 但是提升失败了 就不做任何处理 说明Channel的TcpConnection对象已经不存在了
    }else{
        handleEventWithGuard(receiveTime); // 没绑定,直接处理事件
    }
}
 
void Channel::handleEventWithGuard(Timestamp receiveTime){
    LOG_INFO << "channel handleEvent revents: " << revents_;
    // 关闭,发生了 EPOLLHUP(对端关闭连接)且​​没有数据可读​​(!EPOLLIN)
    if ((revents_ & EPOLLHUP) && !(revents_ & EPOLLIN)){
        if (closeCallback_) closeCallback_();
    }
    // 错误
    if (revents_ & EPOLLERR){
        if (errorCallback_) errorCallback_();
    }
    // 读
    if (revents_ & (EPOLLIN | EPOLLPRI)){
        if (readCallback_) readCallback_(receiveTime);
    }
    // 写
    if (revents_ & EPOLLOUT){
        if (writeCallback_) writeCallback_();
    }
}

3. 模块详解

3.1 events_ vs revents_

  • events_:监听的事件。比如调用 enableReading() 后,events_ 会加上读标志。
  • revents_:实际发生的事件。epoll_wait 返回后,内核告诉 EventLoop 哪个 fd 发生了什么事,EventLoop 把这个结果填到 revents_ 里,然后让 Channel 处理。

3.2 事件控制:委托模式

cpp 复制代码
void enableReading() { events_ |= kReadEvent; update(); }
void disableReading() { events_ &= ~kReadEvent; update(); }

void Channel::update(){
    loop_->updateChannel(this);
}
void Channel::remove(){
    loop_->removeChannel(this);
}
  • 用来修改 events_(要监听的事件)。
  • 修改完后,立刻调用 update()。update() 的作用是通知 EventLoop:"我要监听的事件变了,你去告诉 Poller 更新一下内核里的监听列表"。
  • Channel 自己不能直接操作 epoll_ctl。它通过 loop_(所属的事件循环),让 EventLoop 去调用 Poller 的 updateChannel 或 removeChannel。
  • 典型的委托模式。Channel 只负责记录状态,Poller 负责系统调用。

3.3 生命周期管理 (tie)

防止 Channel 还在处理事件,但拥有它的对象(如 TcpConnection)已经被销毁了。

cpp 复制代码
std::weak_ptr<void> tie_;
bool tied_;
void tie(const std::shared_ptr<void> &);

void Channel::tie(const std::shared_ptr<void> &obj){  // TcpConnection* 转为 void*
    tie_ = obj;
    tied_ = true;  // 标记channel是否绑定了TcpConnection对象, weak_ptr 是否绑定到了TcpConnection对象
}

void Channel::handleEvent(Timestamp receiveTime){
    if (tied_){   // 如果channel绑定了TcpConnection对象
        std::shared_ptr<void> guard = tie_.lock(); // 提升为shared_ptr, 检查TcpConnection对象是否存在
        if (guard){  // 如果提升成功了, 说明TcpConnection对象还存在
            handleEventWithGuard(receiveTime); // 处理事件
        }
        // 绑定了TcpConnection对象, 但是提升失败了 就不做任何处理 说明Channel的TcpConnection对象已经不存在了
    }else{
        handleEventWithGuard(receiveTime); // 没绑定,直接处理事件
    }
}
  • 场景:Channel 是被 TcpConnection 拥有的。TcpConnection 是一个 shared_ptr 管理的对象。
  • 作用:把 TcpConnection 的 shared_ptr 存到 Channel 的 weak_ptr tie_ 里,当 Channel 要处理事件时,先试着把 weak_ptr 提升为 shared_ptr。如果提升成功,说明 TcpConnection 还活着,可以安全调用回调;如果失败,说明 TcpConnection 已经没了,就别处理了,防止崩溃。

tie 作用流程详解:

  1. 正常情况
    • TcpConnection (对象 A) 拥有一个 Channel。
    • TcpConnection 调用 channel->tie(shared_from_this())。
    • Channel 内部 tie_ 持有了 A 的弱引用。
    • 事件来了 -> handleEvent -> tie_.lock() 成功拿到 A 的强引用 -> 调用回调 -> 安全。
  2. 危险情况(没有 tie)
    • 主线程决定关闭连接,销毁了 TcpConnection (对象 A)。
    • 但是 IO 线程的 epoll_wait 刚好返回,通知这个 fd 有事件。
    • Channel 还在,它尝试调用回调,回调里访问 this->connection_->xxx(因为当 Channel 触发 readCallback_ 时,实际上执行的是 TcpConnection::handleRead。)。
    • 错误,connection_ 已经成为野指针。
  3. 安全情况(有 tie)
    • 主线程销毁 TcpConnection (对象 A)。此 时引用计数归 0,A 被析构。
    • Channel 里的 weak_ptr tie_ 自动失效(变成 expired)。
    • IO 线程 epoll_wait 返回,通知事件。
    • Channel::handleEvent 执行 tie_.lock()。
    • 因为 A 已死,lock() 返回空 shared_ptr。
    • if (guard) 判断为假。
    • 函数直接返回,不执行回调。安全!

3.4 事件分发

cpp 复制代码
void Channel::handleEventWithGuard(Timestamp receiveTime){
    LOG_INFO << "channel handleEvent revents: " << revents_;
    
    // 1. 关闭事件 (EPOLLHUP 且没有数据可读)
    if ((revents_ & EPOLLHUP) && !(revents_ & EPOLLIN)){
        if (closeCallback_) closeCallback_();
    }
    // 2. 错误事件
    if (revents_ & EPOLLERR){
        if (errorCallback_) errorCallback_();
    }
    // 3. 读事件
    if (revents_ & (EPOLLIN | EPOLLPRI)){
        if (readCallback_) readCallback_(receiveTime);
    }
    // 4. 写事件
    if (revents_ & EPOLLOUT){
        if (writeCallback_) writeCallback_();
    }
}
  • 根据 revents_(内核告诉我们实际发生的事件),去调用对应的回调函数。
  • 注意:一个 fd 可能同时满足多个条件(比如既可读又可写),所以这里用的是多个 if 而不是 if-else if。
相关推荐
我命由我123451 小时前
Visual Studio 文件的编码格式不一致问题:错误 C2001 常量中有换行符
c语言·开发语言·c++·ide·学习·学习方法·visual studio
MR_Promethus1 小时前
【C++类型转换】static_cast、dynamic_cast、const_cast、reinterpret_cast
开发语言·c++
Trouvaille ~2 小时前
【Linux】epoll 深度剖析:高性能 IO 多路复用的终极方案
linux·运维·服务器·c++·epoll·多路复用·io模型
mjhcsp2 小时前
C++数位 DP解析
开发语言·c++·动态规划
小龙报2 小时前
【算法通关指南:数据结构与算法篇】二叉树相关算法题:1.二叉树深度 2.求先序排列
c语言·开发语言·数据结构·c++·算法·贪心算法·动态规划
仰泳的熊猫2 小时前
题目1529:蓝桥杯算法提高VIP-摆花
数据结构·c++·算法·蓝桥杯
小糯米6013 小时前
C++ 树
数据结构·c++·算法
掘根3 小时前
【C++STL】红黑树(RBTree)
数据结构·c++·算法
我笑了OvO3 小时前
常见位运算及其经典算法题(1)
c++·算法·算法竞赛