我们如何从 boost::asio::tcp::ip::read_some 调用中顺序接收多个数据?

How can we sequentially receive multiple data from boost::asio::tcp::ip::read_some calls?

让我们假设一个客户端持有两个不同的大对象(根据字节大小)并序列化它们,然后发送序列化的对象 使用 boost::asio.

通过 TCP/IP 网络连接到服务器

当客户端调用两个顺序boost::asio::write分别发送两个不同的二进制对象时,服务器端也顺序调用两个相应的recv。 但是,第一个 recv 函数吸收了所有两个传入的大数据,而第二个调用什么也没收到 ;-(。 我不确定为什么会发生这种情况以及如何解决它。

由于两个不同的对象中的每一个都有自己的(反)序列化功能,我想分别发送每个数据。事实上,由于有超过 20 个对象(不仅仅是 2 个)必须通过网络发送。

void recv (
    boost::asio::ip::tcp::socket &socket,
    std::stringstream &is) {

    boost::array<char, 65536> buf;

    for (;;) {
        boost::system::error_code error;
        size_t len = socket.read_some(boost::asio::buffer(buf), error);
        std::cout << " read "<< len << " bytes" << std::endl;  // called multiple times for debugging!

        if (error == boost::asio::error::eof)
          break;
        else if (error)
          throw boost::system::system_error(error); // Some other error.

        std::stringstream buf_ss;
        buf_ss.write(buf.data(), len);
        is << buf_ss.str();
    }
}

客户端主文件:

int main () {
    ... // some 2 different big objects are constructed.
    std::stringstream ss1, ss2;
    ... // serializing bigObj1 -> ss1 and bigObj2-> ss2, where each object is serialized into a string. This is due to the dependency of our using some external library
    const char * big_obj_bin1 = reinterpret_cast<const char*>(ss1.str().c_str());
    const char * big_obj_bin2 = reinterpret_cast<const char*>(ss2.str().c_str());

    boost::system::error_code ignored_error;
    boost::asio::write(socket, boost::asio::buffer(big_obj_bin1, ss1.str().size()), ignored_error);
    boost::asio::write(socket, boost::asio::buffer(big_obj_bin2, ss2.str().size()), ignored_error);

    ... // do something
    return 0;
}

服务器主文件:

int main () {
    ... // socket is generated. (communication established)
    std::stringstream ss1, ss2;
    recv(socket,ss1); // this guy absorbs all of incoming data
    recv(socket,ss2); // this guy receives 0 bytes ;-(
    ... // deserialization to two bib objects
    return 0;
}
recv(socket,ss1); // this guy absorbs all of incoming data

当然是什么都吸收了。您 明确地 编码 recv 进行无限循环,直到 eof。那是流的结尾,这意味着 "whenever the socket is closed on the remote end".

所以协议中缺少的基本内容是框架。最常见的解决方法是:

  • 在数据之前发送数据长度,这样服务器就知道要读取多少
  • 发送一个"special sequence"来分隔帧。在文本中,常见的特殊分隔符是 '[=14=]'。但是,对于二进制数据,(非常)很难找到一个不能自然出现在有效负载中的分隔符。

    当然,如果您知道有效载荷的额外特征,您可以使用它。例如。如果您的有效负载被压缩,您知道您不会经常找到 512 个相同字节的块(它们会被压缩)。或者,您可以采用消除歧义的方式对二进制数据进行编码。 yEnc, Base122 et al. come to mind (see Binary Data in JSON String. Something better than Base64 寻找灵感)。

备注:

无论如何

  1. 手写阅读循环很笨拙。接下来,这样做是非常没有必要的 并且 也将块复制到字符串流中。如果您仍然要进行所有复制,只需直接使用 boost::asio::[async_]readboost::asio::streambuf

  2. 这就清楚了UB:

    const char * big_obj_bin1 = reinterpret_cast<const char*>(ss1.str().c_str());
    const char * big_obj_bin2 = reinterpret_cast<const char*>(ss2.str().c_str());
    

    str() returns a temporary copy of the buffer - 这不仅是浪费,而且意味着 const char* 悬空 它们被初始化的那一刻。