Разбор двоичного потока сообщений в C / C ++ - PullRequest
6 голосов
/ 20 января 2011

Я пишу декодер для двоичного протокола (протокол Javad GRIL).Он состоит из около ста сообщений с данными в следующем формате:

struct MsgData {
    uint8_t num;
    float x, y, z;
    uint8_t elevation;
    ...
};

Поля представляют собой двоичные числа в кодировке ANSI, которые следуют друг за другом без пробелов.Самый простой способ разобрать такие сообщения - привести входной массив байтов к соответствующему типу.Проблема в том, что данные в потоке упакованы, т.е. не выровнены.

На x86 это можно решить с помощью #pragma pack(1).Однако это не будет работать на некоторых других платформах или повлечет за собой снижение производительности из-за дальнейшей работы со смещенными данными.

Другой способ - написать специальную функцию синтаксического анализа для каждого типа сообщения, но, как я уже говорилпротокол включает в себя сотни сообщений.

Еще одна альтернатива - использовать что-то вроде функции Perl unpack() и где-то хранить формат сообщения.Скажем, мы можем #define MsgDataFormat "CfffC", а затем позвонить unpack(pMsgBody, MsgDataFormat).Это намного короче, но все еще подвержено ошибкам и избыточно.Кроме того, формат может быть более сложным, поскольку сообщения могут содержать массивы, поэтому анализатор будет медленным и сложным.

Есть ли какое-либо общее и эффективное решение?Я прочитал этот пост и погуглил, но не нашел лучшего способа сделать это.

Может быть, у C ++ есть решение?

Ответы [ 5 ]

7 голосов
/ 20 января 2011

Хорошо, следующие компиляции для меня с VC10 и с GCC 4.5.1 ( на ideone.com ).Я думаю, что все эти потребности C ++ 1x - <tuple>, которые должны быть доступны (как std::tr1::tuple) и в старых компиляторах.

Вам все еще нужно набрать некоторый код для каждого участника, но это очень минимальный код.(См. Мое объяснение в конце.)

#include <iostream>
#include <tuple>

typedef unsigned char uint8_t;
typedef unsigned char byte_t;

struct MsgData {
    uint8_t num;
    float x;
    uint8_t elevation;

    static const std::size_t buffer_size = sizeof(uint8_t)
                                         + sizeof(float) 
                                         + sizeof(uint8_t);

    std::tuple<uint8_t&,float&,uint8_t&> get_tied_tuple()
    {return std::tie(num, x, elevation);}
    std::tuple<const uint8_t&,const float&,const uint8_t&> get_tied_tuple() const
    {return std::tie(num, x, elevation);}
};

// needed only for test output
inline std::ostream& operator<<(std::ostream& os, const MsgData& msgData)
{
    os << '[' << static_cast<int>(msgData.num) << ' ' 
       << msgData.x << ' ' << static_cast<int>(msgData.elevation) << ']';
    return os;
}

namespace detail {

    // overload the following two for types that need special treatment
    template<typename T>
    const byte_t* read_value(const byte_t* bin, T& val)
    {
        val = *reinterpret_cast<const T*>(bin);
        return bin + sizeof(T)/sizeof(byte_t);
    }
    template<typename T>
    byte_t* write_value(byte_t* bin, const T& val)
    {
        *reinterpret_cast<T*>(bin) = val;
        return bin + sizeof(T)/sizeof(byte_t);
    }

    template< typename MsgTuple, unsigned int Size = std::tuple_size<MsgTuple>::value >
    struct msg_serializer;

    template< typename MsgTuple >
    struct msg_serializer<MsgTuple,0> {
        static const byte_t* read(const byte_t* bin, MsgTuple&) {return bin;}
        static byte_t* write(byte_t* bin, const MsgTuple&)      {return bin;}
    };

    template< typename MsgTuple, unsigned int Size >
    struct msg_serializer {
        static const byte_t* read(const byte_t* bin, MsgTuple& msg)
        {
            return read_value( msg_serializer<MsgTuple,Size-1>::read(bin, msg)
                             , std::get<Size-1>(msg) );
        }
        static byte_t* write(byte_t* bin, const MsgTuple& msg)
        {
            return write_value( msg_serializer<MsgTuple,Size-1>::write(bin, msg)
                              , std::get<Size-1>(msg) );
        }
    };

    template< class MsgTuple >
    inline const byte_t* do_read_msg(const byte_t* bin, MsgTuple msg)
    {
        return msg_serializer<MsgTuple>::read(bin, msg);
    }

    template< class MsgTuple >
    inline byte_t* do_write_msg(byte_t* bin, const MsgTuple& msg)
    {
        return msg_serializer<MsgTuple>::write(bin, msg);
    }
}

template< class Msg >
inline const byte_t* read_msg(const byte_t* bin, Msg& msg)
{
    return detail::do_read_msg(bin, msg.get_tied_tuple());
}

template< class Msg >
inline const byte_t* write_msg(byte_t* bin, const Msg& msg)
{
    return detail::do_write_msg(bin, msg.get_tied_tuple());
}

int main()
{
    byte_t buffer[MsgData::buffer_size];

    std::cout << "buffer size is " << MsgData::buffer_size << '\n';

    MsgData msgData;
    std::cout << "initializing data...";
    msgData.num = 42;
    msgData.x = 1.7f;
    msgData.elevation = 17;
    std::cout << "data is now " << msgData << '\n';
    write_msg(buffer, msgData);

    std::cout << "clearing data...";
    msgData = MsgData();
    std::cout << "data is now " << msgData << '\n';

    std::cout << "reading data...";
    read_msg(buffer, msgData);
    std::cout << "data is now " << msgData << '\n';

    return 0;
}

Для меня это печатает

buffer size is 6
initializing data...data is now [0x2a 1.7 0x11]
clearing data...data is now [0x0 0 0x0]
reading data...data is now [0x2a 1.7 0x11]

(я сократил ваш тип MsgData, чтобы он содержал только три элемента данных, но этотолько для тестирования.)

Для каждого типа сообщения вам необходимо определить его buffer_size статическую константу и две get_tied_tuple() функции-члена, одну const и одну не const, обе реализованы втак же.(Конечно, они также могут быть не членами, но я пытался держать их рядом со списком элементов данных, к которым они привязаны.)
Для некоторых типов (например, std::string) вам нужно будет добавитьспециальные перегрузки этих функций detail::read_value() и detail::write_value().
Остальная часть оборудования остается неизменной для всех типов сообщений.

С полной поддержкой C ++ 1x вы можете избавиться отнеобходимость полностью напечатать явные типы возвращаемых функций-членов get_tied_tuple(), но я на самом деле не пробовал этого.

3 голосов
/ 20 января 2011

Мое решение для анализа двоичного ввода состоит в том, чтобы использовать класс Reader, поэтому для каждой записи сообщения вы можете определить, что читается, и читатель может проверить на переполнение, опустошение, ....

В вашем случае:

msg.num = Reader.getChar();
msg.x = Reader.getFloat();
msg.y = Reader.getFloat();
msg.z = Reader.getFloat();
msg.elevation = Reader.getChar();

Это все еще много работы и подвержено ошибкам, но, по крайней мере, это помогает проверять ошибки.

1 голос
/ 22 января 2011

Не думаю, что вы можете избежать написания специальной процедуры синтаксического анализа для каждого сообщения на чистом C ++ (без использования прагмы).

Если все ваши сообщения простые, POD, C-Как и в случае со структурами, я думаю, что самым простым решением было бы написать генератор кода: поместите свои структуры в заголовок без других вещей на C ++ и напишите простой синтаксический анализатор (сценария perl / python / bash, использующего пару регулярных выражений, будет достаточно) -или искать тот, который может найти имена переменных в любом сообщении;затем используйте его, чтобы автоматически сгенерировать некоторый код для любого сообщения для его прочтения, например:

YourStreamType & operator>>( YourStreamType &stream, MsgData &msg ) {
    stream >> msg.num >> msg.x >> msg.y >> msg.z >> msg.elevation;
    return stream;
}

specialize YourStreamType 'operator>> для любого базового типа, содержащегося в ваших сообщениях, и вы должны сделать это:

MsgData msg;
your_stream >> msg;
1 голос
/ 20 января 2011

Простой ответ - нет, если сообщение представляет собой определенный двоичный формат, который нельзя просто преобразовать, у вас нет другого выбора, кроме как написать для него синтаксический анализатор. Если у вас есть описания сообщений (скажем, xml или какая-либо форма легко разбираемого описания), почему бы вам автоматически не сгенерировать код анализа из этого описания? Это не будет так быстро, как приведение, но будет чертовски быстрее генерировать, чем писать каждое сообщение вручную ...

0 голосов
/ 03 июля 2015

Вы всегда можете выровнять свою память самостоятельно:

uint8_t msg[TOTAL_SIZE_OF_THE_PARTS_OF_MsgData];

Поскольку sizeof(MsgData) возвращает размер байтов заполнения MsgData +, вы можете вычислить

enum { TOTAL_SIZE_OF_THE_PARTS_OF_MsgData = 
    2*sizeof(uint8_t)+
    3*sizeof(float)+sizeof(THE_OTHER_FIELDS)
}

Используя перечислениядля таких констант это хорошо зарекомендовавшая себя концепция на нескольких машинах.

читает двоичное сообщение в массив msg.Позже вы можете преобразовать значения в значения MsgData:

unsigned ofs = 0;
MsgData M;
M.num = (uint8_t)(&msg[ofs]);
ofs += sizeof(M.num);
M.x = (float)(&msg[ofs]);
ofs += sizeof(M.x);

и т. Д. *

или использовать memcpy, если вам не нравится приведение типа:

memcpy(&M.x,&msg[ofs],sizeof(M.x)); ...
...