【问题标题】:Boost ASIO - Invalid Header and Body for TCP packetBoost ASIO - TCP 数据包的标头和正文无效
【发布时间】:2021-01-05 10:16:38
【问题描述】:

我正在使用 Boost ASIO 作为我的项目的 TCP 网络通信解决方案。

这是我的代码:

Client.h:

namespace Vibranium{
    class Client: public std::enable_shared_from_this<Client>
    {
    public:
        Client(tcp::socket socket)
        : socket(std::move(socket))
        {
        }
        void start();
        int connectionId;
        tcp::socket socket;
        void Send(ServerOpcode serverOpcode, const std::string& message);

    private:
        void read_header();
        void read_body();
        Packet _packet;
    };
}
#endif //VIBRANIUM_CORE_CLIENT_H

这是我阅读标题和正文的方式:

void Vibranium::Client::read_header() {
    auto self(shared_from_this());
    boost::asio::async_read(socket,
    boost::asio::buffer(_packet.data_, _packet.header_length),
    [this, self](boost::system::error_code ec, std::size_t /*length*/)
    {
        if (!ec)
        {
            std::cout << "Header: " << std::endl;
            std::cout.write(_packet.data_, _packet.header_length);
            std::cout << "\n";
            read_body();
        }
        else
        {
            std::cerr << "Invalid header sent!" << std::endl;
        }
    });
}


void Vibranium::Client::read_body() {
    auto self(shared_from_this());
    socket.async_read_some(boost::asio::buffer(_packet.data_, _packet.body_length),
   [this, self](boost::system::error_code ec, std::size_t length)
   {
       if ((boost::asio::error::eof == ec) || (boost::asio::error::connection_reset == ec))
       {
           Logger::Log("Disconnected ID: " + std::to_string(connectionId),Logger::Error, true);
           for (int i = 0; i < Server::Clients.size(); ++i) {
               if(Server::Clients[i]->connectionId == connectionId)
                   Server::Clients.erase(Server::Clients.begin()+i);
           }
       }
       else
       {
           std::cout << "Body: " << std::endl;
           std::cout.write(_packet.data_, _packet.body_length);
           std::cout << "\n";
           //Send(ServerOpcode::SMSG_AUTH_CONNECTION_RESPONSE,"How are you, mate?");
           read_header();
       }
   });
}

这是我发送带有标题和正文的消息的方式:

Config config("AuthServer");
std::string defaultIP   = "127.0.0.1";
std::string defaultPort = "8080";
int connectionsNumber   = CommandQuestion<int>::AskQuestion("How many connections do you want established?");
std::cout << "Initializing " << std::to_string(connectionsNumber) << " connection/s." << std::endl;

std::cout << "Trying to connect to  " <<  defaultIP << " on port: " << config.GetConfigValue("AuthServerPort", defaultPort)  << std::endl;
boost::asio::io_context io_context;
std::vector<tcp::socket> sockets;
for (int i = 0; i < connectionsNumber; ++i) {
    try
    {
        sockets.emplace_back(io_context);
        tcp::socket& s{sockets.back()};
        tcp::resolver resolver(io_context);
        boost::asio::connect(s, resolver.resolve( defaultIP,config.GetConfigValue("AuthServerPort", defaultPort)));

        enum { body_length = 1024 };
        enum { header_length = 8 };
        enum { max_length = body_length +  header_length};
        char header_[header_length];
        char body_[body_length];
        char data_[header_length + body_length];

        ServerOpcode opc;
        opc = ServerOpcode::SMSG_AUTH_CONNECTION_RESPONSE;
        std::string message = "I am testing here!!!";


        snprintf(header_,header_length,"%x\n",opc);
        strcpy(body_, message.c_str());
        sprintf(data_, "%s %s", header_, body_);

        size_t request_length = sizeof(data_)/sizeof(*data_);

        std::cout << "header: " << header_ << std::endl;
        std::cout << "body: " << body_ << std::endl;
        std::cout << "whole message is: " << data_ << std::endl;
        std::cout << "size: " << request_length << std::endl;
        std::cout << "max size: " << max_length << std::endl;

        boost::asio::write(s, boost::asio::buffer(data_, request_length));
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

}

这是服务器上的输出:

New Connection (ID: 1)
Header: 
1
 I am 
Body: 
testing here!!!
hP��U��TK���U�U�� K�h
�*�� K�) �J�Uh
 0�y���x������������RK�Px���� K��RK�`x��\TUK��TK��;�#K��TJ�d�"K��XUK�v\TUK� �TK�{�|@X#K� �TJ�`FK��XUK��\TUK� �TK��G�|X^#K� �TJ�4AK��XUK�:\TUK��TK�z���M$K��TJ���#K��XUK��
u��u�z��x�TK��u��<}(K�@u��Pu���aUK��TJ��TK������TJ��TK�x�TK���U�{��������UK��M$K��TK����@K��{����UK�
Invalid header sent!

这是客户端的输出:

header: 1

body: I am testing here!!!
whole message is: 1
 I am testing here!!!
size: 1032
max size: 1032

Process finished with exit code 15

为了不让这个问题被代码过度淹没,这里是Packet.h的内容:https://pastebin.com/cnNzpRpVPacket.cpphttps://pastebin.com/mbZPxf4e

我可以看到,标题和正文都以某种方式被传输,但是它们没有被正确解析。为什么我会在服务器上收到这种未成形的输出?我的错误在哪里,我该如何解决?

【问题讨论】:

    标签: c++ sockets network-programming boost-asio


    【解决方案1】:

    分析

    让我们单步执行代码以了解发生了什么。在您的客户端中,您正在初始化一个大小为 8+1024 的缓冲区,您将一些数据填充到此:

    char data_[header_length + body_length];
    ...
    sprintf(data_, "%s %s", header_, body_);
    

    可以看到写入data_的数据小于整个缓冲区大小。由于您没有对data_ 进行零初始化,它的剩余部分将填充随机数据(无论之前发生在堆栈上的那个位置)。字符串"%s %s" 是一个以零结尾的字符串,这意味着在内存中它后面将跟一个结束的零字节。因此,当sprintf 将标题和正文插入此格式化字符串时,您将再次获得写入data_ 的以零结尾的字符串。所以data_ 缓冲区将包含:

    [--- header ---] <space> [--- body---] <zero-byte> [--- random data ---]
    

    还要注意缓冲区中的标头和正文不与header_lengthbody_length 对齐。 IE。如果你写的头部数据比header_length短,那么&lt;space&gt;会提前到达,body数据会在缓冲区的第一个header_length字节内。

    当你打印缓冲区时,到达零字节时会自动停止:

    std::cout << "whole message is: " << data_ << std::endl;
    

    因此在客户端打印不会显示缓冲区末尾的随机数据。

    然后将整个缓冲区发送到服务器:

    size_t request_length = sizeof(data_)/sizeof(*data_);
    boost::asio::write(s, boost::asio::buffer(data_, request_length));
    

    在服务器中,你读取的缓冲区大小等于标头的大小:

     boost::asio::async_read(socket,boost::asio::buffer(_packet.data_, _packet.header_length), ...);
    

    所以这将处理您在客户端中写入的缓冲区的第一个 header_length 字节。如上所述,缓冲区的第一个header_length 字节可以包含分隔空间和正文的某些部分,具体取决于您写入客户端缓冲区的标头数据有多短。出于这个原因,当你用这一行在服务器中打印 header 时,你会看到 body 数据的一部分:

    std::cout.write(_packet.data_, _packet.header_length);
    

    然后您通过读取body_length 多个字节来继续读取正文。这将是您在客户端中写入的剩余字节。因此它将包含正文的剩余部分(减去已作为标头一部分处理的部分)、零字节,然后是随机数据。

    你用这一行打印缓冲区,它不会在零字节处停止:

    std::cout.write(_packet.data_, _packet.body_length);
    

    因此,您会在日志中看到身体被截断的部分,然后是一些随机数据。

    如何解决?

    首先,当您创建缓冲区时,您应该通过对其进行零初始化来确保它不包含随机数据。否则,在通过网络发送缓冲区时,您有可能泄露敏感数据(加密密钥、密码)的风险。请考虑以下示例,了解如何进行零初始化:

      std::cout << "Printing test" << std::endl;
    
      // Not zero-initialized, will print garbage.
      char test[10];
      std::cout.write(test, sizeof(test));
      std::cout << std::endl;
    
      std::cout << "Printing test2" << std::endl;
    
      // Zero-initialized, will print nothing (because \0 is not printable)
      char test2[10]{};
      std::cout.write(test2, sizeof(test2));
      std::cout << std::endl;
    

    其次,您应该确保在标题数据和正文数据之间插入填充,以防标题数据短于header_length。当然,有许多不同的方法可以做到这一点。这是一个例子:

      int op_code = 1;
      std::string header = std::to_string(op_code);
      constexpr size_t header_length = 8;
    
      std::string body{"abc"};
      constexpr size_t body_length = 32;
    
      // Zero-initialize buffer, requires #include <array>
      std::array<char, header_length + body_length> buffer{};
    
      // TODO Check header.size() <= header_length
      // TODO Check body.size() <= body_length
    
      std::copy(header.begin(), header.end(), buffer.begin());
      std::copy(body.begin(), body.end(), buffer.begin() + header_length);
    

    您可以使用以下方法检查缓冲区内容:

      // Debug: Print buffer content, requires #include <iomanip>
      for (char c : buffer) {
        std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(c) << ' ';
      }
      std::cout << std::endl;
    

    这会给你:

    31 00 00 00 00 00 00 00 61 62 63 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    

    您可以看到没有随机数据出现,因为缓冲区已被零初始化。标头数据的大小仅为 1 字节,但在标头数据的末尾和在 header_length 字节之后开始的正文数据之间存在填充字节。

    【讨论】:

    • 出色的解释!非常感谢先生!在我提出问题后,我碰巧想出了一个解决方案。我已经将 header_ 从 char 更改为 std::string header_; 比在将其写入流之前我做了 header = std::to_string(opc);header.resize(header_length); 然后在阅读方面我再次调整大小。在处理 TCP 数据包时,使用 char 数组而不是 string 调整大小有什么显着的好处吗?
    • 两点:从编码风格的角度来看,我更喜欢std::array 变体,因为它是expresses intent(即你想处理二进制blob而不是文本)。从性能的角度来看,std::array 变体的性能会稍好一些。我做了一些简单的测量,两个变体重复了 10 亿次,用-O3 编译,std::to_string + 复制到std::array 变体得到 5s,std::to_string + 调整大小得到 16s。您可能关心也可能不关心这种差异。
    • 我不确定如何将标题和长度的std::arrays 结合在一起,以及如何在另一侧读取它们。你能举个例子来说明我如何在接收部分用 char 数组读取数据吗?
    • 还有动态车身尺寸。一条消息具有一种体型,另一种则不同。我打算创建一个1.Header 2.BodySize 3.Body 的序列。就此而言,正文大小将设置在第二个平面,因此当阅读部分看到标题时,它将在标题之后立即准备好正文大小,然后知道预期的正文大小将准确读取那么多。然而,这将迫使我调整buffer{} 的大小。我该怎么做?这是个好主意吗?
    • 用你的方法我又问了一个问题:stackoverflow.com/questions/64027066/…你能看看吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-12-10
    • 2014-07-11
    • 1970-01-01
    • 1970-01-01
    • 2018-06-08
    • 2011-08-09
    • 2012-06-06
    相关资源
    最近更新 更多