使用Asio(Boost)

时间:2015-12-15 11:26:11

标签: c++ serialization boost-asio

我有一个客户端和一个服务器应用程序,它将使用Asio(独立)库发送彼此的数据。两个应用程序都包含两个(逻辑)部分:

  1. 高级部分:处理复杂对象,例如用户,权限......
  2. 低级别部分:在客户端和服务器之间通过网络发送数据
  3. 假设复杂对象已经使用Protocoll Buffers序列化,应用程序的低级部分从高级部分接收数据为std :: string。我想从Protocoll Buffers使用此功能来完成这项工作:

      

    bool SerializeToString (string * output)const;:序列化消息   并将字节存储在给定的字符串中。请注意,字节是   二进制,而不是文本;我们只使用字符串类作为方便   容器

    并说我在客户端使用async_write传输此数据:

    size_t dataLength = strlen(data);
    
    //writes a certain number of bytes of data to a stream.
    asio::async_write(mSocket,
                          asio::buffer(data, dataLength),
                          std::bind(&Client::writeCallback, this,
                                    std::placeholders::_1,   
                                    std::placeholders::_2)); 
    

    如何在服务器端读取此数据?我不知道有多少数据需要阅读。因此这不起作用(长度未知):

     asio::async_read(mSocket,
                         asio::buffer(mResponse, length),
                         std::bind(&Server::readCallback, this,
                                   std::placeholders::_1,
                                   std::placeholders::_2));
    

    解决此问题的最佳方法是什么?我可以想到两个解决方案:

    1. data的末尾附加一个“特殊”字符并读取,直到我达到“数据信号结束”。问题是,如果这个角色以某种方式出现在data中怎么办?我不知道Protocoll Buffers如何序列化我的数据。
    2. 使用size_of_data + data而不是data发送二进制字符串。但我不知道如何以独立于平台的方式序列化大小,将其添加到二进制数据中并再次提取它。
    3. 编辑:也许我可以使用它:

          uint64_t length = strlen(data);
          uint64_t nwlength = htonl(length);
          uint8_t len[8];
          len[0] = nwlength >> 56;
          len[1] = nwlength >> 48;
          len[2] = nwlength >> 40;
          len[3] = nwlength >> 32;
          len[4] = nwlength >> 24;
          len[5] = nwlength >> 16;
          len[6] = nwlength >> 8;
          len[7] = nwlength >> 0;
      
          std::string test(len);
      
          mRequest = data;
          mRequest.insert(0, test);
      

      并将mRequest发送到服务器? 使用此代码的任何陷阱或警告? 我如何阅读服务器端的长度和之后的内容? 也许是这样的:

      void Server::readHeader(){
      
          asio::async_read(mSocket,
                           asio::buffer(header, HEADER_LENGTH),
                           std::bind(&Server::readHeaderCallback, this,
                                     std::placeholders::_1,
                                     std::placeholders::_2),
                           asio::transfer_exactly(HEADER_LENGTH));
      }
      
      void Server::readHeaderCallback(const asio::error_code& error,
                                              size_t bytes_transferred){
      
          if(!error && decodeHeader(header, mResponseLength)){
              //reading header finished, now read the content
              readContent();
          }
          else{
              if(error) std::cout << "Read failed: " << error.message() << "\n";
              else std::cout << "decodeHeader failed \n";       
          }
      }
      
      void Server::readContent(){
      
          asio::async_read(mSocket,
                           asio::buffer(mResponse, mResponseLength),
                           std::bind(&Server::readContentCallback, this,
                                     std::placeholders::_1,
                                     std::placeholders::_2),
                           asio::transfer_exactly(mResponseLength));
      }
      
      void Server::readContentCallback(const asio::error_code& error,
                                               size_t bytes_transferred){
          if (!error){
             //handle content
          }
          else{
              //@todo remove this cout
              std::cout << "Read failed: " << error.message() << "\n";      
          }
      }
      

      请注意,我尝试使用transfer_exactly。这有用吗?

1 个答案:

答案 0 :(得分:4)

通过基于流的协议发送可变长度消息时,通常有三种解决方案来指示消息边界:

  • 使用分隔符指定邮件边界。 async_read_until()操作提供了一种读取可变长度分隔消息的便捷方式。使用分隔符时,需要考虑分隔符冲突的可能性,其中分隔符出现在消息的内容中,但不表示边界。处理分隔符冲突有各种技术,例如转义字符或转义序列。
  • 使用带有可变长度主体协议的固定长度标头。标题将提供有关消息的元信息,例如正文的长度。官方的Asio chat example演示了一种处理固定长度标头和可变长度主体协议的方法。

    如果正在发送二进制数据,则需要考虑处理byte-orderinghton()ntoh()系列函数可以帮助进行字节排序。例如,考虑一种协议,该字段将字段定义为网络字节顺序(big-endian)中的两个字节,客户端将字段读取为uint16_t。如果发送值10,并且little-endian机器在不从网络顺序转换为本地顺序的情况下读取它,则客户端将读取值2560。 Asio聊天示例通过将主体长度编码为字符串而不是二进制形式来避免处理字节序。

  • 使用连接的文件结尾来指示消息的结束。虽然这使得发送和接收消息变得容易,但它将发送方限制为每个连接只有一条消息。要发送其他消息,需要建立另一个连接。

关于代码的一些观察:

  • 协议缓冲区&#39; SerializeToString()函数将邮件序列化为二进制表单。应该避免在序列化字符串上使用基于 text 的函数,例如strlen()。例如,strlen()可能会错误地确定长度,因为它会将值为0的第一个字节视为终止空字节,即使该字节是编码值的一部分。
  • 通过asio::buffer(buffer, n)向操作提供显式大小的缓冲区时,transfer_all的默认完成条件将与transfer_exactly(n)的功能相同。因此,可以删除重复使用的变量:

    asio::async_read(mSocket,
                     asio::buffer(header, HEADER_LENGTH),
                     std::bind(&Server::readHeaderCallback, this,
                              std::placeholders::_1,
                              std::placeholders::_2));
    
  • htonl()重载支持uint16_tuint_32t,而非uint64_t

  • Asio支持scatter/gather operations,允许将接收操作分散读入多个缓冲区,并且传输操作可以从多个缓冲区中进行聚合写入。因此,不一定需要将固定长度头和消息体包含在一个缓冲区中。

    std::string body_buffer;
    body.SerializeToString(&body_buffer);
    std::string header_buffer = encode_header(body_buffer.size());
    
    // Use "gather-write" to send both the header and data in a
    // single write operation.
    std::vector<boost::asio::const_buffer> buffers;
    buffers.push_back(boost::asio::buffer(header_buffer));
    buffers.push_back(boost::asio::buffer(body_buffer));
    boost::asio::write(socket_, buffers);