从零构建高性能 Reactor 服务器:

引言

在高并发网络编程领域,Reactor 模型凭借其高效的事件驱动机制,成为构建高性能服务器的首选方案。本文将从零开始,详细剖析一个基于 one loop per thread 架构的 C++ TCP Echo 服务器的实现。该服务器参考了 muduo 网络库的设计思想,支持多核扩展、非阻塞 I/O、ET 模式,并采用异步日志和无锁队列进行优化。最终实现了一个可支撑数数万并发连接(取决于硬件配置)的高性能 Echo 服务。

该服务器为AeroChat(高性能聊天室)的网络部分,我剥离出来进行了压测。项目代码开源在AeroChat/benchmark at master · ywx914705/AeroChathttps://github.com/ywx914705/AeroChat/tree/master/benchmark,这个项目只包含了网络层次,不涉及任何业务层次。

一、服务器架构:

服务器采用 主从 Reactor 多线程模型

主Reactor:一个EventLoop线程,负责监听listenFd_上的新连接事件。

从/子Reactor:多个EventLoop,每个线程负责运行一个EventLoop,负责已连接的socket的I/O读写。

通过负载均衡的方式,新连接通过round-robin算法分配到某个SubReactor/子Reactor,连接一旦分配便固定在该线程,这样做的好处就是不需要锁同步,天然线程安全!

跨线程任务:通过RunInLoop/queueInLoop+eventfd实现线程安全的任务投递。

二、核心组件解析:

由于篇幅有限,以下所有类只列举出部分源码,详细源码可去这里查看:AeroChat/benchmark at master · ywx914705/AeroChathttps://github.com/ywx914705/AeroChat/tree/master/benchmark

1、noncopyable类:

cpp 复制代码
#pragma once
// 禁止拷贝的基类(参考了Muduo核心设计,避免浅拷贝资源错误)
/*
作为一个基类,任何继承自noncopyable的类都无法被拷贝构造与拷贝复制,这是为了防止那些拥有资源的
对象被错误拷贝,进而发生析构时重复释放的错误
次服务器中大部分的类都基层自noncopyable(不得拷贝)
*/
class noncopyable {
protected:
    noncopyable() = default;
    ~noncopyable() = default;
    noncopyable(const noncopyable&) = delete;
    noncopyable& operator=(const noncopyable&) = delete;
};

2、Thread类:

Thread类的主要工作就是使用c++11的thread库对linux原生接口进行封装:

cpp 复制代码
/*
该文件本质上就是对linux posix线程的封装(pthread_create创建出来的线程)
这个类在整个项目中作为基础组件,被EventLoopThread等上层类使用
linux中posix常用函数:
pthread_create()   pthread_join()  
核心优势:支持CPU亲和性,可以将线程绑定到指定CPU核心,优化性能
不可拷贝:该AeroChat中绝大多数的类都继承自nocopyable,不支持拷贝,防止多个对象管理同一个线程
,避免资源被重复释放
这个类的可读性很高,基本上看到函数/变量名就能知道它的功能/作用
*/
#pragma once
#include "noncopyable.hpp"
#include <pthread.h>
#include <functional>
#include <string>
#include <atomic>
#include <iostream>
#include <sched.h>//绑CPU核心所需头文件

class Thread : noncopyable {
public:
    using ThreadFunc = std::function<void()>;

    explicit Thread(ThreadFunc func, const std::string& name = "Thread");
    ~Thread();

    void start();
    void join();//底层调用pthread_join()
    void bindCurrentThreadToCore(int coreId);
    //目的是让当前调用它的线程绑定到指定核心。通常我们在子线程内部调用它,
    //将自己绑定到预先设定的核心。

    void setCoreAffinity(int coreId) { coreId_ = coreId; }
    //设置成员coureId_,记录希望绑定的线程核心号,实际绑定发生在线程启动后

    pthread_t tid() const { return tid_; }
    const std::string& name() const { return name_; }
    static int numCreated() { return numCreated_.load(); }

private:
    static void* runInThread(void* arg);

    ThreadFunc func_;//线程函数
    std::string name_;//线程名字,用于日志标识
    pthread_t tid_;//线程ID
    bool started_;//bool类型,表示线程是否启动 ture:启动了 false:没有启动
    bool joined_;//是否调用join(是否调用linux中的pthrea_join()函数)
    static std::atomic_int numCreated_;//已经创建线程的数量,静态原子变量

    int coreId_ = -1;//期望绑定的 CPU 核心编号,-1 表示不绑定
};

3、Channel(I/O 事件分发器):

Channel包含了对fd的所有封装,一个channel对应一个fd。

Channel的成员变量:

cpp 复制代码
EventLoop* loop_;//所属的事件循环
    const int fd_;//文件描述符
    uint32_t events_;//当前关注的事件(如EPOLLIN/EPOLLOUT/EPOLLET)
    uint32_t revents_;//epoll_wait所返回实际发生的事件
    int index_;//在Poller中的状态(kNew/kAdded/kDeleted)
     //读、写、关闭、错误对应的事件回调  回调函数:用户注册的业务处理函数,当对应事件发生时被调用
    EventCallback readCallback_;
    EventCallback writeCallback_;
    EventCallback closeCallback_;
    EventCallback errorCallback_;

Channel的成员方法:

cpp 复制代码
void Channel::handleEvent() {  //该函数由EventLoop::loop()收到epoll事件后进行调用,且执行在所属的
//EventLoop线程中,保证了线程安全
    if (revents_ & (EPOLLRDHUP | EPOLLHUP)) { //如果对端关闭连接或者挂起,此时连接不可用,直接关闭
        LOG_WARN("[Channel] FD " + std::to_string(fd_) + " 客户端关闭连接");
        if (closeCallback_) closeCallback_();
        return;
    }
    if (revents_ & EPOLLERR) {
        LOG_ERROR("[Channel] FD " + std::to_string(fd_) + " 发生错误");
        if (errorCallback_) errorCallback_();
    }
    if (revents_ & (EPOLLIN | EPOLLPRI)) {
        if (readCallback_) readCallback_();
    }
    if (revents_ & EPOLLOUT) {
        if (writeCallback_) writeCallback_();
    }
}

//通知EventLoop进行更新        Channel不直接操作epoll,而是通过EventLoop统一管理
void Channel::update() {
    loop_->updateChannel(this);
}

为什么要有Channel?

Channel 不直接操作 epoll,而是通过 EventLoop 间接调用 Poller,实现解耦。如果对Poller和EventLoop不是很了解可以继续往下看

4、Poller:

Poller.hpp

cpp 复制代码
/*
Poller:每个EventLoop拥有一个Poller对象(一一对应),负责实际的epoll操作,EventLoop通过updateChannel
将Channel的变化同步到Poller,并在每次循环中调用poll获取活跃的Channel,然后调用它们的handleEvent
*/
#pragma once
#include "noncopyable.hpp"
#include <sys/epoll.h>
#include <unordered_map>
#include <vector>
// 前置声明
class EventLoop;
class Channel;

class Poller : noncopyable {
public:
  explicit Poller(EventLoop *loop);
  ~Poller() = default;

  void poll(int timeoutMs, std::vector<Channel *> *activeChannels);
  void updateChannel(Channel *channel);
  void removeChannel(Channel *channel);

private:
  void fillActiveChannels(int numEvents,
                          std::vector<Channel *> *activeChannels) const;

  EventLoop *ownerLoop_; // 对应的EventLoop
  int epollfd_;          // epoll_create1()创建出来的df
  std::unordered_map<int, Channel *> channels_; // fd与Channel的映射关系
  std::vector<struct epoll_event> events_; // 用于 epoll_wait 的事件数组
};

Poller只有四个成员变量,分别是ownerLoop_、epoolfd、channels、events_,那么分别都是什么意思呢?

1、ownerLoop_表示的是EventLoop对应的指针,一个Poller对应一个EventLoop,Poller和EventLoop是一一对应的关系,所以Poller必须得有EventLoop的指针,间接地标志了该Poller是与哪一个EventLoop对应的。

2、epollfd,这个只要是用过epoll的朋友都应该很熟悉,epoll有三个常用的接口,分别是epoll_create(int size),epoll_ctl(),epoll_wait(),而这个epollfd就是由epoll_create/epoll_create1所返回的文件描述符也就是fd,epoll_create1是新接口,参数为int flag,这个flag用来控制行为,而原接口的size已经被内核所忽略。

3、unordered是STL的常用容器,是一个KV的存储结构,底层是哈希表,为什么要用这个呢?用来存储所有的键值对,上面说了,Channel是对fd的一个封装,fd与channel是一一对应的关系,一个fd对应一个Channel,而这个channels_存储了所有的键值对也就是映射关系。后续就可以通过查找channels_找到对应fd所对应的Channel了。

4、events_,用于存储epoll_wait就绪事件的数组,EventLoop会遍历这个数组,找到已经就绪的事件,然后通过channels_找到对应的Channel,然后调用其回调函数处理读写等逻辑

5、EventLoop(事件循环):

服务器的核心,该服务器采用的是主从Reactor,每个Reactor都有一个EventLoop都在运行loop()。


传统服务器的本质都是在执行一个 while 循环,不断监听、分发并处理 IO 事件,而这里的 EventLoop 就相当于对这套循环的高度封装。所谓主从 Reactor 本质就是:MainReactor 执行 handleAccept() 接收新连接,相当于传统服务器中调用 accept() 这个函数。然而,**与单线程服务器不同的是,MainReactor 并不负责后续的读写 IO,而是将新建立的连接通过负载均衡策略,分发给某个 SubReactor 去监听与管理,在这个服务器里就是调用handleNewConnection()函数,这也就是为什么有了handleAccept()了还要有handleNewConnection()。**每个 SubReactor 拥有独立的 EventLoop 和 epoll 实例,负责对应连接的可读、可写事件分发与处理,实现了 IO 多路复用与多线程并行处理,从而大幅提升了性能!

以下只给出EventLoop的头文件,具体实现可看源码:

cpp 复制代码
/*
EventLoop是AeroChat中Reactor模式的核心组件。
每个EventLoop 对象绑定一个线程,实现 "One Loop Per Thread" 设计。
它负责:
驱动事件循环调用Poller的poll方法获取活跃Channel;
分发 I/O 事件(通过 Channel 的回调)
执行跨线程投递的任务(通过无锁队列 pendingFunctors_)。
 */
#ifndef EVENTLOOP_HPP
#define EVENTLOOP_HPP

#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
#include <sys/eventfd.h>   // for eventfd
#include <unistd.h>         // for close

#include "Channel.hpp"
#include "Poller.hpp"
#include "concurrentqueue.hpp"

class EventLoop {
public:
  EventLoop();
  ~EventLoop();

  // 启动事件循环(阻塞)
  void loop();

  // 请求退出事件循环
  void quit();

  // 更新 Channel 在 Poller 中的注册状态(新增、修改或删除)
  void updateChannel(Channel *channel);
  void removeChannel(Channel *channel);

  // 跨线程任务投递
  void runInLoop(
      const std::function<void()> &cb); // 若在当前线程则直接执行,否则入队
  void queueInLoop(const std::function<void()> &cb); // 总是入队

  // 判断调用者是否在 EventLoop 所属线程中
  bool isInLoopThread() const {
    return threadId_ == std::this_thread::get_id();
  }

  // 索引(用于区分主 Reactor 和子 Reactor)
  int getIndex() const { return index_; }
  void setIndex(int idx) { index_ = idx; }

private:
  // 执行所有待处理的跨线程回调
  void doPendingFunctors();

  // 唤醒 EventLoop 线程(当有任务入队且不在本线程时调用)
  void wakeup();
  // eventfd 读事件回调,用于清空唤醒标志
  void handleWakeup();

  std::atomic<bool> looping_; // 是否正在循环
  std::atomic<bool> quit_;    // 是否退出
  int index_; // 标识(主 Reactor 为 0,子 Reactor 为 1..N)
  std::thread::id threadId_; // 所属线程 ID
  moodycamel::ConcurrentQueue<std::function<void()>>
      pendingFunctors_;         // 无锁队列,存储跨线程任务
  bool callingPendingFunctors_; // 是否正在执行待处理回调(防止重入)

  std::unique_ptr<Poller> poller_; // I/O多路复用器(当前为epoll)

  // 唤醒机制相关 
  int wakeupFd_;                               // eventfd 文件描述符
  std::unique_ptr<Channel> wakeupChannel_;     // 封装 wakeupFd_ 的 Channel
};

#endif // EVENTLOOP_HPP

6、EventLoopThread:

这个类就是实现One Loop Per Thead的核心:

cpp 复制代码
/*
one loop per thread的具体实现
EventLoopThread的目的就是将EventLoop对象与一个独立线程绑定,使该线程运行事件循环,实现一个线程
一个事件循环
这个类主要负责:
1、创建线程,在线程中构造EventLoop对象(栈上)
2、启动时间循环,直到线程退出
3、提供同步机制,确保调用者能获得已经就绪的EventLoop指针
EventLoopThread是如何与Thread建立联系的?

thread_.start() 内部调用 pthread_create(&tid_, nullptr, runInThread, this)。

新线程启动后,执行 runInThread(this)。

runInThread将this 转回 Thread*并调用 thread->func_(),
也就是执行 EventLoopThread::threadFunc。
于是,EventLoopThread::threadFunc就在子线程中运行,创建并运行EventLoop。

*/
#ifndef EVENTLOOPTHREAD_HPP
#define EVENTLOOPTHREAD_HPP
#include "Thread.hpp"
#include "EventLoop.hpp"
#include <condition_variable>
#include <mutex>
#include <string>

class EventLoopThread : noncopyable {
public:
    explicit EventLoopThread(const std::string& name = std::string());
    ~EventLoopThread();

    EventLoop* startLoop();//启动线程,返回关联的EventLoop指针

private:
    void threadFunc();//线程函数

    EventLoop* loop_;//指向本线程中所创建的EventLoop对象
    bool exiting_;//是否退出
    Thread thread_;//对应的线程
    std::mutex mutex_;//互斥锁
    std::condition_variable cond_;//条件变量
};

#endif // EVENTLOOPTHREAD_HPP

为什么说这个类是实现One Loop Per Thread的核心?来看一下这个类的成员函数:

cpp 复制代码
EventLoopThread::EventLoopThread(const std::string& name)
    : loop_(nullptr), exiting_(false), thread_(std::bind(&EventLoopThread::threadFunc, this), name) {}
   //创建Thread对象,线程函数为EventLoopThread::threadFunc,并传入this指针以便在线程函数中访问当前
   //对象,name是线程名称
   //注意:此时线程还没有启动,thread_对象只是构造完整,thread_.start()并没有进行调用
EventLoopThread::~EventLoopThread() {
    exiting_ = true;
    if (loop_) {
        loop_->quit();
        thread_.join();
    }
}

EventLoop* EventLoopThread::startLoop() {
    thread_.start();//启动底层线程,线程入口函是threadFunc
    std::unique_lock<std::mutex> lock(mutex_);
    cond_.wait(lock, [this]() { return loop_ != nullptr; });//带谓词的安全版本,第二个参数返回值为bool类型,是一个lambda表达式
    return loop_;//一旦子线程完成初始化并通知条件变量,startLoop()返回指向该EventLoop的指针
    //此后外部就可以通过这个指针进行跨线程调用(如runInLoop)
}

void EventLoopThread::threadFunc() {
    EventLoop loop;//注意这里并不是new而是在栈上创建对象
    static int idx = 1;
    loop.setIndex(idx++);//设置索引
    
    {
        std::lock_guard<std::mutex> lock(mutex_);
        loop_ = &loop;//把栈对象的地址赋值给成员变量
        cond_.notify_one();//通知startLoop已经就绪
    }
    LOG_INFO("[SUCCESS] Sub Reactor \"" + thread_.name() + "\" (线程ID: " + 
             std::to_string(std::hash<std::thread::id>{}(std::this_thread::get_id())) + ") 已启动");
    loop.loop();//进入事件循环
    //调用 EventLoop::loop(),该函数内部是一个无限循环(知道quit()被调用),处理 I/O 事件和跨线程回调。
    std::lock_guard<std::mutex> lock(mutex_);
    loop_ = nullptr;//循环结束,进行清理
}

threadFunc()在栈上创建EventLoop对象,这样就有一个好处,生命周期与线程绑定,线程退出时自动销毁,无需手动进行delete。

每个EventLoopThread实例唯一对应一个子线程,且该线程只运行这个EventLoop,实现了One Loop Per Thread。

7、EventLoopThreadPool:

EventLoopThreadPool 创建并持有多个 EventLoopThread,每个 EventLoopThread 启动后产生一个子线程及其 EventLoop

cpp 复制代码
/*
EventLoopThreadPool是AeroChat中管理多个子Reactor线程的核心类,它封装了多个EventLoopThead,对外提供
统一的接口来获取子Reactor的EventLoop,负责新连接的负载均衡
*/
#ifndef EVENTLOOPTHREADPOOL_HPP
#define EVENTLOOPTHREADPOOL_HPP

#include <vector>
#include <memory>
#include <string>
#include "EventLoop.hpp"
#include "EventLoopThread.hpp"
#include<atomic>

class EventLoopThreadPool {
public:
    EventLoopThreadPool(EventLoop* baseLoop, int numThreads);
    ~EventLoopThreadPool() = default;

    void start();//开启所以线程,进行loop
    EventLoop* getNextLoop();          // 无参获取下一个loop
    EventLoop* getLoop(int idx);       // 根据索引获取指定loop
    int getThreadNum() const;          // 获取线程数(仅声明)
    std::vector<EventLoop*> getAllLoops() const; // 获取所有loop(仅声明)

private:
    EventLoop* baseLoop_;              // 主Reactor的loop
    int numThreads_;                   // 子Reactor数量
    std::atomic<int>next_{0};                         // 轮询索引
    bool started_;                     // 是否启动
    std::vector<std::unique_ptr<EventLoopThread>> threads_; // 子线程列表
    std::vector<EventLoop*> loops_;    // 子loop列表 存储所以EventLoop对应的指针
};

#endif // EVENTLOOPTHREADPOOL_HPP

三、服务器运行流程:

看了上面的各个组件可能并不能整个服务器的流程,下面我将从main函数入手,详细讲解一下这个服务器的运行流程,以便于大家理解。

cpp 复制代码
int main(int argc, char* argv[]) {
    signal(SIGPIPE, SIG_IGN);

    if (argc < 2) {
        printf("Usage: %s port\n", argv[0]);
        return 1;
    }

    uint16_t port = static_cast<uint16_t>(atoi(argv[1]));
    EventLoop loop; //创建一个EventLoop对象loop
    int subReactorNum = std::thread::hardware_concurrency();//由于我的服务器是8核8GB,所以这里的
    //subReactorNum=我的cpu核心数也就是等于8
    ChatServer server(&loop, port, subReactorNum);

    server.start();//启动服务器
    loop.loop();//开始loop
	printf("服务器启动成功!\n");

    return 0;
}

首先看服务器主函数,首先创建一个EventLoop也就是主EventLoop,然后创建一个server对象,执行ChatServer::start()。

cpp 复制代码
void ChatServer::start() {
    threadPool_->start();

    listenFd_ = sock_.Socket();
    int reuse = 1;
    setsockopt(listenFd_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    setsockopt(listenFd_, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse));
    fcntl(listenFd_, F_SETFL, O_NONBLOCK | FD_CLOEXEC);
    sock_.Bind(listenFd_, port_);
    sock_.Listen(listenFd_, 8192);

    acceptChannel_ = std::make_unique<Channel>(loop_, listenFd_);
    acceptChannel_->setReadCallback([this] { handleAccept(); });
    acceptChannel_->enableReading();
    acceptChannel_->enableET();
    loop_->updateChannel(acceptChannel_.get());

    LOG_INFO("[EchoServer] Started on port " + std::to_string(port_));
}

然后执行threadPool_.start()。

cpp 复制代码
void EventLoopThreadPool::start() {
   //启动所有Reactor对应的线程,开启事件循环(loop)
    if (started_) return;
    started_ = true;
    for (int i = 0; i < numThreads_; ++i) {
        auto t = std::make_unique<EventLoopThread>("SubReactor-" + std::to_string(i));
        EventLoop* subLoop = t->startLoop();
        loops_.push_back(subLoop);
        threads_.push_back(std::move(t));
    }
}

这个函数会开启所有的EventLoop,一个SubReactor/SubLoop对应一个线程。t是一个std::unique_ptr<EventLoopThread> 对象,然后通过这个智能指针去调用EventLoopThread::startLoop(),那么startLoop() 内部具体是如何开启一个线程并运行事件循环的呢?

thread_是一个Thread类的对象,所以我们看一下Thread::start()。

这个Thread类的构造函数的第一个参数是一个回调函数。被绑定到了threadFunc,也就是说Thread构造时就会回调threadFunc这个回调函数。

如上就实现了One Loop Per Thread,然后主线程再执行loop()。当有新连接到来的时候就回调handleAcceptor,主线程只负责接收新连接然后通过轮询的方式将这些新连接分发给不同的子Reactor/子Loop,子Reactor去执行handleNewConnection()。这就是主从Reactor!也是该服务器高性能的关键所在。

更多细节可查看源码:

AeroChat/benchmark at master · ywx914705/AeroChat

完整项目(高性能聊天室服务器)源码:

ywx914705/AeroChat: AeroChat 是一个使用 C++11 开发的高并发聊天服务器,采用 **主从 Reactor + 异步任务队列** 架构,支持单机数万并发连接。项目实现了完整的用户认证、私聊、群聊、离线消息、会话管理等功能,并集成了 MySQL 与 Redis 作为存储层。

项目已开源,配套完整的README,觉得写的不错的朋友可以帮忙star一下,谢谢!!!

四、参考资料:

相关推荐
腾讯蓝鲸智云2 小时前
提升研发效能:DevOps平台高效权限配置与同步方案
运维·服务器·人工智能·云计算·devops
努力努力再努力wz2 小时前
【C++高阶系列】外存查找的极致艺术:数据库偏爱的B+树底层架构剖析与C++完整实现!(附B+树实现的源码)
linux·运维·服务器·数据结构·数据库·c++·b树
财迅通Ai2 小时前
SuperX完成日本全球供应中心首批高性能AI服务器交付,全球战略迈出关键一步
运维·服务器·人工智能·superx·中恒电气
Yungoal2 小时前
c++迭代器
开发语言·c++
踏着七彩祥云的小丑2 小时前
Linux命令——开机自启配置
linux·运维·网络
clear sky .2 小时前
[linux]buildroot什么用途
linux·运维·数据库
济6172 小时前
I.MX6U Linux 驱动开发篇---阻塞IO实验--- Ubuntu20.04
linux·嵌入式·嵌入式linux驱动开发
济6172 小时前
I.MX6ULL Linux 驱动开发篇---Linux非阻塞IO实验-- Ubuntu20.04
linux·嵌入式·嵌入式linux驱动开发
rqtz2 小时前
【C++】ROS2捕获Ctrl+C信号+原子操作与线程生命周期控制
c++·多线程·原子