仿muduo库实现高并发服务器—TCPServer

一、整体架构:为什么是 Reactor 模式?

在传统的网络编程中,常用的模型有:

  • 阻塞IO + 多线程/多进程:每个连接一个线程,连接数上来线程极多
  • 非阻塞IO + select/poll/epoll:单线程循环等待事件,但业务处理会阻塞IO

Reactor 模式的核心思想是:有事件来了才处理,没事就阻塞等着。整个服务器就是一个事件循环,谁有数据来了就调对应的回调函数处理。

muduo 是陈硕写的 C++ 网络库,它的经典设计是 One Loop One Thread

  • 主线程(mainReactor)只负责监听新连接
  • 子线程池(subReactors)负责已连接的IO处理
  • 新连接来了通过轮询(Round-Robin)分配给某个子线程
  • 每个线程内部是一个独立的事件循环,线程之间通过 eventfd 唤醒,几乎无锁

本项目 server.hpp 复刻了这个架构,让我们从最底层的工具类开始,一层层往上拆解。


二、工具基础层

2.1 日志模块

cpp 复制代码
#define INF 0, DBG 1, ERR 2
#define LOG_LEVEL DBG

#define LOG(level, format, ...) do{
    if (level < LOG_LEVEL) break;
    time_t t = time(NULL);
    struct tm *ltm = localtime(&t);
    char tmp[32] = {0};
    strftime(tmp, 31, "%H:%M:%S", ltm);
    fprintf(stdout, "[%p %s %s:%d] " format "\n", 
            (void*)pthread_self(), tmp, __FILE__, __LINE__, ##__VA_ARGS__);
}while(0)

#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)

设计要点

  1. 用宏定义实现,零运行时开销
  2. 自动打印:当前线程ID、时间、文件名、行号
  3. ##__VA_ARGS__ 处理可变参数为空的情况
  4. do{...}while(0) 宏封装经典写法,避免 if 语句不加大括号导致的问题

这是一个生产环境可用的极简日志,不用依赖任何第三方库。


2.2 Buffer:双指针实现的高效缓冲区

网络编程中,读数据不一定一次读完,写数据也不一定一次发完,所以必须要有缓冲区。

cpp 复制代码
class Buffer {
private:
    std::vector<char> _buffer;
    uint64_t _reader_idx;  // 读偏移
    uint64_t _writer_idx;  // 写偏移
    // ...
};

内存布局图

复制代码
┌─────────────────────────────────────────────────────────┐
│  已读空闲区  │      可读数据区      │      可写空闲区       │
│ (HeadIdle)  │  (ReadAbleSize)     │    (TailIdle)        │
└─────────────────────────────────────────────────────────┘
↑             ↑                     ↑                     ↑
0        _reader_idx           _writer_idx            size()

核心操作逻辑

  • 写入数据:从 _writer_idx 开始写,写完移动写指针
  • 读取数据:从 _reader_idx 开始读,读完移动读指针
  • 空间不足怎么办?EnsureWriteSpace() 负责判断空间容量:
    1. 如果尾部空闲够,直接写
    2. 尾部不够,但头部+尾部加起来够:把数据搬移到开头,连续空间就出来了
    3. 还是不够:扩容 resize

实用API

  • WriteAndPush/ReadAndPop:写入/读取后自动移动偏移量
  • FindCRLF()/GetLine():找 \n,按行读取,为HTTP协议解析准备
  • Clear():只需要把偏移量归0,不用清空vector数据,非常高效

这就是 muduo 中 Buffer 类的简化实现,双指针设计避免了频繁的内存分配和数据拷贝。


2.3 Socket:RAII封装系统调用

原生的 socket API 是C风格的,很容易忘记 close,这个类把它封装成了 C++ 对象。

cpp 复制代码
class Socket {
private:
    int _sockfd;
public:
    bool Create();
    bool Bind(const std::string &ip, uint16_t port);
    bool Listen(int backlog = MAX_LISTEN);
    int Accept();
    bool Connect(const std::string &ip, uint16_t port);
    ssize_t Recv(void *buf, size_t len, int flag = 0);
    ssize_t Send(const void *buf, size_t len, int flag = 0);
    void Close();
    
    // 高层封装
    bool CreateServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false);
    bool CreateClient(uint16_t port, const std::string &ip);
    void ReuseAddress();  // 地址复用,解决TIME_WAIT问题
    void NonBlock();     // 设置非阻塞
};

关键细节

  1. RAII :析构函数自动调用 Close(),对象销毁自动关闭fd
  2. 非阻塞Recv/Send:对 EAGAIN/EINTR 做了特殊处理,返回0表示"现在没数据,不是错误"
  3. 地址复用顺序SO_REUSEADDR 必须在 bind() 之前调用,否则不生效!
  4. CreateServer() 一站式完成:创建 → 非阻塞 → 绑定 → 监听 → 地址复用

有了这个类,我们再也不用跟原生的 sockaddr_in 打交道了。


2.4 Any:自己实现一个 std::any

C++17 才有的 std::any 可以装任意类型的数据,这里用类型擦除技术实现了一个,用于保存协议上下文。

cpp 复制代码
class Any {
private:
    class holder {  // 抽象基类
    public:
        virtual ~holder() {}
        virtual const std::type_info& type() = 0;
        virtual holder *clone() = 0;
    };
    
    template<class T>
    class placeholder : public holder {  // 具体类型持有者
    public:
        placeholder(const T &val): _val(val) {}
        virtual const std::type_info& type() { return typeid(T); }
        virtual holder *clone() { return new placeholder(_val); }
        T _val;
    };
    
    holder *_content;
public:
    template<class T>
    T *get() {
        assert(typeid(T) == _content->type());  // 类型安全检查
        return &((placeholder<T>*)_content)->_val;
    }
    // ... 构造、赋值运算符
};

类型擦除原理

  1. 基类 holder 定义虚函数接口
  2. 模板子类 placeholder<T> 保存具体类型数据
  3. 外面Any类只存基类指针 _content,需要的时候用 typeid 检查类型,向下转型
  4. 赋值时实现拷贝并交换 ,创建临时对象Any(val)与自身交换数据指针,函数结束时临时对象自动析构并带走原数据

为什么需要它?服务器要支持多协议(TCP Echo、HTTP等),每个连接需要保存自己协议的解析上下文,用 Any 就不需要用 void*,保证类型安全。


三、Reactor核心层

3.1 Channel:事件通道

每个文件描述符(不管是监听socket、客户端socket、还是timerfd/eventfd)在内核中只是一个整数,我们需要一个东西来管理:这个fd关心什么事件?事件来了该调用什么函数?------这就是 Channel。

cpp 复制代码
class Channel {
private:
    int _fd;
    EventLoop *_loop;       // 所属的事件循环
    uint32_t _events;       // 想要监控的事件(EPOLLIN/EPOLLOUT)
    uint32_t _revents;      // 实际触发的事件
    
    using EventCallback = std::function<void()>;
    EventCallback _read_callback;   // 读事件回调
    EventCallback _write_callback;  // 写事件回调
    EventCallback _error_callback;  // 错误事件回调
    EventCallback _close_callback;  // 关闭事件回调
    EventCallback _event_callback;  // 任意事件回调(刷新活跃度)
    
public:
    // 事件启停控制
    void EnableRead()  { _events |= EPOLLIN;  Update(); }
    void EnableWrite() { _events |= EPOLLOUT; Update(); }
    void DisableRead() { _events &= ~EPOLLIN; Update(); }
    void DisableWrite(){ _events &= ~EPOLLOUT;Update(); }
    void DisableAll()  { _events = 0;        Update(); }
    
    // 事件分发:revents是啥就调啥回调
    void HandleEvent() {
        if ((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI)) {
            if (_read_callback) _read_callback();
        }
        if (_revents & EPOLLOUT) {
            if (_write_callback) _write_callback();
        } else if (_revents & EPOLLERR) {
            if (_error_callback) _error_callback();
        } else if (_revents & EPOLLHUP) {
            if (_close_callback) _close_callback();
        }
        if (_event_callback) _event_callback();
    }
    
    void Update();  // 调用loop更新epoll监控
    void Remove();  // 移除监控
};

Channel的核心理念

  • 不拥有fd,fd的生命周期由Socket/Connection管理,Channel只是"借"来用
  • 它把"fd + 回调函数 + 事件监控"打包在一起
  • Update()/Remove() 不是自己操作epoll,而是通过所属的EventLoop去操作(跨线程安全考虑)

一个Channel对应一个fd,这是Reactor模式最基础的单元。


3.2 Poller:epoll的简单封装

Poller 就是对 epoll 的薄封装,它的职责很单一:

  1. 维护 fd → Channel 的映射
  2. 调用 epoll_wait 等待事件
  3. 返回活跃的 Channel 列表
cpp 复制代码
class Poller {
private:
    int _epfd;
    struct epoll_event _evs[MAX_EPOLLEVENTS];
    std::unordered_map<int, Channel *> _channels;  // fd -> Channel*
    
    void Update(Channel *channel, int op) {
        struct epoll_event ev;
        ev.data.fd = channel->Fd();
        ev.events = channel->Events();
        epoll_ctl(_epfd, op, channel->Fd(), &ev);
    }
    
public:
    void UpdateEvent(Channel *channel);  // ADD/MOD
    void RemoveEvent(Channel *channel);  // DEL
    
    // 等待事件,把活跃的Channel填到active数组里
    void Poll(std::vector<Channel*> *active) {
        int nfds = epoll_wait(_epfd, _evs, MAX_EPOLLEVENTS, -1);
        for (int i = 0; i < nfds; i++) {
            auto it = _channels.find(_evs[i].data.fd);
            assert(it != _channels.end());
            it->second->SetREvents(_evs[i].events);  // 设置实际触发的事件
            active->push_back(it->second);
        }
    }
};

注意:Poller 不拥有Channel,只是存指针,Channel的生命周期在外层管理。


3.3 TimerWheel:时间轮定时器,O(1)

服务器需要定时功能,比如:非活跃连接30秒没通信就断开。如果用定时器队列/最小堆,每次刷新活跃时间都是O(logN),连接多了性能不行。这里用了时间轮算法

想象一个钟表:

  • 表盘有60个格子(默认60秒超时)
  • 有一个秒针 _tick,每秒走一格
  • 添加一个30秒超时的任务,就放在"当前位置+30格"的格子里
  • 秒针走到哪里,就把那个格子里的任务全部执行(释放shared_ptr即可)
cpp 复制代码
class TimerTask {  // 定时任务对象
    uint64_t _id;
    uint32_t _timeout;
    bool _canceled;
    TaskFunc _task_cb;     // 要执行的任务
    ReleaseFunc _release;  // 析构时从时间轮map中移除自己
public:
    ~TimerTask() { 
        if (!_canceled) _task_cb();  // 析构时自动执行任务
        _release(); 
    }
    void Cancel() { _canceled = true; }
};

class TimerWheel {
    int _tick;       // 当前秒针位置
    int _capacity;   // 表盘大小=最大超时时间
    std::vector<std::vector<PtrTask>> _wheel;  // 表盘:每个格子是一个任务数组
    std::unordered_map<uint64_t, WeakTask> _timers;  // id -> weak_ptr(用于刷新/取消)
    
    int _timerfd;  // 用timerfd_create创建,每秒触发一次读事件
    std::unique_ptr<Channel> _timer_channel;
    
    // 每秒被调用一次:秒针走一步,释放该格子任务
    void RunTimerTask() {
        _tick = (_tick + 1) % _capacity;
        _wheel[_tick].clear();  // 释放shared_ptr,TimerTask析构自动执行任务
    }
    
public:
    // 添加定时任务
    void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc &cb) {
        PtrTask pt(new TimerTask(id, delay, cb));
        pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
        int pos = (_tick + delay) % _capacity;
        _wheel[pos].push_back(pt);
        _timers[id] = WeakTask(pt);
    }
    
    // 刷新活跃:把任务再"挂"到新位置,相当于延长寿命
    void TimerRefreshInLoop(uint64_t id) {
        auto it = _timers.find(id);
        if (it == _timers.end()) return;
        PtrTask pt = it->second.lock();  // weak_ptr -> shared_ptr
        int pos = (_tick + pt->DelayTime()) % _capacity;
        _wheel[pos].push_back(pt);
    }
};

TimerTask析构执行任务

  • 任务对象是 shared_ptr,当格子clear时引用计数减1
  • 如果该任务被刷新过(shared_ptr又被放到了新格子),引用计数不为0,不会析构
  • 如果该任务没被刷新,引用计数归0,析构自动执行回调
  • 不需要"到时了去取任务执行",释放就是执行

为什么用 weak_ptr?

  • _wheel 里存shared_ptr(拥有对象)
  • _timers 里存weak_ptr(不拥有,只是查找用)
  • 这样不会因为 _timers 也存了shared_ptr导致任务永远不释放

定时器怎么驱动?

  • 用 Linux 的 timerfd_create 创建一个fd,设置成每秒超时一次
  • 把这个fd封装成Channel加到epoll里
  • 每秒fd读事件就绪 → 读timerfd → RunTimerTask → 秒针走一步

定时器也是一个fd,也走epoll事件循环,和网络IO统一处理


3.4 EventLoop:事件循环

EventLoop 就是那个"无限循环等事件"的东西,每个线程有且只有一个。它是整个框架最核心的类,组合了Poller、TimerWheel,还负责跨线程任务调度

cpp 复制代码
class EventLoop {
    std::thread::id _thread_id;  // 记录自己属于哪个线程
    int _event_fd;               // eventfd,用于跨线程唤醒
    std::unique_ptr<Channel> _event_channel;
    
    Poller _poller;              // epoll监控
    TimerWheel _timer_wheel;     // 定时器
    
    std::vector<Functor> _tasks; // 跨线程投递的任务队列
    std::mutex _mutex;           // 保护_tasks的锁
    
public:
    // 核心:三步走的事件循环
    void Start() {
        while(1) {
            // 1. 阻塞等待事件
            std::vector<Channel *> actives;
            _poller.Poll(&actives);
            
            // 2. 处理所有就绪事件
            for (auto &channel : actives) {
                channel->HandleEvent();
            }
            
            // 3. 执行跨线程投递给我的任务
            RunAllTask();
        }
    }
    
    // 判断当前线程是不是我自己的线程
    bool IsInLoop() { return _thread_id == std::this_thread::get_id(); }
    void AssertInLoop() { assert(_thread_id == std::this_thread::get_id()); }
    
    // 在loop线程中执行cb:如果已在loop线程,直接执行;否则投递到队列
    void RunInLoop(const Functor &cb) {
        if (IsInLoop()) return cb();
        return QueueInLoop(cb);
    }
    
    // 把任务投递给这个loop
    void QueueInLoop(const Functor &cb) {
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _tasks.push_back(cb);
        }
        WeakUpEventFd();  // 关键!给eventfd写个数据,唤醒epoll_wait
    }
    
    // eventfd相关
    static int CreateEventFd() {
        return eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
    }
    void ReadEventfd() {
        uint64_t res; read(_event_fd, &res, sizeof(res));
    }
    void WeakUpEventFd() {
        uint64_t val = 1; write(_event_fd, &val, sizeof(val));
    }
};

最关键的设计:跨线程唤醒机制

假设子线程A正在epoll_wait阻塞着,主线程想让子线程A干点活(比如给新连接启动读监控),怎么办?

  • 不能直接调用,因为Channel/epoll操作不是线程安全的
  • 给任务队列加锁?子线程在epoll_wait里阻塞着根本不会去看队列
  • 解决方案:eventfd

eventfd是Linux提供的一个专门用于事件通知的fd:

  1. EventLoop构造时创建一个eventfd,也封装成Channel加到epoll里,监控读事件
  2. 别的线程想给我派活:加锁把任务放队列 → 给eventfd写个8字节整数 → eventfd读事件就绪 → epoll_wait返回
  3. 我(EventLoop线程)被唤醒:读掉eventfd的数据 → 处理IO事件 → RunAllTask执行队列里的任务

这样就实现了线程安全的跨线程调度,而且整个epoll里只有很短的时间加锁(只锁任务队列的push/swap),性能极高。


四、网络连接层

4.1 LoopThread / LoopThreadPool:线程池管理

我们要多个线程跑多个EventLoop,怎么管理?

cpp 复制代码
class LoopThread {
    std::mutex _mutex;
    std::condition_variable _cond;
    EventLoop *_loop;
    std::thread _thread;
    
    // 线程入口函数
    void ThreadEntry() {
        EventLoop loop;  // 注意:loop是栈上对象!
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _loop = &loop;
            _cond.notify_all();  // 通知GetLoop:我创建好了
        }
        loop.Start();  // 进入事件循环,直到线程退出
    }
    
public:
    LoopThread() : _loop(NULL), _thread(std::thread(&LoopThread::ThreadEntry, this)) {}
    
    EventLoop *GetLoop() {
        EventLoop *loop = NULL;
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _cond.wait(lock, [&](){ return _loop != NULL; });  // 等loop创建好
            loop = _loop;
        }
        return loop;
    }
};

class LoopThreadPool {
    int _thread_count;
    int _next_idx;  // 轮询索引
    EventLoop *_baseloop;
    std::vector<LoopThread*> _threads;
    std::vector<EventLoop *> _loops;
    
public:
    void Create() {
        if (_thread_count > 0) {
            _threads.resize(_thread_count);
            _loops.resize(_thread_count);
            for (int i = 0; i < _thread_count; i++) {
                _threads[i] = new LoopThread();
                _loops[i] = _threads[i]->GetLoop();  // 这里会阻塞等待线程创建好loop
            }
        }
    }
    
    // 轮询选择下一个EventLoop(Round-Robin)
    EventLoop *NextLoop() {
        if (_thread_count == 0) return _baseloop;  // 没子线程就用主线程
        _next_idx = (_next_idx + 1) % _thread_count;
        return _loops[_next_idx];
    }
};

条件变量的作用

  • 线程创建了,但EventLoop是在线程函数里栈上构造的
  • 如果外部线程马上调用GetLoop,很可能loop还是NULL
  • 用条件变量wait:loop构造好才notify,GetLoop拿到的一定是有效的loop指针

这就是多线程编程中的"线程安全初始化"经典写法。


4.2 Acceptor:专门处理新连接

Acceptor 很简单,它只干一件事:监听端口,有新连接来了就accept,然后回调给上层

cpp 复制代码
class Acceptor {
    Socket _socket;       // 监听套接字
    EventLoop *_loop;     // 固定跑在baseloop(主线程)
    Channel _channel;     // 监听fd的事件通道
    
    using AcceptCallback = std::function<void(int)>;
    AcceptCallback _accept_callback;  // 新连接回调:参数是newfd
    
    void HandleRead() {
        int newfd = _socket.Accept();
        if (newfd < 0) return;
        if (_accept_callback) _accept_callback(newfd);  // 新fd交给上层
    }
    
public:
    Acceptor(EventLoop *loop, int port)
        : _socket(CreateServer(port)), _loop(loop), _channel(loop, _socket.Fd()) {
        _channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));
    }
    void SetAcceptCallback(const AcceptCallback &cb) { _accept_callback = cb; }
    void Listen() { _channel.EnableRead(); }  // 构造完设置好回调,再启动读监控!
};

重要细节 :为什么不在构造函数里直接 EnableRead()

因为构造时 _accept_callback 还没设置,如果启动监控后马上有连接进来,HandleRead里回调是空的,新连接就丢了。所以要:

  1. 构造对象
  2. 外部调用SetAcceptCallback设置回调
  3. 再调用Listen()启动读监控

这是"二阶段初始化"思想,避免对象没构造完就被回调。


4.3 Connection:客户端连接的完整生命周期管理

这是最复杂的一个类,一个Connection对象代表一个已建立的TCP客户端连接。

cpp 复制代码
typedef enum { DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING } ConnStatu;
using PtrConnection = std::shared_ptr<Connection>;

class Connection : public std::enable_shared_from_this<Connection> {
    uint64_t _conn_id;
    int _sockfd;
    bool _enable_inactive_release;
    EventLoop *_loop;     // 这个连接属于哪个EventLoop
    ConnStatu _statu;     // 连接状态机
    
    Socket _socket;       // 套接字操作
    Channel _channel;     // 事件通道
    Buffer _in_buffer;    // 输入缓冲区
    Buffer _out_buffer;   // 输出缓冲区
    Any _context;         // 协议上下文(任意类型)
    
    // 两组回调:用户设置的 + 服务器内部的
    ConnectedCallback _connected_callback;
    MessageCallback _message_callback;      // 用户业务处理回调!
    ClosedCallback _closed_callback;
    AnyEventCallback _event_callback;
    ClosedCallback _server_closed_callback;  // TcpServer用来从_conns移除自己
    
public:
    // 五个Channel事件回调函数
    void HandleRead() {
        char buf[65536];
        ssize_t ret = _socket.NonBlockRecv(buf, 65535);
        if (ret < 0) return ShutdownInLoop();
        
        _in_buffer.WriteAndPush(buf, ret);
        if (_in_buffer.ReadAbleSize() > 0) {
            // 关键:shared_from_this() 把自己的shared_ptr传给用户回调
            return _message_callback(shared_from_this(), &_in_buffer);
        }
    }
    
    void HandleWrite() {
        ssize_t ret = _socket.NonBlockSend(
            _out_buffer.ReadPosition(), _out_buffer.ReadAbleSize());
        if (ret < 0) {
            if (_in_buffer.ReadAbleSize() > 0)
                _message_callback(shared_from_this(), &_in_buffer);
            return Release();
        }
        _out_buffer.MoveReadOffset(ret);
        if (_out_buffer.ReadAbleSize() == 0) {
            _channel.DisableWrite();  // 发完了就关掉写监控,不然一直触发
            if (_statu == DISCONNECTING) return Release();
        }
    }
    
    void HandleClose() {
        if (_in_buffer.ReadAbleSize() > 0)
            _message_callback(shared_from_this(), &_in_buffer);
        return Release();
    }
    
    void HandleEvent() {
        // 任意事件都刷新活跃度(重置超时定时器)
        if (_enable_inactive_release) _loop->TimerRefresh(_conn_id);
        if (_event_callback) _event_callback(shared_from_this());
    }
    
    // 供用户使用的API:线程安全
    void Send(const char *data, size_t len) {
        Buffer buf;
        buf.WriteAndPush(data, len);
        _loop->RunInLoop(std::bind(&Connection::SendInLoop, this, std::move(buf)));
    }
    void Shutdown() {
        _loop->RunInLoop(std::bind(&Connection::ShutdownInLoop, this));
    }
    void EnableInactiveRelease(int sec) {
        _loop->RunInLoop(std::bind(&Connection::EnableInactiveReleaseInLoop, this, sec));
    }
    
    // 协议升级:HTTP升级WebSocket之类的可以用
    void Upgrade(const Any &context, const ConnectedCallback &conn, 
                 const MessageCallback &msg, const ClosedCallback &closed, 
                 const AnyEventCallback &event);
};

Connection的状态机设计

复制代码
DISCONNECTED ──创建──> CONNECTING ──Established()──> CONNECTED
                           ↑                             │
                           │                             │ Shutdown()
                           │                             ▼
                        RELEASE <──Release()─────── DISCONNECTING
  • CONNECTING:刚创建,fd拿到了,但还没启动读监控
  • CONNECTED:正式建立,可以收发数据
  • DISCONNECTING:用户调用Shutdown,但可能还有数据没发完,等发完再关
  • DISCONNECTED:最终关闭状态

为什么要继承 enable_shared_from_this?

  • 回调函数需要传 shared_ptr<Connection> 给用户
  • 但在类成员函数里,this是裸指针
  • shared_from_this() 可以从this安全地创建出一个shared_ptr,保证引用计数正确
  • 注意:不能在构造函数里用shared_from_this,那时候对象还没被shared_ptr管理

发送数据的"假写"设计

  • 用户调用Send,不是直接send(),而是:
    1. 把数据拷贝到局部Buffer buf
    2. 通过RunInLoop把SendInLoop任务投递给所属EventLoop
  • 为什么要拷贝一份?因为用户传的data可能是栈上临时变量,任务执行的时候可能已经释放了
  • SendInLoop里:数据写到_out_buffer → EnableWrite
  • 真正的send在HandleWrite里:epoll通知可写了才发
  • 发完了立刻DisableWrite,不然水平触发的epoll会一直报可写事件,busy loop

4.4 TcpServer:顶层入口,用户唯一接触的类

用户用这个库只需要:

  1. 创建TcpServer对象
  2. 设置回调函数(连接建立、收到消息、连接关闭)
  3. 设置线程数
  4. 调用Start()
cpp 复制代码
class TcpServer {
    uint64_t _next_id;
    int _port;
    int _timeout;
    bool _enable_inactive_release;
    
    EventLoop _baseloop;       // 主线程事件循环:只负责accept
    Acceptor _acceptor;        // 监听管理
    LoopThreadPool _pool;      // IO线程池
    
    std::unordered_map<uint64_t, PtrConnection> _conns;  // 所有连接
    
    // 用户设置的四个回调
    ConnectedCallback _connected_callback;
    MessageCallback _message_callback;
    ClosedCallback _closed_callback;
    AnyEventCallback _event_callback;
    
    // 新连接到来时的处理
    void NewConnection(int fd) {
        _next_id++;
        // 轮询选一个子线程的EventLoop
        EventLoop *sub_loop = _pool.NextLoop();
        // 创建Connection对象
        PtrConnection conn(new Connection(sub_loop, _next_id, fd));
        
        // 设置回调
        conn->SetMessageCallback(_message_callback);
        conn->SetClosedCallback(_closed_callback);
        conn->SetConnectedCallback(_connected_callback);
        conn->SetAnyEventCallback(_event_callback);
        // 内部回调:关闭时从_conns移除
        conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, _1));
        
        if (_enable_inactive_release)
            conn->EnableInactiveRelease(_timeout);
        
        conn->Established();  // 启动读监控,进入CONNECTED状态
        _conns.insert(std::make_pair(_next_id, conn));
    }
    
    void RemoveConnectionInLoop(const PtrConnection &conn) {
        int id = conn->Id();
        auto it = _conns.find(id);
        if (it != _conns.end()) _conns.erase(it);
        // 从map中erase,shared_ptr引用计数减1,没人用就自动析构
    }
    
public:
    TcpServer(int port)
        : _port(port), _next_id(0), _enable_inactive_release(false),
          _acceptor(&_baseloop, port), _pool(&_baseloop) {
        _acceptor.SetAcceptCallback(
            std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));
        _acceptor.Listen();
    }
    
    void SetThreadCount(int count) { _pool.SetThreadCount(count); }
    void SetConnectedCallback(const ConnectedCallback&cb) { _connected_callback = cb; }
    void SetMessageCallback(const MessageCallback&cb) { _message_callback = cb; }
    void SetClosedCallback(const ClosedCallback&cb) { _closed_callback = cb; }
    void SetAnyEventCallback(const AnyEventCallback&cb) { _event_callback = cb; }
    void EnableInactiveRelease(int timeout) { _timeout = timeout; _enable_inactive_release = true; }
    
    void Start() {
        _pool.Create();  // 创建IO线程池
        _baseloop.Start();  // 主线程进入事件循环
    }
};

五、完整工作流程:一次请求的旅行

现在把所有模块串起来,看看一个客户端从连接到发送数据到服务器,发生了什么:

复制代码
  TcpServer server(8080);
  server.SetThreadCount(4);
  server.SetMessageCallback(OnMessage);
  server.Start();

阶段1:服务器启动

  1. TcpServer构造函数:
    • 实例化 _baseloop(主线程EventLoop)
    • 构造 _acceptor:创建监听socket,绑定8080,监听
    • 设置Acceptor回调为TcpServer::NewConnection
    • Acceptor::Listen() 给监听fd启动读监控(挂在baseloop)
  2. Start():
    • _pool.Create():创建4个LoopThread,每个线程内部跑一个EventLoop
    • _baseloop.Start():主线程进入while循环,epoll_wait等待

阶段2:新连接到来

  1. 客户端 connect(8080)
  2. 主线程epoll_wait返回,监听fd读事件就绪
  3. Acceptor::HandleRead() 调用 → accept()拿到newfd
  4. 回调 TcpServer::NewConnection(newfd)
  5. _pool.NextLoop() 轮询选一个子线程的EventLoop(比如子线程2)
  6. 创建Connection对象,设置好所有回调,包括业务的OnMessage
  7. conn->Established() → RunInLoop给子线程2投递任务:EnableRead
  8. eventfd唤醒子线程2的epoll,子线程2执行EnableRead:把conn的fd加到子线程2的epoll
  9. 新连接正式进入CONNECTED状态,可以收发数据

阶段3:客户端发送数据

  1. 客户端send数据
  2. 子线程2的epoll_wait返回,该conn的fd读事件就绪
  3. Channel::HandleEvent() → Connection::HandleRead()
  4. NonBlockRecv把数据读到_in_buffer
  5. 回调用户的 OnMessage(conn, &_in_buffer),业务处理
  6. HandleEvent刷新定时器:_conn_id的定时任务再延长_timeout秒

阶段4:服务器回复

  1. 用户业务处理中调用 conn->Send(response)
  2. Send() → RunInLoop投递SendInLoop任务(如果是当前线程直接执行)
  3. SendInLoop:数据写入_out_buffer,EnableWrite启动写监控
  4. 下一次epoll_wait返回写事件 → HandleWrite()
  5. NonBlockSend把_out_buffer的数据发出去
  6. 发完了 → DisableWrite,避免busy loop
  7. 如果是半关闭状态(DISCONNECTING),发完直接Release

阶段5:连接断开/超时

  1. 如果客户端正常close:
    • EPOLLRDHUP/EPOLLHUP事件触发 → HandleClose() → Release()
  2. 如果客户端长时间没通信:
    • 每次事件都TimerRefresh,没事件就不刷新
    • 时间轮秒针走到任务所在格子 → shared_ptr释放
    • TimerTask析构 → 执行Release任务
  3. ReleaseInLoop:
    • 状态设为DISCONNECTED
    • Channel::Remove() 从epoll移除
    • Socket::Close() 关闭fd
    • 取消定时器
    • 调用用户_closed_callback
    • 调用_server_closed_callback → TcpServer::RemoveConnection → 从_conns erase
    • 最后一个shared_ptr释放,Connection对象析构

六、设计思想总结

6.1 线程模型:One Loop Per Thread

  • 主线程只做accept,IO密集型场景下accept不会被业务处理阻塞
  • 连接轮询分配给子线程,负载均衡
  • 每个线程独立EventLoop,线程之间不共享Channel/Connection,几乎不需要加锁
  • 唯一需要加锁的地方:跨线程投递任务队列(非常短的临界区)

6.2 RAII与智能指针

  • Socket析构自动close,Channel跟随Connection,Connection用shared_ptr管理
  • 生命周期问题是网络编程最头疼的,智能指针 + enable_shared_from_this 解决了"回调时对象已经被释放"的经典bug

6.3 一切皆文件描述符

在Linux世界里,一切皆文件。这个框架把:

  • 网络socket
  • timerfd(定时器)
  • eventfd(跨线程唤醒)

全部封装成Channel,统一在epoll里等待,全部走事件回调。没有特殊情况,没有多余的wait/notify,代码高度统一。

6.4 回调的分层设计

回调不是一层,而是分了两层:

  1. 底层回调:Channel给Connection的(HandleRead/Write/Close/Error),框架内部设置
  2. 上层回调:Connection给用户的(MessageCallback/ClosedCallback等),用户设置
  3. 内部管理回调:Connection给TcpServer的(ServerClosedCallback),框架内部用来从map移除

每一层只关心自己该做的事,边界清晰。

相关推荐
余额瞒着我当琳1 小时前
C++--vector底层,leetcode118杨辉三角实战,对于传统reserve的坑点,迭代器失效
开发语言·c++
为思念酝酿的痛1 小时前
Socket编程--TCP
服务器·网络·tcp/ip
牢姐与蒯1 小时前
Linux首战之进度条
linux·运维·服务器
醉城夜风~2 小时前
(超详细)C++多态(Polymorphism)
开发语言·c++
北城笑笑2 小时前
Server 18 ,Nginx + Vite 前端部署排错实战:从 `/api/gpt/chat` 404 到 9012 后端服务的完整定位过程
linux·运维·前端·nginx·ubuntu·vue
小小龙学IT2 小时前
现代 C++ 内存管理与资源安全深度解析:从 RAII 到所有权模型
c++·安全
欧特克_Glodon2 小时前
OpenCV计算机视觉开发入门与实践<十三>:图像转换之灰度图、二值图
c++·人工智能·opencv·计算机视觉
NJCloud2 小时前
ELK企业级日志分析平台(四)——基于 ELFK + Kafka 的日志采集与传输平台部署实践
linux·运维·分布式·elk·kafka
神仙别闹2 小时前
基于C++ MFC实现动态分区分配存储管理
开发语言·c++·mfc
skr爱码士2 小时前
03_第一个Qt项目:Hello World 的完整拆解
c++·qt·客户端