C++网络编程主从Reactor总结与推理
2026/7/20 15:50:02
网站开发
前情提要作者学习的资料来源于GitHub上的仓库地址GitHub - yuesong-feng/30dayMakeCppServer: 30天自制C服务器包含教程和源代码 · GitHub30天自制C服务器包含教程和源代码. Contribute to yuesong-feng/30dayMakeCppServer development by creating an account on GitHub.https://github.com/yuesong-feng/30dayMakeCppServer下面我会由浅入深的来讨论这个项目的内容也是我整个学习的历程。从刚开始的第一天到最后作者一步一步的增加知识项目里面的类也是一步步的增加。一.主要结构的发展一重天Socket先把裸系统调用收起来统一管理 fd、bind、listen、accept、connect 这些基础能力。class Socket { private: int fd_{-1}; public: Socket(); explicit Socket(int fd); ~Socket(); DISALLOW_COPY_AND_MOVE(Socket); void Bind(InetAddress *addr); void Listen(); int Accept(InetAddress *addr); void Connect(InetAddress *addr); void SetNonBlocking(); int GetFd(); };可以看到还有一个设置套接字非阻塞的API二重天Channel它把“一个 fd 什么时候关心读、什么时候关心写”抽象成事件对象并把事件处理转成回调。class Channel { public: Channel(EventLoop *loop, int fd); ~Channel(); DISALLOW_COPY_AND_MOVE(Channel); void HandleEvent(); void EnableRead(); int GetFd(); uint32_t GetListenEvents(); uint32_t GetReadyEvents(); bool GetInEpoll(); void SetInEpoll(bool in true); void UseET(); void SetReadyEvents(uint32_t ev); void SetReadCallback(std::functionvoid() const callback); private: EventLoop *loop_; int fd_; uint32_t listen_events_; uint32_t ready_events_; bool in_epoll_; std::functionvoid() read_callback_; std::functionvoid() write_callback_; };其实就是把上个socket类给重新封装了一下更加强大里面加了回调函数这样更方便我们去处理事件一个fd就有一个channel对象这个对象有自己关心的事件有自己的回调函数比如监听套接字的回调函数处理逻辑就是accept新的client_fd然后添加到epoll中进行统一管理客户端套接字的回调函数就是读数据和写数据。这里可以把所有的channel用一个stl::mapint,channel*管理起来只要获得到客户端fd就可以立马找到对应的channel。三重天Acceptor这里就开始区分channel的类型了以前的所有的套接字不管是监听套接字还是客户端套接字都封装成channel显然还不是分的很清楚Acceptor类将监听套接字和客户端套接字给区分开来它只负责监听 socket 上的新连接事件accept 出来以后交给上层。class Acceptor { public: explicit Acceptor(EventLoop *loop); ~Acceptor(); DISALLOW_COPY_AND_MOVE(Acceptor); void AcceptConnection(); void SetNewConnectionCallback(std::functionvoid(Socket *) const callback); private: EventLoop *loop_; Socket *sock_; Channel *channel_; std::functionvoid(Socket *) new_connection_callback_; };可以看到里面只有一个回调函数名字叫做new_connection_callback_因为他只关心处理新的连接四重天Connection它专门表示一个客户端连接把读缓冲、回写、断开清理都封装在一起。class Connection { public: Connection(EventLoop *loop, Socket *sock); ~Connection(); DISALLOW_COPY_AND_MOVE(Connection); void Echo(int sockfd); void SetDeleteConnectionCallback(std::functionvoid(int) const callback); void Send(int sockfd); private: EventLoop *loop_; Socket *sock_; Channel *channel_; std::functionvoid(int) delete_connectioin_callback_; Buffer *read_buffer_; };实际上我们看到的Connection类和Acceptor类长得差不多因为他们其实是并行的把连接套接字封装成Acceptor把每一个客户端套接字封装成Connection。差异1.从回调函数的名字上就能知道acceptor关心的是如何把新的连接给加入到epollconnection关心的是处理数据和不需要的收怎么把套接字删除掉。2.Connection中多了一个Buffer对象这个类是由我们自己创建的它里面主要是维护了一段缓冲区可以是一个string字符串或者一个char数组方便我们间接的操作套接字读写缓冲区。last重天Server它把新连接分配到子 reactor并用线程池把多个 EventLoop 跑起来。我们先看代码class Server { private: EventLoop *main_reactor_; Acceptor *acceptor_; std::mapint, Connection * connections_; std::vectorEventLoop * sub_reactors_; ThreadPool *thread_pool_; public: explicit Server(EventLoop *loop); ~Server(); DISALLOW_COPY_AND_MOVE(Server); void NewConnection(Socket *sock); void DeleteConnection(int sockfd); };这里面的内容比较多听我娓娓道来1.这里面的两个函数首先是NewConnection那么有人就要问了“为什么我们的acceptor里面有AcceptConnectserver里面怎么还有NewConnection呢”因为有多个Loop所以我们在新建连接的时候要选择加入到哪一个Loop(sub_reactors_)里面并且要加入到connections_里面删除的时候也要从这里面删除这些都是server级别的东西在acceptor和connection对象里面不能完成这任务它找不到connections_和.但是connection对象的读写功能就无所谓了他们不需要这么开阔的视野。有一个办法就是我们在server里面去写好这些增添和删除连接的函数然后acceptor_ new Acceptor(main_reactor_); std::functionvoid(Socket *) cb std::bind(Server::NewConnection, this, std::placeholders::_1); acceptor_-SetNewConnectionCallback(cb);这样吧server对像和函数绑定到acceptor里面当acceptor对象调用这个功能回调函数的时候就会调用server并且回调函数还会把目标socket对象作为参数给返回出来。通俗的讲就是acceptor里面执行AcceptConnectserver里面就会执行NewConnectSocket *sock。然后是void DeleteConnectionint socket前边讲过了删除一个connection也要从server里的connections_里面删除connection对象的级别不够所以Connection也要设置相同逻辑的回调函数这里就不再多讲了看一下代码演示Connection *conn new Connection(sub_reactors_[random], sock); std::functionvoid(int) cb std::bind(Server::DeleteConnection, this, std::placeholders::_1); conn-SetDeleteConnectionCallback(cb);小tips这里说一个我的建议删除套接字的时候可以直接close关掉套接字他所在的epoll会自动删除掉这个fd。虽然这样做是可以的但是还有一个更加有逻辑性的做法就是先从epoll上删除掉再用close关掉。看似是多次一举实际上逻辑上更加严密了。EventLoop这个没算到以上的任何一层因为它有点特殊他主要是处理Epoll上的任务的每一个epoll都会封装成Eventloop我们来看一下他的逻辑是怎么样的。void EventLoop::Loop() { while (!quit_) { std::vectorChannel * chs; chs epoll_-Poll(); for (auto ch : chs) { ch-HandleEvent(); } } }就是只是这样非常的简单用一个是事件数组来接受epoll_wait返回的事件然后循环处理这些事件不过我们又多了一层封装返回的是每个事件的fd对应的channel对象。以上是我们所有的类的的描述一步一步的进化很清楚的看到他们的功能越来越强大从刚开始一大坨代码变成后边越发模块化这正是我们从学习阶段到工程阶段的破茧。这个图表就涵盖了所有的这个图表就清除的涵盖了主要思想二.重中之重 :也是我们的标题里面所提到的主从Reactor大家可以想一下我们刚开始用的是一个单进程单线程的server服务器里面的逻辑就是很简单的线性逻辑简单的描述就是一个Eventloop在那里循环的当黑奴又累效率又底下。所以我们引入了线程池也就是多线程编程每一个EventLoop就占一个线程我们的主线程main_thread是acceptor对象进行监听接受新的客户端线程池开多个线程来处理客户端套接字的读写。int size static_castint(std::thread::hardware_concurrency()); thread_pool_ new ThreadPool(size);size是什么他是当前机器支持的硬件并发线程数也就是说我们的线程池最多可以开这么多子线程。我们代码里面直接开了最多的线程具体情况看大家反正不要超过这个值。一个EventLoop对象就是一个干活的黑奴他只要开始干活就会一直循环不会停止所以我们一般来说一个EventLoop就占一个线程。既然有size个子线程那么我们就创建size个黑奴最大程度上提升我们的效率那库库就是干啊少耍贫嘴我们来看示例int size static_castint(std::thread::hardware_concurrency()); thread_pool_ new ThreadPool(size); for (int i 0; i size; i) { sub_reactors_.push_back(new EventLoop()); } for (int i 0; i size; i) { std::functionvoid() sub_loop std::bind(EventLoop::Loop, sub_reactors_[i]); thread_pool_-Add(std::move(sub_loop)); }注意这里面用了一个movesub_loopc移动语义因为后边不需要sub_loop了所以直接move进去Add其实不用也没有关系但是这样更加严谨一些防止后边有谁再将这个event_loop的函数运行权夺走了。OK现在每个子线程创建好了每个子线程上的EvevntLoop也创建好了下面学习的主线程如何将客户端connection对象分发给这一群黑奴。uint64_t random sock-GetFd() % sub_reactors_.size(); Connection *conn new Connection(sub_reactors_[random], sock); std::functionvoid(int) cb std::bind(Server::DeleteConnection, this, std::placeholders::_1); conn-SetDeleteConnectionCallback(cb); connections_[sock-GetFd()] conn第一行就是计算一个数字fd取模于总Loop的数量这样可以均匀分配给每一个Loop因为据我观察我们每创建一个client_fd他们的数字会加1所以每一个Loop分配的fd差别不会太大。最后一行就是将我们新创建的connnection对象给加入map中去。三.服务端main函数的使用教程#include Server.h #include EventLoop.h int main() { EventLoop *loop new EventLoop(); Server *server new Server(loop); loop-Loop(); delete server; delete loop; return 0; }分别创建一个EventLoop对象和Server对象然后loop传给server作为主线程acceptor使用的专用loop。然后要启动loop函数进行工作这个是总开关没有这个就不会开始接受客户端。总结所谓的主从Reactor就是主线程专门监听新的连接子线程去处理客户端套接字的读写事件其实还可以发散一下思维多个Acceptor多队列是什么样子的。