c++ - 使用 Asio (Boost) 通过网络发送灵活数量的数据

标签 c++ serialization boost-asio

我有一个客户端和一个服务器应用程序,它们将使用 Asio(独立)库相互发送数据。这两个应用程序都由两个(逻辑)部分组成:

  1. 高级部分:处理复杂对象,例如用户、权限……
  2. 底层部分:在客户端和服务器之间通过网络发送数据

让我们假设复杂对象已经使用 Protocoll Buffers 序列化了。应用程序的低层部分从高层部分接收数据作为 std::string。我想使用 Protocoll Buffers 中的这个功能对于这份工作:

bool SerializeToString(string* output) const;: serializes the message and stores the bytes in the given string. Note that the bytes are binary, not text; we only use the string class as a convenient container.

假设我在客户端使用 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。但我不知道如何以独立于平台的方式序列化大小,将其添加到二进制数据并再次提取。

编辑:也许我可以用这个:

    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。这行得通吗?

最佳答案

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

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

    如果发送二进制数据,则需要考虑处理 byte-ordering . hton()ntoh() 系列函数可以帮助字节排序。例如,考虑将字段定义为网络字节顺序(big-endian)中的两个字节的协议(protocol),并且客户端将字段读取为 uint16_t。如果发送值 10,并且小端机器读取它时没有将网络顺序转换为本地顺序,那么客户端将读取该值作为 2560。 Asio 聊天示例通过将正文长度编码为字符串而不是二进制形式来避免处理字节顺序。

  • 使用连接的文件结尾来指示消息的结尾。虽然这使得发送和接收消息变得容易,但它限制了发件人每个连接只能发送一条消息。要发送额外的消息,需要建立另一个连接。


关于代码的一些观察:

  • Protocol Buffers 的SerializeToString() 函数将消息序列化为二进制 形式。应避免使用基于文本 的函数,例如 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 ,允许接收操作分散读取到多个缓冲区,传输操作可以从多个缓冲区收集写入。因此,不一定需要将固定长度的 header 和消息体都包含在一个缓冲区中。

    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);
    

关于c++ - 使用 Asio (Boost) 通过网络发送灵活数量的数据,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34287997/

相关文章:

c++ - 如何在不知道成员类型的情况下检查 SFINAE 是否存在成员?

c++ - 如何通过其他类打开和关闭mfc gui?

django - 扩展django rest框架以允许在嵌套序列化程序中继承上下文

java - 如何使用 GSON 序列化和反序列化包含 HashMap 和 Pairs 的对象

c++ - 如何使用 boost::asio 从网络中断中恢复

c++ - std::vector::erase()(多线程) 'Assertion ` px != 0' failed.'

c++ - C++11 中的空指针算法

c++ - 将一个函数模板的模板参数映射到另一个函数模板的模板参数 (c++)

json - WCF DataMember DateTime 序列化格式

c++ - Boost ASIO receive_from 如何返回底层套接字错误?