ROS中的序列化实现

发布时间:2023-09-14  

理解了序列化,再回到。我们发现,ROS没有采用第三方的序列化工具,而是选择自己实现,代码在roscpp_core项目下的roscpp_serialization中,见下图。这个功能涉及的代码量不是很多。

为什么ROS不使用现成的序列化工具或者库呢?可能ROS诞生的时候(2007年),有些序列化库可能还不存在(protobuf诞生于2008年),更有可能是ROS的创造者认为当时没有合适的工具。

1.2.1 serialization.h

核心的函数都在serialization.h里,简而言之,里面使用了标准库的memcpy函数把消息拷贝到流中。

下面来看一下具体的实现。

序列化功能的特点是要处理很多种数据类型,针对每种具体的类型都要实现相应的序列化函数。

为了尽量减少代码量,ROS使用了模板的概念,所以代码里有一堆的mplate。

从后往前梳理,先看Stream这个结构体吧。在里结构体和类基本没什么区别,结构体里也可以定义函数。

Stream翻译为流,流是一个计算机中的抽象概念,前面我们提到过字节流,它是什么意思呢?

在需要传输数据的时候,我们可以把数据想象成传送带上连续排列的一个个被传送的物体,它们就是一个流。

更形象的,可以想象磁带或者图灵机里连续的纸带。在文件读写、使用串口、Socket等领域,流经常被使用。例如我们常用的输入输出流:

cout<struct Stream { // Returns a pointer to the current position of the stream inline uint8_t* getData() { return data_; } // vances the stream, checking bounds, and returns a pointer to the position befe it was advanced. // throws StreamOverrunException if len would take this stream past the end of its buffer ROS_FORCE_INLINE uint8_t* advance(uint32_t len) { uint8_t* old_data = data_; data_ += len; if (data_ > end_) { // Throwing directly here causes a signifant speed hit due to the extra code generated for the throw statement throwStreamOverrun(); } return old_data; } // Returns the amount of spe left in the stream inline uint32_t getLength() { return static_cast< uint32_t >(end_ - data_); } protected: Stream(uint8_t* _data, uint32_t _count) : data_(_data), end_(_data + _count) {} private: uint8_t* data_; uint8_t* end_; };

注释表明Stream是个基类,输入输出流IStream和OStream都继承自它。

Stream的成员变量data_是个指针,指向序列化的字节流开始的位置,它的类型是uint8_t。

在Ubuntu系统中,uint8_t的定义是typedef unsigned char uint8_t;

所以uint8_t就是一个字节,可以用size_of()函数检验。data_指向的空间就是保存字节流的。

输出流类OStream用来序列化一个对象,它引用了serialize函数,如下。

struct OStream : public Stream
{
  static const StreamType stream_type = stream_types::Output;
  OStream(uint8_t* data, uint32_t count) : Stream(data, count) {}
  /* Serialize an item to this output stream*/
  template< typename T >
  ROS_FORCE_INLINE void next(const T& t)
{
    serialize(*this, t);
  }
  template< typename T >
  ROS_FORCE_INLINE OStream& operator< const T& t)
  {
    serialize(*this, t);
    return *this;
  }
};

输入流类IStream用来反序列化一个字节流,它引用了deserialize函数,如下。

struct ROSCPP_SERIALIZATION_DECL IStream : public Stream
{
  static const StreamType stream_type = stream_types::Input;
  IStream(uint8_t* data, uint32_t count) : Stream(data, count) {}
  /* Deserialize an item from this input stream */
  template< typename T >
  ROS_FORCE_INLINE void next(T& t)
{
    deserialize(*this, t);
  }
  template< typename T >
  ROS_FORCE_INLINE IStream& operator >>(T& t)
  {
    deserialize(*this, t);
    return *this;
  }
};

自然,serialize函数和deserialize函数就是改变数据形式的地方,它们的定义在比较靠前的地方。它们都接收两个模板,都是内联函数,然后里面没什么东西,只是又调用了Serializer类的成员函数write和read。所以,serialize和deserialize函数就是个二道贩子。

// Serialize an object.  Stream here should normally be a ros::serialization::OStream
template< typename T, typename Stream >
inline void serialize(Stream& stream, const T& t)
{
  Serializer< T >::write(stream, t);
}
// Deserialize an object.  Stream here should normally be a ros::serialization::IStream
template< typename T, typename Stream >
inline void deserialize(Stream& stream, T& t)
{
  Serializer< T >::read(stream, t);
}

所以,我们来分析Serializer类,如下。我们发现,write和read函数又调用了类型里的serialize函数和deserialize函数。

头别晕,这里的serialize和deserialize函数跟上面的同名函数不是一回事。

注释中说:“Specializing the Serializer class is the only thing you need to do to get the ROS serialization system to work with a type”(要想让ROS的序列化功能适用于其它的某个类型,你唯一需要做的就是特化这个Serializer类)。

这就涉及到的另一个知识点——模板特化(template specialization)。

template< typename T > struct Serializer
{
  // Write an object to the stream.  Normally the stream passed in here will be a ros::serialization::OStream
  template< typename Stream >
  inline static void write(Stream& stream, typename boost::call_trts< T >::pa_type t)
{
    t.serialize(stream.getData(), 0);
  }
   // Read an object from the stream.  Normally the stream passed in here will be a ros::serialization::IStream
  template< typename Stream >
  inline static void read(Stream& stream, typename boost::call_traits< T >::reference t)
{
    t.deserialize(stream.getData());
  }
  // Determine the serialized length of an object.
  inline static uint32_t serializedLength(typename boost::call_traits< T >::param_type t)
{
    return t.serializationLength();
  }
};

接着又定义了一个带参数的宏函数ROS_CREATE_PLE_SERIALIZER(Type),然后把这个宏作用到了ROS中的10种基本数据类型,分别是:uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t, int64_t, float, double。

说明这10种数据类型的处理方式都是类似的。看到这里大家应该明白了,write和read函数都使用了memcpy函数进行数据的移动。

注意宏定义中的template<>语句,这正是模板特化的标志,关键词template后面跟一对尖括号。

关于模板特化可以看这里。

#define ROS_CREATE_SIMPLE_SERIALIZER(Type) 
  template<  > struct Serializer Type > 
  { 
    template< typename Stream > inline static void write(Stream& stream, const Type v) 
{ 
      memcpy(stream.advance(sizeof(v)), &v, sizeof(v) ); 
    } 
    template< typename Stream > inline static void read(Stream& stream, Type& v) 
{ 
      memcpy(&v, stream.advance(sizeof(v)), sizeof(v) ); 
    } 
    inline static uint32_t serializedLength(const Type&) 
{ 
      return sizeof(Type); 
    } 
};
ROS_CREATE_SIMPLE_SERIALIZER(uint8_t)
ROS_CREATE_SIMPLE_SERIALIZER(int8_t)
ROS_CREATE_SIMPLE_SERIALIZER(uint16_t)
ROS_CREATE_SIMPLE_SERIALIZER(int16_t)
ROS_CREATE_SIMPLE_SERIALIZER(uint32_t)
ROS_CREATE_SIMPLE_SERIALIZER(int32_t)
ROS_CREATE_SIMPLE_SERIALIZER(uint64_t)
ROS_CREATE_SIMPLE_SERIALIZER(int64_t)
ROS_CREATE_SIMPLE_SERIALIZER(float)
ROS_CREATE_SIMPLE_SERIALIZER(double)

对于其它类型的数据,例如bool、std::string、std::vector、ros::me、ros::Duration、boost::array等等,它们各自的处理方式有细微的不同,所以不再用上面的宏函数,而是用模板特化的方式每种单独定义,这也是为什么serialization.h这个文件这么冗长。

对于int、double这种单个元素的数据,直接用上面特化的Serializer类中的memcpy函数实现序列化。

对于vector、array这种多个元素的数据类型怎么办呢?方法是分成几种情况,对于固定长度简单类型的(fixed-size simple types),还是用各自特化的Serializer类中的memcpy函数实现,没啥太大区别。

对于固定但是类型不简单的(fixed-size non-simple types)或者既不固定也不简单的(non-fixed-size, non-simple types)或者固定但是不简单的(fixed-size, non-simple types),用for循环遍历,一个元素一个元素的单独处理。

那怎么判断一个数据是不是固定是不是简单呢?这是在roscpp_traits文件夹中的message_traits.h完成的。

其中采用了萃取Type Traits,这是相对高级一点的技巧了,笔者也不太懂。

对序列化的介绍暂时就到这里了,有一些细节还没讲,等笔者看懂了再补。

文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。

相关文章

    文件这么冗长。 对于int、double这种单个元素的数据,直接用上面特化的Serializer类中的memcpy函数实现序列化。 对于vector、array这种多个元素的数据类型怎么办呢?方法......
    车载以太网静态架构SOME/IP的业务流程;SOME/IP的业务流程: 1)SWC通过RTE接口将数据传递至RTE中。 2)RTE通过使用SOMEIPXF的功能实现序列化,即将SWC输出的结构体数据序列化......
    应用层功能。 3.以太网架构完成的主要功能 3.1 Someip业务报文收发 Someip报文收发架构图 1)SWC通过RTE接口将数据传递至RTE中。 2)RTE通过使用SOMEIPXF的功能实现序列化......
    。 · 支持对xml、json、binary以及apple的plist(xplist/bplist)格式序列化和反序列化。并且实现自有的binary序列化格式, 针对......
    提供了广泛的中间件功能,如序列化、远程过程调用 (RPC)、服务发现和订阅,以使 ECU 软件能够相互通信。   SOME/IP 的一些主要特点: 序列化和反序列化:将数据结构转换成字节序列或者将字节序列......
    发布的新品进一步拓宽了公司旗下的产品组合。全新DDS系列产品的单输出通道能够提供50个正弦载波,为工程师和科学家生成并独立控制多音正弦信号提供了全新的方法。DDS是“直接数字合成”的缩写,这种强大的技术被用于生成高纯度信号(通常......
    固有的宽总线 - 这增加了它们的逻辑使用率。 Spoc0 采用相反的方法,将总线宽度与时钟周期进行权衡。 换句话说,Spoc0 序列化了它的许多任务(它每条指令使用更多的时钟周期),但仍然很纤薄。 将来,一些......
    高速时钟和数据链路 多千兆位序列化/反序列化 (SERDES) PCB对PCB光纤更换(例如:短距离光纤) 受美国专利第 7,075,329、9,941,565、以及 10,205,442 号保护。其他产品专利正在申请中。......
    如何学习单片机?单片机c语言编程入门教程;我当初是自学单片机的,也用同样的方法带出过月入15K以上的徒弟。 我的方法不能说是最好的,但绝对是靠谱的,毕竟都是曾经自己趟过的路。 如果......
    FPGA串行接口(RS-232);是将连接到PC的简单方法。 我们只需要一个发射器和接收器模块。本文引用地址:异步发射器 它通过序列化要传输的数据来创建信号“TxD”。 异步接收器 它从 外部......

我们与500+贴片厂合作,完美满足客户的定制需求。为品牌提供定制化的推广方案、专属产品特色页,多渠道推广,SEM/SEO精准营销以及与公众号的联合推广...详细>>

利用葫芦芯平台的卓越技术服务和新产品推广能力,原厂代理能轻松打入消费物联网(IOT)、信息与通信(ICT)、汽车及新能源汽车、工业自动化及工业物联网、装备及功率电子...详细>>

充分利用其强大的电子元器件采购流量,创新性地为这些物料提供了一个全新的窗口。我们的高效数字营销技术,不仅可以助你轻松识别与连接到需求方,更能够极大地提高“闲置物料”的处理能力,通过葫芦芯平台...详细>>

我们的目标很明确:构建一个全方位的半导体产业生态系统。成为一家全球领先的半导体互联网生态公司。目前,我们已成功打造了智能汽车、智能家居、大健康医疗、机器人和材料等五大生态领域。更为重要的是...详细>>

我们深知加工与定制类服务商的价值和重要性,因此,我们倾力为您提供最顶尖的营销资源。在我们的平台上,您可以直接接触到100万的研发工程师和采购工程师,以及10万的活跃客户群体...详细>>

凭借我们强大的专业流量和尖端的互联网数字营销技术,我们承诺为原厂提供免费的产品资料推广服务。无论是最新的资讯、技术动态还是创新产品,都可以通过我们的平台迅速传达给目标客户...详细>>

我们不止于将线索转化为潜在客户。葫芦芯平台致力于形成业务闭环,从引流、宣传到最终销售,全程跟进,确保每一个potential lead都得到妥善处理,从而大幅提高转化率。不仅如此...详细>>