以面向对象的方式解析自定义数据包

Parsing custom data packets in an object oriented manner

本文关键字:自定义 数据包 方式解 面向对象的      更新时间:2023-10-16

我目前正在C++开发一些软件,用于发送和接收自定义数据包。我想以结构良好的方式解析和管理这些数据包。显然,我首先接收标头,然后接收数据正文。主要问题是我不喜欢创建一个只包含标头信息的数据包对象,然后添加正文数据。解析和存储自定义数据包的优雅方法是什么?

以下是这种自定义数据包的粗略草图:

+-------+---------+---------+----------+------+
| Magic | Command | Options | Bodysize | Body |
+-------+---------+---------+----------+------+

(假设魔术是 4 个字节,命令是 1 个字节,选项 2 个字节,正文大小是 4 个字节,主体本身的长度是可变的。 如何在不使用任何第三方库的情况下解析它?

通常我会说可以做这样的事情来存储数据包数据:

#include <array>
class Packet {
public:
explicit Packet(std::array<char, 10> headerbytes);
void set_body(std::vector<char> data);
std::vector<char> get_body();
int8_t get_command();
int16_t get_options();
bool is_valid();
private:
bool valid;
int8_t _command;
int16_t _options;
int32_t body_size;
std::vector<char> _data;
};

问题是我首先提供标题信息,然后以黑客方式添加正文数据。数据包对象具有可在不完整状态下访问的时间点。

我首先收到标头,在收到标头后,会发出另一个接收调用来读取正文。 让一个将信息填充到数据包对象的解析器实例只有在它包含所有需要的信息后才能访问它是否有意义?为标头和正文设置一个单独的类是否有意义?最好的设计选择是什么?

我正在使用C++进行开发,并且为了通过套接字发送和接收数据,使用了升压库。

如果您不想将数据读取绑定到一个完整的构造函数中(出于可以理解的关注点分离的原因),这是非多态继承的良好应用程序:

struct Header {
static constexpr SIZE=10;
Header(std::array<char,SIZE>);
std::int8_t get_command() const {return command;}
std::int16_t get_options() const {return options;}
std::int32_t body_size() const {return length;}
private:
std::int8_t command;
std::int16_t options;
std::int32_t length;
};
struct Packet : private Header {
using Body=std::vector<char>;
Packet(const Header &h,Body b) : Header(h),body(std::move(b))
{if(body.size()!=body_size()) throw …;}
using Header::get_command;
using Header::get_options;
const Body& get_body() const {return body;}
private:
Body body;
};
// For some suitable Stream class:
Header read1(Stream &s)
{return {s.read<Header::SIZE>()};}
Packet read2(const Header &h,Stream &s)
{return {h,s.read(h.body_size())};}
Packet read(Stream &s)
{return read2(read1(s),s);}

请注意,私有继承可防止未定义的行为通过Header*删除Packet,以及肯定是意外的

const Packet p=read(s);
const Packet q=read2(p,s);   // same header?!

组合当然也可以工作,但可能会导致在完整实现中产生更多的适配器代码。

如果你真的在优化,你可以在没有体型的情况下做一个HeaderOnly,并从中得出HeaderPacket

对于这种情况,我将使用管道设计模式创建 3 个数据包处理器类:

  • 命令(也处理魔术字节)
  • 选项
  • 身体
  • (处理身体大小也)

全部派生自一个基类。

typedef unsigned char byte;
namespace Packet
{
namespace Processor
{
namespace Field
{
class Item
{
public:
/// Returns true when the field was fully processed, false otherwise.
virtual bool operator () (const byte*& begin, const byte* const end) = 0;
};
class Command: public Item
{
public:
virtual bool operator () (const byte*& begin, const byte* const end);
};
class Options: public Item
{
public:
virtual bool operator () (const byte*& begin, const byte* const end);
};
class Body: public Item
{
public:
virtual bool operator () (const byte*& begin, const byte* const end);
};
}
class Manager
{
public:
/// Called every time new data is received
void operator () (const byte* begin, const byte* const end)
{
while((*fields[index])(begin, end))
{
incrementIndex();
}
}
protected:
void incrementIndex();
Field::Command command;
Field::Options options;
Field::Body body;
Field::Item* const fields[3] = { &command, &options, &body };
byte index;
};
}
}

您可以使用异常来防止创建不完整的数据包对象。

为了提高性能,我会使用字符指针而不是向量。

// not intended to be inherited
class Packet final {
public:
Packet(const char* data, unsigned int data_len) {
if(data_len < header_len) {
throw std::invalid_argument("data too small");
}
const char* dataIter = data;
if(!check_validity(dataIter)) {
throw std::invalid_argument("invalid magic word");
}
dataIter += sizeof(magic);
memcpy(&command, dataIter, sizeof(command)); // can use cast & assignment, too
dataIter += sizeof(command);
memcpy(&options, dataIter, sizeof(options)); // can use cast & assignment, too
dataIter += sizeof(options);
memcpy(&body_size, dataIter, sizeof(body_size)); // can use cast & assignment, too
dataIter += sizeof(body_size);
if( data_len < body_size+header_len) {
throw std::invalid_argument("data body too small");
}
body = new char[body_size];
memcpy(body, dataIter, body_size);
}
~Packet() {
delete[] body;
}
int8_t get_command() const {
return command;
}
int16_t get_options() const {
return options;
}
int32_t get_body_size() const {
return body_size;
}
const char* get_body() const {
return body;
}
private:
// assumes len enough, may add param in_len for robustness
static bool check_validity(const char* in_magic) {
return ( 0 == memcmp(magic, in_magic, sizeof(magic)) );
}
constexpr static char magic[] = {'a','b','c','d'};
int8_t command;
int16_t options;
int32_t body_size;
char* body;
constexpr static unsigned int header_len = sizeof(magic) + sizeof(command)
+ sizeof(options) + sizeof(body_size);
};

注意:这是我在SO的第一篇文章,所以如果帖子有问题,请告诉我,谢谢。

我猜你正在尝试面向对象的网络。如果是这样,这种解析的最佳解决方案是 Flatbuffers 或 Cap'n Proto C++代码生成器。通过定义模式,您将获得状态机代码,该代码将以高效和安全的方式解析数据包。