使用C/C++解析二进制消息流

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)。这样做虽然更短,但仍然容易出错且冗余。此外,由于消息可能包含数组,所以格式可能更加复杂,因此解析器会变得缓慢和复杂。
有没有通用且有效的解决方案?我读了this post并进行了谷歌搜索,但没有找到更好的方法。
也许C++有一个解决方案?

我想,使用元组类型来定义消息,您可以编写函数模板,迭代元组成员并调用适当的提取函数以获取您正在使用的任何类型。但是,我无法想出一种自动将这些元组转换为结构体的方法。 - sbi
假设您正在使用MSVC++,#pragma pack(1)应该即使在其他平台上也可以工作。打包是通过位移和掩码实现的,而不是操作系统对齐修复。 - Billy ONeal
您的数据未经过打包和对齐。因此,唯一正确的方法是按字节访问,就像@larsmans建议的那样使用unpack函数。 - 9dan
@sbi 我也想不出来) @Billy 不幸的是,我正在为QNX和其他奇特的平台编写代码。 - gaga
我已经玩了一会儿,并更新了我的答案,提供了一个完整的版本,似乎可以做到你想要的。希望有帮助。 - sbi
5个回答

7

好的,以下内容对我来说在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;
}

对我来说,这会打印出

缓冲区大小为6
正在初始化数据...数据现在为[0x2a 1.7 0x11]
正在清除数据...数据现在为[0x0 0 0x0]
正在读取数据...数据现在为[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()成员函数的显式返回类型,但我实际上还没有尝试过。


使用元组的很好的例子...使语法非常简洁。C++11 真的很棒。最好提供在 ideone.com 上的完整源代码! - oliver

3
我的解决方案是使用Reader类来解析二进制输入,因此您可以为每个消息条目定义要读取的内容,而Reader可以检查是否有溢出、欠流等问题。
在您的情况下:
msg.num = Reader.getChar();
msg.x = Reader.getFloat();
msg.y = Reader.getFloat();
msg.z = Reader.getFloat();
msg.elevation = Reader.getChar();

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


3
"Reader Class" 等同于 std::istream 或者 std::streambuf - Billy ONeal
@Billy:确实如此。我已经使用Reader类有一段时间了,所以从来没有用过更标准的系统。很好发现。 - stefaanv
1
是的,但这就是我所说的“为每个消息编写特定的解析程序”。 - gaga
+1 很好。我喜欢你如何显示数据被读取到每个成员,而不是读取整个结构。我也喜欢读者可以处理字节序而不需要修改接收结构的方式。 - Thomas Matthews
2
@gaga:当然,这是特定于消息的,但如果您在某个地方定义了消息,比如在头文件中,您可以编写一个脚本,以该头文件作为输入生成类似上面的内容。 - murrekatt

1

我认为你无法避免在纯C++中为每个消息编写特定的解析程序(不使用#pragma)。

如果所有的消息都是简单的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;

1
简单的回答是不行的,如果消息是一个特定的二进制格式,不能简单地转换,你别无选择,只能为它编写一个解析器。如果你有消息描述(比如xml或某种易于解析的描述形式),为什么不从那个描述自动生成解析代码呢?虽然不会像转换一样快,但比手动编写每个消息要快得多...

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)); ...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接