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

时间:2019-04-21 16:39:40

标签: c++ parsing networking packet

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

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

MainBoard

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

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

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

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

我首先收到标头,并且在收到标头后,再进行一次接收调用以读取正文。 具有一个将信息填充到数据包对象中的解析器实例,使其仅在拥有所有必需信息后才可访问,这是否有意义?在标题和正文中使用单独的类是否有意义?最佳的设计选择是什么?

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

4 个答案:

答案 0 :(得分:1)

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

function getEvenAverage(tab) {
        var numberOfEvens = 0;
        var sum = 0;
        for(var i=0;i<tab.length;i++){
             if(tab[i]%2 == 0 ){
                 numberOfEvens++;
                 sum += tab[i];
             }
        }
        if(numberOfEvens == 0)return null;
        return sum/numberOfEvens;
    }
    
    console.log(getEvenAverage([0,1,2,3,4,5]))
    console.log(getEvenAverage([1,2,3,4,5]))
    console.log(getEvenAverage([0,1,11,3,4,5]))
    console.log(getEvenAverage([1,5,3]))

请注意,私有继承会阻止未定义的行为通过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);} 删除Packet以及确实意外的

Header*

合成当然也可以,但是在完整的实现中可能会导致更多适配器代码。

如果您确实在进行优化,则可以制作一个const Packet p=read(s); const Packet q=read2(p,s); // same header?! 而没有正文大小,然后从中得出HeaderOnlyHeader

答案 1 :(得分:1)

在这种情况下,我将使用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;
        };
    }
}

答案 2 :(得分: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上的第一篇文章,因此,如果有问题,请告诉我。

答案 3 :(得分:0)

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