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


    陈硕 (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 <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_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 <muduo/base/Logging.h>
       8: #include <muduo/net/EventLoop.h>
       9:  
      10: #include <boost/bind.hpp>
      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 的聊天服务器时我们再来讨论这个问题。

    (待续)

  • 相关阅读:
    Silverlight中弹出网页
    Silverlight中嵌套html、swf、pdf
    silverlight 用户浏览器未安装SL插件问题
    silverlight视频、音频
    Silverlight形状、画笔、变换、图像处理、几何图形
    Silverlight动画学习笔记(三):缓动函数
    Silverlight动画的基本知识、关键帧动画
    Silverlight独立存储
    关于Silverlight调用天气预报接口问题
    c#获取今天星期几
  • 原文地址:https://www.cnblogs.com/Solstice/p/1948839.html
Copyright © 2020-2023  润新知