本文最后更新于:9 个月前
Muduo库源码剖析(1) Reactor架构三大核心模块
写在前面
好久不见,距离上次更新过去两个月了,一直在忙各种各样的事,搁置了很长时间。服务器的实现先暂且告一段落,因为我想一味地去抄袭别人的代码起到的作用可能会很有限,所以还是想自己去实现一下。正好这段时间也读了APUE
、UNP
等等的材料,所以觉得准备的也差不多了。先从陈硕老师赫赫有名的Muduo
网络库开始学习吧。
本文介绍一下Muduo
库所采用的Reactor架构的三大核心模块。
正文
1 Reactor模式简介
Reactor模式也叫反应器模式,大多数IO相关组件如Netty、Redis在使用的IO模式,用于需要并发处理多个客户端的服务器。
Reactor模式就是基于建立连接与具体服务之间线程分离的模式。在Reactor模式中,会有一个线程负责与所有客户端建立连接,这个线程通常称之为 Reactor
。然后在建立连接之后,Reactor 线程 会使用其它线程(可以有多个)来处理与每一个客户端之间的数据传输,这个(些)线程通常称之为 Handler
。
由于服务端需要与多个客户端通信,它的通信是一对多的关系,所以它需要使用 Reactor 模式。对客户端,它只需要与服务端通信,它的通信是一对一的关系,所以它不需要使用 Reactor 模式。也就是说,对客户端来讲,它不需要进行建立连接与传输数据之间的线程分离。
Muduo
库有三个核心组件支撑一个Reactor实现持续的监听一组fd,并根据每个fd上发生的事件调用相应的处理函数。这三个组件分别是Channel
类、Poller/EpollPoller
类以及EventLoop
类。
2 Channel类
2.1 Channel类简介
在TCP网络编程中,想要IO多路复用监听某个fd,就要把这个fd和该fd感兴趣的事件通过epoll_ctl
注册到IO多路复用模块(也叫事件监听器)上。当事件监听器监听到该fd发生了某个事件。事件监听器返回发生事件的fd集合以及每个fd都发生了什么事件。
Channel类则封装了一个fd和该fd感兴趣的事件以及事件监听器监听到的该fd实际发生的事件。同时Channel类还提供了设置该fd的感兴趣事件,以及将该fd及其感兴趣事件注册到事件监听器或从事件监听器上移除,以及保存了该fd的每种事件对应的处理函数。
每个Channel对象自始至终只属于一个EventLoop,因此每个Channel对象都只属于一个IO线程,自始至终只负责一个文件描述符(fd)的IO事件分发,但它不拥有这个fd,也不会在析构时关闭这个fd。
用户不直接使用Channel,即不继承Channel,而会用到更上层的封装,如TcpConnection。
Channel的生命期由其owner class负责管理。
2.2 Channel类一些重要的成员变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private : static string eventsToString (int fd, int ev) ; void update () ; void handleEventWithGuard (Timestamp receiveTime) ; static const int kNoneEvent; static const int kReadEvent; static const int kWriteEvent; EventLoop* loop_; const int fd_; int events_; int revents_; int index_; bool logHup_; std::weak_ptr<void > tie_; bool tied_; bool eventHandling_; bool addedToLoop_; ReadEventCallback readCallback_; EventCallback writeCallback_; EventCallback closeCallback_; EventCallback errorCallback_;
2.3 Channel类一些重要的成员方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public : typedef std::function<void ()> EventCallback; typedef std::function<void (Timestamp)> ReadEventCallback; Channel (EventLoop* loop, int fd); ~Channel (); 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); } void tie (const std::shared_ptr<void >&) ; int fd () const { return fd_; } int events () const { return events_; } void set_revents (int revt) { revents_ = revt; } bool isNoneEvent () const { return events_ == kNoneEvent; } 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 isWriting () const { return events_ & kWriteEvent; } bool isReading () const { return events_ & kReadEvent; } int index () { return index_; } void set_index (int idx) { index_ = idx; } string reventsToString () const ; string eventsToString () const ; void doNotLogHup () { logHup_ = false ; } EventLoop* ownerLoop () { return loop_; } void remove () ;
3 Poller/EpollPoller类
3.1 Poller/EpollPoller简介
负责监听文件描述符事件是否触发以及返回发生事件的文件描述符以及具体事件的模块就是Poller。所以一个Poller对象对应一个事件监听器。在multi-reactor模型(多线程)中,有多少reactor就有多少Poller。
Muduo提供了epoll
和poll
两种IO多路复用方法来实现事件监听。不过默认是使用epoll
来实现,也可以通过选项选择poll
。
这个Poller
是个抽象虚类,由EpollPoller
和PollPoller
继承实现,与监听文件描述符和返回监听结果的具体方法也基本上是在这两个派生类中实现。EpollPoller
就是封装了用epoll
方法实现的与事件监听有关的各种方法,PollPoller
就是封装了poll
方法实现的与事件监听有关的各种方法。
3.2 Poller/EpollPoller的一些成员变量
Poller
类:
1 2 3 4 5 6 7 8 9 10 protected : typedef std::map<int , Channel*> ChannelMap; ChannelMap channels_;private : EventLoop* ownerLoop_;
EpollPoller
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private : static const int kInitEventListSize = 16 ; static const char * operationToString (int op) ; void fillActiveChannels (int numEvents, ChannelList* activeChannels) const ; void update (int operation, Channel* channel) ; typedef std::vector<struct epoll_event> EventList; int epollfd_; EventList events_;
3.3 EpollPoller类一些重要的成员方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public : EPollPoller (EventLoop* loop); ~EPollPoller () override ; Timestamp poll (int timeoutMs, ChannelList* activeChannels) override ; void updateChannel (Channel* channel) override ; void removeChannel (Channel* channel) override ;
4 EventLoop类
4.1 EventLoop类简介
刚才的Poller是封装了和事件监听有关的方法和成员,调用一次Poller::poll
方法它就能给你返回事件监听器的监听结果(发生事件的fd及其发生的事件)。作为一个网络服务器,需要有持续监听、持续获取监听结果、持续处理监听结果对应的事件的能力,也就是我们需要循环的去调用Poller:poll
方法获取实际发生事件的Channel集合,然后调用这些Channel里面保管的不同类型事件的处理函数(调用Channel::HandleEvent
方法)。
EventLoop
就是负责实现“循环”,负责驱动“循环”的重要模块!!Channel和Poller其实相当于EventLoop的手下,EventLoop整合封装了二者并向上提供了更方便的接口来使用。
4.2 EventLoop重要方法loop()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void EventLoop::loop () { assert (!looping_); assertInLoopThread (); looping_ = true ; quit_ = false ; LOG_TRACE << "EventLoop " << this << " start looping" ; while (!quit_) { activeChannels_.clear (); pollReturnTime_ = poller_->poll (kPollTimeMs, &activeChannels_); ++iteration_; if (Logger::logLevel () <= Logger::TRACE) { printActiveChannels (); } eventHandling_ = true ; for (Channel* channel : activeChannels_) { currentActiveChannel_ = channel; currentActiveChannel_->handleEvent (pollReturnTime_); } currentActiveChannel_ = NULL ; eventHandling_ = false ; doPendingFunctors (); } LOG_TRACE << "EventLoop " << this << " stop looping" ; looping_ = false ; }
每个EventLoop 对象都唯一绑定了一个线程,这个线程其实就在一直执行这个函数里面的while
循环,这个while
循环的大致逻辑比较简单。就是调用Poller::poll
方法获取事件监听器上的监听结果。接下来在loop里面就会调用监听结果中每一个Channel的处理函数HandlerEvent( )
。每一个Channel 的处理函数会根据Channel 类中封装的实际发生的事件,执行Channel 类中封装的各事件处理函数。
5 全局概览Poller、Channel和EventLoop在整个Reactor通信架构中的角色
EventLoop 起到一个驱动循环的功能,Poller 负责从事件监听器上获取监听结果。而Channel 类则在其中起到了将fd及其相关属性封装的作用,将fd及其感兴趣事件和发生的事件以及不同事件对应的回调函数封装在一起,这样在各个模块中传递更加方便。
上面图中,每一个EventLoop
都绑定了一个线程(一对一绑定),这种运行模式是Muduo 库的特色!!充份利用了多核cpu的能力,每一个核的线程负责循环监听一组文件描述符的集合。这就是书中一直在强调的One Loop Per Thread 。
写在后面
本篇解读了Reactor模式的三大核心模块,也大致缕清了Muduo 库的大体脉络,后面要在去深挖一些其他的主要类。革命尚未成功,同志仍需努力!