【问题标题】:Parsing binary message stream in C/C++在 C/C++ 中解析二进制消息流
【发布时间】:2011-06-12 13:39:25
【问题描述】:

我正在为二进制协议(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 并在 Google 上搜索过,但没有找到更好的方法。

也许 C++ 有解决方案?

【问题讨论】:

  • 我想使用元组类型来定义消息,您可以编写函数模板来迭代元组成员并为您使用的任何类型调用适当的提取函数。但是,我想不出将这些元组自动转换为结构的想法。
  • 假设您使用的是 MSVC++ #pragma pack(1) 即使在其他平台上也应该可以工作。打包是根据位移和掩码实现的,而不是操作系统对齐修复。
  • 您的数据未打包,未对齐。因此,唯一正确的做法是按字节访问,例如@larsmans 建议的unpack
  • @sbi 我也想不通)@Billy unfortunaltey 我正在为 QNX 和其他异国平台编写代码。
  • 我已经玩了一段时间,并用似乎可以满足您要求的完整版本更新了我的答案。 HTH。

标签: c++ c parsing binary memory-alignment


【解决方案1】:

你总是可以自己调整你的记忆:

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

      好的,以下是使用 VC10 和 GCC 4.5.1 (on 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 Rocks。最好在 ideone.com 上提供完整源代码!
      【解决方案4】:

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

      在你的情况下:

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

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

      【讨论】:

      • “读者类” == std::istreamstd::streambuf
      • @Billy:原来如此。我使用 Reader 类已经有一段时间了,所以我从来没有使用过更标准的系统。很好发现。
      • 是的,但这就是我所说的“为每条消息编写特定的解析例程”)
      • +1 不错。我喜欢你如何显示读取每个成员的数据,而不是读取整个结构。我也喜欢读者无需修改接收结构即可处理 Endianess 的方式。
      • @gaga:确定这是特定于消息的,但是如果您在某处定义了消息,例如在头文件中,您可以编写一个脚本,使用该头文件为您生成类似于上述内容的内容输入。
      【解决方案5】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-02-20
        • 1970-01-01
        • 2011-03-17
        • 1970-01-01
        • 2019-11-16
        • 1970-01-01
        • 2018-11-08
        相关资源
        最近更新 更多