【问题标题】:Parsing custom data packets in an object oriented manner以面向对象的方式解析自定义数据包
【发布时间】:2019-09-11 01:43:19
【问题描述】:

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

以下是此类自定义数据包的粗略示意图:

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

(假设 Magic 是 4 个字节,Command 是 1 个字节,Options 是 2 个字节,Bodysize 是 4 个字节,并且 body 本身的长度是可变的。) 我如何在不使用任何第三方库的情况下解析它?

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

#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++ 进行开发,并且为了通过套接字发送和接收数据,使用了 boost 库。

【问题讨论】:

  • “我不喜欢创建一个仅包含标头信息的 Packet-Object,然后再添加正文数据” - 为什么不呢?
  • 您是否考虑过使用现有的广泛使用的序列化系统(例如 Google protobuf),而不是重新发明轮子?
  • @Jesper 我认为这将是一个糟糕的设计,因为可以在不完整的状态下访问该对象。我不确定什么是常见的做法,我也不知道什么是最好的设计选择。我正在考虑一些执行解析的数据包处理器和一个已填充且只有在填充了所有数据后才能访问的数据包数据容器
  • @rici 是的,我有,我更喜欢自己做。不是因为这一定更好,而是因为它帮助我加深了对这些东西如何工作以及它们是如何设计的理解。
  • @JesperJuhl 我更新了我的帖子并添加了更多信息。

标签: c++ parsing networking packet


【解决方案1】:

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

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

// 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 的第一篇文章,所以如果帖子有问题,请告诉我,谢谢。

【讨论】:

  • 感谢您的贡献。据我了解,这仅在事先已知数据包长度的情况下才有效?标头包含正文长度,但您的构造函数要求我提前提供给定数量的数据,然后进行解析?此外,包含已发布代码的使用示例总是有帮助的;-)
  • 虽然我不认为std::vector 在这里是一个重大的性能问题,但如果你不想使用它,你应该使用std::unique_ptr&lt;char[]&gt;。此外,“cast & assignment”是未定义的行为(cf. plans for implicit object creation)。
  • @Kyu96 你说得对,也许你可以使用带有 header 和 body setter 的 packetbuilder,当请求不完整的 body/packet 时抛出异常?
  • @DavisHerring 正如您所建议的那样,使用唯一(共享取决于上下文)指针是 IMO 的正确方法,感谢您提供额外信息。
【解决方案2】:

对于这种情况,我将使用 pipeline design pattern 创建 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;
        };
    }
}

【讨论】:

  • 这听起来是一个非常有趣的方法,也许你可以为我的场景提供一个粗略的例子?你会做header解析,body解析两个pipeline步骤吗?
  • 客户端如何获知要发送到Manager的数据量?
  • @Davis Herring:Manager 是一个解析数据包内容的状态机。它一次接收整个数据包或一大块数据包。它处理碎片数据包。
  • @Kyu96:将其更改为只有两个步骤,标题和正文非常简单。
  • @Flaviu:如果一个读取的长度足以包含多个,那么客户端如何接收这些数据包?
【解决方案3】:

我猜你正在尝试Object-oriented networking。如果是这样,这种解析的最佳解决方案是FlatbuffersCap’n Proto C++ 代码生成器。通过定义模式,您将获得状态机代码,该代码将以高效且安全的方式解析数据包。

【讨论】:

    【解决方案4】:

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

    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

    【讨论】:

    • 有趣的方法。还有几个问题:为 Body 制作一个单独的结构不是更好的做法吗?另外,您为什么选择结构而不是类?此外,我在想是否可以让 Packet 类本身成为一个接口?也许像其他类可以继承的 IPacket 之类的东西?您对此有何看法?
    • @Kyu96:只包含Body 的结构有什么用途?它不会抽象任何东西。至于struct vs. class,除了默认的访问控制之外,它们完全一样。如果您需要动态多态性,则需要一个接口,而您的问题陈述没有建议。
    猜你喜欢
    • 1970-01-01
    • 2013-03-11
    • 1970-01-01
    • 1970-01-01
    • 2019-11-20
    • 2020-08-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多