最近在作一个解析rdb文件的功能,途中遇到了一些问题,也解决了一些问题。具体为何要作这件事情以后再详谈,本次主要想聊聊遇到的开始处理文件时遇到的第一个难题:理解RDB文件的协议、如何读取二进制文件。html
[Redis源码阅读]redis持久化
文章介绍过,Redis的持久化是经过RDB和AOF实现的。Redis的RDB文件是二进制格式的文件,从这个方面再次体现了Redis是基于内存的缓存数据库,无论对于存储到硬盘仍是恢复数据都十分快捷。Redis有多种数据类型:string、list、hash、set、zset,不一样数据类型占用的内存大小是不同的,解析出天然语言能够识别的数据就须要使用不一样的方法,保存到文件的时候也须要一些协议或者规则。这有点相似于编程语言里面的数据类型,不一样的数据类型占用的字节大小不一致,可是保存到计算机都是二进制的编码,就看是读取多少个字节,以怎样的方式解读。linux
举个例子,redis的对象类型是特定的几个字符表示,0表明字符串,读取到字符串类型后,紧接着就是字符串的长度,保存着接下来须要读取的字节大小,读取到的字节最终构成完整字符串对象的值。对于保存了"name" => "hoohack"
键值对的字符串对象保存到内存能够用下图表示:redis
固然,除了字符串,redis还有列表,集合,哈希等各类对象,针对这些类型,在RDB文件里面都有不一样的规则定义,只须要按照RDB文件格式的协议来解读文件,就能完整无误地把文件解读成天然语言能描述的字符。数据库
仔细对比,能够发现跟计算机的操做方式是相似的,数据保存在计算机都是二进制的,具体的值应该看须要多少个字节,以什么类型解析,读取不一样的字节解析到的值是不同的,一样的字节大小,可是使用不一样类型保存,只要作适当的转换,也是正确的。好比在C语言中的void *
指针是4个字节,int也是4个字节,定义一个int整数,将它保存到void *
也是没问题的,读取的时候只须要作一次类型转换就能够了。编程
所以,解读RDB文件最关键的就是理解RDB文件的协议,只要理解完RDB文件格式的协议,根据定义好的协议来解析各类数据类型的数据。更详细的RDB文件协议能够参考RDB文件格式的定义文档:RDB file format。api
先清空redis数据库,保存一个键值对,而后执行save命令将当前数据库的数据保存的rdb文件,获得文件dump.rdb。数组
127.0.0.1:6379> flushall OK 127.0.0.1:6379> set name hoohack OK 127.0.0.1:6379> save OK
能够看到是一个包含乱码的文件,由于文件是以二进制的格式保存,要想打印出人类能看出的语言,能够经过linux的od命令查看。od命令用于输出文件的八进制、十六进制或其余格式编码的字节,一般用于输出文件中不能直接显示在终端的字符,注意输出的是字节,分隔符之间的字符都是保存在一个字节的。缓存
经过man手册能够看到,打印出16进制格式的参数是x,字符是c,将字符与十六进制的对应关系打印出来:od -A x -t x1c -v dump.rdb
编程语言
打印得出结果以下:函数
从上图看到,文件打印出来的都是一些十六进制的数字,转换成十进制再去ASCII码表就能查找到对应的字符。好比第一个字符,52=516+21=82='R’。
在这里说一句,我的以为这od命令很是有用,在解析数据出现疑惑的时候,就是经过这个命令排查遇到的问题。把文件内容打印出来,找到当前读取的字符,对比RDB文件协议,看看究竟要解析的是什么数据,再对比代码中的解析逻辑,看看是否有问题,而后再修正代码。
若是以为敲命令麻烦,能够把文件上传而后在线查看:https://www.onlinehexeditor.com
咱们知道,计算机的全部数据都是以二进制的格式保存的,咱们看到的字符是经过读取二进制而后解析出来的数据,程序根据不一样数据类型作相应的转换,而后展现出来的就是咱们看到的字符。
计算机容许多种数据类型,好比有:32位整数、64位整数、字符串、浮点数、布尔值等等,不一样的数据类型是经过读取不一样大小的字节,根据类型指定的读取方式读取出来,就是想要的数据了。
解析数据的第一步,就是读取数据。在计算机里面,你们所知道的数据都是逐个字节地读取数据。所以,首先要实现的就是按照字节去读取文件。
本次采用实现的解析RDB文件功能的语言是Golang,在Golang的文件操做的API里,提供了按字节读取的函数File.ReadAt
。
函数原型以下:
func (f *File) ReadAt(b []byte, off int64) (n int, err error)
函数从File指针的指向的位置off开始,读取len(b)个字节的数据,并保存到b中。
根据对API的理解,本身实现了一个按照字节读取文件数据的函数,函数接收长度值,表明须要读取的字节长度。具体实现代码以下:
type Rdb struct { fp *os.File ... // other field } func (r *Rdb) ReadBuf(length int64) ([]byte, error) { // 初始化一个大小为length的字节数组 buf := make([]byte, length) // 从curIndex开始读取length个字节 size, err := r.fp.ReadAt(buf[:length], r.curIndex) checkErr(err) if size < 0 { fmt.Fprintf(os.Stderr, "cat: error reading: %s\n", err.Error()) return []byte{}, err } else { // 读取成功,更新文件操做的偏移量 r.curIndex += length return buf, nil } }
上面实现的函数返回的是字节数组,当函数返回读取到的数据后,若是须要保存在不一样的数据类型就须要作转换,Golang也提供了比较强大的api。如下是我在解析数据时遇到的数据类型转换的解决方案,但愿对你们有帮助。
字符串
str := string(buf)
int整数,先转为二进制的值,而后再用int32类型格式化
intVal := int(binary.BigEndian.Uint32(buf))
int64整数,先转为二进制的值,而后再用int64类型格式化
int64Val := int64(int16(binary.LittleEndian.Uint16(valBuf)))
浮点数
floatVal, err := strconv.ParseFloat(string(floatBuf), 64)
float64浮点数,先转为二进制的值,再调用math库的Float64frombits函数转换二进制的值为float64类型
floatBit := binary.LittleEndian.Uint64(buf) floatVal := math.Float64frombits(floatBit)
理论上的理解和实践上的应用是不同的,虽然你们都知道数据是二进制的,就是怎么怎么解析,可是真正实现起来仍是很多问题。经过操做二进制文件的一次实践,收获了如下几点:
一、更深入地理解到数据在计算机中的保存方式,一切都是0和1的二进制内容,只是看你要怎么用而已,适当的相似转换也能够获得你想要的内容
二、在某个系统下操做就要遵循已定义好的协议,否则获得的都是乱套或者乱码的东西,好比数据的字节序不一样也会使数据的解析结果不一致
原创文章,文笔有限,才疏学浅,文中如有不正之处,万望告知。
若是本文对你有帮助,请点个赞吧,谢谢^_^
更多精彩内容,请关注我的公众号。