0%

《7天以太坊源码解读》 — 6、RLP编码解析

以太坊的RLP编码的代码部分全部位于rlp文件夹

其实已经存在很多的编码方式,比如 golang 自带的 json 以及 gob,那么为什么以太坊不用这些呢?

我们来实测对比一下

>>> RLP、JSON、GOB编码后容量占用对比

下面是我写的测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
func ExampleStorageEncode() {
test := Test{
Str: `123`,
Uint64_: 234,
}
bytes, err := rlp.EncodeToBytes(test)
if err != nil {
panic(err)
}

var test1 Test
err = rlp.DecodeBytes(bytes, &test1)
if err != nil {
panic(err)
}
fmt.Printf("RLP: %x ", bytes)
fmt.Printf("%#v\n", test1)

testBuf := bytes2.Buffer{}
gobEncoder := gob.NewEncoder(&testBuf)
err = gobEncoder.Encode(test)
if err != nil {
panic(err)
}

gobDecoder := gob.NewDecoder(bytes2.NewReader(testBuf.Bytes()))
var test2 Test
err = gobDecoder.Decode(&test2)
if err != nil {
panic(err)
}
fmt.Printf(`GOB: %x `, testBuf.Bytes())
fmt.Printf("%#v\n", test2)


bytes1, err := json.Marshal(test)
if err != nil {
panic(err)
}

var test3 Test
err = json.Unmarshal(bytes1, &test3)
if err != nil {
panic(err)
}
fmt.Printf("JSON: %x ", bytes1)
fmt.Printf("%#v\n", test3)

// Output:
// RLP: c68331323381ea test.Test{Str:"123", Uint64_:0xea}
// GOB: 26ff81030101045465737401ff820001020103537472010c00010755696e7436345f01060000000bff82010331323301ffea00 test.Test{Str:"123", Uint64_:0xea}
// JSON: 7b22537472223a22313233222c2255696e7436345f223a3233347d test.Test{Str:"123", Uint64_:0xea}
}

从这里可以看出,占用容量的差距。

节约存储的次序是:RLP > JSON > GOB

>>> RLP、JSON、GOB编码、解码效率对比

下面是测试编码10w次的执行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
func ExampleEncodeEfficiency() {
test := Test{
Str: `123`,
Uint64_: 234,
}
num := 100000


t1 := time.Now() // get current time
for i := 0; i < num; i++ {
_, err := rlp.EncodeToBytes(test)
if err != nil {
panic(err)
}
}
elapsed := time.Since(t1)
fmt.Println("RLP elapsed: ", elapsed)


testBuf := bytes.Buffer{}
gobEncoder := gob.NewEncoder(&testBuf)
t2 := time.Now()
for i := 0; i < num; i++ {
err := gobEncoder.Encode(test)
if err != nil {
panic(err)
}
}
elapsed2 := time.Since(t2)
fmt.Println("GOB elapsed: ", elapsed2)


t3 := time.Now() // get current time
for i := 0; i < num; i++ {
_, err := json.Marshal(test)
if err != nil {
panic(err)
}
}
elapsed3 := time.Since(t3)
fmt.Println("JSON elapsed: ", elapsed3)

// Output:
// RLP elapsed: 37.708578ms
// GOB elapsed: 51.050196ms
// JSON elapsed: 36.320832ms
}

我执行了了多次,大体上就是

按照快的次序是 JSON > RLP > GOB

>>> RLP 源码解读

从上面的对比来看,RLP 相对非常节约存储,而且效率也很高

那他究竟是怎么做的呢?

我们分析一下 rlp/encode.go 文件

来看 makeWriter 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func makeWriter(typ reflect.Type, ts tags) (writer, error) {
kind := typ.Kind()
switch {
case typ == rawValueType:
return writeRawValue, nil
case typ.AssignableTo(reflect.PtrTo(bigInt)):
return writeBigIntPtr, nil
case typ.AssignableTo(bigInt):
return writeBigIntNoPtr, nil
case kind == reflect.Ptr:
return makePtrWriter(typ, ts)
case reflect.PtrTo(typ).Implements(encoderInterface):
return makeEncoderWriter(typ), nil
case isUint(kind):
return writeUint, nil
case kind == reflect.Bool:
return writeBool, nil
case kind == reflect.String:
return writeString, nil
case kind == reflect.Slice && isByte(typ.Elem()):
return writeBytes, nil
case kind == reflect.Array && isByte(typ.Elem()):
return writeByteArray, nil
case kind == reflect.Slice || kind == reflect.Array:
return makeSliceWriter(typ, ts)
case kind == reflect.Struct:
return makeStructWriter(typ)
case kind == reflect.Interface:
return writeInterface, nil
default:
return nil, fmt.Errorf("rlp: type %v is not RLP-serializable", typ)
}
}

它根据要编码的值的类型,返回相应的编码器

我们来看字符串怎么编码的

1
2
3
4
5
6
7
8
9
10
11
func writeString(val reflect.Value, w *encbuf) error {
s := val.String()
if len(s) == 1 && s[0] <= 0x7f {
// fits single byte, no string header
w.str = append(w.str, s[0])
} else {
w.encodeStringHeader(len(s))
w.str = append(w.str, s...)
}
return nil
}

如果字符串只有一个字符,而且对应ascii数值小于0x7f,则直接将字符串对应的字节放到第0个位置

如果不止一个字符串或者大于0x7f,则需要在前面附加一个header

1
2
3
4
5
6
7
8
9
10
func (w *encbuf) encodeStringHeader(size int) {
if size < 56 {
w.str = append(w.str, 0x80+byte(size))
} else {
// TODO: encode to w.str directly
sizesize := putint(w.sizebuf[1:], uint64(size))
w.sizebuf[0] = 0xB7 + byte(sizesize)
w.str = append(w.str, w.sizebuf[:sizesize+1]...)
}
}

如果长度小于56,则附加 0x80 + 长度 的字节到前面当头部

如果不小于56,则附加 0xB7 + 长度占用的字节个数 的字节到最开始,接着放入 长度 的字节,作为头部

比如字符串123,长度是3,则头部就是 0x83。最终编码后就是 83313233

其他的类型编码器类似可以分析出来

解码器则是根据结果的类型来相应的解码数据

编码和解码都是根据参数的类型来选择如果编码解码的,所以并不需要多余的字节来表示数据的类型,是的存储量更加小,数据也更加的紧凑。

文章仅供参考,若有错误,还望不吝指正 !!!




微信关注我,及时接收最新技术文章