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

简介:

陈硕 (giantchen_AT_gmail)

Blog.csdn.net/Solstice

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

全系列文章列表: 

本文将介绍第一个示例:五个简单 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 的聊天服务器时我们再来讨论这个问题。

(待续)


    本文转自 陈硕  博客园博客,原文链接:http://www.cnblogs.com/Solstice/archive/2011/02/02/1948839.html,如需转载请自行联系原作者



相关文章
|
2月前
|
负载均衡 网络协议 算法
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
这网络层就像搭积木一样,上层协议都是基于下层协议搭出来的。不管是ping(用了ICMP协议)还是tcp本质上都是基于网络层IP协议的数据包,而到了物理层,都是二进制01串,都走网卡发出去了。 如果网络环境没发生变化,目的地又一样,那按道理说他们走的网络路径应该是一样的,什么情况下会不同呢? 我们就从路由这个话题聊起吧。
78 4
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
|
2月前
|
前端开发 网络协议 安全
【网络原理】——HTTP协议、fiddler抓包
HTTP超文本传输,HTML,fiddler抓包,URL,urlencode,HTTP首行方法,GET方法,POST方法
|
2月前
|
网络协议
TCP报文格式全解析:网络小白变高手的必读指南
本文深入解析TCP报文格式,涵盖源端口、目的端口、序号、确认序号、首部长度、标志字段、窗口大小、检验和、紧急指针及选项字段。每个字段的作用和意义详尽说明,帮助理解TCP协议如何确保可靠的数据传输,是互联网通信的基石。通过学习这些内容,读者可以更好地掌握TCP的工作原理及其在网络中的应用。
|
2月前
|
网络协议 安全 网络安全
探索网络模型与协议:从OSI到HTTPs的原理解析
OSI七层网络模型和TCP/IP四层模型是理解和设计计算机网络的框架。OSI模型包括物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,而TCP/IP模型则简化为链路层、网络层、传输层和 HTTPS协议基于HTTP并通过TLS/SSL加密数据,确保安全传输。其连接过程涉及TCP三次握手、SSL证书验证、对称密钥交换等步骤,以保障通信的安全性和完整性。数字信封技术使用非对称加密和数字证书确保数据的机密性和身份认证。 浏览器通过Https访问网站的过程包括输入网址、DNS解析、建立TCP连接、发送HTTPS请求、接收响应、验证证书和解析网页内容等步骤,确保用户与服务器之间的安全通信。
162 3
|
3月前
|
安全 搜索推荐 网络安全
HTTPS协议是**一种通过计算机网络进行安全通信的传输协议
HTTPS协议是**一种通过计算机网络进行安全通信的传输协议
92 11
|
3月前
|
网络安全 Python
Python网络编程小示例:生成CIDR表示的IP地址范围
本文介绍了如何使用Python生成CIDR表示的IP地址范围,通过解析CIDR字符串,将其转换为二进制形式,应用子网掩码,最终生成该CIDR块内所有可用的IP地址列表。示例代码利用了Python的`ipaddress`模块,展示了从指定CIDR表达式中提取所有IP地址的过程。
81 6
|
3月前
|
网络协议
网络通信的基石:TCP/IP协议栈的层次结构解析
在现代网络通信中,TCP/IP协议栈是构建互联网的基础。它定义了数据如何在网络中传输,以及如何确保数据的完整性和可靠性。本文将深入探讨TCP/IP协议栈的层次结构,揭示每一层的功能和重要性。
102 5
|
3月前
|
监控 网络协议 网络性能优化
网络通信的核心选择:TCP与UDP协议深度解析
在网络通信领域,TCP(传输控制协议)和UDP(用户数据报协议)是两种基础且截然不同的传输层协议。它们各自的特点和适用场景对于网络工程师和开发者来说至关重要。本文将深入探讨TCP和UDP的核心区别,并分析它们在实际应用中的选择依据。
101 3
|
3月前
|
网络协议 网络安全 网络虚拟化
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算。通过这些术语的详细解释,帮助读者更好地理解和应用网络技术,应对数字化时代的挑战和机遇。
192 3
|
3月前
|
存储 网络协议 安全
30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场
本文精选了 30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场。
194 2

热门文章

最新文章