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

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();
}

運行結果:
啓動服務端
這裏寫圖片描述

啓動一個客戶端來連接,連接建立後會收到服務端源源不斷髮送過來的消息
這裏寫圖片描述

這裏寫圖片描述

再次啓動客戶端,將接收到的數據重定向到一個文件中,可以看到短短几秒就接收到了如此多的數據
這裏寫圖片描述

這裏寫圖片描述

發佈了44 篇原創文章 · 獲贊 34 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章