muduo原始碼閱讀

工程師WWW發表於2016-04-07

最近簡單讀了下muduo的原始碼,本文對其主要實現/結構簡單總結下。

muduo的主要原始碼位於net資料夾下,base資料夾是一些基礎程式碼,不影響理解網路部分的實現。muduo主要類包括:

  • EventLoop
  • Channel
  • Poller
  • TcpConnection
  • TcpClient
  • TcpServer
  • Connector
  • Acceptor
  • EventLoopThread
  • EventLoopThreadPool

其中,Poller(及其實現類)包裝了Poll/EPoll,封裝了OS針對裝置(fd)的操作;Channel是裝置fd的包裝,在muduo中主要包裝socket;TcpConnection抽象一個TCP連線,無論是客戶端還是伺服器只要建立了網路連線就會使用TcpConnection;TcpClient/TcpServer分別抽象TCP客戶端和伺服器;Connector/Acceptor分別包裝TCP客戶端和伺服器的建立連線/接受連線;EventLoop是一個主控類,是一個事件發生器,它驅動Poller產生/發現事件,然後將事件派發到Channel處理;EventLoopThread是一個帶有EventLoop的執行緒;EventLoopThreadPool自然是一個EventLoopThread的資源池,維護一堆EventLoopThread。

閱讀庫原始碼時可以從庫的介面層著手,看看關鍵功能是如何實現的。對於muduo而言,可以從TcpServer/TcpClient/EventLoop/TcpConnection這幾個類著手。接下來看看主要功能的實現:

建立連線

    TcpClient::connect 
        -> Connector::start 
            -> EventLoop::runInLoop(Connector::startInLoop...
            -> Connector::connect             

EventLoop::runInLoop介面用於在this所在的執行緒執行某個函式,這個後面看下EventLoop的實現就可以瞭解。 網路連線的最終建立是在Connector::connect中實現,建立連線之後會建立一個Channel來代表這個socket,並且繫結事件監聽介面。最後最重要的是,呼叫Channel::enableWritingChannel有一系列的enableXX介面,這些介面用於標識自己關心某IO事件。後面會看到他們的實現。

Connector監聽的主要事件無非就是連線已建立,用它監聽讀資料/寫資料事件也不符合設計。TcpConnection才是做這種事的。

客戶端收發資料

當Connector發現連線真正建立好後,會回撥到TcpClient::newConnection,在TcpClient建構函式中:

    connector_->setNewConnectionCallback(
      boost::bind(&TcpClient::newConnection, this, _1));

TcpClient::newConnection中建立一個TcpConnection來代表這個連線:

    TcpConnectionPtr conn(new TcpConnection(loop_,
                                            connName,
                                            sockfd,
                                            localAddr,
                                            peerAddr));

    conn->setConnectionCallback(connectionCallback_);
    conn->setMessageCallback(messageCallback_);
    conn->setWriteCompleteCallback(writeCompleteCallback_);
    ...
    conn->connectEstablished();

並同時設定事件回撥,以上設定的回撥都是應用層(即庫的使用者)的介面。每一個TcpConnection都有一個Channel,畢竟每一個網路連線都對應了一個socket fd。在TcpConnection建構函式中建立了一個Channel,並設定事件回撥函式。

TcpConnection::connectEstablished函式最主要的是通知Channel自己開始關心IO讀取事件:

    void TcpConnection::connectEstablished()
    {
        ...
        channel_->enableReading();

這是自此我們看到的第二個Channel::enableXXX介面,這些介面是如何實現關心IO事件的呢?這個後面講到。

muduo的資料傳送都是通過TcpConnection::send完成,這個就是一般網路庫中在不使用OS的非同步IO情況下的實現:快取應用層傳遞過來的資料,在IO裝置可寫的情況下儘量寫入資料。這個主要實現在TcpConnection::sendInLoop中。

    TcpConnection::sendInLoop(....) {
        ...
        // if no thing in output queue, try writing directly
        if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)  // 裝置可寫且沒有快取時立即寫入
        { 
            nwrote = sockets::write(channel_->fd(), data, len);
        }
        ...
        // 否則加入資料到快取,等待IO可寫時再寫
        outputBuffer_.append(static_cast<const char*>(data)+nwrote, remaining);
        if (!channel_->isWriting())
        {
            // 註冊關心IO寫事件,Poller就會對寫做檢測
            channel_->enableWriting();
        }
        ...     
    }

當IO可寫時,Channel就會回撥TcpConnection::handleWrite(建構函式中註冊)

    void TcpConnection::handleWrite()
    {
        ...
        if (channel_->isWriting())
        {
            ssize_t n = sockets::write(channel_->fd(),
                               outputBuffer_.peek(),
                               outputBuffer_.readableBytes());

伺服器端的資料收發同客戶端機制一致,不同的是連線(TcpConnection)的建立方式不同。

伺服器接收連線

伺服器接收連線的實現在一個網路庫中比較重要。muduo中通過Acceptor類來接收連線。在TcpClient中,其Connector通過一個關心Channel可寫的事件來通過連線已建立;在Acceptor中則是通過一個Channel可讀的事件來表示有新的連線到來:

    Acceptor::Acceptor(....) {
        ...
        acceptChannel_.setReadCallback(
            boost::bind(&Acceptor::handleRead, this));
        ... 
    }

    void Acceptor::handleRead()
    {
        ...
        int connfd = acceptSocket_.accept(&peerAddr); // 接收連線獲得一個新的socket
        if (connfd >= 0)
        {
            ...
            newConnectionCallback_(connfd, peerAddr); // 回撥到TcpServer::newConnection

TcpServer::newConnection中建立一個TcpConnection,並將其附加到一個EventLoopThread中,簡單來說就是給其配置一個執行緒:

    void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
    {
        ...
        EventLoop* ioLoop = threadPool_->getNextLoop();
        TcpConnectionPtr conn(new TcpConnection(ioLoop,
                                                connName,
                                                sockfd,
                                                localAddr,
                                                peerAddr));
        connections_[connName] = conn;
        ...
        ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));

IO的驅動

之前提到,一旦要關心某IO事件了,就呼叫Channel::enableXXX,這個如何實現的呢?

    class Channel {
        ...
        void enableReading() { events_ |= kReadEvent; update(); }
        void enableWriting() { events_ |= kWriteEvent; update(); }
       
    void Channel::update()
    {
        loop_->updateChannel(this);
    }

    void EventLoop::updateChannel(Channel* channel)
    {
        ...
        poller_->updateChannel(channel);
    }

最終呼叫到Poller::upateChannel。muduo中有兩個Poller的實現,分別是Poll和EPoll,可以選擇簡單的Poll來看:

    void PollPoller::updateChannel(Channel* channel)
    {
      ...
      if (channel->index() < 0)
      {
        // a new one, add to pollfds_
        assert(channels_.find(channel->fd()) == channels_.end());
        struct pollfd pfd;
        pfd.fd = channel->fd();
        pfd.events = static_cast<short>(channel->events()); // 也就是Channel::enableXXX操作的那個events_
        pfd.revents = 0;
        pollfds_.push_back(pfd); // 加入一個新的pollfd
        int idx = static_cast<int>(pollfds_.size())-1;
        channel->set_index(idx);
        channels_[pfd.fd] = channel;

可見Poller就是把Channel關心的IO事件轉換為OS提供的IO模型資料結構上。通過檢視關鍵的pollfds_的使用,可以發現其主要是在Poller::poll介面裡。這個介面會在EventLoop的主迴圈中不斷呼叫:

    void EventLoop::loop()
    {
      ...
      while (!quit_)
      {
        activeChannels_.clear();
        pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
        ...
        for (ChannelList::iterator it = activeChannels_.begin();
            it != activeChannels_.end(); ++it)
        {
          currentActiveChannel_ = *it;
          currentActiveChannel_->handleEvent(pollReturnTime_); // 獲得IO事件,通知各註冊回撥
        }

整個流程可總結為:各Channel內部會把自己關心的事件告訴給Poller,Poller由EventLoop驅動檢測IO,然後返回哪些Channel發生了事件,EventLoop再驅動這些Channel呼叫各註冊回撥。

從這個過程中可以看出,EventLoop就是一個事件產生器。

執行緒模型

在muduo的伺服器中,muduo的執行緒模型是怎樣的呢?它如何通過執行緒來支撐高併發呢?其實很簡單,它為每一個執行緒配置了一個EventLoop,這個執行緒同時被附加了若干個網路連線,這個EventLoop服務於這些網路連線,為這些連線收集並派發IO事件。

回到TcpServer::newConnection中:

    void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
    {
      ...
      EventLoop* ioLoop = threadPool_->getNextLoop();
      ...
      TcpConnectionPtr conn(new TcpConnection(ioLoop, // 使用這個選擇到的執行緒中的EventLoop
                                              connName,
                                              sockfd,
                                              localAddr,
                                              peerAddr));
      ...
      ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));

注意TcpConnection::connectEstablished是如何通過Channel註冊關心的IO事件到ioLoop的。

極端來說,muduo的每一個連線執行緒可以只為一個網路連線服務,這就有點類似於thread per connection模型了。

網路模型

傳說中的Reactor模式,以及one loop per thread,基於EventLoop的作用,以及執行緒池與TcpConnection的關係,可以醍醐灌頂般理解以下這張muduo的網路模型圖了:


總結

本文主要對muduo的主要結構及主要機制的實現做了描述,其他如Buffer的實現、定時器的實現大家都可以自行研究。muduo的原始碼很清晰,通過原始碼及配合陳碩部落格上的內容可以學到一些網路程式設計方面的經驗。

相關文章