Boost ASIO - TCP 数据包的 Header 和 Body 无效
Boost ASIO - Invalid Header and Body for TCP packet
我正在使用 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
这是我阅读 header 和 body 的方式:
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();
}
});
}
这是我用 header 和 body 发送消息的方式:
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
为了不把这个问题搞得极端over-flooded,代码这里是Packet.h
的内容:https://pastebin.com/cnNzpRpV and Packet.cpp
: https://pastebin.com/mbZPxf4e
我能看到的是 header 和 body 都以某种方式被转移,但是它们没有被正确解析。为什么我在服务器上收到这种未成形的输出?我的错误在哪里,我该如何解决?
分析
让我们逐步查看代码以了解发生了什么。在您的客户端中,您正在初始化一个大小为 8+1024
的缓冲区,您将一些数据填充到其中:
char data_[header_length + body_length];
...
sprintf(data_, "%s %s", header_, body_);
可以看到写入data_
的数据小于整个缓冲区大小。因为你没有 zero-initialize data_
,它的剩余部分将填充随机数据(无论之前碰巧在堆栈的那个位置)。字符串 "%s %s"
是一个 zero-terminated 字符串,这意味着在内存中它将后跟一个结束符 zero-byte。因此,当 sprintf
将 header 和 body 插入此格式字符串时,您将再次获得写入 data_
的 zero-terminated 字符串。所以 data_
缓冲区将包含:
[--- header ---] <space> [--- body---] <zero-byte> [--- random data ---]
另请注意,缓冲区中的 header 和 body 与 header_length
和 body_length
不对齐。 IE。如果您写入的 header 数据比 header_length
短,那么 <space>
会提前并且 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));
在服务器中,您读取的缓冲区大小等于 header:
boost::asio::async_read(socket,boost::asio::buffer(_packet.data_, _packet.header_length), ...);
所以这将处理您在客户端中写入的缓冲区的前 header_length
个字节。如上所述,缓冲区的前 header_length
字节可以包含分隔符 space 和 body 的某些部分,具体取决于您写入的 header 数据的长度客户端中的缓冲区是。因此,当您使用此行在服务器中打印 header 时,您将看到 body 数据的某些部分:
std::cout.write(_packet.data_, _packet.header_length);
然后您通过读取 body_length
个字节继续读取 body。这将是您在客户端中写入的剩余字节。因此它将包含 body 的剩余部分(减去已作为 header 的一部分处理的部分)、零字节,然后是随机数据。
你用这一行打印缓冲区,它不会停在 zero-byte:
std::cout.write(_packet.data_, _packet.body_length);
因此,您将在日志中看到 body 的 cut-off 部分,然后是一些随机数据。
如何解决这个问题?
首先,当您创建缓冲区时,您应该通过 zero-initializing 确保它不包含随机数据。否则,当通过网络发送缓冲区时,您将面临敏感数据(加密密钥、密码)可能泄露的风险。考虑以下示例,了解如何 zero-initialize:
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 [=17=] is not printable)
char test2[10]{};
std::cout.write(test2, sizeof(test2));
std::cout << std::endl;
其次,您应该确保在 header 数据和 body 数据之间插入填充,以防 header 数据短于 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
您可以看到没有随机数据出现,因为缓冲区已被零初始化。 header 数据的大小仅为 1 个字节,但在 header 数据的末尾和 header_length
字节之后开始的 body 数据之间存在填充字节。
我正在使用 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
这是我阅读 header 和 body 的方式:
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();
}
});
}
这是我用 header 和 body 发送消息的方式:
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
为了不把这个问题搞得极端over-flooded,代码这里是Packet.h
的内容:https://pastebin.com/cnNzpRpV and Packet.cpp
: https://pastebin.com/mbZPxf4e
我能看到的是 header 和 body 都以某种方式被转移,但是它们没有被正确解析。为什么我在服务器上收到这种未成形的输出?我的错误在哪里,我该如何解决?
分析
让我们逐步查看代码以了解发生了什么。在您的客户端中,您正在初始化一个大小为 8+1024
的缓冲区,您将一些数据填充到其中:
char data_[header_length + body_length];
...
sprintf(data_, "%s %s", header_, body_);
可以看到写入data_
的数据小于整个缓冲区大小。因为你没有 zero-initialize data_
,它的剩余部分将填充随机数据(无论之前碰巧在堆栈的那个位置)。字符串 "%s %s"
是一个 zero-terminated 字符串,这意味着在内存中它将后跟一个结束符 zero-byte。因此,当 sprintf
将 header 和 body 插入此格式字符串时,您将再次获得写入 data_
的 zero-terminated 字符串。所以 data_
缓冲区将包含:
[--- header ---] <space> [--- body---] <zero-byte> [--- random data ---]
另请注意,缓冲区中的 header 和 body 与 header_length
和 body_length
不对齐。 IE。如果您写入的 header 数据比 header_length
短,那么 <space>
会提前并且 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));
在服务器中,您读取的缓冲区大小等于 header:
boost::asio::async_read(socket,boost::asio::buffer(_packet.data_, _packet.header_length), ...);
所以这将处理您在客户端中写入的缓冲区的前 header_length
个字节。如上所述,缓冲区的前 header_length
字节可以包含分隔符 space 和 body 的某些部分,具体取决于您写入的 header 数据的长度客户端中的缓冲区是。因此,当您使用此行在服务器中打印 header 时,您将看到 body 数据的某些部分:
std::cout.write(_packet.data_, _packet.header_length);
然后您通过读取 body_length
个字节继续读取 body。这将是您在客户端中写入的剩余字节。因此它将包含 body 的剩余部分(减去已作为 header 的一部分处理的部分)、零字节,然后是随机数据。
你用这一行打印缓冲区,它不会停在 zero-byte:
std::cout.write(_packet.data_, _packet.body_length);
因此,您将在日志中看到 body 的 cut-off 部分,然后是一些随机数据。
如何解决这个问题?
首先,当您创建缓冲区时,您应该通过 zero-initializing 确保它不包含随机数据。否则,当通过网络发送缓冲区时,您将面临敏感数据(加密密钥、密码)可能泄露的风险。考虑以下示例,了解如何 zero-initialize:
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 [=17=] is not printable)
char test2[10]{};
std::cout.write(test2, sizeof(test2));
std::cout << std::endl;
其次,您应该确保在 header 数据和 body 数据之间插入填充,以防 header 数据短于 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
您可以看到没有随机数据出现,因为缓冲区已被零初始化。 header 数据的大小仅为 1 个字节,但在 header 数据的末尾和 header_length
字节之后开始的 body 数据之间存在填充字节。