muduo库

前言

这个假期跟着教学视频手写了muduo库,该muduo库是经过改造,不依赖boost库,用C++11重构。

手写muduo库的过程虽然有些枯燥,但学会的东西确实也很多。因为本人基础较差,因此代码几乎全是照抄老师,若涉及侵权请提醒在下删除。

下面附上代码,以及我在写完全部代码后对整个muduo库的调用过程的部分理解,感兴趣的朋友可以看看。

muduo网络库核心代码模块

Channel

其内部成员主要有fd,events(fd感兴趣的事件),loop(用以管理channel和poller,也是channel调用poller的桥梁),以及部分回调函数

Poller 和EPollPoller

其主要作用其实就是epoll_wait,epoll_ctl等作用

EventLoop

事件循环

Socket

获取套接字,设置其配置信息

Acceptor

主要封装了listenfd相关的操作 socket bind listen baseLoop

Buffer

缓冲区

TcpConnection

客户端连接后对应一个TcpConnection

TcpServer

代码

Acceptor
Buffer
Callbacks
Channel
CurrentThread
DefaultPoller
EPollPoller
EventLoop
EventLoopThread
EventLoopThreadPool
InetAddress
Logger
noncopyable
Poller
Socket
TcpConnection
TcpServer
testserver测试代码
Thread
Timestamp

个人理解

1.mainloop初始化的过程,以及设置新连接回调函数的过程

在TcpServer的构造函数中,将TcpServer::newConnection绑定给了Acceptor::newConnectionCallback,(该函数实际是打包chaneel,然后轮询,找subloop)。

在Acceptor的构造函数中,将Acceptor::handleRead绑定给acceptChannel::readCallback

( Acceptor::handleRead的作用就是调用Acceptor::newConnectionCallback,也就是TcpServer::newConnection)。

意思就是调用readCallback就是调用TcpServer::newConnection

重点来了,acceptChannel::readCallback什么时候调用?

答案:在调用Channel::handleEventWithGuard 时,而调用Channel::handleEvent调用Channel::handleEventWithGuard。

那么什么时候调用了Channel::handleEvent呢?在EventLoop中。

我们知道在调用muduo库时,创建了一个loop_,并且最后loop_.loop()开启事件循环。这个loop_就是mainloop的这个loop。查看EventLoop::loop下的代码时发现

while(!quit_)
    {
        activeChannels_.clear();
        pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
        for (Channel *channel : activeChannels_)
        {
            channel->handleEvent(pollReturnTime_);
        }
        doPendingFunctors();
    }

channel->handleEvent(pollReturnTime_); 这句代码调用了Channel::handleEven。

再仔细看,for (Channel *channel : activeChannels_)意思是从activeChannel里面来取Channel来调用该Channel的Channel::handleEven。而 activeChannels_ 由谁来设置呢?

别急,我们再看到上面那行代码 pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);

EventPoller::poll里面实现的是epoll_wait

当其检测到有客户端到来时就会解除阻塞,然后它就会调用EventPoller::fillActiveChannels函数

void EPollPoller::fillActiveChannels(int numEvents, ChannelList *activeChannels) const
{
    for (int i=0; i < numEvents; ++i)
    {
        Channel *channel = static_cast<Channel*>(events_[i].data.ptr);
        channel->set_revents(events_[i].events);
        activeChannels->push_back(channel); // EventLoop就拿到了它的poller给它返回的所有发生事件的channel列表了
    }
}

这样就完全解释通了。

注意:loop_.loop事件循环,是一直在循环,直到程序退出,我们可以看到

while(!quit_)

同时解释一下,testserver代码中的

int main()
{
    EventLoop loop;
    InetAddress addr(8000);
    EchoServer server(&loop, addr, "EchoServer-01"); // Acceptor non-blocking listenfd  create bind 
    server.start(); // listen  loopthread  listenfd => acceptChannel => mainLoop =>
    loop.loop(); // 启动mainLoop的底层Poller

    return 0;
}

前面都是在设置回调函数,初始化等,直到loop.loop();才是开启了epoll_wait 。

注意:TcpServer的start,里面调用的loop_->runInLoop,绑定的是Acceptor::listen

其作用是调用Socket::listen函数 (该函数作用即listen)

还有使mainloop的acceptChannel_对读事件(连接到来就是读事件)感兴趣

void Acceptor::listen()
{
    listenning_ = true;
    acceptSocket_.listen(); // listen
    acceptChannel_.enableReading(); // acceptChannel_ => Poller
}

那些绑定端口啥的初始化操作都在acceptor中实现。

2.以上是启动mainloop的过程,现在来看subloop的构造过程

在testserver中,server.start() 调用 TcpServer::start()函数,该函数内部有一行代码

threadPool_->start(threadInitCallback_);

这个threadPoll_是由Tcpserver的构造函数数中,构造的一个EventLoopThreadPoll ,

构造过程平平无奇,主要看 其start函数

void EventLoopThreadPool::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 = new EventLoopThread(cb, buf);
        threads_.push_back(std::unique_ptr<EventLoopThread>(t));
        loops_.push_back(t->startLoop()); // 底层创建线程,绑定一个新的EventLoop,并返回该loop的地址
    }

    // 整个服务端只有一个线程,运行着baseloop
    if (numThreads_ == 0 && cb)
    {
        cb(baseLoop_);
    }
}

显然,会在for循环中开辟多个子线程,我们来看里面这行代码

EventLoopThread *t = new EventLoopThread(cb, buf);

查看EventLoopThread 的构造函数

EventLoopThread::EventLoopThread(const ThreadInitCallback &cb, 
        const std::string &name)
        : loop_(nullptr)
        , exiting_(false)
        , thread_(std::bind(&EventLoopThread::threadFunc, this), name)
        , mutex_()
        , cond_()
        , callback_(cb)
{
}

注意到

thread_(std::bind(&EventLoopThread::threadFunc, this), name)

其最终将使EventLoopThread::threadFunc绑定给Thread::thread_

EventLoopThread::startLoop函数会调用 thread_.start(),thread.start内部会调用到threadFunc函数,最终会返回一个新线程loop,而EventLoopThread::startLoop则由EventLoopThreadPool::start里面的

loops_.push_back(t->startLoop());

来调用,调用完,vectorloops_将保存了一个loop,经过for循环,也就得到了多个线程loop,

有一个问题我们需要注意的是,在上面说的开启子线程的过程中

EventLoopThread::threadFunc内部有一句代码

loop.loop()

也就是说,我们保存在loops_里面的几个子线程,已经全部开启了loop(),也就是说已经全部阻塞在了各自loop下的EPollPoller::poll中的epoll_wait里面了。

自此,已经全部开启子线程subloop,并且已经和mainloop联系上了。后面就是新连接到来,mainloop打包新连接成channel,唤醒subloop然后塞给subloop。

3.wakeupfd是如何使用以及唤醒subloop是一个什么样的过程?

当有新连接到来,mainloop打包后塞入subloop,中间的过程涉及到wakefd。那么该过程为什么要唤醒subloop呢?

我们知道在测试代码中,对mainloop开启loop时,整个服务才会正式开启,mainloop才会阻塞在其poll下的epoll_wait

但是在TcpServer开启start时,就会创建subloop,全部subloop都会立刻阻塞在各自的epoll_wait了

这时,即使我们打包好了TcpConnection,也无法塞给subloop。只能先唤醒subloop。

还有一点值得说明的是:一开始我根本无法理解,唤醒subloop后,把TcpConnection塞给subloop究竟是一个什么样的过程,代码并没有一目了然得,比如说弄了个接口将TcpConnection放到subloop的某个变量里面。其中稍微有点绕。

现在来说明这个过程:

前面我们提到了,只要有新连接到来,mainloop就会调用到newConnection这个回调函数。这个回调函数的前半部分是在获取ioloop,也就是subloop,还有处理新连接,获取新连接的一些基本信息还有命名等,这些都是比较容易理解的。后半部分的代码才是涉及将TcpConnection塞给subloop的过程,其代码是这样的:

// 根据连接成功的sockfd,创建TcpConnection连接对象
    TcpConnectionPtr conn(new TcpConnection(
                            ioLoop,
                            connName,
                            sockfd,   // Socket Channel
                            localAddr,
                            peerAddr));
    connections_[connName] = conn;
    // 下面的回调都是用户设置给TcpServer=>TcpConnection=>Channel=>Poller=>notify channel调用回调
    conn->setConnectionCallback(connectionCallback_);
    conn->setMessageCallback(messageCallback_);
    conn->setWriteCompleteCallback(writeCompleteCallback_);

    // 设置了如何关闭连接的回调   conn->shutDown()
    conn->setCloseCallback(
        std::bind(&TcpServer::removeConnection, this, std::placeholders::_1)
    );

    // 直接调用TcpConnection::connectEstablished
    ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));

重点关注这行

ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));

PS:ioloop就是要送往的subloop。

TcpConnection::connectEstablished函数的具体实现为

void TcpConnection::connectEstablished()
{
    setState(kConnected);
    channel_->tie(shared_from_this());
    channel_->enableReading(); // 向poller注册channel的epollin事件

    // 新连接建立,执行回调
    connectionCallback_(shared_from_this());
}

意思就是说,将新连接最后注册到poller中,这步操作是整个注册过程的最后一步。如何才能调用到这个函数呢?

runInLoop就是判断一下该loop是否在当前线程,运行这行代码

ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));

的线程当然是mainloop的线程,ioloop有它自己的线程,所以肯定不是在当前线程。因此wakeup()唤醒subloop线程,其实这个唤醒的意思就是解除阻塞而已,因为它阻塞在epoll_wait。我们理所当然可以猜想到,当epoll_wait解除阻塞后,其后续代码,应该会有一个过程,能调用到TcpConnection::connectEstablished函数来将新连接加入自己poller上面。

这个过程就是runInLoop->queueInLoop->wakeup

这时注意:queueInLoop在调用wakeup()之前里面有这样一个操作

{
        std::unique_lock lock(mutex_);
        pendingFunctors_.emplace_back(cb);
    }

将回调函数cb放入vector pendingFunctors中。这个cb其实就是&TcpConnection::connectEstablished。

巧了,在EventLoop::loop里就有个跟pendingFunctors有关系的代码

while(!quit_)
    {
        activeChannels_.clear();
        // 监听两类fd   一种是client的fd,一种wakeupfd
        pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
        for (Channel *channel : activeChannels_)
        {
            // Poller监听哪些channel发生事件了,然后上报给EventLoop,通知channel处理相应的事件
            channel->handleEvent(pollReturnTime_);
        }
        // 执行当前EventLoop事件循环需要处理的回调操作
        /**
         * IO线程 mainLoop accept fd《=channel subloop
         * mainLoop 事先注册一个回调cb(需要subloop来执行)    wakeup subloop后,执行下面的方法,执行之前mainloop注册的cb操作
         */ 
        doPendingFunctors();
    }

doPendingFunctors()

之前我们说了,wakeup就是解除epoll_wait的阻塞,解除后 loop里面就能运行到doPendingFunctors()了,那么也就可以调用TcpConnection::connectEstablished了。

TcpConnection::connectEstablished中的

channel_->enableReading(); // 向poller注册channel的epollin事件

channel_->enableReading ->updateChannel->poller->updataChannel更新poller感兴趣的事件

自此,连接成功建立。

4.用户所设置的message函数(即客户端读事件函数)是怎么被设置绑定和最终调用的

在testserver中,用户自己定义了onMessage函数,即客户端发数据时会调用这个函数。

在testserver中绑定给了Tcpserver的messagecallback,上面说过,当新连接到来时,最终会调用到TcpServer::newConnection,其内部创建打包TcpConnection,有一段代码为

TcpConnectionPtr conn(new TcpConnection(
                            ioLoop,
                            connName,
                            sockfd,   // Socket Channel
                            localAddr,
                            peerAddr));
    connections_[connName] = conn;
    // 下面的回调都是用户设置给TcpServer=>TcpConnection=>Channel=>Poller=>notify channel调用回调
    conn->setConnectionCallback(connectionCallback_);
    conn->setMessageCallback(messageCallback_);
    conn->setWriteCompleteCallback(writeCompleteCallback_);

    // 设置了如何关闭连接的回调   conn->shutDown()
    conn->setCloseCallback(
        std::bind(&TcpServer::removeConnection, this, std::placeholders::_1)
    );
conn->setMessageCallback(messageCallback_);

将TcpServer::messagecallback,(也即客户定义的onmessage函数) 设置给了Tcpconnection的 messagecallback。

现在看TcpConnection,注意到构造函数中,有这样一行代码

channel_->setReadCallback(
        std::bind(&TcpConnection::handleRead, this, std::placeholders::_1)
    );

设置了handleRead给channenl的readcallback,和1中讲解mainloop处理连接一样,当mainloop唤醒subloop后最终就会调用到channle::readcallback。

现在我们自然而然可以想到,TcpConnection::handleRead内部肯定调用到TcpServer::messagecallback,

看代码

void TcpConnection::handleRead(Timestamp receiveTime)
{
    int savedErrno = 0;
    ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
    if (n > 0)
    {
        // 已建立连接的用户,有可读事件发生了,调用用户传入的回调操作onMessage
        messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
    }
    else if (n == 0)
    {
        handleClose();
    }
    else
    {
        errno = savedErrno;
        LOG_ERROR("TcpConnection::handleRead");
        handleError();
    }
}

你可能感兴趣的:(笔记,c++,后端)