一、序列化与反序列化

在计算机网络中,数据都是以二进制形式进行传输的。将对象转换成可以传输的二进制数据的过程叫做序列化。将二进制的数据转换成对象的过程叫做反序列化。

go 原生序列化与反序列化

go 语言中提供了一个 encoding/binary 包实现了简单的数字(固定长度的数字类型或者只包含定长值的结构体或数组)与字节流的转换和 varint 的编解码。varint 也是一种编解码方式,google 的 protobuffer 也大量使用了 varint ,这里不详细介绍,有兴趣的同学可以自行 google。我们重点来看看 go binary 包提供的数据和 []byte 的转换能力,核心 api 如下:

// r - 实现了 io.Reader 接口的可以被读取的数据源,如 net.Conn
// order - 特殊字节序,binary 中提供了大端序和小端序
// data - 需要被解码的数据
func Read(r io.Reader, order ByteOrder, data interface{}) error

// w - 实现了 io.Writer 接口的可以被写入数据源,如 net.Conn
// order - 特殊字节序,binary 中提供了大端序和小端序
// data - 需要被编码的数据
func Write(w io.Writer, order ByteOrder, data interface{}) error

它提供了具有读写能力的 Read 和 Write 接口。下面是一个简单的使用 binary 进行读写的 example

func main() {

	var pi float64
	b := []byte{0x18,0x2d,0x44,0x54,0xfb,0x21,0x09,0x40}
	buf := bytes.NewBuffer(b)
	err := binary.Read(buf, binary.BigEndian, &pi)
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println("pi = ", pi)


	buf = new(bytes.Buffer)
	pi = math.Pi

	err = binary.Write(buf, binary.BigEndian, pi)
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println("math.Pi bytes : ", buf.Bytes())
}

二、序列化方案的选型

之前介绍 rpc 原理的时候,介绍过一些序列化方案的选型,这里再展开说一下。每种序列化库的性能数据如下:(引用网上的数据)

benchmark _name                               iter                 time/iter        alloc bytes/iter    allocs/iter
-------------------------------------------------------------------------------------------------------------------------
BenchmarkMarshalByJson-4                         1000000          1795 ns/op         376 B/op           4 allocs/op
BenchmarkUnmarshalByJson-4                        500000          3927 ns/op         296 B/op           9 allocs/op

BenchmarkMarshalByXml-4                           200000          8216 ns/op        4801 B/op          12 allocs/op
BenchmarkUnmarshalByXml-4                          50000         26284 ns/op        2807 B/op          67 allocs/op

BenchmarkMarshalByBson-4                          500000          3258 ns/op        1248 B/op          14 allocs/op
BenchmarkUnmarshalByBson-4                       1000000          1433 ns/op         272 B/op           7 allocs/op

BenchmarkMarshalByMsgp-4                         5000000           259 ns/op          80 B/op           1 allocs/op
BenchmarkUnmarshalByMsgp-4                       3000000           466 ns/op          32 B/op           5 allocs/op

BenchmarkMarshalByProtoBuf-4                     2000000           955 ns/op         328 B/op           5 allocs/op
BenchmarkUnmarshalByProtoBuf-4                   1000000          1571 ns/op         400 B/op          11 allocs/op

BenchmarkMarshalByGogoProtoBuf-4                10000000           224 ns/op          48 B/op           1 allocs/op
BenchmarkUnmarshalByGogoProtoBuf-4               2000000           828 ns/op         144 B/op           8 allocs/op

BenchmarkMarshalByFlatBuffers-4                  3000000           626 ns/op          16 B/op           1 allocs/op
BenchmarkUnmarshalByFlatBuffers-4               100000000            10.4 ns/op           0 B/op           0 allocs/op
BenchmarkUnmarshalByFlatBuffers_withFields-4     3000000           493 ns/op          32 B/op           5 allocs/op

BenchmarkMarshalByThrift-4                       2000000           840 ns/op          64 B/op           1 allocs/op
BenchmarkUnmarshalByThrift-4                     1000000          1575 ns/op          96 B/op           6 allocs/op

BenchmarkMarshalByAvro-4                         1000000          1330 ns/op         133 B/op           7 allocs/op
BenchmarkUnmarshalByAvro-4                        200000          7036 ns/op        1680 B/op          63 allocs/op

BenchmarkMarshalByGencode-4                     20000000          66.2 ns/op           0 B/op           0 allocs/op
BenchmarkUnmarshalByGencode-4                    5000000           258 ns/op          32 B/op           5 allocs/op

可以看到,比较常用的库里面,gogoprotobuf > msgpack > flatbuffers > thrift > protobuf > json

之前我们说过,我们的框架有两种调用方式,一种是使用反射、一种是使用代码生成。

假如使用反射的话,我们发现只有 msgpack 、json 能够对原生的 go struct 进行序列化。由于 msgpack 性能远高于 json,所以这里我们就直接选择了 msgpack。

假如使用代码生成的调用方式,这里有 gogoprotobuf、flatbuffers、thrift、protobuf 四种方案可以选。从性能上讲,四种序列化库其实性能都已经比较优秀了,但是其中 gogoprotobuf 的序列化性能是最好的。由于目前使用最广泛的还是 protobuf,考虑到 gogoprotobuf 和 protobuf 不兼容,这里选择了比较广泛的通用化方案 protobuf,当然,后续我们也支持 gogoprotobuf 序列化方式的实现,业务可以自己选择用哪种方式进行序列化。

三、何时进行序列化与反序列化

讲到序列化这里,可能有些朋友会把序列化和编解码搞混。把对象转为二进制数据不是编码的过程吗?这里编解码和序列化/反序列化的关系是什么呢?下面一张图告诉你。

img

所以编码是在对象序列化之后,将对象的二进制数据进行编码,加上包头、帧头使其成为一个完整的消息帧。反序列化是在对象解码之后,从一个完整的消息帧解析出帧头、包头,得到对象的二进制数据,然后再进行反序列化得到一个对象。

四、序列化和反序列化实现

这里首先定义一个 Serialization 的接口,用与序列化的通用实现标准,所有的序列化组件都需要实现这两个方法。

type Serialization interface {
   Marshal(interface{})([]byte, error)
   Unmarshal([]byte, interface{}) error
}

、msgpack 序列化实现

msgpack 的序列化与反序列化直接使用 github 上第三方包实现 msgpack

这里的实现比较简单,只需要实现 Marshal、Unmarshal 两个方法即可

type MsgpackSerialization struct {}


func (c *MsgpackSerialization) Marshal(v interface{}) ([]byte, error) {
   var buf bytes.Buffer
   encoder := msgpack.NewEncoder(&buf)
   err := encoder.Encode(v)
   return buf.Bytes(), err
}

func (c *MsgpackSerialization) Unmarshal(data []byte, v interface{}) error {
   decoder := msgpack.NewDecoder(bytes.NewReader(data))
   err := decoder.Decode(v)
   return err
}

2、protobuf 序列化实现

protobuf 序列化与反序列化这里,为了避免对象的频繁创建于销毁,使用了一个内存池。如下:

var bufferPool = &sync.Pool{
	New : func() interface {} {
		return &cachedBuffer {
			Buffer : proto.Buffer{},
			lastMarshaledSize : 16,
		}
	},
}

type cachedBuffer struct {
	proto.Buffer
	lastMarshaledSize uint32
}

func (d *pbSerialization) Marshal(v interface{}) ([]byte, error) {
   if pm, ok := v.(proto.Marshaler); ok {
      // 可以 marshal 自身,无需 buffer
      return pm.Marshal()
   }
   buffer := bufferPool.Get().(*cachedBuffer)
   protoMsg := v.(proto.Message)
   lastMarshaledSize := make([]byte, 0, buffer.lastMarshaledSize)
   buffer.SetBuf(lastMarshaledSize)
   buffer.Reset()

   if err := buffer.Marshal(protoMsg); err != nil {
      return nil, err
   }
   data := buffer.Bytes()
   buffer.lastMarshaledSize = upperLimit(len(data))

   return data, nil
}

func (d *pbSerialization) Unmarshal(data []byte, v interface{}) error {
   protoMsg := v.(proto.Message)
   protoMsg.Reset()

   if pu, ok := protoMsg.(proto.Unmarshaler); ok {
      // 可以 unmarshal 自身,无需 buffer
      return pu.Unmarshal(data)
   }

   buffer := bufferPool.Get().(*cachedBuffer)
   buffer.SetBuf(data)
   err := buffer.Unmarshal(protoMsg)
   buffer.SetBuf(nil)
   bufferPool.Put(buffer)
   return err
}

小结

本章主要介绍了 go 原生序列化的实现,序列化的选型,序列化与编解码的区别。同时提供了 msgpack 和 protobuf 两种方式的序列化和反序列化实现。