程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> Muduo 網絡編程示例(一) 五個簡單TCP協議

Muduo 網絡編程示例(一) 五個簡單TCP協議

編輯:關於C++

本文將介紹第一個示例:五個簡單 TCP 網絡服務協議,包括 echo (RFC 862)、discard (RFC 863) 、chargen (RFC 864)、daytime (RFC 867)、time (RFC 868),以及 time 協議的客戶端。各協議的功 能簡介如下:

* discard - 丟棄所有收到的數據;

* daytime - 服務端 accept 連接之 後,以字符串形式發送當前時間,然後主動斷開連接;

* time - 服務端 accept 連接之後,以 二進制形式發送當前時間(從 Epoch 到現在的秒數),然後主動斷開連接;我們需要一個客戶程序來 把收到的時間轉換為字符串。

* echo - 回顯服務,把收到的數據發回客戶端;

* chargen - 服務端 accept 連接之後,不停地發送測試數據。

以上五個協議使用不同的端口, 可以放到同一個進程中實現,且不必使用多線程。完整的代碼見 muduo/examples/simple,下載地址 http://muduo.googlecode.com/files/muduo-0.1.6-alpha.tar.gz 。

discard

Discard 恐怕算是最簡單的長連接 TCP 應用層協議,它只需要關注“三個半事件”中的“消息 /數據到達”事件,事件處理函數如下:

  1: void DiscardServer::onMessage(const muduo::net::TcpConnectionPtr& conn,
   2:                  muduo::net::Buffer* buf,
   3:                  muduo::Timestamp time)
   4: {
   5:   string msg(buf->retrieveAsString());  // 取回讀到的全部數據
   6:   LOG_INFO << conn->name() << " discards " << msg.size() << " bytes at " << time.toString();
   7: }

剩下的都是例行公事的代碼:

定義一個 DiscardServer class,以 TcpServer 為成員。

  1: #ifndef MUDUO_EXAMPLES_SIMPLE_DISCARD_DISCARD_H
   2: #define MUDUO_EXAMPLES_SIMPLE_DISCARD_DISCARD_H
   3: 
   4: #include <muduo/net/TcpServer.h>
   5: 
   6: // RFC 863
   7: class DiscardServer
   8: {
   9:  public:
  10:   DiscardServer(muduo::net::EventLoop* loop,
  11:                 const muduo::net::InetAddress& listenAddr);
  12: 
  13:   void start();
  14: 
  15:  private:
  16:   void onConnection(const muduo::net::TcpConnectionPtr& conn);
  17: 
  18:   void onMessage(const muduo::net::TcpConnectionPtr& conn,
  19:                  muduo::net::Buffer* buf,
  20:                  muduo::Timestamp time);
  21: 
  22:   muduo::net::EventLoop* loop_;
  23:   muduo::net::TcpServer server_;
  24: };
  25: 
  26: #endif  // MUDUO_EXAMPLES_SIMPLE_DISCARD_DISCARD_H

注冊回調函數

 

 1: DiscardServer::DiscardServer(muduo::net::EventLoop* loop,
   2:                              const muduo::net::InetAddress& listenAddr)
   3:   : loop_(loop),
   4:     server_(loop, listenAddr, "DiscardServer")
   5: {
   6:   server_.setConnectionCallback(
   7:       boost::bind(&DiscardServer::onConnection, this, _1));
   8:   server_.setMessageCallback(
   9:       boost::bind(&DiscardServer::onMessage, this, _1, _2, _3));
  10: }
  11: 
  12: void DiscardServer::start()
  13: {
  14:   server_.start();
  15: }

處理連接與數據事件

  1: void DiscardServer::onConnection(const 

muduo::net::TcpConnectionPtr& conn)
   2: {
   3:   LOG_INFO << "DiscardServer - " << conn->peerAddress().toHostPort() << " -> "
   4:     << conn->localAddress().toHostPort() << " is "
   5:     << (conn->connected() ? "UP" : "DOWN");
   6: }
   7: 
   8: void DiscardServer::onMessage(const muduo::net::TcpConnectionPtr& conn,
   9:                  muduo::net::Buffer* buf,
  10:                  muduo::Timestamp time)
  11: {
  12:   string msg(buf->retrieveAsString());
  13:   LOG_INFO << conn->name() << " discards " << msg.size() << " bytes at " << time.toString();
  14: }

在 main() 裡用 EventLoop 讓整個程序轉起來

  1: #include "discard.h"
   2: 
   3: #include <muduo/base/Logging.h>
   4: #include <muduo/net/EventLoop.h>
   5: 
   6: using namespace muduo;
   7: using namespace muduo::net;
   8: 
   9: int main()
  10: {
  11:   LOG_INFO << "pid = " << getpid();
  12:   EventLoop loop;
  13:   InetAddress listenAddr(2009);
  14:   DiscardServer server(&loop, listenAddr);
  15:   server.start();
  16:   loop.loop();
  17: }

daytime

Daytime 是短連接協議,在發送完當前時間後,由服務端主動斷開 連接。它只需要關注“三個半事件”中的“連接已建立”事件,事件處理函數如 下:

  1: void DaytimeServer::onConnection(const muduo::net::TcpConnectionPtr& conn)
   2: {
   3:   LOG_INFO << "DaytimeServer - " << conn->peerAddress().toHostPort() << " -> "
   4:     << conn->localAddress().toHostPort() << " is "
   5:     << (conn->connected() ? "UP" : "DOWN");
   6:   if (conn->connected())
   7:   {
   8:     conn->send(Timestamp::now().toFormattedString() + "n"); // 發送時間字符串
   9:     conn->shutdown(); // 主動斷開連接
  10:   }
  11: }

剩下的都是例行公事的代碼,為節省篇幅,此處從略,請閱讀 muduo/examples/simple/daytime。

用 netcat 扮演客戶端,運行結果如下:

$ nc 127.0.0.1 2013

2011-02-02 03:31:26.622647    # 服務器返回的時間字符串

time

Time 協議與 daytime 極為類似,只不過它返回的不是日期時間字符串,而是一個 32 -bit 整數,表示從 1970-01-01 00:00:00Z 到現在的秒數。當然,這個協議有“2038 年問題 ”。服務端只需要關注“三個半事件”中的“連接已建立”事件,事件處 理函數如下:

  1: void TimeServer::onConnection(const muduo::net::TcpConnectionPtr& conn)
   2: {
   3:   LOG_INFO << "TimeServer - " << conn->peerAddress().toHostPort() << " -> "
   4:     << conn->localAddress().toHostPort() << " is "
   5:     << (conn->connected() ? "UP" : "DOWN");
   6:   if (conn->connected())
   7:   {
   8:     int32_t now = sockets::hostToNetwork32(static_cast<int>(::time(NULL)));
   9:     conn->send(&now, sizeof now);  // 發送 4 個字節
  10:     conn->shutdown();  // 主動斷開連接
  11:   }
  12: }

剩下的都是例行公事的代碼,為節省篇幅,此處從略,請閱讀 muduo/examples/simple/time。

用 netcat 扮演客戶端,並用 hexdump 來打印二進制數據,運 行結果如下:

$ nc 127.0.0.1 2037 | hexdump -C
00000000  4d 48 d0 d5                                  

     |MHÐÕ|
00000004

time_client

因為 time 服務端發送的是二進制數據,不便直接閱讀,我們 編寫一個客戶端來解析並打印收到的 4 個字節數據。這個程序只需要關注“三個半事件” 中的“消息/數據到達”事件,事件處理函數如下:

  1: void TimeClient::onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime)
   2: {
   3:   if (buf->readableBytes() >= sizeof(int32_t))
   4:   {
   5:     const void* data = buf->peek();
   6:     int32_t time = *static_cast<const int32_t*>(data);
   7:     buf->retrieve(sizeof(int32_t));
   8:     time_t servertime = sockets::networkToHost32(time);
   9:     Timestamp t(servertime * Timestamp::kMicroSecondsPerSecond);
  10:     LOG_INFO << "Server time = " << servertime << ", " << t.toFormattedString();
  11:   }
  12:   else
  13:   {
  14:     LOG_INFO << conn->name() << " no enough data " << buf->readableBytes()
  15:      << " at " << receiveTime.toFormattedString();
  16:   }
  17: }

注意其中考慮到了如果數據沒有一次性收全,已經收到的數據會暫存在 Buffer 裡 ,以等待下一次機會,程序也不會阻塞。這樣即便服務器一個字節一個字節地發送數據,代碼還是能正 常工作,這也是非阻塞網絡編程必須在用戶態使用接受緩沖的主要原因。

這是我們第一次用到 TcpClient class,完整的代碼如下:

  1: #include <muduo/base/Logging.h>
   2: #include <muduo/net/EventLoop.h>
   3: #include <muduo/net/InetAddress.h>
   4: #include <muduo/net/SocketsOps.h>
   5: #include <muduo/net/TcpClient.h>
   6: 
   7: #include <boost/bind.hpp>
   8: 
   9: #include <utility>
  10: 
  11: #include <stdio.h>
  12: #include <unistd.h>
  13: 
  14: using namespace muduo;
  15: using namespace muduo::net;
  16: 
  17: class TimeClient : boost::noncopyable
  18: {
  19:  public:
  20:   TimeClient(EventLoop* loop, const InetAddress& listenAddr)
  21:     : loop_(loop),
  22:       client_(loop, listenAddr, "TimeClient")
  23:   {
  24:     client_.setConnectionCallback(
  25:         boost::bind(&TimeClient::onConnection, this, _1));
  26:     client_.setMessageCallback(
  27:         boost::bind(&TimeClient::onMessage, this, _1, _2, _3));
  28:     // client_.enableRetry();
  29:   }
  30: 
  31:   void connect()
  32:   {
  33:     client_.connect();
  34:   }
  35: 
  36:  private:
  37:   void onConnection(const TcpConnectionPtr& conn)
  38:   {
  39:     LOG_INFO << conn->localAddress().toHostPort() << " -> "
  40:         << conn->peerAddress().toHostPort() << " is "
  41:         << (conn->connected() ? "UP" : "DOWN");
  42: 
  43:     if (!conn->connected())  // 如果連接斷開,則終止主循環,退出程序
  44:       loop_->quit();
  45:   }
  46: 
  47:   void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime)
  48:   {
  49:     if (buf->readableBytes() >= sizeof(int32_t))
  50:     {
  51:       const void* data = buf->peek();
  52:       int32_t time = *static_cast<const int32_t*>(data);
  53:       buf->retrieve(sizeof(int32_t));
  54:       time_t servertime = sockets::networkToHost32(time);
  55:       Timestamp t(servertime * Timestamp::kMicroSecondsPerSecond);
  56:       LOG_INFO << "Server time = " << servertime << ", " << t.toFormattedString();
  57:     }
  58:     else
  59:     {
  60:       LOG_INFO << conn->name() << " no enough data " << buf->readableBytes()
  61:        << " at " << receiveTime.toFormattedString();
  62:     }
  63:   }
  64: 
  65:   EventLoop* loop_;
  66:   TcpClient client_;
  67: };
  68: 
  69: int main(int argc, char* argv[])
  70: {
  71:   LOG_INFO << "pid = " << getpid();
  72:   if (argc > 1)
  73:   {
  74:     EventLoop loop;
  75:     InetAddress serverAddr(argv[1], 2037);
  76: 
  77:     TimeClient timeClient(&loop, serverAddr);
  78:     timeClient.connect();
  79:     loop.loop();
  80:   }
  81:   else
  82:   {
  83:     printf("Usage: %s host_ipn", argv[0]);
  84:   }
  85: }

查看本欄目

程序的運行結果如下,假設 time server 運行在本機:

$ 

./simple_timeclient 127.0.0.1
2011-02-02 04:10:35.181717  4296 INFO pid = 4296 - timeclient.cc:71
2011-02-02 04:10:35.183668  4296 INFO TcpClient::connect[TimeClient] - connecting to 

127.0.0.1:2037 - TcpClient.cc:60
2011-02-02 04:10:35.185178  4296 INFO 127.0.0.1:40960 -> 127.0.0.1:2037 is UP - timeclient.cc:39
2011-02-02 04:10:35.185279  4296 INFO Server time = 1296619835, 2011-02-02 

04:10:35.000000 - timeclient.cc:56
2011-02-02 04:10:35.185354  4296 INFO 127.0.0.1:40960 -> 127.0.0.1:2037 is DOWN - timeclient.cc:39

echo

Echo 是我們遇到的第一個帶交互的協議:服務端把客戶端 發過來的數據原封不動地傳回去。它只需要關注“三個半事件”中的“消息/數據到達 ”事件,事件處理函數如下:

  1: void EchoServer::onMessage(const TcpConnectionPtr& conn,
   2:                            Buffer* buf,
   3:                            Timestamp time)
   4: {
   5:   string msg(buf->retrieveAsString());
   6:   LOG_INFO << conn->name() << " echo " << msg.size() << " bytes at " << time.toString();
   7:   conn->send(msg);
   8: }

這段代碼實現的不是行回顯(line echo)服務,而是有一點數據就發送一點數據。 這樣可以避免客戶端惡意地不發送換行字符,而服務端又必須緩存已經收到的數據,導致服務器內存暴 漲。但這個程序還是有一個安全漏洞,即如果客戶端故意不斷發生數據,但從不接收,那麼服務端的發 送緩沖區會一直堆積,導致內存暴漲。解決辦法可以參考下面的 chargen 協議。

剩下的都是例 行公事的代碼,為節省篇幅,此處從略,請閱讀 muduo/examples/simple/echo。

練習 1:修改 EchoServer::onMessage(),實現大小寫互換。

練習 2:修改 EchoServer::onMessage(),實現 rot13 加密。

chargen

Chargen 協議很特殊,它只發送數據,不接收數據。而且,它發 送數據的速度不能快過客戶端接收的速度,因此需要關注“三個半事件”中的半個“ 消息/數據發送完畢”事件(onWriteComplete),事件處理函數如下:

  1: void ChargenServer::onConnection(const muduo::net::TcpConnectionPtr& conn)
   2: {
   3:   LOG_INFO << "ChargenServer - " << conn->peerAddress().toHostPort() << " -> "
   4:     << conn->localAddress().toHostPort() << " is "
   5:     << (conn->connected() ? "UP" : "DOWN");
   6:   if (conn->connected())
   7:   {
   8:     conn->send(message_);  // 在連接建立時發生第一次數據
   9:   }
  10: }
  11: 
  12: void ChargenServer::onMessage(const muduo::net::TcpConnectionPtr& conn,
  13:                  muduo::net::Buffer* buf,
  14:                  muduo::Timestamp time)
  15: {
  16:   string msg(buf->retrieveAsString());
  17:   LOG_INFO << conn->name() << " discards " << msg.size() << " bytes at " << time.toString();
  18: }
  19: 
  20: void ChargenServer::onWriteComplete(const TcpConnectionPtr& conn)
  21: {
  22:   transferred_ += message_.size();
  23:   conn->send(message_);  // 繼續發送數據
  24: }

剩下的都是例行公事的代碼,為節省篇幅,此處從略,請閱讀 muduo/examples/simple/chargen。

完整的 chargen 服務端還帶流量統計功能,用到了定時器 ,我們會在下一篇文章裡介紹定時器的使用,到時候再回頭來看相關代碼。

用 netcat 扮演客 戶端,運行結果如下:

$ nc localhost 2019 | head
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefgh
"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghi
#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghij
$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijk
%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijkl
&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklm
'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmn
()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmno
)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnop
*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopq
Five in one

前面五個程序都用到了 EventLoop,這其實是個 Reactor,用於注冊和分發 IO 事件。Muduo 遵循 one loop per thread 模型,多個服務端(TcpServer)和客戶端(TcpClient)可以共 享同一個 EventLoop,也可以分配到多個 EventLoop 上以發揮多核多線程的好處。這裡我們把五個服 務端用同一個 EventLoop 跑起來,程序還是單線程的,功能卻強大了很多:

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved