《7天以太坊源码解读》— 第六天,RLP编码解析

623 阅读4分钟

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

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

我们来实测对比一下

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

下面是我写的测试代码

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次的执行时间

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 函数

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)
	}
}

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

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

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

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

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

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

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

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