本文將介紹第一個示例:五個簡單 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 跑起來,程序還是單線程的,功能卻強大了很多: