7

我正在为二进制协议(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++ 有解决方案?

4

5 回答 5

7

好的,以下使用 VC10 和 GCC 4.5.1(在 ideone.com 上)为我编译。我认为 C++1x 的所有这些需求都是<tuple>,它也应该std::tr1::tuple在旧编译器中可用(as )。

它仍然需要您为每个成员键入一些代码,但那是非常少的代码。(见最后我的解释。)

#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;
}

对我来说,这打印

缓冲区大小为 6
初始化数据...数据现在是 [0x2a 1.7 0x11]
清除数据...数据现在是 [0x0 0 0x0]
正在读取数据...数据现在是 [0x2a 1.7 0x11]

(我已将您的MsgData类型缩短为仅包含三个数据成员,但这仅用于测试。)

对于每种消息类型,您需要定义其buffer_size静态常量和两个get_tied_tuple()成员函数,一个const和一个 non- const,两者都以相同的方式实现。(当然,这些也可以是非成员,但我试图让它们靠近它们所绑定的数据成员列表。)
对于某些类型(如std::string),您将需要添加这些detail::read_value()detail::write_value()函数的特殊重载.
对于所有消息类型,其余机制保持不变。

有了完整的 C++1x 支持,您也许可以不必完全键入get_tied_tuple()成员函数的显式返回类型,但我实际上并没有尝试过。

于 2011-01-20T16:39:04.057 回答
3

我解析二进制输入的解决方案是使用 Reader 类,因此每个消息条目都可以定义读取的内容,并且阅读器可以检查溢出、欠载、...。

在你的情况下:

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

它仍然需要大量工作并且容易出错,但至少它有助于检查错误。

于 2011-01-20T16:10:05.087 回答
1

简单的答案是否定的,如果消息是无法简单转换的特定二进制格式,您别无选择,只能为其编写解析器。如果您有消息描述(比如 xml 或某种形式的易于解析的描述),为什么不从该描述中自动生成解析代码?它不会像演员表那样快,但会比手动编写每条消息更快地生成视觉......

于 2011-01-20T16:08:49.513 回答
1

我认为您无法避免为纯 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;
}

专门针对您的消息包含YourStreamTypeoperator>>任何基本类型,您应该完成:

MsgData msg;
your_stream >> msg;
于 2011-01-21T22:11:41.313 回答
0

您可以随时调整自己的记忆:

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)); ...
于 2015-07-02T23:02:59.297 回答