Muduo 网络编程示例之二:Boost.Asio 的聊天服务器

本文介绍了Muduo网络编程中的TCP分包处理,通过聊天服务示例展示了如何处理分包和打包,以及使用LengthHeaderCodec编解码器。文中还提供了服务端和客户端的实现,以及简单的测试流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

陈硕 (giantchen_AT_gmail)

Blog.youkuaiyun.com/Solstice

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

Muduo 全系列文章列表: http://blog.youkuaiyun.com/Solstice/category/779646.aspx

本文讲介绍一个与 Boost.Asio 的示例代码中的聊天服务器功能类似的网络服务程序,包括客户端与服务端的 muduo 实现。这个例子的主要目的是介绍如何处理分包,并初步涉及 Muduo 的多线程功能。Muduo 的下载地址: http://muduo.googlecode.com/files/muduo-0.1.7-alpha.tar.gz ,SHA1 873567e43b3c2cae592101ea809b30ba730f2ee6,本文的完整代码可在线阅读

http://code.google.com/p/muduo/source/browse/trunk/examples/asio/chat/

TCP 分包

前面一篇《五个简单 TCP 协议》中处理的协议没有涉及分包,在 TCP 这种字节流协议上做应用层分包是网络编程的基本需求。分包指的是在发生一个消息(message)或一帧(frame)数据时,通过一定的处理,让接收方能从字节流中识别并截取(还原)出一个个消息。“粘包问题”是个伪问题。

对于短连接的 TCP 服务,分包不是一个问题,只要发送方主动关闭连接,就表示一条消息发送完毕,接收方 read() 返回 0,从而知道消息的结尾。例如前一篇文章里的 daytime 和 time 协议。

对于长连接的 TCP 服务,分包有四种方法:

  1. 消息长度固定,比如 muduo 的 roundtrip 示例就采用了固定的 16 字节消息;
  2. 使用特殊的字符或字符串作为消息的边界,例如 HTTP 协议的 headers 以 "/r/n" 为字段的分隔符;
  3. 在每条消息的头部加一个长度字段,这恐怕是最常见的做法,本文的聊天协议也采用这一办法;
  4. 利用消息本身的格式来分包,例如 XML 格式的消息中 ... 的配对,或者 JSON 格式中的 { ... } 的配对。解析这种消息格式通常会用到状态机。

在后文的代码讲解中还会仔细讨论用长度字段分包的常见陷阱。

聊天服务

本文实现的聊天服务非常简单,由服务端程序和客户端程序组成,协议如下:

  • 服务端程序中某个端口侦听 (listen) 新的连接;
  • 客户端向服务端发起连接;
  • 连接建立之后,客户端随时准备接收服务端的消息并在屏幕上显示出来;
  • 客户端接受键盘输入,以回车为界,把消息发送给服务端;
  • 服务端接收到消息之后,依次发送给每个连接到它的客户端;原来发送消息的客户端进程也会收到这条消息;
  • 一个服务端进程可以同时服务多个客户端进程,当有消息到达服务端后,每个客户端进程都会收到同一条消息,服务端广播发送消息的顺序是任意的,不一定哪个客户端会先收到这条消息。
  • (可选)如果消息 A 先于消息 B 到达服务端,那么每个客户端都会先收到 A 再收到 B。

这实际上是一个简单的基于 TCP 的应用层广播协议,由服务端负责把消息发送给每个连接到它的客户端。参与“聊天”的既可以是人,也可以是程序。在以后的文章中,我将介绍一个稍微复杂的一点的例子 hub,它有“聊天室”的功能,客户端可以注册特定的 topic(s),并往某个 topic 发送消息,这样代码更有意思。

消息格式

本聊天服务的消息格式非常简单,“消息”本身是一个字符串,每条消息的有一个 4 字节的头部,以网络序存放字符串的长度。消息之间没有间隙,字符串也不一定以 '/0' 结尾。比方说有两条消息 "hello" 和 "chenshuo",那么打包后的字节流是:

0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o', 0x00, 0x00, 0x00, 0x08, 'c', 'h', 'e', 'n', 's', 'h', 'u', 'o'

共 21 字节。

打包的代码

这段代码把 const string& message 打包为 muduo::net::Buffer,并通过 conn 发送。

   1: void send(muduo::net::TcpConnection* conn, const string& message)
   2: {
   
   
   3:   muduo::net::Buffer buf;
   4:   buf.append(message.data(), message.size());
   5:   int32_t len = muduo::net::sockets::hostToNetwork32(static_cast(message.size()));
   6:   buf.prepend(&len, sizeof len);
   7:   conn->send(&buf);
   8: }

muduo::Buffer 有一个很好的功能,它在头部预留了 8 个字节的空间,这样第 6 行的 prepend() 操作就不需要移动已有的数据,效率较高。

分包的代码

解析数据往往比生成数据复杂,分包打包也不例外。

   1: void onMessage(const muduo::net::TcpConnectionPtr& conn,
   2:                muduo::net::Buffer* buf,
   3:                muduo::Timestamp receiveTime)
   4: {
   
   
   5:   while (buf->readableBytes() >= kHeaderLen)
   6:   {
   
   
   7:     const void* data = buf->peek();
   8:     int32_t tmp = *static_cast<const int32_t*>(data);
   9:     int32_t len = muduo::net::sockets::networkToHost32(tmp);
  10:     if (len > 65536 || len < 0)
  11:     {
   
   
  12:       LOG_ERROR << "Invalid length " << len;
  13:       conn->shutdown();
  14:     }
  15:     else if (buf->readableBytes() >= len + kHeaderLen)
  16:     {
   
   
  17:       buf->retrieve(kHeaderLen);
  18:       muduo::string message(buf->peek(), len);
  19:       buf->retrieve(len);
  20:       messageCallback_(conn, message, receiveTime);  // 收到完整的消息,通知用户
  21:     }
  22:     else
  23:     {
   
   
  24:       break;
  25:     }
  26:   }
  27: }

上面这段代码第 7 行用了 while 循环来反复读取数据,直到 Buffer 中的数据不够一条完整的消息。请读者思考,如果换成 if (buf->readableBytes() >= kHeaderLen) 会有什么后果。

以前面提到的两条消息的字节流为例:

0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o', 0x00, 0x00, 0x00, 0x08, 'c', 'h', 'e', 'n', 's', 'h', 'u', 'o'

假设数据最终都全部到达,onMe

st_asio_wrapper是一组类库,功能是对boost.asio装(调试环境:boost-1.51.0),目的是简化boost.asio开发; 其特点是效率高、跨平台、完全异步(当然这是从boost.asio继承而来)、自动重连,数据透明传输,自动解决分问题(你可以像udp一样使用它); 注:只支持tcp协议; 教程:http://blog.csdn.net/yang79tao/article/details/7724514 2.3版更新内容: 消息(std::string装)不再用boost::shared_ptr装,之前有过度使用智能指针之嫌。效率上,std::string如果支持引用记数,或者编译器支持std::move语义,是没有损失的(因为也不存在内存的拷贝,反而省了智能指针使用上的开销),幸好vc支持std::move语义(虽然它不支持引用记数,linux则都支持)。这样带来一个问题,原来所有的接口中的boost::shared_ptr<std::string>数据类型,全部换成了std::string引用,升级到2.3的朋友要注意修改之前重写虚函数的签名,如果不改,则重写肯定不生效,变成了新增加虚函数了(因为签名不一样)。这样向大家道歉,接口签名以后应该不会变化了,但可能增加接口; 修复使用std::advance的一个BUG,此BUG在linux下不存在,这里顺便向大家说一下,std::advance在vc和gcc下面,语义一样,但处理方式有些不同,一定要注意; 增加了个专门用于服务端压力测试的客户端框架st_test_client,并写了一个demo test_client,可以在performance_test目录下面找到; 把连接服务端逻辑从st_client剥离出来,定义了一个新的类st_connector,st_client和st_test_client将从它继承; 增加对vc2010的支持,和编译时对编译器版本的检测,如果达不到vc2010及其以上的版本,st_asio_wrapper将直接报错。
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值