Golang gob与rpc简介

懒皮 · · 1108 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

本文参考
Gob的数据
手机网络游戏应用协议设计(一)

关于protobuf基础知识,可以参考Protobuf 以及在Laya中的使用
关于varint,参考数值压缩存储方法Varint图解Protobuf编码

为了让某个数据结构能够在网络上传输或能够保存至文件,它必须被编码然后再解码。当然,已经有许多可用的编码方式了:JSON,XML,Google 的 protocol buffers,等等。而现在,又多了一种,由 Go 的 gob 包提供的方式。

为什么定义新的编码?这要做许多繁重的工作。为什么不使用某个现成的格式?呃,无论如何,我们这样做了!Go 已经有刚才提到的所有编码方式的包(protocol buffer 包在另外一个代码库中,但它是下载得最多的包之一)。并且在许多情况下,包括同其他语言编写的工具和系统通讯,这些都是正确的选择。

但是在特定的 Go 环境中,例如在两个 Go 编写的服务之间通讯,这需要某些东西使得其更加容易使用,并且可能更加有效率。Gobs 协同 Go 语言的工作方式,对于那些外部定义的、同语言无关的编码方式来说无法做到。同时,从现有的系统中也吸取了很多教训。

一、目标

gob 包有在设计时有许多目标。

首先,也是最显然的,它被设计成为非常容易使用的。一方面,由于 Go 有反射(reflection),就没有必要弄一个单独的接口定义语言或“协议编译器”。数据结构本身提供了编码和解码所需要的全部信息。 另一方面,这种方法也意味着 gob 永远无法良好的同其他语言协同工作,但这没问题:gob 是厚颜无耻的以 Go 为中心(译注:呃,以XXX为中心,坚决贯彻XXX的领导……)。

效率也是非常重要的。基于文本形式的,如 XML 和 JSON ,应用于高效通讯网络会太慢了。二进制编码是必须的(译注:二进制神马的,是必须的!回音:必须的……)!

Gob 流必须可以自解释。每个 gob 流,从开始读取,整个流将由包含足够的信息,以便在终端对其内容毫不知情的前提下对整个流加以解析。这一特性意味,即便你忘记了保存在文件中的 gob 流表示什么,也总是可以对其解码。

同样,这里有一些从 Google protocol buffers 获得的经验。

二、Protocol buffer 的硬伤

Protocol buffers 对 gob 的设计产生了主要的影响,但是有三个特性被谨慎的避开了。(暂且不说 protocol buffer 不是自解释的:如果你不知道 protocol buffer 编码时的数据的定义,你就无法解析它。)

首先,protocol buffer 仅工作于 Go 的 struct 数据类型。不能在最顶级编码一个整数或者数组,只可以将其至于 struct 中作为一个字段。 至少在 Go 中,这个限制似乎没有什么意义。如果你希望传输的仅仅是一个数组或者整数,为什么你要先将其放到 struct 中?

其次,可能 protocol buffer 的定义指定字段 T.x 和 T.y 需要解析,无论是在编码还是解码类型 T 的值。虽然,这样的必须字段看起来是个好主意,但是由于编解码器中必须含有用于编码和解码的特定的数据结构,用于报告必须字段是否丢失,实现的开销是大的。这同样也产生了问题。过一段时间后,某人可能希望修改数据定义,移除了必须的字段,但这导致现有接收数据的客户端崩溃。最好是在编码时就根本没有这些字段。(Protocol buffer 也有可选字段。但是,如果我们没有必须字段,所有的字段就是可选的。等一下还会针对可选字段进行一些讨论。)

第三个 protocol buffer 的硬伤是默认值。当 protocol buffer 在某个“默认”字段上设置了默认值,而解码后的结构就像那个字段被设置了某个值一样。这个想法在有 getter 和 setter 控制字段的访问的时候非常棒,但是当容器是一个原始结构的时候就很难控制其保持清晰了。必须的字段也存在同样的麻烦:在哪定义默认值,它们的类型是什么(是UTF-8文本?无符号字节串?在浮点型中有几位?)尽管有许多看起来很简单,protocol buffer 的设计和实现还是有许多伴随的问题。我们决定让这些都远离 gob,并且回到我们的 Go 旅程中,一个很有效率的默认规则:除非你设置了一些内容,否则就是那个类型的“零值”,而这个不需要被传输。

所以 gob 最终看起来是个更加通用、简单的 protocol buffer。它又是如何工作的呢?

三、值

编码后的 gob 数据不是 int8 或者 uint16 的串。作为代替,其看起来更象是 Go 的常量,不论是有符号的还是无符号的整数值是虚拟的、无大小定义的数字。当你编码一个 int8 的时候,其值被转换为一个无大小定义的变长整数。当你对 int64 编码时,其值也是转换为一个无大小定义的变长整数。(有符号和无符号是相同处理的,无大小定义也适用于无符号值。)如果都是值 7,在线传输的位是一致的。当接收者解码其值,它将其放入接收者变量中,可能是任意的一个整数类型。因此,编码方发送了一个来自 int8 的 7,而接收方可能将其保存在 int64 中。这没有问题:这个值永远匹配于一个整数。(如果不匹配,会产生错误。)在变量的大小上解偶,为编码提供了一些灵活性:我们可以随着软件演化扩展整数类型,但是仍然可以解码旧的数据。

这种灵活性对于指针同样有效。在传输前,所有指针都进行整理。int8、int8、*int8、****int8等等的值,被传输为可能被存储于任何大小的 int,或者 *int,或者 ******int等等的整数值。这同样是一种灵活性。

同样的原因,在解码一个 struct,当其字段由编码方发送,存储于目标方的时候,也体现出这种灵活性。给出这样一个值:type T struct { X, Y, Z int } // 只有导出字段(exported fields)被编码和解码。 var t = T{X: 7, Y: 0, Z: 8}编码后仅发送 7 和 8。由于为零,Y 不会被发送;没有必要发送一个零值。

接收方可能用下面的结构解码:type U struct { X, Y *int8 } // 注意:int8 的指针 var u U而获得的 u 的值只有 X (值为 7 的 int8 变量的地址);Z 字段被忽略了——你应将其放到哪里呢?当解码一个 struct 的时候,字段会匹配其名字和类型,只有双方都有的字段会生效。这个简单的办法巧妙处理了“可选字段”问题:类型 T 添加了字段,过期的接收者仍然能处理它们知道的那部分。因此 gob 在可选字段上提供了重要的特性——无须任何额外的机制或标识。

从整数串可以构造其他类型:字节数组、字符串、数组、内存片段、Map,甚至浮点数组。IEEE 754 浮点位定义描述了浮点值存储为整数,在你知道其类型的时候,这会工作得很好,我们总是知道类型的吧。另外,这里的整数使用字节翻转的顺序发送,因为一般的浮点数字,就像是小整数数组,在低位上有许多个零是不用传递的。

gob 还有一个非常棒的特性是 Go 使得通过 GobEncoder 和 GobDecoder 接口使得自定义类型的编码成为可能,从某个意义上说类似于 JSON 包的 Marshaler 和 Unmarshaler,以及 fmt 包的 String 化接口。这个技巧使一些特殊功能成为可能,强制使用常量,或者传输数据的时候隐藏信息。

四、类型的传输

在第一次传输给定类型的时候,gob 包中包含了这个类型的描述。实际上,是这样的,编码器编码的是gob标准格式,而内部的 struct 则带有类型描述并给其标识一个唯一编号。(基本类型、类型描述结构的层级,在软件启动时已经定义好了。)在类型被描述后,它可以通过编号来引用。

因此,当我们发送类型 T 时,gob 编码器发送 T 的描述,并对其编号,例如 127。包括第一个数据包在内的所有的数据,都使用这个编号,所以 T 值的数据流看起来是这样:("define type id" 127, definition of type T)(127, T value)(127, T value), ...

类型编号使得描述递归类型,以及发送这些类型的数据成为可能。因此,gob 可以对树状类型做编码:type Node struct { Value int Left, Right *Node }

带有了类型信息,gob 流就完全自说明了。除了那些初始类型,它们已经在开始的时候就定义好了。

五、编译机

在第一次传输给定类型的时候,gob 包会构造一个针对这个类型的小翻译机。在这个类型上使用了反射来构造这个翻译机,但是一旦翻译机构建完成,它就不再依赖反射。这个翻译机使用了 unsafe 和其他一些巧妙的机制来高速的将数据转化为编码后的字节流。也可以使用反射来避免 unsafe,但是会明显变慢。(受到 gob 实现的影响,Go 的 protocol buffer 使用了类似的机制提高速度。)而后的同样类型的值使用已经编译好的翻译机,这样就可以总是有一致的编码。

解码类似,但是略微复杂。当你解码一个数据,gob 包用一个字节片保存编码后的类型的值用于来解码,再加上得到解码的 Go 的值。gob 包构造一个翻译机用于这个过程:gob 类型在线传输用于 Go 类型的解码。一旦解码翻译机构造,一个没有反射的使用 unsafe 方法的引擎能提供最快的速度。

六、例子

参考 golang - gob与rpc

例1:数据结构与bytes.Buffer之间的转换(编码成字节切片)

 1package main
 2
 3import (
 4    "bytes"
 5    "fmt"
 6    "encoding/gob"
 7    "io"
 8)
 9
10//准备编码的数据
11type P struct {
12    X, Y, Z int
13    Name    string
14}
15
16//接收解码结果的结构
17type Q struct {
18    X, Y *int32
19    Name string
20}
21
22func main() {
23    //初始化一个数据
24    data := P{3, 4, 5, "CloudGeek"}
25    //编码后得到buf字节切片
26    buf := encode(data)
27    //用于接收解码数据
28    var q *Q
29    //解码操作
30    q = decode(buf)
31    //"CloudGeek": {3,4}
32    fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)
33
34}
35
36func encode(data interface{}) *bytes.Buffer {
37    //Buffer类型实现了io.Writer接口
38    var buf bytes.Buffer
39    //得到编码器
40    enc := gob.NewEncoder(&buf)
41    //调用编码器的Encode方法来编码数据data
42    enc.Encode(data)
43    //编码后的结果放在buf中
44    return &buf
45}
46
47func decode(data interface{}) *Q {
48    d := data.(io.Reader)
49    //获取一个解码器,参数需要实现io.Reader接口
50    dec := gob.NewDecoder(d)
51    var q Q
52    //调用解码器的Decode方法将数据解码,用Q类型的q来接收
53    dec.Decode(&q)
54    return &q
55}

例2:数据结构到文件的序列化和反序列化

 1package main
 2
 3import (
 4    "encoding/gob"
 5    "os"
 6    "fmt"
 7)
 8
 9//试验用的数据类型
10type Address struct {
11    City    string
12    Country string
13}
14
15//序列化后数据存放的路径
16var filePath string
17
18func main() {
19    filePath = "./address.gob"
20    encode()
21    pa := decode()
22    fmt.Println(*pa) //{Chengdu China}
23}
24
25//将数据序列号后写到文件中
26func encode() {
27    pa := &Address{"Chengdu", "China"}
28    //打开文件,不存在的时候新建
29    file, _ := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY, 0666)
30    defer file.Close()
31
32    //encode后写到这个文件中
33    enc := gob.NewEncoder(file)
34    enc.Encode(pa)
35}
36
37//从文件中读取数据并反序列化
38func decode() *Address {
39    file, _ := os.Open(filePath)
40    defer file.Close()
41
42    var pa Address
43    //decode操作
44    dec := gob.NewDecoder(file)
45    dec.Decode(&pa)
46    return &pa
47}
七、golang中的rpc

参考
如何给老婆解释什么是RPC
golang - gob与rpc

1.rpc服务端

 1package main
 2
 3import (
 4    "net"
 5    "net/rpc"
 6    "net/http"
 7)
 8
 9type Args struct {
10    A, B int
11}
12
13//定义一个算术类型,其实就是int
14type Arith int
15
16//实现乘法的方法绑定到Arith类型,先不管为什么是这样的形式
17func (t *Arith) Multiply(args *Args, reply *int) error {
18    *reply = args.A * args.B
19    return nil
20}
21
22func main() {
23    //得到一个Arith类型的指针实例
24    arith := new(Arith)
25    //注册到rpc服务
26    rpc.Register(arith)
27    //挂到http服务上
28    rpc.HandleHTTP()
29    //开始监听
30    l, _ := net.Listen("tcp", ":1234")
31    http.Serve(l, nil)
32}

2.rpc客户端

 1package main
 2
 3import (
 4    "net/rpc"
 5    "fmt"
 6)
 7
 8type Args struct {
 9    A, B int
10}
11
12func main() {
13    //连接服务器端,创建一个client
14    client, _ := rpc.DialHTTP("tcp", "127.0.0.1:1234")
15    args := &Args{7, 8}
16    var reply int
17    //通过Call方法调用Arith类型的Multiply方法,注意形参
18    client.Call("Arith.Multiply", args, &reply)
19    //得到调用结果,输出Arith: 7*8=56
20    fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
21}

下面我们再来看一些rpc相关的细节,首先能够被rpc调用的方法应该看起来像这样:func (t *T) MethodName(argType T1, replyType *T2) error,大概解释一下:

  • 函数必须是可导出的(首字母大写)
  • 必须有两个导出类型的参数,第一个参数用来接收参数,第二个参数是返回给客户端的结果参数,第二个参数必须是指针类型的
  • 函数还要有一个返回值error
  • T1、T2能够被encoding/gob编码

看到这里你应该对于rpc的作用有了一定的认识,go中rpc包的用法简单来看就是准备一个类型,绑定一堆符合规范的方法,然后注册给rpc服务,监听客户端连接,客户端通过rpc包提供的Call方法可以调用到server注册好的方法。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:懒皮

查看原文:Golang gob与rpc简介

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1108 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传