go语言RPC实战

go语言RPC实战

本文将介绍rpc基本知识以及go中自带rpc应用实践


RPC基本知识

RPC(Remote Procedure Call):远程过程调用。本质是一种协议,允许一台计算机的程序通过网络从远程计算机程序上请求服务,不需要使用者了解底层网络技术的协议。在OSI网络通信模型中,RPC跨越了传输层和应用层,使得开发包括网络分布式程序在内的应用程序更加容易。
RPC采用client/server模式,client调用句柄,发送参数,调用本地系统内核,发送网络消息到server等待应答,server句柄收到调用信息,获得进程参数,执行远程过程,计算结果,返回服务器句柄,句柄返回response,调用远程系统内核,消息传回本地,client句柄从本地主机内核接收信息,client接收句柄返回的数据。
go语言RPC实战_第1张图片

RPC与HTTP调用区别

了解了RPC基本知识之后,大家可能会有一个疑问,那就是RPC与HTTP调用同样都是在client调用远程服务,两者到底有什么区别?
大家都知道,ISO五层网络结构模型:应用层->传输层->网络层->链路层->物理层 (应用层-合并了七层模型中的表示层和会话层)
而RPC主要是基于TCP/IP协议,HTTP调用主要是基于HTTP协议,而HTTP协议是封装在传输层TCP之上(HTTP是应用层协议),用于标准化数据传输的应用层,显而易见,RPC效率会更高。

go中RPC实践

go的RPC中,如果客户端是go语言编写,则将用go特有的gob序列化,同时可以选择rpc/jsonrpc包,用json格式序列化以便与其他rpc系统交互。
go语言中的RPC在使用时有以下限制:
(1)远程程序必须是公共的,即函数名首字母大写
(2)远程程序必须有且仅有两个参数,第一个参数是从客户端接收的数据,第二个参数是一个指向返回给客户端数据的指针。
(3)返回值是一个error
eg1:func (t *T) F(argType T1, replyType *T2) error

RPC的server端实现

注册:server端需要通过register方法为一个接口完成注册,注册完成之后,client可以通过这个接口的方法调用远程过程,eg:rpc.Register(object),默认的名字是对象的类型名字. 如果需要指定特殊的名字, 可以用 rpc.RegisterName 进行注册。
被注册对象至少要有一个方法满足eg1的特征(供client调用),否则可能会注册失败。满足该特征的方法会被导出到RPC服务接口。

package main

import (
    "github.com/haisum/rpcexample"
    "log"
    "net"
    "net/http"
    "net/rpc"
)

func main() {
    //注册Arith 对象作为一个服务
    arith := new(rpcexample.Arith)
    err := rpc.Register(arith)
    if err != nil {
        log.Fatalf("Format of service Arith isn't correct. %s", err)
    }
    //将Rpc绑定到HTTP协议上
    rpc.HandleHTTP()
    //开始监听端口 1234的消息
    l, e := net.Listen("tcp", ":1234")
    if e != nil {
        log.Fatalf("Couldn't start listening on port 1234. Error %s", e)
    }
    log.Println("Serving RPC handler")
    //启动http服务,处理连接请求l
    err = http.Serve(l, nil)
    if err != nil {
        log.Fatalf("Error serving: %s", err)
    }
}

RPC的client端实现

package main

import (
    "github.com/haisum/rpcexample"
    "log"
    "net/rpc"
)

func main() {
    //与远程过程调用服务器建立连接
    client, err := rpc.DialHTTP("tcp", ":1234")
    if err != nil {
        log.Fatalf("Error in dialing. %s", err)
    }
    //构造参数对象
    args := &rpcexample.Args{
        A: 2,
        B: 3,
    }
    //this will store returned result
    var result rpcexample.Result
    //调用远程过程Arith.Multiply,参数存放在args中,result中存放结果
    err = client.Call("Arith.Multiply", args, &result)
    if err != nil {
        log.Fatalf("error in Arith", err)
    }
    //输出result中的结果
    log.Printf("%d*%d=%d\n", args.A, args.B, result)
}

基于json的RPC调用

上述示例中,我们采用默认的http协议作为RPC调用的传输载体
使用内置的net/rpc,无法使用jsonrpc等定制的编码作为rpc.DialHTTP的底层协议。除了指定传输协议,还可以指定RPC编码协议,用来编码和解码RPC调用的函数参数和返回值。不指定编码协议时,默认采用GO特有的gob编码协议。但是其他语言一般不支持go的gob协议,所以如果需要跨语言的RPC调用,需要采用通用的编码协议。GO标准库提供“net/rpc/jsonrpc”包,用于提供基于json编码的RPC支持。
Server部分需要用rpc.ServeCodec指定json编码

func main() {
    lis, err := net.Listen("tcp", ":1234")
    if err != nil {
        return err
    }
    defer lis.Close()

    srv := rpc.NewServer()
    if err := srv.RegisterName("Echo", new(Echo)); err != nil {
        return err
    }

    for {
        conn, err := lis.Accept()
        if err != nil {
            log.Fatalf("lis.Accept(): %v\n", err)
        }
        go srv.ServeCodec(jsonrpc.NewServerCodec(conn))
    }
}

client部分用jsonrpc.Dial代替rpc.Dial

func main() {
    client, err := jsonrpc.DialHTTP("tcp", "127.0.0.1:1234")
    if err != nil {
        log.Fatal("dialing:", err)
    }
    ...
}

基于protobuf使用RPC

protobuf(Google Protocol Buffer)是google公司内部的混合语言数据标准,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,即序列化,适合做数据存储或者RPC数据交换格式,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式(protobuf另行开文介绍)
protobuf的官方团队提供java、c++、python的支持,go语言版本的支持是由Go团队支持,protobuf中没有go语言生成RPC的实现,Go团队又在go语言版本的protobuf基础上开发了RPC的实现:protorpc,同时提供了protoc-gen-go命令可以生成相应的rpc代码,protorpc项目地址
protobuf安装:
(1)安装下载protoc,很多种安装方法,下载地址https://github.com/google/protobuf/releases
(2)安装下载proto的go插件,命令是go get github.com/golang/protobuf/protoc-gen-go,也可以自己手动下载安装(如果使用go get则会自动生成protoc-gen-go的可执行文件)
(3)将protoc-gen-go可执行文件路径加到PATH环境变量中,如果是go get安装是会在GOBIN路径下生成protoc-gen-go,执行export PATH= PATH: P A T H : GOBIN(原因在于, protoc-gen-go可执行文件需要被protoc调用),简单粗爆的将protoc.exe放在C:\Windows\System32下面也是可以运行的。
(4)安装goprotobuf库(注意,protoc-gen-go只是一个插件,goprotobuf的其他功能比如marshal、unmarshal等功能还需要由protobuf库提供)go get github.com/golang/protobuf/proto
要使用 protorpc, 需要先在proto文件定义接口(arith.pb/arith.proto):

package arith;

// protorpc使用cc_generic_services选择控制是否输出RPC代码. 因此, 需要设置cc_generic_services为true.
option cc_generic_services = true;

message ArithRequest {
    optional int32 a = 1;
    optional int32 b = 2;
}

message ArithResponse {
    optional int32 val = 1;
    optional int32 quo = 2;
    optional int32 rem = 3;
}

service ArithService {
    rpc multiply (ArithRequest) returns (ArithResponse);
    rpc divide (ArithRequest) returns (ArithResponse);
}

需要确保 protoc.exe 和 protoc-gen-go.exe 都在 $PATH 中. 然后运行以下命令将前面的接口文件转换为Go代码:

cd arith.pb && protoc --go_out=. arith.proto

新生成的文件为arith.pb/arith.pb.go.

基于protobuf-RPC的服务器:

package main

import (
    "errors"

    "code.google.com/p/goprotobuf/proto"

    "./arith.pb"
)

type Arith int

func (t *Arith) Multiply(args *arith.ArithRequest, reply *arith.ArithResponse) error {
    reply.Val = proto.Int32(args.GetA() * args.GetB())
    return nil
}

func (t *Arith) Divide(args *arith.ArithRequest, reply *arith.ArithResponse) error {
    if args.GetB() == 0 {
        return errors.New("divide by zero")
    }
    reply.Quo = proto.Int32(args.GetA() / args.GetB())
    reply.Rem = proto.Int32(args.GetA() % args.GetB())
    return nil
}

func main() {
    arith.ListenAndServeArithService("tcp", ":1984", new(Arith))
}

arith.ArithRequest和arith.ArithResponse是RPC接口的输入和输出参数, 也是在在arith.pb/arith.proto 文件中定义的.

同时生成的还有一个arith.ListenAndServeArithService函数, 用于启动RPC服务. 该函数的第三个参数是RPC的服务对象, 必须要满足 arith.EchoService 接口的定义.

客户端的使用也很简单, 只要一个 arith.DialArithService 就可以链接了:

stub, client, err := arith.DialArithService("tcp", "127.0.0.1:1984")
if err != nil {
    log.Fatal(`arith.DialArithService("tcp", "127.0.0.1:1984"):`, err)
}
defer client.Close()

var args ArithRequest
var reply ArithResponse

args.A = proto.Int32(7)
args.B = proto.Int32(8)
if err = stub.Multiply(&args, &reply); err != nil {
     
    log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d", args.GetA(), args.GetB(), reply.GetVal())

arith.DialArithService 返回了一个 stub 对象, 该对象已经绑定了RPC的各种方法, 可以直接调用(不需要用字符串指定方法名字):
相比标准的RPC的库, protorpc 由以下几个优点:

采用标准的Protobuf协议, 便于和其他语言交互
自带的 protoc-gen-go 插件可以生成RPC的代码, 简化使用
服务器注册和调用客户端都是具体类型,而不是字符串和interface{}, 这样可以由编译器保证安全
底层采用了snappy压缩传输的数据, 提高效率
不足之处是使用流程比标准RPC要繁复(需要将proto转换为Go代码).


你可能感兴趣的:(go,go,rpc,protobuf)