muduo網路庫學習筆記(14):chargen服務示例

li27z發表於2016-11-22

chargen簡介

chargen(Character Generator Protocol)是指在TCP連線建立後,伺服器不斷傳送任意的字元到客戶端,直到客戶端關閉連線。

它生成資料的邏輯如下:

for (int i = 33; i < 127; ++i)
{
    line.push_back(char(i));
}
line += line;

for (size_t i = 0; i < 127-33; ++i)
{
    message_ += line.substr(i, 72) + '\n';
}

其輸出資料格式類似下圖,每行有72個字元,完整的一組輸出資料有95行:
這裡寫圖片描述

在介紹muduo chargen服務示例前,我們需要了解muduo::TcpConnection是如何傳送資料的。

TcpConnection傳送資料

一、程式碼的改動
之前幾篇部落格介紹了TcpConnection關於連線建立和連線斷開的處理,在此基礎上,TcpConnection的介面中新增了兩個函式send()和shutdown(),這兩個函式都可以跨執行緒呼叫。其內部實現增加了sendInLoop()和shutdownInLoop()兩個成員函式,並使用Buffer(muduo對應用層緩衝區的封裝)作為輸出緩衝區。

TcpConnection的狀態也增加到了四個:

// 連線斷開,連線中,已連線,斷開連線中
enum StateE { kDisconnected, kConnecting, kConnected, kDisconnecting };

TcpConnection介面的變化:

// send()有多種過載
void send(const void* message, size_t len);
void send(const StringPiece& message);
void shutdown(); 

......

void sendInLoop(const StringPiece& message);
void sendInLoop(const void* message, size_t len);
void shutdownInLoop();

Buffer outputBuffer_;  // 應用層傳送緩衝區

傳送資料會用到Channel的WriteCallback,且由於muduo採用Level Trigger(水平觸發),因此我們只在需要的時候才關注可寫事件,否則會造成busy loop。

Channel.h的改動如下:

void enableReading() { events_ |= kReadEvent; update(); }
void enableWriting() { events_ |= kWriteEvent; update(); }
void disableWriting() { events_ &= ~kWriteEvent; update(); }
void disableAll() { events_ = kNoneEvent; update(); }
bool isWriting() const { return events_ & kWriteEvent; }

二、原始碼分析
我們首先來看傳送資料的TcpConnection::send()函式,它是執行緒安全的,可以跨執行緒呼叫。如果在非IO執行緒中呼叫,它會把message複製一份,傳給IO執行緒中的sendInLoop()來傳送。

程式碼片段1:TcpConnection::send()
檔名:TcpConnection.cc

void TcpConnection::send(const std::string& message)
{
  if (state_ == kConnected)
  {
    if (loop_->isInLoopThread())
    {
      sendInLoop(message);
    }
    else
    {
      loop_->runInLoop(
          boost::bind(&TcpConnection::sendInLoop, this, message));
    }
  }
}

sendInLoop()會先嚐試直接傳送資料,如果一次傳送完畢就不會啟用WriteCallback;如果只傳送了部分資料,則把剩餘的資料放入outputBuffer_,並開始關注writable事件,以後在handleWrite()中傳送剩餘的資料。如果當前outputBuffer_已經有待傳送的資料,那麼就不能先嚐試傳送了,因為這會造成資料亂序。

程式碼如下:

程式碼片段2TcpConnection::sendInLoop()
檔名:TcpConnection.cc

void TcpConnection::sendInLoop(const std::string& message)
{
    loop_->assertInLoopThread();
    ssize_t nwrote = 0;

    // if no thing in output queue, try writing directly
    if(!channel_->isWriting() && outputBuffer_.readableBytes() == 0) {
        nwrote = ::write(channel_->fd(), message.data(), message.size());
        if(nwrote >= 0) {
            if(implicit_cast<size_t>(nwrote) < message.size()) {
                LOG_TRACE << "I am going to write more data.";
            }
        } else {
            nwrote = 0;
            if(errno != EWOULDBLOCK) {
                LOG_SYSERR << "TcpConnection::sendInLoop";
            }
        }
    }

    assert(nwrote >= 0);
    if(implicit_cast<size_t>(nwrote) < message.size()) {
        outputBuffer_.append(message.data()+nwrote, message.size()-nwrote);
        if(!channel_->isWriting()) {
            channel_->enableWriting();
        }
    }
}

當socket變得可寫時,Channel會呼叫TcpConnection::handleWrite(),這裡會繼續傳送outputBuffer_中的資料。一旦傳送完畢,立即停止關注可寫事件,避免busy loop。另外如果此時連線正在關閉,則呼叫shutdownInLoop(),繼續執行關閉過程。

程式碼片段3:TcpConnection::handleWrite()
檔名:TcpConnection.cc

// 核心傳送緩衝區有空間了,回撥該函式
void TcpConnection::handleWrite()
{
  loop_->assertInLoopThread();
  if (channel_->isWriting())
  {
    ssize_t n = ::write(channel_->fd(),
                               outputBuffer_.peek(),
                               outputBuffer_.readableBytes());
    if (n > 0)
    {
      outputBuffer_.retrieve(n);
      if (outputBuffer_.readableBytes() == 0)  // 傳送緩衝區已清空
      {
        channel_->disableWriting();  // 停止關注POLLOUT事件,以免出現busy loop
        if (state_ == kDisconnecting)  // 傳送緩衝區已清空並且連線狀態是kDisconnecting, 要關閉連線
        {
          shutdownInLoop();  // 關閉連線
        }
      }
      else
      {
        LOG_TRACE << "I am going to write more data";
      }
    }
    else
    {
      LOG_SYSERR << "TcpConnection::handleWrite";
    }
  }
  else
  {
    LOG_TRACE << "Connection is down, no more writing";
  }
}

傳送資料時,我們還需考慮如果傳送資料的速度高於對方接收資料的速度該怎麼辦,這會造成資料在本地記憶體中堆積。muduo使用“高水位回撥”HighWaterMarkCallback和“低水位回撥”WriteCompleteCallback來處理這個問題。

WriteCompleteCallback

問題:在大流量的場景下,不斷生成資料然後傳送,如果對等方接收不及時,受到通告視窗的控制,核心傳送緩衝區空間不足,這時就會將使用者資料新增到應用層傳送緩衝區,一直這樣下去,可能會撐爆應用層傳送緩衝區。

一種解決方法就是調整傳送頻率,通過設定WriteCompleteCallback,等傳送緩衝區傳送完畢為空後,呼叫WriteCompleteCallback,然後繼續傳送。

TcpConnection有兩處可能觸發此回撥,如下:

程式碼片段4:TcpConnection::sendInLoop()
檔名:TcpConnection.cc

void TcpConnection::sendInLoop(const void* data, size_t len)
{
  loop_->assertInLoopThread();
  ssize_t nwrote = 0;                 // 已傳送的大小
  size_t remaining = len;             // 還未傳送的大小
  bool error = false;                 // 是否有錯誤發生
  if (state_ == kDisconnected)
  {
    LOG_WARN << "disconnected, give up writing";
    return;
  }

  if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
  {
    nwrote = sockets::write(channel_->fd(), data, len);
    if (nwrote >= 0)
    {
      remaining = len - nwrote;
      // 寫完了,回撥writeCompleteCallback_
      if (remaining == 0 && writeCompleteCallback_)
      {
        loop_->queueInLoop(boost::bind(writeCompleteCallback_, shared_from_this()));
      }
    }
    else // nwrote < 0
    {
      nwrote = 0;
      if (errno != EWOULDBLOCK)
      {
        LOG_SYSERR << "TcpConnection::sendInLoop";
        if (errno == EPIPE) // FIXME: any others?
        {
          error = true;
        }
      }
    }
  }

  assert(remaining <= len);
  // 沒有錯誤,並且還有未寫完的資料(說明核心傳送緩衝區滿,要將未寫完的資料新增到output buffer中)
  if (!error && remaining > 0)
  {
    LOG_TRACE << "I am going to write more data";
    size_t oldLen = outputBuffer_.readableBytes();
    // 如果超過highWaterMark_(高水位標),回撥highWaterMarkCallback_
    if (oldLen + remaining >= highWaterMark_
        && oldLen < highWaterMark_
        && highWaterMarkCallback_)
    {
      loop_->queueInLoop(boost::bind(highWaterMarkCallback_, shared_from_this(), oldLen + remaining));
    }
    outputBuffer_.append(static_cast<const char*>(data)+nwrote, remaining);
    if (!channel_->isWriting())
    {
      channel_->enableWriting();  // 關注POLLOUT事件
    }
  }
}
程式碼片段5:TcpConnection::handleWrite()
檔名:TcpConnection.cc

void TcpConnection::handleWrite()
{
  loop_->assertInLoopThread();
  if (channel_->isWriting())
  {
    ssize_t n = sockets::write(channel_->fd(),
                               outputBuffer_.peek(),
                               outputBuffer_.readableBytes());
    if (n > 0)
    {
      outputBuffer_.retrieve(n);
      if (outputBuffer_.readableBytes() == 0)
      {
        channel_->disableWriting();
        if (writeCompleteCallback_)  // 回撥writeCompleteCallback_
        {
          // 應用層傳送緩衝區被清空,就回撥用writeCompleteCallback_
          loop_->queueInLoop(boost::bind(writeCompleteCallback_, shared_from_this()));
        }
        if (state_ == kDisconnecting)
        {
          shutdownInLoop();
        }
      }
      else
      {
        LOG_TRACE << "I am going to write more data";
      }
    }
    else
    {
      LOG_SYSERR << "TcpConnection::handleWrite";
    }
  }
  else
  {
    LOG_TRACE << "Connection fd = " << channel_->fd()
              << " is down, no more writing";
  }
}

muduo chargen示例

chargen服務只傳送資料,不接收資料,而且它傳送資料的速度不能快過客戶端接收的速度,因此需要關注TCP“三個半事件”中的半個“訊息/資料傳送完畢”事件。

示例程式碼:

#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/InetAddress.h>

#include <boost/bind.hpp>

#include <stdio.h>

using namespace muduo;
using namespace muduo::net;

// TestServer中包含一個TcpServer
class TestServer
{
 public:
  TestServer(EventLoop* loop,
             const InetAddress& listenAddr)
    : loop_(loop),
      server_(loop, listenAddr, "TestServer")
  {
    // 設定連線建立/斷開、訊息到來、訊息傳送完畢回撥函式
    server_.setConnectionCallback(
        boost::bind(&TestServer::onConnection, this, _1));
    server_.setMessageCallback(
        boost::bind(&TestServer::onMessage, this, _1, _2, _3));
    server_.setWriteCompleteCallback(
      boost::bind(&TestServer::onWriteComplete, this, _1));

    // 生成資料
    string line;
    for (int i = 33; i < 127; ++i)
    {
      line.push_back(char(i));
    }
    line += line;

    for (size_t i = 0; i < 127-33; ++i)
    {
      message_ += line.substr(i, 72) + '\n';
    }
  }

  void start()
  {
      server_.start();
  }

 private:
  void onConnection(const TcpConnectionPtr& conn)
  {
    if (conn->connected())
    {
      printf("onConnection(): new connection [%s] from %s\n",
             conn->name().c_str(),
             conn->peerAddress().toIpPort().c_str());

      conn->setTcpNoDelay(true);
      conn->send(message_);
    }
    else
    {
      printf("onConnection(): connection [%s] is down\n",
             conn->name().c_str());
    }
  }

  void onMessage(const TcpConnectionPtr& conn,
                 Buffer* buf,
                 Timestamp receiveTime)
  {
    muduo::string msg(buf->retrieveAllAsString());
    printf("onMessage(): received %zd bytes from connection [%s] at %s\n",
           msg.size(),
           conn->name().c_str(),
           receiveTime.toFormattedString().c_str());

    conn->send(msg);
  }

  // 訊息傳送完畢回撥函式,繼續傳送資料
  void onWriteComplete(const TcpConnectionPtr& conn)
  {
    conn->send(message_);
  }

  EventLoop* loop_;
  TcpServer server_;

  muduo::string message_;
};


int main()
{
  printf("main(): pid = %d\n", getpid());

  InetAddress listenAddr(8888);
  EventLoop loop;

  TestServer server(&loop, listenAddr);
  server.start();

  loop.loop();
}

執行結果:
啟動服務端
這裡寫圖片描述

啟動一個客戶端來連線,連線建立後會收到服務端源源不斷髮送過來的訊息
這裡寫圖片描述

這裡寫圖片描述

再次啟動客戶端,將接收到的資料重定向到一個檔案中,可以看到短短几秒就接收到了如此多的資料
這裡寫圖片描述

這裡寫圖片描述

相關文章