美文网首页
Golang gob与rpc简介

Golang gob与rpc简介

作者: 合肥黑 | 来源:发表于2019-03-13 16:13 被阅读0次

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

    关于protobuf基础知识,可以参考Cocos Creator Protobuf的js版本使用
    关于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 的值

    编码后的 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 包中包含了这个类型的描述。实际上,是这样的,编码器编码的是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 编译机

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

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

    六、gob 例子

    参考 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实现RPC的几种方式
    1.net/rpc库

    在golang中实现RPC非常简单,有封装好的官方库和一些第三方库提供支持。Go RPC可以利用tcp或http来传递数据,可以对要传递的数据使用多种类型的编解码方式。golang官方的net/rpc库使用encoding/gob进行编解码,支持tcp或http数据传输方式,由于其他语言不支持gob编解码方式,所以使用net/rpc库实现的RPC方法没办法进行跨语言调用

    在上一节中已经简单介绍过:

    客户端通过rpc包提供的Call方法可以调用到server注册好的方法

    $GOPATH/src/test/rpc/rpc_server.go
    
    package main
    
    import (
        "errors"
        "fmt"
        "log"
        "net"
        "net/http"
        "net/rpc"
        "os"
    )
    
    // 算数运算结构体
    type Arith struct {
    }
    
    // 算数运算请求结构体
    type ArithRequest struct {
        A int
        B int
    }
    
    // 算数运算响应结构体
    type ArithResponse struct {
        Pro int // 乘积
        Quo int // 商
        Rem int // 余数
    }
    
    // 乘法运算方法
    func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
        res.Pro = req.A * req.B
        return nil
    }
    
    // 除法运算方法
    func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
        if req.B == 0 {
            return errors.New("divide by zero")
        }
        res.Quo = req.A / req.B
        res.Rem = req.A % req.B
        return nil
    }
    
    func main() {
        rpc.Register(new(Arith)) // 注册rpc服务
        rpc.HandleHTTP()         // 采用http协议作为rpc载体
    
        lis, err := net.Listen("tcp", "127.0.0.1:8095")
        if err != nil {
            log.Fatalln("fatal error: ", err)
        }
    
        fmt.Fprintf(os.Stdout, "%s", "start connection")
    
        http.Serve(lis, nil)
    }
    
    $GOPATH/src/test/rpc/rpc_client.go
    
    package main
    
    import (
        "fmt"
        "log"
        "net/rpc"
    )
    
    // 算数运算请求结构体
    type ArithRequest struct {
        A int
        B int
    }
    
    // 算数运算响应结构体
    type ArithResponse struct {
        Pro int // 乘积
        Quo int // 商
        Rem int // 余数
    }
    
    func main() {
        conn, err := rpc.DialHTTP("tcp", "127.0.0.1:8095")
        if err != nil {
            log.Fatalln("dailing error: ", err)
        }
    
        req := ArithRequest{9, 2}
        var res ArithResponse
    
        err = conn.Call("Arith.Multiply", req, &res) // 乘法运算
        if err != nil {
            log.Fatalln("arith error: ", err)
        }
        fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)
    
        err = conn.Call("Arith.Divide", req, &res)
        if err != nil {
            log.Fatalln("arith error: ", err)
        }
        fmt.Printf("%d / %d, quo is %d, rem is %d\n", req.A, req.B, res.Quo, res.Rem)
    }
    
    2.net/rpc/jsonrpc库

    golang官方还提供了net/rpc/jsonrpc库实现RPC方法,JSON RPC采用JSON进行数据编解码,因而支持跨语言调用。但目前的jsonrpc库是基于tcp协议实现的,暂时不支持使用http进行数据传输,数据编解码性能不高。

    $GOPATH/src/test/rpc/jsonrpc_server.go
    
    package main
    
    import (
        "errors"
        "fmt"
        "log"
        "net"
        "net/rpc"
        "net/rpc/jsonrpc"
        "os"
    )
    
    // 算数运算结构体
    type Arith struct {
    }
    
    // 算数运算请求结构体
    type ArithRequest struct {
        A int
        B int
    }
    
    // 算数运算响应结构体
    type ArithResponse struct {
        Pro int // 乘积
        Quo int // 商
        Rem int // 余数
    }
    
    // 乘法运算方法
    func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
        res.Pro = req.A * req.B
        return nil
    }
    
    // 除法运算方法
    func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
        if req.B == 0 {
            return errors.New("divide by zero")
        }
        res.Quo = req.A / req.B
        res.Rem = req.A % req.B
        return nil
    }
    
    func main() {
        rpc.Register(new(Arith)) // 注册rpc服务
    
        lis, err := net.Listen("tcp", "127.0.0.1:8096")
        if err != nil {
            log.Fatalln("fatal error: ", err)
        }
    
        fmt.Fprintf(os.Stdout, "%s", "start connection")
    
        for {
            conn, err := lis.Accept() // 接收客户端连接请求
            if err != nil {
                continue
            }
    
            go func(conn net.Conn) { // 并发处理客户端请求
                fmt.Fprintf(os.Stdout, "%s", "new client in coming\n")
                jsonrpc.ServeConn(conn)
            }(conn)
        }
    }
    
    $GOPATH/src/test/rpc/jsonrpc_client.go
    
    package main
    
    import (
        "fmt"
        "log"
        "net/rpc/jsonrpc"
    )
    
    // 算数运算请求结构体
    type ArithRequest struct {
        A int
        B int
    }
    
    // 算数运算响应结构体
    type ArithResponse struct {
        Pro int // 乘积
        Quo int // 商
        Rem int // 余数
    }
    
    func main() {
        conn, err := jsonrpc.Dial("tcp", "127.0.0.1:8096")
        if err != nil {
            log.Fatalln("dailing error: ", err)
        }
    
        req := ArithRequest{9, 2}
        var res ArithResponse
    
        err = conn.Call("Arith.Multiply", req, &res) // 乘法运算
        if err != nil {
            log.Fatalln("arith error: ", err)
        }
        fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)
    
        err = conn.Call("Arith.Divide", req, &res)
        if err != nil {
            log.Fatalln("arith error: ", err)
        }
        fmt.Printf("%d / %d, quo is %d, rem is %d\n", req.A, req.B, res.Quo, res.Rem)
    }
    
    3.protorpc库

    为了实现跨语言调用,在golang中实现RPC方法的时候我们应该选择一种跨语言的数据编解码方式,比如JSON,上述的jsonrpc可以满足此要求,但是也存在一些缺点,比如不支持http传输,数据编解码性能不高等。于是呢,一些第三方rpc库都选择采用protobuf进行数据编解码,并提供一些服务注册代码自动生成功能。下面的例子我们使用protobuf来定义RPC方法及其请求响应参数,并使用第三方的protorpc库来生成RPC服务注册代码。

    $GOPATH/src/test/rpc/pb/arith.proto
    
    syntax = "proto3";
    package pb;
    
    // 算术运算请求结构
    message ArithRequest {
        int32 a = 1;
        int32 b = 2;
    }
    
    // 算术运算响应结构
    message ArithResponse {
        int32 pro = 1;  // 乘积
        int32 quo = 2;  // 商
        int32 rem = 3;  // 余数
    }
    
    // rpc方法
    service ArithService {
        rpc multiply (ArithRequest) returns (ArithResponse);    // 乘法运算方法
        rpc divide (ArithRequest) returns (ArithResponse);      // 除法运算方法
    }
    
    4.其他RPC库

    除了上面提到的三种在golang实现RPC的方式外,还有一些其他的rpc库提供了类似的功能,比较出名的有google开源的grpc,但是grpc的初次安装比较麻烦,这里就不做进一步介绍了,有兴趣的可以自己了解。
    参考https://www.grpc.io/https://github.com/grpc/grpc-go
    在gRPC客户端可以直接调用不同服务器上的远程程序,就想调用本地程序一样,很容易构建分布式应用和服务。和很多RPC系统一样,服务负责实现定义好的接口并处理客户端请求,客户端根据接口描述直接调用需要的服务。客户端和服务器可以分别使用gRPC支持的不同语言实现。
    gRPC在Go中的使用(一)Protocol Buffers语法与相关使用
    gRPC在Go中的使用(二)gRPC实现简单通讯
    gRPC在Go中的使用(三)gRPC实现TLS加密通信与流模式

    Dive into gRPC(1):gRPC简介
    Dive into gRPC(2):实现一个服务
    Dive into gRPC(3):安全通信
    Dive into gRPC(4):Streaming
    Dive into gRPC(5):验证客户端
    Dive into gRPC(6):metadata

    相关文章

      网友评论

          本文标题:Golang gob与rpc简介

          本文链接:https://www.haomeiwen.com/subject/xkzzpqtx.html