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

lit*_*net 3 c++ serialization boost-asio

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

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

让我们假设复杂对象已经使用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)); 
Run Code Online (Sandbox Code Playgroud)

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

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

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

  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);
Run Code Online (Sandbox Code Playgroud)

并将 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";      
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,我尝试使用transfer_exactly. 这会起作用吗?

Tan*_*ury 6

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

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

    如果正在发送二进制数据,则需要考虑处理字节顺序。在hton()ntoh()系列函数可以用字节序帮助。例如,考虑一个协议,该协议将字段定义为网络字节顺序(大端)中的两个字节,并且客户端将该字段读取为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));
    
    Run Code Online (Sandbox Code Playgroud)
  • htonl()重载支持uint16_tuint_32tuint64_t

  • Asio 支持scatter/gather 操作,允许接收操作将读取操作分散到多个缓冲区中,传输操作可以从多个缓冲区收集写入。因此,不一定需要将固定长度的标头和消息体都包含在单个缓冲区中。

    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);
    
    Run Code Online (Sandbox Code Playgroud)