Разбор потока двоичных сообщений в C / C ++

Пишу декодер для бинарного протокола (протокол 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). Это намного короче, но все же подвержено ошибкам и избыточно. Более того, формат может быть более сложным, поскольку сообщения могут содержать массивы, поэтому синтаксический анализатор будет медленным и сложным.

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

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


person gaga    schedule 20.01.2011    source источник
comment
Я полагаю, что, используя типы кортежей для определения сообщений, вы могли бы написать шаблоны функций, которые перебирают элементы кортежа и вызывают соответствующую функцию извлечения для любого типа, который вы используете. Однако я не могу придумать идею автоматического преобразования этих кортежей в структуры.   -  person sbi    schedule 20.01.2011
comment
Предполагая, что вы используете MSVC ++, #pragma pack(1) должен работать даже на других платформах. Упаковка реализована в терминах битовых сдвигов и масок, а не исправлений выравнивания операционной системы.   -  person Billy ONeal    schedule 20.01.2011
comment
Ваши данные неупакованы, не выровнены. Так что единственный правильный способ сделать это - побайтовый доступ, такой как unpack, предложенный @larsmans.   -  person 9dan    schedule 20.01.2011
comment
@sbi, я тоже не могу понять) @Billy un luckaltey Я пишу для QNX и других экзотических платформ.   -  person gaga    schedule 20.01.2011
comment
Я некоторое время играл с этим и обновил свой ответ полноценной версией, которая, похоже, делает то, что вы хотите. HTH.   -  person sbi    schedule 22.01.2011


Ответы (5)


Хорошо, следующие компилируются для меня с 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(), но я на самом деле этого не пробовал.

person sbi    schedule 20.01.2011
comment
хороший пример использования кортежа ... дает довольно хороший синтаксис. C ++ 11 великолепен. Тем лучше, если вы предоставите полный исходный код на ideone.com! - person oliver; 27.07.2012

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

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

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

Это по-прежнему требует много работы и подвержено ошибкам, но, по крайней мере, помогает проверять наличие ошибок.

person stefaanv    schedule 20.01.2011
comment
Класс читателя == std::istream или std::streambuf. - person Billy ONeal; 20.01.2011
comment
@ Билли: так оно и есть. Некоторое время я использую класс Reader, поэтому мне никогда не приходилось использовать более стандартную систему. Хорошо подмечено. - person stefaanv; 20.01.2011
comment
Да, но это то, что я называю написанием специальной процедуры синтаксического анализа для каждого сообщения) - person gaga; 20.01.2011
comment
+1 Красиво. Мне нравится, как вы показываете, что данные читаются каждому члену, а не всей структуре. Мне также нравится, как читатель может обрабатывать Endianess без необходимости изменять структуру получения. - person Thomas Matthews; 20.01.2011
comment
@gaga: убедитесь, что это конкретное сообщение, но если у вас есть сообщения, определенные где-то, скажем, в файле заголовка, вы можете написать сценарий, который генерирует для вас что-то подобное, указанное выше, с этим файлом заголовка в качестве ввода. - person murrekatt; 20.01.2011

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

person Nim    schedule 20.01.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;
}

Специализируйте YourStreamType operator>> для любого базового типа, который содержится в ваших сообщениях, и вы должны:

MsgData msg;
your_stream >> msg;
person peoro    schedule 21.01.2011

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

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)); ...
person ikrabbe    schedule 02.07.2015