我们一起来探寻rpcx框架,本系列会详细讲解rpcx,尽量覆盖它的所有代码,看看这款优秀的rpc框架是如何实现的。

什么是序列化

编码/解码对应也就是就是序列化/反序列化的过程,我们知道,网络只能传输字节。所以在rpc中,如果客户端要传输一个Person对象给服务端,客户端必须先将**Person对象转换成转换成 []byte**(这就是序列化),然后发起rpc调用,服务端在接收到请求后,将接收到的[]byte 还原成成Person对象(这就是反序列化)。

type Person struct{
	Age uint8
	Name string
}

常用的序列化协议

json

简洁,易读,层次清晰。格式与编程语言无关,是一种轻量级的数据交换格式。不足就是空间开销比较大,且性能开销比较大

Thrift

Thrift并不仅仅是序列化协议,而是一个RPC框架。具有体积小, 速度快,支持多语言,数据类型丰富的优点,但作为RPC框架,性能不太理想

Avro

属于Hadoop的子项目,是Hive、Pig和MapReduce的持久化的存储格式。提供json格式或者Binary两种个格式。其中Binary的空间开销和解析性能方面非常不错

Hessian

一款支持多种语言进行序列化操作的框架技术,同时在进行序列化之后产生的码流也较小,处理数据的性能方面远超于java内置的jdk序列化方式。具备非常好的兼容性。

Protobuf

谷歌开源,描述文件:.proto。优点:空间小,性能高,跨语言,且按照特定的字段顺序可以实现协议的前向兼容。

Message pack

内置的LZ4压缩功能,可以实现超快速序列化,占用空间小,支持多语言,按照特定顺序也可以实现前向兼容。这个是rpcx默认的序列化选择

关于序列化的协议很多,这里就不一一列举了,我们甚至可以定义自己的协议,前提是序列化和反序列化能匹配上。当然,我们不会这么做,已有的协议已经够好了,那选择协议的时候有几个非常重要的维度:是否支持多语言,性能开销如何,体积大小,是否向前兼容

进入正题,我们来学习rpcx的序列化

源码

先看rpcx支持哪些序列化方式:

var (
	// Codecs are codecs supported by rpcx. You can add customized codecs in Codecs.
	Codecs = map[protocol.SerializeType]codec.Codec{
        //用户自己实现与[]byte直接的转换
		protocol.SerializeNone: &codec.ByteCodec{},
        //json
		protocol.JSON:          &codec.JSONCodec{},
        //protobuf:github.com/gogo/protobuf 使用的并非官方库
		protocol.ProtoBuffer:   &codec.PBCodec{},
        //msgpack
		protocol.MsgPack:       &codec.MsgpackCodec{},
        //thrift
		protocol.Thrift:        &codec.ThriftCodec{},
	}
)

默认使用MsgPack,这个从默认的配置可以看到

var DefaultOption = Option{
    Retries:        3,
    RPCPath:        share.DefaultRPCPath,
    ConnectTimeout: 10 * time.Second,
    Breaker:        CircuitBreaker,
    SerializeType:  protocol.MsgPack,//默认MsgPack
    CompressType:   protocol.None,
}

修改默认配置,可以通过传入Option参数,每种序列化方式,我都会举一个例子

func NewXClient(servicePath string, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option)

我们先来看编码器的接口定义,我们也可以通过实现Codec接口,来定义自己的编码解码器。

type Codec interface {
    //序列化(编码)
	Encode(i interface{}) ([]byte, error)
    //反序列化(解码)
	Decode(data []byte, i interface{}) error
}

接下里,我们开始介绍每种编码解码器

JSON

这个直接调用json库即可完成

// Encode encodes an object into slice of bytes.
func (c JSONCodec) Encode(i interface{}) ([]byte, error) {
	return json.Marshal(i)
}

// Decode decodes an object from slice of bytes.
func (c JSONCodec) Decode(data []byte, i interface{}) error {
	d := json.NewDecoder(bytes.NewBuffer(data))
	d.UseNumber()
	return d.Decode(i)
}

需要注意的是:解码使用了d.UseNumber():这个方法的作用是告诉反序列化JSON的数字类型的时候,不要直接转换成float,而是转换成json.Number类型,用户可以根据自己的需求转换成float类型,下面是json.Number的定义(前提字段是interface{}类型),之所以单拎出来讲,是担心你们会踩坑

type Number string

// String returns the literal text of the number.
func (n Number) String() string { return string(n) }

// Float64 returns the number as a float64.
func (n Number) Float64() (float64, error) {
    return strconv.ParseFloat(string(n), 64)
}

// Int64 returns the number as an int64.
func (n Number) Int64() (int64, error) {
    return strconv.ParseInt(string(n), 10, 64)
}

使用例子

option := client.DefaultOption
option.SerializeType = protocol.JSON
xclient := client.NewXClient("Arith", client.Failtry, client.ConsistentHash, d, option)

Protobuf

直接上代码,对protobuf api不熟悉的可以参考

官方文档:https://developers.google.com/protocol-buffers/docs/proto3,

也可以参考译文:https://colobu.com/2017/03/16/Protobuf3-language-guide

// PBCodec uses protobuf marshaler and unmarshaler.
type PBCodec struct{}

// Encode encodes an object into slice of bytes.
func (c PBCodec) Encode(i interface{}) ([]byte, error) {
	if m, ok := i.(proto.Marshaler); ok {
		return m.Marshal()
	}

	if m, ok := i.(pb.Message); ok {
		return pb.Marshal(m)
	}

	return nil, fmt.Errorf("%T is not a proto.Marshaler", i)
}

// Decode decodes an object from slice of bytes.
func (c PBCodec) Decode(data []byte, i interface{}) error {
	if m, ok := i.(proto.Unmarshaler); ok {
		return m.Unmarshal(data)
	}

	if m, ok := i.(pb.Message); ok {
		return pb.Unmarshal(data, m)
	}

	return fmt.Errorf("%T is not a proto.Unmarshaler", i)
}

在前面讲过,作者选用的protobuf并非官方库:https://github.com/golang/protobuf,而是:https://github.com/gogo/protobuf。从这些细节方面,就可以看出作者对性能的执着(更进一步可以看作者的博客,很多文章都是关于性能方面的,博客地址:https://colobu.com/)

如果对序列化框架的性能感兴趣的话,可以参考:https://github.com/alecthomas/go_serialization_benchmarks,也可以只看结论:

Recommendation
If performance, correctness and interoperability are the most important factors, gogoprotobuf is currently the best choice. It does require a pre-processing step (eg. via Go 1.4's "go generate" command).

But as always, make your own choice based on your requirements.

使用例子

1、先新建一个arith_service.proto文件

// protoc --gogofaster_out=. arith_service.proto
syntax = "proto3";

package pb;

message ProtoArgs { 
    int32 A = 1;
    int32 B = 2;
}

message ProtoReply { 
    int32 C = 1;
}

2、编译arith_service.proto文件,在编译之前需要先安装protoc

安装protoc

在 https://github.com/protocolbuffers/protobuf/releases 下载对应自己系统的文件,如果是window,则可以直接下载可以执行包,其他则需要先编译。

下载完成后,将protoc.exe添加到环境变量中,然后下载gogofaster

go get github.com/gogo/protobuf/protoc-gen-gogofaster

编译:在arith_service.proto所在的文件目录

protoc --gogofaster_out=. arith_service.proto

3、编写例子

这里给出一个完整的例子,文件的目录结构(包含了上面的proto文件和编译生成的go文件)

rpc框架负载均衡策略 rpcx框架_编码器

客户端 client.go

package main

import (
	"context"
	"flag"
	"github.com/rpcxio/rpcx-examples/codec/protobuf/pb"
	"github.com/smallnest/rpcx/client"
	"github.com/smallnest/rpcx/protocol"
	"log"
)

var (
	addr = flag.String("addr", "localhost:8972", "server address")
)

func main() {
	flag.Parse()

	// register customized codec
	option := client.DefaultOption
	option.SerializeType = protocol.ProtoBuffer

	d := client.NewPeer2PeerDiscovery("tcp@"+*addr, "")
	xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, option)
	defer xclient.Close()

	args := &pb.ProtoArgs{
		A: 10,
		B: 20,
	}

	reply := &pb.ProtoReply{}
	err := xclient.Call(context.Background(), "Mul", args, reply)
	if err != nil {
		log.Fatalf("failed to call: %v", err)
	}

	log.Printf("%d * %d = %d", args.A, args.B, reply.C)

}

服务端 server.go

package main

import (
	"context"
	"flag"
	"fmt"

	"github.com/rpcxio/rpcx-examples/codec/protobuf/pb"
	"github.com/smallnest/rpcx/server"
)

var (
	addr = flag.String("addr", "localhost:8972", "server address")
)

type Arith int

func (t *Arith) Mul(ctx context.Context, args *pb.ProtoArgs, reply *pb.ProtoReply) error {
	reply.C = args.A * args.B
	fmt.Printf("call: %d * %d = %d\n", args.A, args.B, reply.C)
	return nil
}

func main() {
	flag.Parse()

	s := server.NewServer()
	//s.RegisterName("Arith", new(example.Arith), "")
	s.Register(new(Arith), "")
	s.Serve("tcp", *addr)
}

这里用了较多的篇幅讲解protobuf,主要基于它应用很广,几乎是rpc框架必备的协议,所以希望读者能掌握它

MsgPack

rpcx默认的协议,是一款高性能的序列化共工具,支持多语言:https://msgpack.org/index.html

// MsgpackCodec uses messagepack marshaler and unmarshaler.
type MsgpackCodec struct{}

// Encode encodes an object into slice of bytes.
func (c MsgpackCodec) Encode(i interface{}) ([]byte, error) {
	var buf bytes.Buffer
	enc := msgpack.NewEncoder(&buf)
	// enc.UseJSONTag(true)
	err := enc.Encode(i)
	return buf.Bytes(), err
}

// Decode decodes an object from slice of bytes.
func (c MsgpackCodec) Decode(data []byte, i interface{}) error {
	dec := msgpack.NewDecoder(bytes.NewReader(data))
	// dec.UseJSONTag(true)
	err := dec.Decode(i)
	return err
}

例子就省略了,他是默认的协议,不需要配置

Thrift

这里就只贴一下源码了,如果想看Thrift的使用例子,欢迎留言,我会择时补充(请允许我偷个懒,在rpc中使用Thrift的场景并不多见)。

type ThriftCodec struct{}

func (c ThriftCodec) Encode(i interface{}) ([]byte, error) {
	b := thrift.NewTMemoryBufferLen(1024)
	p := thrift.NewTBinaryProtocolFactoryDefault().GetProtocol(b)
	t := &thrift.TSerializer{
		Transport: b,
		Protocol:  p,
	}
	t.Transport.Close()
	return t.Write(context.Background(), i.(thrift.TStruct))
}

func (c ThriftCodec) Decode(data []byte, i interface{}) error {
	t := thrift.NewTMemoryBufferLen(1024)
	p := thrift.NewTBinaryProtocolFactoryDefault().GetProtocol(t)
	d := &thrift.TDeserializer{
		Transport: t,
		Protocol:  p,
	}
	d.Transport.Close()
	return d.Read(i.(thrift.TStruct), data)
}

自定义序列化

自定义非常简单,只要实现codec接口就行了,定义如下:

type Codec interface {
	Encode(i interface{}) ([]byte, error)
	Decode(data []byte, i interface{}) error
}

我们自定义如下(使用了官方的例子,内嵌God编码解码器)

import (
	"bytes"
	"encoding/gob"
)

type GobCodec struct {
}

func (c *GobCodec) Decode(data []byte, i interface{}) error {
	enc := gob.NewDecoder(bytes.NewBuffer(data))
	err := enc.Decode(i)
	return err
}

func (c *GobCodec) Encode(i interface{}) ([]byte, error) {
	var buf bytes.Buffer
	enc := gob.NewEncoder(&buf)
	err := enc.Encode(i)
	return buf.Bytes(), err
}

使用例子

// 注入GodCodec到rpcx中
share.Codecs[protocol.SerializeType(4)] = &GobCodec{}
option := client.DefaultOption
option.SerializeType = protocol.SerializeType(4)
xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, option)

至此,已经对序列化做了全部的讲解

结语

我打算花足够多的时间来和大家读一读rpcx的源码,来一层层的剖解rpcx,有兴趣的朋友,可以关注我。

下一篇我们分析通信协议