Go語言的RPC介紹(含Protobuf-RPC)

jopen 10年前發布 | 155K 次閱讀 Go語言 Google Go/Golang開發

標準庫的RPC

RPC是遠程調用的簡稱, 簡單的說就是要像調用本地函數一樣調用服務器的函數.

Go語言的標準庫已經提供了RPC框架和不同的RPC實現.

下面是一個服務器的例子:

type Echo int

func (t *Echo) Hi(args string, reply *string) error {
    *reply = "echo:" + args
    return nil
}

func main() {
    rpc.Register(new(Echo))
    rpc.HandleHTTP()
    l, e := net.Listen("tcp", ":1234")
    if e != nil {
        log.Fatal("listen error:", e)
    }
    http.Serve(l, nil)
}

其中 rpc.Register 用于注冊RPC服務, 默認的名字是對象的類型名字(這里是Echo). 如果需要指定特殊的名字, 可以用 rpc.RegisterName 進行注冊.

被注冊對象的類型所有滿足以下規則的方法會被導出到RPC服務接口:

func (t *T) MethodName(argType T1, replyType *T2) error

被注冊對應至少要有一個方法滿足這個特征, 否則可能會注冊失敗.

然后 rpc.HandleHTTP 用于指定 RPC 的傳輸協議, 這里是采用 http 協議作為RPC調用的載體. 用戶也可以用rpc.ServeConn接口, 定制自己的傳輸協議.

客戶端可以這樣調用Echo.Hi接口:

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

    var args = "hello rpc"
    var reply string
    err = client.Call("Echo.Hi", args, &reply)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
}

客戶端先用rpc.DialHTTP和RPC服務器進行一個鏈接(協議必須匹配).

然后通過返回的client對象進行遠程函數調用. 函數的名字是由client.Call 第一個參數指定(是一個字符串).

基于HTTP的RPC調用一般是在調試時使用, 默認可以通過瀏覽"127.0.0.1:1234/debug/rpc"頁面查看RPC的統計信息.

基于 JSON 的 RPC 調用

在上面的RPC例子中, 我們采用了默認的HTTP協議作為RPC調用的傳輸載體.

因為內置net/rpc包接口設計的缺陷, 我們無法使用jsonrpc等定制的編碼作為rpc.DialHTTP的底層協議. 如果需要讓jsonrpc支持rpc.DialHTTP函數, 需要調整rpc的接口.

以前有個Issue2738是針對這個問題. 我曾提交的 CL10704046 補丁用于修復這個問題. 不過因為涉及到增加rpc的接口, 官方沒有接受(因為自己重寫一個DialHTTP會更簡單).

除了傳輸協議, 還有可以指定一個RPC編碼協議, 用于編碼/節目RPC調用的函數參數和返回值. RPC調用不指定編碼協議時, 默認采用Go語言特有的gob編碼協議.

因為, 其他語言一般都不支持Go語言的gob協議, 因此如果需要跨語言RPC調用就需要
采用通用的編碼協議.

Go的標準庫還提供了一個"net/rpc/jsonrpc"包, 用于提供基于JSON編碼的RPC支持.

服務器部分只需要用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))
    }
}

客戶端部分值需要用 jsonrpc.Dial 代替 rpc.Dial 就可以了:

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

如果需要在其他語言中使用jsonrpc和Go語言進行通訊, 需要封裝一個和jsonrpc
匹配的庫.

關于jsonrpc的實現細節這里就不展開講了, 感興趣的話可以參考這篇文章: JSON-RPC: a tale of interfaces.

基于 Protobuf 的 RPC 調用

Protobuf 是 Google 公司開發的編碼協議. 它的優勢是編碼后的數據體積比較小(并不是壓縮算法), 比較適合用于命令的傳輸編碼.

Protobuf 官方團隊提供 Java/C++/Python 幾個語言的支持, Go語言的版本由Go團隊提供支持, 其他語言由第三方支持.

Protobuf 的語言規范中可以定義RPC接口. 但是在Go語言和C++版本的Protobuf中都沒有生成RPC的實現.

不過作者在 Go語言版本的Protobuf基礎上開發了 RPC 的實現 protorpc, 同時提供的 protoc-gen-go命令可以生成相應的RPC代碼. 項目地址: https://code.google.com/p/protorpc/

該實現支持Go語言和C++語言, 在Protobuf官方wiki的第三方RPC實現列表中有介紹: https://code.google.com/p/protobuf/wiki/ThirdPartyAddOns#RPC_Implementations

要使用 protorpc, 需要先在proto文件定義接口(arith.pb/arith.proto):

package arith;

// go use cc_generic_services option
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);
}

protorpc使用cc_generic_services選擇控制是否輸出RPC代碼. 因此, 需要設置cc_generic_servicestrue.

然后下載 protoc-2.5.0-win32.zip, 解壓后可以得到一個 protoc.exe 的編譯命令.

然后使用下面的命令獲取 protorpc 和對應的 protoc-gen-go 插件.

go get code.google.com/p/protorpc
go get code.google.com/p/protorpc/protoc-gen-go

需要確保 protoc.exeprotoc-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.pb" 的名字為 arith, 在 arith.pb/arith.proto 文件中定義(這2個可能不同名, 導入時要小心).

arith.ArithRequestarith.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()

arith.DialArithService 返回了一個 stub 對象, 該對象已經綁定了RPC的各種方法, 可以直接調用(不需要用字符串指定方法名字):

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())

相比標準的RPC的庫, protorpc 由以下幾個優點:

  1. 采用標準的Protobuf協議, 便于和其他語言交互
  2. 自帶的 protoc-gen-go 插件可以生成RPC的代碼, 簡化使用
  3. 服務器注冊和調用客戶端都是具體類型而不是字符串和interface{}, 這樣可以由編譯器保證安全
  4. 底層采用了snappy壓縮傳輸的數據, 提高效率

不足之處是使用流程比標準RPC要繁復(需要將proto轉換為Go代碼).

C++ 調用 Go 提供的 Protobuf-RPC 服務

protorpc 同時也提供了 C++ 語言的實現.

C++版本的安裝如下:

  1. hg clone https://code.google.com/p/protorpc.cxx/
  2. cd protorpc.cxx
  3. build with cmake

C++ 版本 的 protorpcprotoc.exe 擴展了一個
--cxx_out 選項, 用于生成RPC的代碼:

${protorpc_root}/protobuf/bin/protoc --cxx_out=. arith.proto

注:--cxx_out 選項生成的代碼除了RPC支持外, 還有xml的序列化和反序列化支持.

下面是 C++ 的客戶端鏈接 Go 語言版本的 服務器:

#include "arith.pb.h"

#include <google/protobuf/rpc/rpc_server.h>
#include <google/protobuf/rpc/rpc_client.h>

int main() {
  ::google::protobuf::rpc::Client client("127.0.0.1", 1234);

  service::ArithService::Stub arithStub(&client);

  ::service::ArithRequest arithArgs;
  ::service::ArithResponse arithReply;
  ::google::protobuf::rpc::Error err;

  // EchoService.mul
  arithArgs.set_a(3);
  arithArgs.set_b(4);
  err = arithStub.multiply(&arithArgs, &arithReply);
  if(!err.IsNil()) {
    fprintf(stderr, "arithStub.multiply: %s\n", err.String().c_str());
    return -1;
  }
  if(arithReply.c() != 12) {
    fprintf(stderr, "arithStub.multiply: expected = %d, got = %d\n", 12, arithReply.c());
    return -1;
  }

  printf("Done.\n");
  return 0;
}

詳細的使用說明請參考: README.md .
更多的例子請參考: rpcserver.cc
rpcclient.cc

總結

Go語言的RPC客戶端是一個使用簡單, 而且功能強大的RPC庫. 基于標準的RPC庫我們可以方便的定制自己的RPC實現(傳輸協議和串行化協議都可以定制).

不過在開發 protorpc 的過程中也發現了net/rpc包的一些不足之處:

  • 內置的HTTP協議的RPC的串行化協議和傳輸協議耦合過于緊密, 用戶擴展的協議無法支持內置的HTTP傳輸協議(因為rpc.Serverrpc.Client接口缺陷導致的問題)
  • rpc.Server 只能注冊 rpc.ServerCodec, 而不能注冊工廠函數. 而jsonrpc.NewServerCodec需要依賴先建立鏈接(conn參數), 這樣導致了HTTP協議只能支持內置的gob協議
  • rpc.Client 的問題和 rpc.Server 類似

因為Go1需要保證API的兼容性, 因此上述的問題只能希望在未來的Go2能得到改善.

本文在 Golang中國博客 的地址: http://blog.go-china.org/09-protorpc

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!