Rosbag格式分析

首先打开Rosbag,会在第一行标识出Rosbag的版本号,由于Rosbag有1.0,1.1,1.2,2.0几个版本,通过版本号进行区分,这里我们只介绍最新的2.0版本。

版本号

首先打开Rosbag,会看到以下格式。也就是说Bag包的第一行是人眼可以识别的版本号,后面紧跟着的是一系列记录序列。

#ROSBAG V2.0
<record 1><record 2>....<record N>

记录(Records)

上面已经知道了,Bag包由一系列的记录序列组成,接下来我们再详细的看每个记录的格式。

<header_len><header><data_len><data>

也就是说每个record由header和data组成,而为了找到header和data,还需要保存header_len和data_len,结合2者,我们可以得到Bag包的结构如下
在这里插入图片描述

信息头(Headers)

接下来,我们再详细分析上述的信息头(Headers)。每个记录头包含一系列 name=value 字段,格式如下:

<field1_len><field1_name>=<field1_value><field2_len><field2_name>=<field2_value>...<fieldN_len><fieldN_name>=<fieldN_value>

和header_len和header类似,这里的field1_len后面跟着<field1_name>=<field1_value> ,其中field1_len 的长度包含= 号在内,所有的field的长度等于 header_len 。

  1. field_name 字段名称可以包含任何可打印的 ASCII 字符 (0x20 - 0x7e),但 =(0x3d) 除外。
  2. field_value字段值可以包含任何数据(包括嵌入空值、换行符等的二进制数据)
    在这里插入图片描述

Op 码

所有的信息头必须包含Op码字段,也就是说上述field_name必须有一个字段为Op 。这个字段是为了区分 record的类型而准备的,这也是为什么这个字段是必须的原因,因为没有这个字段就没法区分record的类型。从侧面也可以看出header信息头可以灵活的用来保存record的信息,甚至用户可以自定义一些信息。
不同的Op码对应不同的record类型,一共有6种类型,下面我们逐个介绍。

  1. Bag header 主要存放bag包整体的信息,必须是第一个record。
  2. Chunk 主要的数据结构,可以被压缩,可以理解为把N个消息打包为一个块(Chunk),方便索引节省空间。
  3. Connection 块的结构之一,存放信息的格式信息,有了消息的格式,才能解析消息。
  4. Message data 块的结构之一,消息序列化之后以2进制存储,通过Connection获取消息格式后进行反序列化。
  5. Index data 索引数据,因为一个块比较大,索引消息在块中的位置,方便快速查找,缺点在于会占用额外的空间
  6. Chunk info 块的结构之一,主要描述块的信息,例如消息的起始和结束时间等。
    至此Bag包的格式基本上就分析清楚了。接下来我们分别介绍这6种record的数据格式,也就是record data部分的内容。
    在这里插入图片描述

6种记录类型

Bag header

必须是Bag包的第一条记录。Op码=0x03,包括以下信息,注意这些字段全部保存在header中,而它的data是空白的,可以直接跳过。

  1. index_pos bag header之后第一条记录的偏移
  2. conn_count connections的数量
  3. chunk_count chunk的数量

Chunk

块,类似一个档案袋,打包了固定大小的多条消息。以下结构保存在header中。

  1. compression 压缩方式
  2. size 块大小
    而data由Connection和Message data组成。

Connection

连接,通过连接可以获取消息的定义,用来反序列化消息,以下消息在header结构中

  1. conn 连接的id
  2. topic 消息的topic名称
    数据段中包含
  3. topic 消息名称
  4. type 消息类型
  5. md5sum md5值
  6. message_definition 消息定义

Message data

消息本身,消息通过序列化保存,通过conn可以找到对应的连接(Connection),从而获取到消息类型,进行反序列化。

  1. conn 消息连接id,通过id可以找到对应的Connection
  2. time 消息发布时间

Index data

索引消息,主要是为了快速检索信息。

  1. ver 版本号
  2. conn 消息连接id
  3. count 消息数量
    由于有多个消息,因此以下字段也会出现多次。
  4. time 时间
  5. offset 偏移

Chunk info

块信息,也是为了方便查找。
以下字段保存在header中

  1. ver
  2. chunk_pos
  3. start_time
  4. end_time
  5. count
    因为一个块中可能有多种不同的消息,因此以下2个字段会出现多次,类似一个哈希表的结构,保存在data结构中。
  6. conn
  7. count

读取顺序

上述6种类型的数据结构的读取顺序为:

  1. 先解析Bag header,获取到index_pos
  2. 然后跳到index_pos读取Connection
  3. 接着读取Chunk info,上述2个步骤相当于建立起了整个Bag包,块的索引
  4. 接着根据Chunk info逐个读取Chunk,先解析Chunk,获取压缩类型和数据大小
  5. 接着跳到Chunk尾部解析Index data,这里是一个消息对应一个index
  6. 最后根据index data实例化消息(通过接口instantiateBuffer)
    在这里插入图片描述

设计原理

至此Bag包的分析就完成了,其实通过看上述的结构,第一遍可能只能有个大概的印象,为了进一步加深理解,这里我们对Bag的设计思路进行进一步的分析。

序列化和反序列化

首先我们知道bag包就是为了录制消息,而消息的保存和读取就涉及到一个广义上的问题序列化和反序列化,它基本上无处不在,只是大部分人没有注意到,举个很简单的例子,程序运行的时候,我们是直接操作的内存,也就是一个结构体或者一个对象,但内存里面的数据会消失,当我们要保存内存的数据到磁盘的时候就需要序列化之后保存,常见的序列化方式有XML、json等等,而protobuf也就是其中的一种。而当我们需要读取磁盘中的数据使用的时候,又需要把磁盘中的数据转换为内存中的数据,这个过程叫反序列化。

当然持久化并不是唯一利用到序列化和反序列化的一种,比如2个进程之间通信,由于进程之间的内存映射并不相同,也需要序列化和反序列化,同理还有2台机器之间的通信,例如最常见的网页应用也需要序列化和反序列化,当然还有更厉害的技术,例如跨语言的调用,制作一种通用的对象消息格式,从而实现不同语言之间的数据交换。

回到这个问题本身,也就是说持久化需要序列化消息,然后保存到硬盘,读取消息的时候,我们又需要反序列化消息为内存的对象。那么问题来了。

反序列化

如果说写入消息很简单,但是读取消息的时候就麻烦了,因为你不知道消息的类型,就无法解析消息,假设你保存了3条消息,包括图片,位置和轨迹消息,你读取了一条消息,你怎么知道这条消息是什么类型呢?方法很简单,我们可以在消息头中标识这条消息是什么类型,然后再用这种消息类型去解析消息,这样就解决了。

通过消息类型名称(字符串)来生成对象,这在很多语言中叫做反射(reflection),这样我们就解决了消息解析的问题,而bag包中的Connection就是用来解决上述问题,它包含了数据类型和格式,而每个Message data中可以找到Connection,从而找到消息类型,进行解析。

还有一点设计的比较巧妙的地方在于,每个消息头如果都包含数据的消息定义的化,比较浪费空间,因此通过Connection中包含数据的格式(消息字段的定义),而Message data中只包含id,从而节省了空间。

索引

消息保存和解析的问题解决了,那么我们如何快速的查找呢?因为一个包可能比较大,所以把一个包拆分为几个块(Chunk),而chunk中有消息的时间段,这样查找指定时间段的消息的时候,就可以跳过一些块,从而避免读取整个块之后再进行查找,提高检索效率。

而Index data则更进一步,直接索引了不同类型的消息在块中的时间戳和偏移,从而方便快速查找。当然索引确实可以加快速度,但是过多的索引也会消耗空间,也就是经典的时间和空间的算法复杂度问题,需要取舍。

以上就是Rosbag的整个分析过程。

转自:https://zhuanlan.zhihu.com/p/494474804

猜你喜欢

转载自blog.csdn.net/qq_21033663/article/details/130142782