Muduo 网络编程示例之一:五个简单 TCP 协议

    技术2024-08-13  64

    陈硕 (giantchen_AT_gmail)

    Blog.csdn.net/Solstice

    这是《Muduo 网络编程示例》系列的第一篇文章。

    全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx

    本文将介绍第一个示例:五个简单 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 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 4: #include 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 2: #include 3: #include 4: #include 5: #include 6:  7: #include 8:  9: #include 10:  11: #include 12: #include 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_ip/n", 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: #include "../chargen/chargen.h" 2: #include "../daytime/daytime.h" 3: #include "../discard/discard.h" 4: #include "../echo/echo.h" 5: #include "../time/time.h" 6:  7: #include 8: #include 9:  10: #include 11:  12: using namespace muduo; 13: using namespace muduo::net; 14:  15: int main() 16: { 17: LOG_INFO << "pid = " << getpid(); 18: EventLoop loop; 19:  20: ChargenServer ChargenServer(&loop, InetAddress(2019)); 21: ChargenServer.start(); 22:  23: DaytimeServer daytimeServer(&loop, InetAddress(2013)); 24: daytimeServer.start(); 25:  26: DiscardServer discardServer(&loop, InetAddress(2009)); 27: discardServer.start(); 28:  29: EchoServer echoServer(&loop, InetAddress(2007)); 30: echoServer.start(); 31:  32: TimeServer timeServer(&loop, InetAddress(2037)); 33: timeServer.start(); 34:  35: loop.loop(); 36: }

     

    以上几个协议的消息格式都非常简单,没有涉及 TCP 网络编程中常见的分包处理,在下一篇文章讲 Boost.Asio 的聊天服务器时我们再来讨论这个问题。

    (待续)

    最新回复(0)