0


国庆弯道超车(手写Muduo库剖析核心代码及编程思想)

手写muduo库

前言

Muduo库是陈硕老师个人编写的TCP网络编程库。博主出于学习的目的使用了C++11将陈硕老师的muduo库重构了一下,当然只是重构了其核心的思想。muduo库当中还有很多牛逼的实现个人现在能力有限只能看懂其主要的实现,后面能力提升了再写博客进行剖析。同时这篇博客的目的主要是为了剖析muduo库的核心代码梳理其代码的执行逻辑。这个了博主能力有限,可能写着写着就写出来了一些不知所云的东西,还请各位担待。如果有写错的地方请在下方留言指正

Multi-Reactor的三大组件

Muduo库是基于Reactor模式实现的TCP网络编程库,下面让我们来看看其模型是什么样子的。
在这里插入图片描述
此模式的特点是one loop per thread, 有一个main Reactor负责accept连接, 然后把该连接挂在某个sub Reactor中(可以采用round-robin或者随机方法),这样该连接的所有操作都在哪个sub Reactor所处的线程中完成。多个连接可能被分配到多个线程中,充分利用CPU。在应用场景中,Reactor的个数可以采用 固定的个数,比如跟CPU数目一致。此模式与模式二相比,减少了进出thread pool两次上线文切换,小规模的计算可以在当前IO线程完成并且返回结果,降低响应的延迟。并可以有效防止当IO压力过大时一个Reactor处理能力饱和问题。纯转发的proxy服务适合使用这种模式。
Muduo库当中有三个核心的组件来支撑一个Reactor实现持续监听一组fd,并根据fd上发生的事件调用对应的回调函数。这三大组件分别是Channel,poller/Epollpoller,Eventloop。其中其他两大组件都离不开Channel
下面让我们一一来看一下这些组件的用处吧。

Channel类

Channel类相当于文件描述符的保姆。我们之前在学习多路转接的时候,我们想要监听器监听某个文件描述符我们需要通过epoll_ctl将其注册到多路io复用当中。当监听器检测到了这个文件描述符有事件就绪时监听器返回发生事件的文件描述符集合及每个文件描述那些事件就绪了。在muduo库当中Channel当中则是封装了一个fd和这个文件描述符感兴趣的事件以及将fd和其感兴趣的事件注册到事件监听器当中当然也包括移除。
下面我们来看看Channel类的声明:

#pragmaonce#include"noncopyable.h"#include<functional>#include"Timestamp.h"#include<memory>#include<iostream>classEventLoop;classChannel:public muduo::noncopyable{/* 
        Channel封装了sockfd和它感兴趣的event,比如EPOLLIN和EPOLLOUT事件
        还绑定了poller返回的具体事件。这个poller指代的就是epoll或poll,不过我只
        实现了epoll没有实现poll。
     */public:using EventCallback = std::function<void()>;//事件回调using ReadEventCallback = std::function<void(TimeStamp)>;//只读事件的回调,他其实也是一个EventCallback,只不过需要一个TimStamp而已Channel(EventLoop* loop,int fd);~Channel();voidHandlerEvent(TimeStamp receive_time);//这个TimeStamp作为函数参数类型,编译的时候是需要知道他的大小的,所以必须要include//Tmiestamp.h而不能仅仅是声明 TimeStamp类。//设置回调函数对象voidsetReadCallback(ReadEventCallback cb){read_callback_ = std::move(cb);}//将cb这个左值转换为右值引用voidsetWriteCallback(EventCallback cb){write_callback_ = std::move(cb);}//这个cb是左值对象,通过move转换为右值引用赋值给write_call_back_,这里编译器会把这个对象的所有权转移给write_call_back_。不用复制voidsetCloseCallback(EventCallback cb){close_callback_ = std::move(cb);}voidsetErrorCallback(EventCallback cb){error_callback_ = std::move(cb);}voidtie(const std::shared_ptr<void>&);//防止当channel被手动remove掉,channel还正在执行回调操作intfd()const{return fd_;}intevents()const{return events_;}intset_revents(int revt){ revents_ = revt;}//通过这个设置得知当前channel的fd发生了什么事件类型(可读、可写、错误、关闭)voidenableReading(){events_ |= kReadEvent;update();}//设置fd相应的事件状态,比如让这个fd监听可读事件,你就必须要用epoll_ctl设置它!voiddisableReading(){events_ &=~kReadEvent;update();}voidenableWriting(){events_ |= kWriteEvent;update();}voiddisableWriting(){events_ &=~kWriteEvent;update();}voiddisableAll(){events_ = kNoneEvent;update();}boolisWriting()const{return events_ & kWriteEvent;}//当前感兴趣的事件是否包含可写事件boolisReading()const{return events_ & kReadEvent;}//当前感兴趣的事件是否包含可读事件boolisNoneEvent()const{return events_ == kNoneEvent;}intindex(){return index_;}voidset_index(int idx){index_ = idx;}//one loop per thread 
    EventLoop*ownerLoop(){return loop_;}//当前这个channel属于哪一个event loopvoidremove();private:voidupdate();voidHandleEventWithGuard(TimeStamp receiveTime);//和上面的handleEvent差不多,处理受保护的事件//下面三个变量代表这个fd对哪些事件类型感兴趣staticconstint kNoneEvent;//0staticconstint kReadEvent;// = EPOLLIN | EPOLLPRI; EPOLLPRI带外数据,和select的异常事件集合对应staticconstint kWriteEvent;// = EPOLLOUT

    EventLoop* loop_;//这个channel属于哪个EventLoop对象constint fd_;//fd, poller监听的对象int events_;//注册fd感兴趣的事件int revents_;//poller通知我们这个文件描述符发生了那些事件int index_;//这个index_其实表示的是这个channel的状态,是kNew还是kAdded还是kDeleted//kNew代表这个channel未添加到poller中,kAdded表示已添加到poller中,kDeleted表示从poller中删除了
    
    std::weak_ptr<void> tie_;//这个tie_ 绑定了....TCPConnectionbool tied_;//channel通道里面能够获知fd最终发生的具体事件revents,所以它负责调用具体事件的回调操作。
    ReadEventCallback read_callback_;
    EventCallback write_callback_;
    EventCallback close_callback_;
    EventCallback error_callback_;};

下面我们来解释一下Channel类当中最重要的成员

  • fd_ :Channel照看的文件描述符
  • events_:该文件描述符关心的事件
  • loop_:当前的Channel属于那个Eventloop对象,Channel不能直接和Poller直接通信所以只能通过Eventloop来建立两者之间的通信
  • read_callback_ write_callback_,close_callback_,error_callback_:这几个类型都是std::function用于事件就绪调用相应的回调函数。当这个文件描述符有事件发生需要处理了,对应的回调在Channel当中都有,可谓是真的贴心

Channel类当中重要的成员方法

voidsetReadCallback(ReadEventCallback cb){read_callback_ = std::move(cb);}//将cb这个左值转换为右值引用voidsetWriteCallback(EventCallback cb){write_callback_ = std::move(cb);}//这个cb是左值对象,通过move转换为右值引用赋值给write_call_back_,这里编译器会把这个对象的所有权转移给write_call_back_。不用复制voidsetCloseCallback(EventCallback cb){close_callback_ = std::move(cb);}voidsetErrorCallback(EventCallback cb){error_callback_ = std::move(cb);}voidenableReading(){events_ |= kReadEvent;update();}//设置fd相应的事件状态,比如让这个fd监听可读事件,你就必须要用epoll_ctl设置它!voiddisableReading(){events_ &=~kReadEvent;update();}voidenableWriting(){events_ |= kWriteEvent;update();}voiddisableWriting(){events_ &=~kWriteEvent;update();}voiddisableAll(){events_ = kNoneEvent;update();}

我们都知道一个文件描述符可能发生的事件有:可读、可写、错误、关闭
当这些事件到来时需要调用对应的处理方法来处理。外部可以调用这几个成员函数将对应的回调放入channel类当中。外部可以调用channel类当中的使能方法告知Channel类你所管理的文件描述符关心那些事件并把这个文件描述符及其感兴趣的事件注册到监听器当中。这里channel类当中有一个私有的方法update其本质还是通过Eventloop调用到poller当中epoll_ctl.

下面我们再来看看channel类当中的HandlerEvent方法:

voidChannel::HandleEventWithGuard(TimeStamp receiveTime){/**
     * @brief 根据poller通知的channel发生的具体事件类型,由channel负责调用具体的回调操作。
     */LOG_INFO("channel HandleEvent revents:%d", revents_);if((revents_ & EPOLLHUP)&&!(revents_ & EPOLLIN))//EPOLLHUP表示这个设备已经断开连接{if(close_callback_)close_callback_();}if(revents_ & EPOLLERR){if(error_callback_)error_callback_();}if(revents_ &(EPOLLIN | EPOLLPRI)){if(read_callback_)read_callback_(receiveTime);}if(revents_ & EPOLLOUT){if(write_callback_)write_callback_();}}

当epoll_wait知道那些Channel(文件描述符)发生了那些事件。既然事件方式了那么肯定需要处理。HandlerEvent就是用来处理这个文件描述符对应事件的回调方法,根据发生的事件选择是调用四个回调当中的哪一个

poller/Epollpoller

poller/Epollpoller这个组件主要负责监听文件描述符是否是事件发生以及返回那些文件描述符发生了事件及其具体的事件。
在muduo库当中poller对应一个事件监听器,muduo库提供了两种io复用方法来实现事件监听不过默认是通过epoll但是博主这里只重构了epoll.这个poller是一个抽象类可以由Pollpoller和Epollpoller来继承实现重写里面的方法。
下面让我们来看看这个类的声明

#pragmaonce#include"noncopyable.h"#include<vector>#include<unordered_map>#include"Channel.h"classChannel;classEventLoop;classPoller:public muduo::noncopyable{public:using ChannelList = std::vector<Channel*>;Poller(EventLoop *loop);virtual~Poller();//给所有io复用保留统一的接口,当前激活的channels,需要poller去循查的channel(fd)virtual TimeStamp poll(int timeoutMs, ChannelList *ativateChannels)=0;virtualvoidupdateChannel(Channel *channel)=0;virtualvoidremoveChannel(Channel *channel)=0;boolhasChannel(Channel *channel)const;//判断一个poller里面有没有这个channel//EventLoop可以通过该接口获取默认的IO复用的具体实现static Poller*newDefaultPoller(EventLoop *loop);protected:using ChannelMap = std::unordered_map<int, Channel*>;//key为sockfd value为:sockfd所对应的事channel//当poller检测到某个套接字有事件发生时,通过文件描述符可以直接找到Channel,Channel里面有读回调写回调
    ChannelMap channels_;private:
    EventLoop *ownerLoop_;};

其中Poller类当中重要的成员变量

  • epollfd_:用来接收epoll_create返回的epoll句柄
  • channels_:这个变量是一个哈西表可以通过文件描述符找到对应的事件
  • ownerloop_:所属的Eventloop对象后面再慢慢解释

重要的成员方法:

virtual TimeStamp poll(int timeoutMs, ChannelList *ativateChannels)=0;

这个函数可以说是Poller的核心,当外界调用了poll这个方法该方法的底层其实调用的是epoll_wait函数。我们知道在muduo库当中每一个fd都对应着一个Channel,哈西表channels_可以让我们通过fd找到与之对应的channel。将来事件监听器监听到该fd上有事件发生,就可以通过哈西表将具体发生了什么事件写入到channel当中。poll当中的ativateChannels他的类型是(vector<Channel*>),当外界调用poll方法之后可以通过activateChannels拿到监听结果,也就是那些文件描述符就绪了,并调用Channel当中的对应事件的回调函数

EventLoop

之前我们说过Channel无法直接和Poll/Epollpoller直接交互是通过EventLoop来进行间接交互的。作为一个网络服务器需要有持续监听并获取监听的结果和持续处理监听结果的能力这也就要求我们必须循环的去调用Poll模块当中的poll方法去获取发生事件的Channel集合,然后再调用Channel当中的回调方法。说白了EventLoop就是负责实现循环,负责驱动循环的重要模块,通过EventLoop可以让Channel和Poll/Epollpoller间接的实现通信。
再这里需要提前说一下muduo库当中的一个特色 One Loop Per Thread.即一个线程一个EventLoop(一一对应)并且没一个线程负责一组文件描述符集合,执行对应文件描述符对应的回调的时候,必须再当前线程执行回调不准跑到其线程去执行,至于这个是怎么实现的后面再说
下面我们来看一下EventLoop的声明部分再看一下重要的方法和成员变量

#pragmaonce#include<functional>#include<vector>#include<atomic>#include"noncopyable.h"#include"Timestamp.h"#include<memory>#include<mutex>#include"CurrentThread.h"classChannel;classPoller;//事件循环类, 主要包含了两大模块,channel 和 pollerclassEventLoop:public muduo::noncopyable{public:using Functor = std::function<void()>;EventLoop();~EventLoop();voidloop();//开启事件循环voidquit();//关闭事件循环
    TimeStamp poolReturnTime()const{return pollReturnTime_;}voidrunInLoop(Functor cb);//mainReactor用于唤醒Subreactor的voidqueueInLoop(Functor cb);//voidwakeup();voidupdateChannel(Channel *channel);voidremoveChannel(Channel *channel);boolhasChannel(Channel *channel);//判断当前的eventloop对象是否在自己的线程里面boolisInLoopThread()const{return threadId_ ==CurrentThread::tid();}private:voidhandleRead();//处理唤醒相关的逻辑。voiddoPendingFunctors();//执行回调的using ChannelList = std::vector<Channel*>;
    std::atomic<bool> looping_;//标志进入loop循环
    std::atomic<bool> quit_;//标志退出loop循环 这个和looping_ 其实本质上有重叠
    std::atomic<bool> callingPendingFunctors_;//标识当前loop是否有需要执行回调操作const pid_t threadId_;//当前loop所在的线程的id
    TimeStamp pollReturnTime_;//poller返回发生事件的channels的时间点
    std::unique_ptr<Poller> poller_;//一个EventLoop需要一个poller,这个poller其实就是操控这个EventLoop的对象。//统一事件源int wakeupFd_;//主要作用,当mainLoop获取一个新用户的channel通过轮询算法选择一个subloop(subreactor)来处理channel。
    std::unique_ptr<Channel> wakeupChannel_;
    ChannelList activeChannels_;
    Channel *currentActiveChannel_;
    std::vector<Functor> pendingFunctors_;//存储loop需要执行的所有回调操作。
    std::mutex mutex_;//保护上面vector容器的线程安全操作。};

其中重要的成员方法为loop:

voidEventLoop::loop(){//EventLoop 所属线程执行
    looping_ =true;
    quit_ =false;LOG_INFO("EventLoop %p start looping \n",this);while(!quit_){//在我们的Epoller里面poll方法里面,我们把EventLoop的ActiveChannels传给了poll方法//当poll方法调用完了epoll_wait()之后,把有事件发生的channel都装进了这个ActiveChannels数组里面
        activeChannels_.clear();//监听两类fd,一个是client的fd,一个是wakeupfd,用于mainloop和subloop的通信
        pollReturnTime_ = poller_->poll(kPollTimeMs,&activeChannels_);//此时activeChannels已经填好了事件发生的channelfor(Channel *channel : activeChannels_){//Poller监听哪些channel发生事件了,然后上报(通过activeChannels)给EventLoop,通知channel处理相应事件
            channel->HandlerEvent(pollReturnTime_);}/**
        IO线程 mainLoop负责接收新用户的连接,
        mainloop实现注册一个回调cb,这个回调由subloop来执行。
        wakeup subloop后执行下面的方法,执行mainloop的cb操作。
         */doPendingFunctors();//执行当前EventLoop事件循环需要处理的回调操作。}LOG_INFO("EventLoop %p stop looping. \n", t_loopInThisThread);}

前面我们说过EventLoop都绑定了一个线程,这个线程一直再这个函数当中执行while循环,其实就是调用Poll:poll方法获取事件监听器的监听结果,拿到监听结果之后调用Channel当中的回调方法也就是HandlerEvent. 下面我们来看看runinLoop和queInLoop这两个函数之后我们再看看doPendingFunctors

下面我们来看看runInLoop函数:

voidEventLoop::runInLoop(Functor cb){//保证了调用这个cb一定是在其EventLoop线程中被调用。if(isInLoopThread()){//如果当前调用runInLoop的线程正好是EventLoop的运行线程,则直接执行此函数cb();}else{//否则调用 queueInLoop 函数queueInLoop(cb);}}

这个函数的作用就是执行回调的时候,首先判断是否在当前线程当中,如果在就直接执行如果不在将其放到回调队列当中

我们在看看queueInLoop函数的实现:

voidEventLoop::queueInLoop(Functor cb){{
        unique_lock<mutex>lock(mutex_);
        pendingFunctors_.emplace_back(cb);}//唤醒相应的,需要执行上面回调操作的loop线程// || callingPendingFunctors_的意思是:当前loop正在执行回调,但是loop又有了新的回调,// 这个时候就要wakeup()loop所在线程,让它继续去执行它的回调。if(!isInLoopThread()|| callingPendingFunctors_){/***
        这里还需要结合下EventLoop循环的实现,其中doPendingFunctors()是每轮循环的最后一步处理。 
        如果调用queueInLoop和EventLoop在同一个线程,且callingPendingFunctors_为false时,
        则说明:此时尚未执行到doPendingFunctors()。 那么此时即使不用wakeup,也可以在之后照旧
        执行doPendingFunctors()了。这么做的好处非常明显,可以减少对eventfd的io读写。
        ***/wakeup();/***
        为什么要唤醒 EventLoop,我们首先调用了 pendingFunctors_.push_back(cb), 
        将该函数放在 pendingFunctors_中。EventLoop 的每一轮循环在最后会调用 
        doPendingFunctors 依次执行这些函数。而 EventLoop 的唤醒是通过 epoll_wait 实现的,
        如果此时该 EventLoop 中迟迟没有事件触发,那么 epoll_wait 一直就会阻塞。 
        这样会导致,pendingFunctors_中的任务迟迟不能被执行了。
        所以必须要唤醒 EventLoop ,从而让pendingFunctors_中的任务尽快被执行。
        ***/}}

具体精华的内容以上面的注释都已经有了在这里就不过多解释了,这个函数的作用就是唤醒在epoll_wait当中阻塞的EventLoop让在唤醒之后执行回调队列当中的回调方法。此时一定就是在当前线程执行这些回调方法这样就保证了,回调方法只能在当前线程执行。
那么是如何实现这套机制的了,这个问题值得我们学习。下面让我们来看看是如何实现的

首先我们需要认识一个函数叫做eventfd:

#include<sys/eventfd.h>inteventfd(unsignedint initval,int flags);

函数说明:
调用该函数会创建一个eventfd对象或者我们可以理解打开了一个eventfd类型的文件,就类似于open操作。eventfd在内核当中会维护一个无符号64位的计数器初始化为initval.
第二个参数flag主要有以下几个选项:

EFD_CLOEXEC表示返回的eventfd文件描述符在fork后exec其他程序时会自动关闭这个文件描述符;
EFD_NONBLOCK设置返回的eventfd非阻塞;
EFD_SEMAPHORE表示将eventfd作为一个信号量来使用

有了前面的基础知识下面我们就可以看看这到底是怎么实现的

//__thread是一个thread_local的机制,代表这个变量是这个线程独有的全局变量,而不是所有线程共有
__thread EventLoop *t_loopInThisThread =nullptr;//防止一个线程创建多个EventLoop//当一个eventloop被创建起来的时候,这个t_loopInThisThread就会指向这个Eventloop对象。//如果这个线程又想创建一个EventLoop对象的话这个t_loopInThisThread非空,就不会再创建了。constint kPollTimeMs =10000;//定义默认的Pooler IO复用接口的超时时间EventLoop::EventLoop():looping_(false),quit_(false),callingPendingFunctors_(false),threadId_(CurrentThread::tid()),//获取当前线程的tidpoller_(Poller::newDefaultPoller(this)),//获取一个封装着控制epoll操作的对象wakeupFd_(createEventfd()),//生成一个eventfd,每个EventLoop对象,都会有自己的eventfdwakeupChannel_(newChannel(this, wakeupFd_)),//每个channel都要知道自己所属的eventloop,currentActiveChannel_(nullptr){LOG_DEBUG("EventLoop created %p in thread %d \n",this, threadId_);if(t_loopInThisThread)//如果当前线程已经绑定了某个EventLoop对象了,那么该线程就无法创建新的EventLoop对象了LOG_FATAL("Another EventLoop %p exits in this thread %d \n", t_loopInThisThread, threadId_);else{
        t_loopInThisThread =this;}
    wakeupChannel_->setReadCallback(std::bind(&EventLoop::handleRead,this));//用了functtion就要用bind,别用什么函数指针函数地址之类的。

    wakeupChannel_->enableReading();//每一个EventLoop都将监听wakeupChannel的EpollIN读事件了。//mainReactor通过给wakeupFd_给sbureactor写东西。}

在这里说一下这个__thread,被这个关键字修饰的全局变量t_loopInThisThread会具备一个属性那就是该变量在没一个线程内部有一个独立的实体,一个线程修改了不会影响另外一个线程,在EventLoop的构造函数当中如果当前线程没用绑定EventLoop对象那么t_loopThisThread一定是nullptr然后我们就可以让它指向当前的Eventloop,如果已经指向了一个EventLoop此时EventLoop对象就构造失败。在构造函数当中将wakupfd_注册到了epoll当中,以后可以通过向这个对象中写入数据从而将EventLoop给唤醒。而wakeupfd_是通过eventfd函数创建,下面看一下创建wakeupfd_的函数:

//创建wakeupfd,用来通notify subreactor处理新来的channelxintcreateEventfd(){int evtfd =eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);if(evtfd <0){LOG_FATAL("eventfd error: %d \n", errno);}return evtfd;}

当MainEventLoop也就是主Loop中,负责新连接的建立的操作都必须要在MainLoop线程当中运行。已经建立的连接必须分配给某个SubLoop之后,这个连接的所有操作比如:接收数据连接断开等数据都必须在这个SubEventLoop当中运行,不准跑到其它线程当中运行。
上面这个函数用来创建一个文件描述符并且这个文件描述符是非阻塞和子进程不可拷贝的的。改文件描述符在EventLoop的构造函数当中被封装成为了一个wakeupChannel_并将改文件描述符注册到了poll当中.

下面我们来构造一个场景前面我们已经提到过了每个EventLoop线程主要是再执行loop函数其实就是一个死循环,循环的获取事件监听的结果以及调用每一个发生事件Channel的事件处理函数此时.如果此时SubEventLoop上注册的TCP连接没有任何反应那么整个SubEventLoop就阻塞在了epoll_wait上。此时如果MainEventLoop接受到了一个连接请求,并把这个连接请求封装成为了一个TcpConnection对象希望在一个SubEventLoop当中执行其内部的connectionEstablished()函数(这几个函数后面会一一介绍)。该函数功能就是将TcpConnection注册到SubEventLoop的事件监听器当中并调用用户自定义的连接建立之后的处理函数。当他被注册到一个SubEventLoop之后这个TcpConnection的任何操作都必须在这个SubEventLoop当中执行。那么问题来了TcpConnection当中的connectionEstablished函数是怎么在subEventLoop当中运行的了。上面我们已经提到了runInLoop和queueInLoop。当主线程来到SubEventLoop当中runInLoop之后发现不是当前线程,于是就将其加入到回调队列当中。并调用wakeup向wakeupfd_当中写入一个事件此时SubEventLoop的监听器监听到事件发生返回指向WakeChannel当中的回调方法。执行完毕之后此时肯定是再自己的线程当中,此时可以执行回调队列当中的回调方法

Thread&ThreadEventLoop&ThreadEventPool

下面我们来看一下线程篇也就是Muduo库当中的线程池。首先我们来学习一下这个Thread类
1.

#pragmaonce#include"noncopyable.h"#include<functional>#include<thread>#include<memory>#include<unistd.h>#include<string>#include<atomic>classThread:muduo::noncopyable{//只关注一个线程public:using ThreadFunc = std::function<void()>;explicitThread(ThreadFunc,const std::string &name = std::string());//这个写法我倒是少见~Thread();voidstart();voidjoin();boolstarted()const{return started_;}
    pid_t tid()const{return tid_;}//muduo库上返回的tid相当于linux上用top命令查出来的tid,不是pthread的tid。staticintnumCreated(){return numCreated_;}private:voidsetDefaultName();bool started_;bool joined_;
    pid_t tid_;
    ThreadFunc func_;
    std::string name_;
    std::shared_ptr<std::thread> thread_;//注意这里,如果你直接定义一个thread对象,//那这个线程就直接开始运行了,所以这里定义一个智能指针,在需要运行的时候再给他创建对象。static std::atomic<int> numCreated_;};

Thread类非常的简单,个人觉得其中最重要的方法就是start方法,以及值得学习的是定义Thread的时候不是直接使用Thread类定义而是使用了智能指针。如果我们直接使用Thread定义对象的话其实这个线程已经开始运行了,我们想要的是再需要运行的时候再运行。
下面我们具体看一下这个start方法:

voidThread::start(){
    started_ =true;
    sem_t sem;sem_init(&sem,false,0);
    thread_ =shared_ptr<thread>(newthread([&](){//获取线程的tid值
        tid_ =CurrentThread::tid();//子线程获取当前所在线程的tid,注意执行到这一行已经是在新创建的子线程里面了。sem_post(&sem);func_();//开启一个新线程,专门执行一个线程函数。}));//这里必须等待上面的新线程获取了它的tid值才能继续执行。sem_wait(&sem);//当这个start函数结束后,就可以放心的访问这个新线程了,因为他的tid已经获取了。}

其它start方法据说通过new 一个thread对象当这个线程获取到了自己的线程id时另外一个线程才能结束线程执行的函数func_()来自于这个EventThread当中。再这个类当中会细说

2.EventThread
下面让我们来看看EventThread类:

classEventLoop;classEventLoopThread:public muduo::noncopyable{public:using ThreadInitCallback = std::function<void(EventLoop*)>;//用于线程初始化的回调函数EventLoopThread(const ThreadInitCallback &cb =ThreadInitCallback(),const std::string &name = std::string());~EventLoopThread();
    EventLoop*startLoop();private:voidthreadFunc();
    EventLoop *loop_;bool exiting_;
    Thread thread_;
    std::mutex mutex_;
    std::condition_variable cond_;
    ThreadInitCallback callback_;};

其中最重要的方法就是startLoop和threadFunc(上面说过的线程创建之后执行的线程函数)。首先我们先来看thredFunc函数:

voidEventLoopThread::threadFunc(){//该方法是在单独的新线程里面运行的。
    EventLoop loop;//创建一个独立的EventLoop,和上面的线程是一一对应的,//在面试的时候要是能把one loop per thread给说到具体的方法上,就能认同你了。if(callback_){callback_(&loop);}{
        unique_lock<mutex>lock(mutex_);
        loop_ =&loop;//EventLoopThread里面绑定的loop对象就是在这个线程里面创建的
        cond_.notify_one();}
    loop.loop();//EventLoop loop => Poller.poll开始监听远端连接或者已连接的fd的事件
    unique_lock<mutex>lock(mutex_);
    loop_ =nullptr;//如果走到这里说明服务器程序要关闭了,不进行事件循环了。//为什么这里要加锁,大概是悲观锁的思想吧,这个loop_毕竟会被多个程序使用}

其主要功能就是创建一个EventLoop对象如果有初始回调就执行没有直接EventLoop当中的loop方法也就是开启事件循环。这个函数是再线程的执行函数。这也就是为什么说一个线程一个EventLoop的原因。至于为什么需要加锁和条件变量的使用我们等下看到startLoop就知道了。

下面我们一起来学一下startLoop函数:

//线程里面运行的EventLoop的结果
EventLoop*EventLoopThread::startLoop(){
    thread_.start();//启动底层新线程//必须要等待threadFunc的loop_初始化好了下面才能继续执行
    EventLoop* loop =nullptr;{
        unique_lock<mutex>lock(mutex_);while(loop_ ==nullptr){//https://segmentfault.com/a/1190000006679917 为什么把_loop放在nullptr里面,因为//要防止假唤醒,就是明明自己被唤醒了,刚想要得到资源的时候却被别人剥夺了。
            cond_.wait(lock);}
        loop = loop_;//这里相当于做了一个线程间的通信操作,另外一个线程初始化的loop_,在这个线程里面获取}return loop;}

其主要作用就是开启Thread底层的start方法让线程跑起来但是必须保证线程对应的EventLoop对象被创建之后才能获对应线程的EventLoop对象并返回给EventThreadPool

3.EventThreadPool
看着这个名字就大概知道他是干什么的不就是事件线程池吗?下面让我们一起来看看其声明然后再学习一下重要的方法

classEventLoopThreadPool:public muduo::noncopyable{/*
        管理EventLoopThread的线程池
    */public:using ThreadInitCallback = std::function<void(EventLoop*)>;EventLoopThreadPool(EventLoop *baseLoop,const std::string &nameArg);~EventLoopThreadPool();voidsetThreadNum(int numThreads){numThreads_ = numThreads;}voidstart(const ThreadInitCallback &cb =ThreadInitCallback());//如果是工作在多线程中,baseLiio_默认以轮循的方式分配channel给subloop//当然还有一个哈希的定制方式分配channel,这里就暂时不写他了
    EventLoop*getNextLoop();
    std::vector<EventLoop*>getAllGroups();boolstarted()const{return started_;}const std::string name()const{return name_;}private:
    EventLoop *baseLoop_;//如果你没有通过setThreadNum来设置线程数量,那整个网络框架就只有一个//线程,这唯一的一个线程就是这个baseLoop_,既要处理新连接,还要处理已连接的事件监听。
    std::string name_;bool started_;int numThreads_;int next_;
    std::vector<std::unique_ptr<EventLoopThread>> threads_; 
    std::vector<EventLoop*> loops_;//包含了所有EventLoop线程的指针};

EventThreadPool主要是用来管理EventThread所以了其成员变量当中包含了threads_和loops_保存了所有的EventLoop和EventLoopThread。其重要的方法主要有两个一个是getNextLoop一个是start方法

下面先来学习一下getNextLoop:

//如果是工作在多线程中,baseLiio_默认以轮循的方式分配channel给subloop//当然还有一个哈希的定制方式分配channel,这里就暂时不写他了
EventLoop*EventLoopThreadPool::getNextLoop(){/**
     * 这个很好理解,就是普通的轮循而已
     */
    EventLoop *loop = baseLoop_;if(!loops_.empty()){
        loop = loops_[next_];++next_;if(next_ >= loops_.size())
            next_ =0;}return loop;}

muduo库当中获取下一个EventLoop的方式是采用下标轮询的方式来寻找的找到了末尾就从起始位置开始寻找即可。非常的简单主要是用于mainLoop收到了一个新的连接通过这种方式把这个新的连接交给一个SubLoop即可。

start方法:

voidEventLoopThreadPool::start(const ThreadInitCallback &cb){
    started_ =true;for(int i =0; i < numThreads_; i++){char buf[name_.size()+32];snprintf(buf,sizeof(buf),"%s%d",name_.c_str(),i);
        EventLoopThread *t =newEventLoopThread(cb, buf);
        threads_.push_back(std::unique_ptr<EventLoopThread>(t));//其实这个EventLoopThread只提供了一些控制EventLoop线程的方法,而EventLoop对象的创建//则是在EventLoopThread::threadFunc()函数内创建的,当EventLoopThread析构了,//局部对象EvetLoop对象就会从栈中删除。
        loops_.push_back(t->startLoop());}if(numThreads_ ==0&&cb){//整个服务端只有一个线程运行着baseLoop_;cb(baseLoop_);}}

这个方法主要是用来创建底层新的线程并开启事件循环,并将对应的EventLoop和EventThread给保存起来。上面也有注释。非常的简单。

Acceptor类

Accepter封装了服务器的监听套接字fd已经对应的处理方法,Accper类当中其实没有什么核心的贡献主要是对其它类的方法进行封装。用来接受新用户的连接并将其分配给subEventLoop.下面我们直接来看他的声明是怎么样的:

classEventLoop;classInetAddress;classAcceptor:public muduo::noncopyable{public:using NewConnectionCallback = std::function<void(int sockfd,const InetAddress&)>;Acceptor(EventLoop* loop,const InetAddress &listenAddr,bool reuseport);~Acceptor();voidsetNewConnectionCallback(const NewConnectionCallback &cb){//设置连接事件发生的回调函数。
        newConnectionCallback_ = cb;//当有连接事件发生的时候调用newConnectionCallback_其实就是tcpserver当中的回调方法}boollistenning()const{return listenning_;}voidlisten();private:voidhandleRead();

    EventLoop *loop_;//Accepter用的就是用户定义的baseLoop我们也称为mainloop
    Socket acceptSocket_;//listenfd
    Channel acceptChannel_;//文件描述符的保姆
    NewConnectionCallback newConnectionCallback_;bool listenning_;//是否再监听};

其中Accpeter当中重要的成员变量主要有:

  • acceptSocket:这个是服务器监听套接字封装好的文件描述符
  • acceptChannel_:把监听套接字和其感兴趣的事件给封装起来,已经事件就绪的回调方法也封装起来
  • loop:监听套接字是属于那个EventLoop的其实就是属于mainLoop
  • newConnectionCallback_:新连接到来之后的回调其实就是TCPServer当中的newConnection函数该函数其实就是公平的选择一个subEventLoop将这个连接分配给这个subEventLoop

下面我们来看看其重要的成员方法

  • listen:该函数底层调用了linux的系统调用listen开启accpeterSocket_的监听同时将acceptChannel和其感兴趣的事件注册到mainLoop当中的事件监听器当中。
  • handleRead:这是Accepter类当中的一个私有方法这个方法主要的功能就是当有连接到来的时候就会调用这方法内部通过封装好的Socket将连接获取上来并调用newConnection函数公平的选择一个subEventLoop并把这个连接分配给他。

Buffer类

Buffer类封装的其实就是一个用户缓冲区以及包含了对该缓冲区数据操作的一系列方法。在这里我们主要学习Buffer类的读写配合,以及内部调整及其动态扩容

这个缓冲区值得我们学习和借鉴,本人以前写的只支持一次性全部读取和写入而这个Buffer类可以读一点点,写一点点。Buffer类采用的是vector非常的方便扩容。其内部的原理维护两个游标(readerIndex_和writerIndex_)用来标记可读缓冲区的起始位置和空闲空间的起始位置。下面我们来看看刚开始时缓冲区的样子:
在这里插入图片描述
如果我们向里面写入了数据此时缓冲区变为:
在这里插入图片描述
如果此时从从可读缓冲区当中读取一部分数据走了之后,此时结果又会是怎么样的:

在这里插入图片描述
如果此时我们想要写入的空间比这个可写缓冲区要大但是加上读走的那块空间是能够容纳你想写入的空间的此时我们可以将可读缓冲区当中的数据往前移。

在这里插入图片描述
将数据移动完毕之后我们就就可以将数据写进去了,写进去的图在这里就不画了。

注意:随着写入数据的不断增加,可写缓冲区的空间可能会被耗尽此时我们需要扩容,扩充缓冲区的长度。由于我们的缓冲区使用的是vector,所有我们扩容就非常的舒服直接调用vector的resize函数就可以实现扩容了。
下面我们只把这个Buffer类当中最核心的方法拿出来看看。首先我们来看看这个ensureWriteableBytes

voidensureWritableBytes(size_t len){// writableBytes()返回可写缓冲区的大小if(writableBytes()< len){//扩容makeSpace(len);}}voidmakeSpace(size_t len){if(writableBytes()+prependableBytes()- kCheapPrepend < len){//能用来写的缓冲区大小 < 我要写入的大小len,那么就要扩容了
           buffer_.resize(writerIndex_+len);}else{//如果能写的缓冲区大小 >= 要写的len,那么说明要重新调整一下Buffer的两个游标了。
            size_t readable =readableBytes();
            std::copy(begin()+ readerIndex_,//源 首迭代器 begin()+ writerIndex_,//源 尾迭代器begin()+ kCheapPrepend);//目标 首迭代器//这里把可读区域的数据给前移了
            readerIndex_ = kCheapPrepend;
            writerIndex_ = readerIndex_ + readable;//更新可写下标的位置}}

这个函数是当我们想要往缓冲区里面写入长度为len的数据时,这个函数就会检查缓冲区的可写空间是否能够容纳得了你想写入的数据,如果不能那么我们就需要扩容。

2.retrieveAsString(size_t len)函数,首先我们来看他的代码:

constchar*peek()const//peek函数返回可读数据的起始地址{returnbegin()+ readerIndex_;}
 std::string retrieveAsString(size_t len){
        std::string result(peek(), len);//peek()返回的是可读数据的起始地址retrieve(len);//上面把一句把缓冲区中的可读数据读取出来,要然后要对缓冲区进行复位操作。return result;}voidretrieve(size_t len){//将buffer类型转换成string类型if(len <readableBytes()){
            readerIndex_ += len;//应用只读取可读缓冲区数据的一部分,就是len}else//len == readableBytes(){retrieveAll();}}

这个函数是获取缓冲区内的数据也就是读取可读缓冲区内的数据,并更新可读区域的下标,并将读取到的数据以string的形式进行返回。是不是非常的简单。

下面我们再来看看这两个比较核心的函数一个是这个readFd,一个是writeFd.首先来看一下readFd

ssize_t Buffer::readFd(int fd,int* saveErrno){//从客户端套接字fd上读取数据, Poller工作在LT模式,只要数据没读完epoll_wait就会一直上报char extrabuf[65536]={0};//栈上的内存空间structiovec vec[2];const size_t writableSpace =writableBytes();//可写缓冲区的大小
    vec[0].iov_base =begin()+ writerIndex_;//第一块缓冲区
    vec[0].iov_len = writableSpace;//当我们用readv从socket缓冲区读数据,首先会先填满这个vec[0]//也就是我们的Buffer缓冲区
    vec[1].iov_base = extrabuf;//第二块缓冲区,如果Buffer缓冲区都填满了,那就填到我们临时创建的
    vec[1].iov_len =sizeof(extrabuf);//栈空间上。constint iovcnt =(writableSpace <sizeof(extrabuf)?2:1);//如果Buffer缓冲区大小比extrabuf(64k)还小,那就Buffer和extrabuf都用上//如果Buffer缓冲区大小比64k还大或等于,那么就只用Buffer。这意味着,我们最少也能一次从socket fd读64k空间const ssize_t n =::readv(fd, vec, iovcnt);if(n <0){*saveErrno = errno;//出错了!!}elseif(n <= writableSpace){//说明Buffer空间足够存了
        writerIndex_ += n;//}else{//Buffer空间不够存,需要把溢出的部分(extrabuf)倒到Buffer中(会先触发扩容机制)
        writerIndex_ = buffer_.size();append(extrabuf, n-writableSpace);}return n;}

客户端发来数据,readFd从TCP缓冲区当中把数据读取出来并写入到Buffer缓冲区当中。在这里对这个readv函数说明一下:

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
使用read()将数据读到不连续的内存、使用write将不连续的内存发送出去,需要多次调用read和write
如果要从文件中读一片连续的数据到进程的不同区域,有两种方案,要么使用read一次将它们读到
一个较大的缓冲区中,然后将他们分成若干部分复制到不同的区域,要么调用read若干次分批把他们读至
不同的区域,这样,如果想将程序中不同区域的连续数据块写到文件,也必须进行类似的处理。
频繁系统调用和拷贝开销比较大,所以Unix提供了另外两个函数readv()和writev(),它们只需要
一次系统调用就可以实现在文件和进程的多个缓冲区之间传送数据,免除多次系统调用或复制数据的开销。
readv叫散布读,即把若干连续的数据块读入内存分散的缓冲区中,
writev叫聚集写,吧内存中分散的若干缓冲区写到文件的连续区域中
从fd当中读取数据 Poll工作再LT模式下 buffer缓冲区的大小从fd当中读取数据是不知道tcp的数据包的大小
readv可以自动的填充缓冲区的数据,缓冲区不一定要连续

下面我们来讨论一下readFd这个函数。用户定义的缓冲区大小是有限的,我们一开始不知道TCP缓冲区里面的数据量有多少。如果我们一次性读取会不会导致我们定义的缓冲区Buffer溢出了还是装不下了。这里我感觉是非常精华的地方,在readFd的时候在栈上创建了一个缓冲区extrabuf,利用readv分散读特性将TCP缓冲区的数据拷贝到Buffer当中,如果此时Buffer的容量不够那么会将剩下的数据拷贝到这个extrabuf当中由于是在栈上创建的当这个函数结束之后这个缓冲区也被释放了,还有就是在栈上开辟的空间的速度要比在堆上开辟空间要快的多。

2.writeFd函数:

ssize_t Buffer::writeFd(int fd,int* saveErrno){//向socket fd上写数据,假如TCP发送缓冲区满const size_t readableSpace =readableBytes();
    ssize_t n =::write(fd,peek(), readableSpace);//从Buffer中有的数据(readableBytes)写到socket中if(n <0){*saveErrno = errno;}return n;}

当服务器要想TCP连接发送数据时可以通过这个方法将Buffer当中的数据拷贝到TCP缓冲区当中。

TCPConnection

在上面讲述Accepter类的时候我们提到过这个类。而现在这个类是用来封装一个已经建立连接的TCP连接,已经控制该连接的方法(包括连接建立、关闭和销毁)已经改连接发生的各种事件(读/写/错误)对应的处理函数还有这个连接对应TCP服务端和客户端套接字地址信息等。有一个连接到来mainLoop都会将其打包成为一个TCPConnection。Accepter类和TcpConnection更像是兄弟关系,Accepter用于mainLoop中对服务器监听套接字fd和相关方法进行封装然后分配给SubLoop.而这个TcpConnection用于已建立的连接将连接套接字的封装起来。
下面我们来看一下TcpConnection的声明

classTcpConnection:public muduo::noncopyable,public std::enable_shared_from_this<TcpConnection>{/**
     * @brief TcpConnection主要用于打包成功连接的TCP连接通信链路。
     * TcpServer => Acceptor => 新用户连接,通过accept函数拿到connfd =》 TcpConnection设置回调 => Channel => Poller => Poller监听到事件 => Channel的回调操作
     */public:TcpConnection(EventLoop* loop,const std::string &name,int sockfd,const InetAddress &localAddr,const InetAddress &peerAddr);~TcpConnection();
    EventLoop*getLoop()const{return loop_;}const InetAddress&localAddress()const{return localAddr_;}const InetAddress&peerAddress()const{return peerAddr_;}boolconnected()const{return state_ == kConnected;}voidsend(const std::string &buf);voidshutdown();const std::string&name()const{return name_;}voidsetConnectionCallback(const ConnectionCallback& cb){connectionCallback_ = cb;}voidsetMessageCallback(const MessageCallback& cb){messageCallback_ = cb;}voidsetWriteCompleteCallback(const WriteCompleteCallback& cb){writeCompleteCallback_ = cb;}voidsetCloseCallback(const CloseCallback& cb){closeCallback_ = cb;}voidsetHighWaterMarkCallback(const HighWaterMarkCallback& cb, size_t highWaterMark){highWaterMarkCallback_ = cb; highWaterMark_ = highWaterMark;}// 连接建立voidconnectEstablished();// 连接销毁voidconnectDestroyed();private:enumStateE{kDisconnected, kConnecting, kConnected, kDisconnecting};voidsetState(StateE state){state_ = state;}voidhandleRead(TimeStamp receiveTime);voidhandleWrite();voidhandleClose();voidhandleError();voidsendInLoop(constvoid* message, size_t len);voidshutdownInLoop();

    EventLoop *loop_;//这里绝对不是baseLoop,因为TcpConnection都是在subLoop里面管理的const std::string name_;
    std::atomic<int> state_;bool reading_;

    std::unique_ptr<Socket> socket_;
    std::unique_ptr<Channel> channel_;const InetAddress localAddr_;//当前主机的ip和端口号const InetAddress peerAddr_;//远端地址
    
    ConnectionCallback connectionCallback_;//有新连接时的回调
    MessageCallback messageCallback_;//有读写消息时的回调
    WriteCompleteCallback writeCompleteCallback_;//消息发送完后的回调
    HighWaterMarkCallback highWaterMarkCallback_;//发送和接收缓冲区超过水位线的话容易导致溢出,这是很危险的。
    CloseCallback closeCallback_;
    size_t highWaterMark_;//水位线
    Buffer inputBuffer_;//接收的缓冲区大小,我猜的
    Buffer outputBuffer_;//发送的缓冲区大小,我猜的};

TcpConnection对象当中重要的成员方法:

  • socket_:用来保存已连接套接字文件描述符
  • channel_:用来封装已经建立连接的文件描述符已经各种事件发生时对应的回调函数(其实就是在TcpConnection的构造函数当中设置的)
  • loop_:用来记录该这个TCP连接是属于那个EventLoop的,因为这个TCPConection对象后面会分配给SubLoop
  • inputBufer_:缓冲区代表的是接收用户发送过来的数据
  • outputBuffer:同样的这也是一个缓冲区不过这个缓冲区是用来保存那些暂时发送不出去的数据,TCP的发送缓冲区也是有大小限制的,如果此时无法将数据一次性拷贝到TCP缓冲区当中,那么剩余的数据可以暂时保存在我们自己定义的缓冲区当中并将给文件描述对应的写事件注册到对应的Poller当中,等到写事件就绪了,在调用回调方法将剩余的数据发送给客户端。
  • connectionCallback_、messageCallback、writerCallback_、closeCallback_:用户自定义的连接建立关闭之后的回调函数,收到消息的回调函数、消息发送完毕的回调函数、以及连接关闭之后调用的回调函数。

1.下面我们来看看TcpConnection类当中重要的成员方法:handleRead,handleWrite,handleClose,handleError。下面我们一个一个的来看 :

  • handleRead函数主要是用于当已经建立的连接,对方发送数据给你了此时这个文件描述符的读事件就绪调用的回调方法就是这个。将用户发过来的数据拷贝到接收缓冲区当中然后再调用用户自定义的消息到来时的回调函数
  • handleWrite:负责处理Tcp连接的可写事件,这个函数有点复杂后面我们再继续讨论
  • handleClose:主要负责Tcp连接关闭事件大致的过程就是把TcpConnection对象当中channel从事件监听器当中移除掉并调用connectionCallback_(这个我不太明白为啥调这个)和closeCallback_

下面我们来梳理一下连接建立的过程:
首先我们需要定义一个TcpServer对象只执行代码TcpServer(&loop,listenAddr);调用其构造函数
而TcpServer当中最重要的是初始化了一个Accepter对象并且往这个类当中注册了一个newConnection的回调。当我Accpter对象构建完毕之后,监听套接字就封装再这个Accpter对象当中此时它并没有注册到mainLoop当中的事件监听器当中,Accpter在构造函数当中将handleRead方法注册到了对应的Channel当中这就意味着以后这个Channel如果有读事件发生将会调用到handleRead方法。TcpServer对象构建完毕之后用户调用start方法开启TcpServer.

voidTcpServer::start()//开启服务器监听{if(started_++==0)//防止一个TcpServer对象被start多次{
        threadPool_->start(threadInitCallback_);//启动底层的loop线程池
        loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));//让这个EventLoop,也就是mainloop来执行Acceptor的listen函数,开启服务端监听}}voidAcceptor::listen(){/**
     * @brief 开启对server socket fd的可读事件监听
     * 
     */
    listenning_ =true;
    acceptSocket_.listen();
    acceptChannel_.enableReading();}

这里其实就是调用了Accepter类当中的listen方法并将acceptChannel注册到mainEventLoop的事件监听器当中监听是否有连接到来。然后用户调用loop.loop()方法开始事件循环获取事件对应的监听结果。如果此时有连接到来此时一定是读事件那么就会调用到accpterChannel_之前注册好的读回调方法
该方法其实就是调用TcpServer当中的newConnection方法。该方法做的事件主要就是把这个连接打包成为一个TcpConnection对象采用轮询的方式选择一个SubLoop把这个连接交给它来处理

2.下面我们再来看看消息的读取逻辑

MainLoop接受新的连接请求后将这条TCP连接之后将其打包成为一个TCPconnectoion对象,然后公平的选择一个SubLoop将这个连接分配给他并将其注册到对应的事件监听器当中。此时subLoop就监听这个文件描述上是否有事件发生如果有事件发生此时回调用事先注册好的回调方法也就是TcpConnection当中的handleRead方法,消息的处理逻辑也就是这个,下面我们一起来看看这个函数吧

voidTcpConnection::handleRead(TimeStamp receiveTime){int savedErrno =0;//用来看是否再读取时候出错了
    ssize_t n = inputBuffer_.readFd(channel_->fd(),&savedErrno);//这里的channel的fd也一定仅有socket fdif(n >0)//从fd读到了数据,并且放在了inputBuffer_上{//已建立连接的用户,有可读事件发生了,调用用户传入的回调操作onMessage//这个shared_from_this()就是TcpConnection对象的智能指针messageCallback_(shared_from_this(),&inputBuffer_, receiveTime);}elseif(n ==0)//对方关闭handleClose();else{
        errno = savedErrno;LOG_ERROR("TcpConnection::handleRead");handleError();//处理错误}}

这个方法首先回调用缓冲区当中的readFd方法其底层其实调用的是readv函数将数据读取上来并且将其写入到缓冲区当中,如果这个过程当中出现了什么错误错误码会保存到errno当中。如果读取到了数据此时会调用用户自定义的读取消息后的处理函数。如果此时读取到了0个字节那么说明对方已经把连接给关闭了此时直接调用handlclose即可。

3.消息的发送逻辑
当用户调用TcpConnection当中的send方法之后,muduo库会将数据发送给客户端此时事件监听器上是没有写事件的,其底层就是调用write函数将数据写入到TCP发送缓冲区当中但是这就有一个问题
TCP缓冲区能不能容纳得了这么多的数据了。如果此时TCP缓冲区不能容纳得了这么多数据那么

  • 首先他会将这些数据保存到用户层定义的发送缓冲区当中并且向事件监听器注册写事件
  • 当事件监听器检测到写事件就绪就会调用TcpConnection对象当中的handlwrite将剩余的数据写入到TCP的发送缓冲区当中
  • 当我们调用handlwrite时如果还是不能完全写入到TCP的发送缓冲区当中,此时尽可能的将数据写入到发送缓冲区当中依然保持事件
  • 如果此时我们一次性将数据写入到缓冲区当中,此时需要将写事件从事件监听器当中删除这样做的目的是为了不让epoll_wait一直通知我们写事件就绪,在这里是毫无意义的。

4.连接断开
连接断开分为几种情况下面我们一个一个的来看

TCPConnection当中的handleRead函数如果调用了readv函数发现返回值是0此时说明对方把连接给关闭了此时我们需要调用handlclose方法而handlclose方法其实就是调用TCPserver当中的removeConnection最后还是调用到了TCPConnection当中的connectionDestroy方法
在这里插入图片描述

  • 在执行TcpConnection::handle_Close()的时候,该函数还是在SubEventLoop线程中运行的,接着调用closeCallback_(connPtr)回调函数,该函数保存的其实是TcpServer::removeConnection( )函数 TcpServer::removeConnection( )函数调用了remvoveConnectionInLoop( )函数,该函数的运行是在MainEventLoop线程中执行的,这里涉及到线程切换技术,后面再讲。
  • removeConnectionInLoop( )函数:TcpServer对象中有一个connections_成员变量,这是一个unordered_map,负责保存【string --> TcpConnection】的映射,其实就是保存着Tcp连接的名字到TcpConnection对象的映射。因为这个Tcp连接要关闭了,所以也要把这个TcpConnection对象从connections_中删掉。然后再调用TcpConnection::connectDestroyed函数。 另外为什么removeConnectionInLoop()要在MainEventLoop中运行,因为该函数主要是从TcpServer对象中删除某条数据。而TcpServer对象是属于MainEventLoop的。这也是贯彻了One Loop Per Thread的理念。
  • TcpConnection::connectDestroyed( )函数的执行是又跳回到了subEventLoop线程中。该函数就是将Tcp连接的监听描述符从事件监听器中移除。另外SubEventLoop中的Poller类对象还保存着这条Tcp连接的channel_,所以调用channel_.remove( )将这个Tcp连接的channel对象从Poller内的数据结构中删除。

如果此时是服务器主动断开连接
当服务器主动断开连接,调用TcpServer::~TcpServer()析构函数 。在创建TcpConnection对象时,Acceptor都要将这个对象分发给一个SubEventLoop来管理。这个TcpConnection对象的一切函数执行都要在其管理的SubEventLoop线程中运行。再一次贯彻One Loop Per Thread的设计模式。比如要想彻底删除一个TcpConnection对象,就必须要调用这个对象的connecDestroyed()方法,这个方法执行完后才能释放这个对象的堆内存。每个TcpConnection对象的connectDestroyed()方法都必须在这个TcpConnection对象所属的SubEventLoop绑定的线程中执行

在这里插入图片描述
所有上面的TcpServer::TcpServer()函数就是干这事儿的,不断循环的让这个TcpConnection对象所属的SubEventLoop线程执行TcpConnection::connectDestroyed()函数,同时在MainEventLoop的TcpServer::TcpServer()函数中调用item.second.reset()释放保管TcpConnection对象的共享智能指针,以达到释放TcpConnection对象的堆内存空间的目的。
 但是这里面其实有一个问题需要解决,TcpConnection::connectDestroyed()函数的执行以及这个TcpConnection对象的堆内存释放操作不在同一个线程中运行,所以要考虑怎么保证一个TcpConnectino对象的堆内存释放操作是在TcpConnection::connectDestroyed()调用完后。
 这个析构函数巧妙利用了共享智能指针的特点,当没有共享智能指针指向这个TcpConnection对象时(引用计数为0),这个TcpConnection对象就会被析构删除(堆内存释放)。

非常值得学习的编程思想

首先我们来看看这段代码:

voidTcpConnection::connectEstablished(){setState(kConnected);//channel的回调都是TCPConnection设置的有可能由于一些原因TCPConnection对象没有了那么这些回调是否需要执行
    channel_->tie(shared_from_this());//防止出现错误//tie(const std::shared_ptr<void> &obj);//shared_from_this()返回的是shared_ptr<TcpConnection>,这能被tie函数接受?//这个shared_ptr<void>实际上底层就是一个void*指针,而shared_ptr<TcpConnection>//实际上是一个TcpConnection*指针//Channel类里面有一个weak_ptr会指向这个传进来的shared_ptr<TcpConnection>//如果这个TcpConnection已经被释放了,那么Channel类中的weak_ptr就没办法在//Channel::handleEvent 就没法提升weak_ptr。/**
     * @brief 我倒是觉得这个思想很不错,当你的TcpConnection对象以shared_ptr的形式
     * 进入到Channel中被绑定,然后Channel类通过调用handleEvent把Channel类中的
     * weak_ptr提升为shared_ptr共同管理这个TcpConnection对象,这样的话,即使
     * 外面的这个TcpConnection智能指针被释放析勾删除,Channel类里面还有一个智能指针
     * 指向这个对象,这个TcpConnection对象也不会被释放。因为引用计数没有变为0.
     * 这个思想超级好,防止你里面干得好好的,外边却突然给你釜底抽薪
     * 
     */
    channel_->enableReading();//向poller注册channel的epollin事件//新连接建立,执行回调connectionCallback_(shared_from_this());}

这个shared_from_this()可以返回一个指向当前类的shared_ptr对象,TcpConnection对象继承之后才能使用。下面我们再看看遗留的一个问题再Channel当当中

voidChannel::tie(const shared_ptr<void>& obj){/**
     * @brief channel的tie方法什么时候调用?一个TcpConnection新连接创建的时候,
     * TcpConnection => Channel;
     */
    tie_ = obj;
    tied_ =true;}voidChannel::HandlerEvent(TimeStamp receiveTime){if(tied_){
        shared_ptr<void> guard = tie_.lock();//进行提升防止这个TCPConnection失效if(guard)HandleEventWithGuard(receiveTime);}elseHandleEventWithGuard(receiveTime);}

当事件监听器返回监听结果,就要对每一个发生事件的channel对象调用他们的HandlerEvent()函数。在这个HandlerEvent函数中,会先把tie_这个weak_ptr提升为强共享智能指针。这个强共享智能指针会指向当前的TcpConnection对象。就算你外面调用删除析构了其他所有的指向该TcpConnection对象的智能指针。你只要HandleEventWithGuard()函数没执行完,你这个TcpConnetion对象都不会被析构释放堆内存。而HandleEventWithGuard()函数里面就有负责处理消息发送事件的逻辑。当HandleEventWithGuard()函数调用完毕,这个guard智能指针就会被释放。

标签: 网络 tcp/ip 服务器

本文转载自: https://blog.csdn.net/qq_56999918/article/details/127046645
版权归原作者 一个山里的少年 所有, 如有侵权,请联系我们删除。

“国庆弯道超车(手写Muduo库剖析核心代码及编程思想)”的评论:

还没有评论