在Go語言中使用 Protobuf-RPC

chai2010發表於2013-04-25

Go語言版本的Protobuf-RPC基本算完成了. 現在簡單說下使用方法.

安裝測試環境

先下載程式碼(不支援go get):

hg clone https://bitbucket.org/chai2010/protorpc

然後下載後的目錄設定為GOPATH, 並新增$GOPATH/binPATH環境變數.

$GOPATH/bin中已經包含了Windows下的2.4.1版本的protoc.exe. 如果是Linux等系統, 請自行下載並安裝protoc程式.

安裝protoc.exe的Go語言外掛:

go install encoding/protobuf/protoc-gen-go

該外掛是基於code.google.com/p/goprotobuf/protoc-gen-go實現, 主要增加了encoding/protobuf/protoc-gen-go/generator/service.go檔案, 用於RPC的程式碼生成. 生成的RPC程式碼依賴net/rpc/protorpc, 這個包是Protobuf-RPC的底層實現, 可以單獨使用.

現在可以執行一下測試程式:

C:\>go test net/rpc/protorpc/service.pb
ok      net/rpc/protorpc/service.pb     2.123s

測試通過, 繼續.


編寫 proto 檔案

建立一個名為pbrpc的工作目錄, 再建立pbrpc/arith.pb的子目錄. 將net/rpc/protorpc/service.pb/service.proto檔案複製到pbrpc/arith.pb的子目錄.

包名字改為arith, 檔案arith.proto的內容如下:

package arith;

option cc_generic_services = true;
option java_generic_services = true;
option py_generic_services = true;

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

message ArithResponse {
    optional int32 c = 1;
}

service ArithService {
    rpc add (ArithRequest) returns (ArithResponse);
    rpc mul (ArithRequest) returns (ArithResponse);
    rpc div (ArithRequest) returns (ArithResponse);
    rpc error (ArithRequest) returns (ArithResponse);
}

主要是定義了一個ArithService介面. 要注意的是cc_generic_services/java_generic_services, py_generic_services幾個選項. 我前提提到的protoc-gen-go在生成程式碼的時候, 這3個選項至少要有一個為true, 才會生成RPC的程式碼.

當然, 如果不生成RPC程式碼的話, 也是可以單獨使用net/rpc/protorpc包的. 不過protoc-gen-go生成的程式碼會簡便很多.

進入pbrpc/arith.pb的子目錄, 編譯arith.proto檔案:

protoc --go_out=. arith.proto

生成 arith.pb.go 檔案, 其中RPC的程式碼主要是下面這些:

type ArithService interface {
    Add(in *ArithRequest, out *ArithResponse) error
    Mul(in *ArithRequest, out *ArithResponse) error
    Div(in *ArithRequest, out *ArithResponse) error
    Error(in *ArithRequest, out *ArithResponse) error
}

// RegisterArithService publish the given ArithService implementation on the server.
func RegisterArithService(srv *rpc.Server, x ArithService) error {
    if err := srv.RegisterName("ArithService", x); err != nil {
        return err
    }
    return nil
}

// ServeArithService serves the given ArithService implementation on conn.
func ServeArithService(conn io.ReadWriteCloser, x ArithService) error {
    srv := rpc.NewServer()
    if err := srv.RegisterName("ArithService", x); err != nil {
        return err
    }
    srv.ServeCodec(protorpc.NewServerCodec(conn))
    return nil
}

// ListenAndServeArithService listen announces on the local network address laddr
// and serves the given ArithService implementation.
func ListenAndServeArithService(network, addr string, x ArithService) error {
    clients, err := net.Listen(network, addr)
    if err != nil {
        return err
    }
    srv := rpc.NewServer()
    if err := srv.RegisterName("ArithService", x); err != nil {
        return err
    }
    for {
        conn, err := clients.Accept()
        if err != nil {
            return err
        }
        go srv.ServeCodec(protorpc.NewServerCodec(conn))
    }
    panic("unreachable")
}

type rpcArithServiceStub struct {
    *rpc.Client
}

func (c *rpcArithServiceStub) Add(in *ArithRequest, out *ArithResponse) error {
    return c.Call("ArithService.Add", in, out)
}
func (c *rpcArithServiceStub) Mul(in *ArithRequest, out *ArithResponse) error {
    return c.Call("ArithService.Mul", in, out)
}
func (c *rpcArithServiceStub) Div(in *ArithRequest, out *ArithResponse) error {
    return c.Call("ArithService.Div", in, out)
}
func (c *rpcArithServiceStub) Error(in *ArithRequest, out *ArithResponse) error {
    return c.Call("ArithService.Error", in, out)
}

// DialArithService connects to an ArithService at the specified network address.
func DialArithService(network, addr string) (*rpc.Client, ArithService, error) {
    conn, err := net.Dial(network, addr)
    if err != nil {
        return nil, nil, err
    }
    c, srv := NewArithServiceClient(conn)
    return c, srv, nil
}

// NewArithServiceClient returns a ArithService rpc.Client and stub to handle
// requests to the set of ArithService at the other end of the connection.
func NewArithServiceClient(conn io.ReadWriteCloser) (*rpc.Client, ArithService) {
    c := rpc.NewClientWithCodec(protorpc.NewClientCodec(conn))
    return c, &rpcArithServiceStub{c}
}

// NewArithServiceStub returns a ArithService stub to handle rpc.Client.
func NewArithServiceStub(c *rpc.Client) ArithService {
    return &rpcArithServiceStub{c}
}

其中生成的伺服器端的程式碼有: ListenAndServeArithService, ServeArithService, RegisterArithService. 生成的客戶端的介面有: DialArithService, NewArithServiceClient, NewArithServiceStub. 其中RPC介面對應ArithService介面.


編寫測試程式碼

pbrpc目錄建立rpc_server.go檔案, 程式碼如下:

package main

import (
    "encoding/protobuf/proto"
    "errors"

    "./arith.pb"
)

type Arith int

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

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

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

func (t *Arith) Error(args *arith.ArithRequest, reply *arith.ArithResponse) error {
    return errors.New("ArithError")
}

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

最關鍵的是arith.ListenAndServeArithService("tcp", ":1234", new(Arith)). 當然, 也可以使用RegisterArithServiceServeArithService等介面進行定製.

然後在pbrpc建立rpc_client.go對應客戶端, 程式碼如下:

package main

import (
    "encoding/protobuf/proto"
    "log"

    "./arith.pb"
)

func main() {
    // client
    client, stub, err := arith.DialArithService("tcp", "127.0.0.1:1234")
    if err != nil {
        log.Fatalf(`arith.DialArithService("tcp", "127.0.0.1:1234"): %v`, err)
    }
    defer client.Close()

    var args arith.ArithRequest
    var reply arith.ArithResponse

    // Add
    args.A = proto.Int32(1)
    args.B = proto.Int32(2)
    if err = stub.Add(&args, &reply); err != nil {
        log.Fatalf(`arith.Add: %v`, err)
    }
    if reply.GetC() != 3 {
        log.Fatalf(`arith.Add: expected = %d, got = %d`, 3, reply.GetC())
    }

    // Mul
    args.A = proto.Int32(2)
    args.B = proto.Int32(3)
    if err = stub.Mul(&args, &reply); err != nil {
        log.Fatalf(`arith.Mul: %v`, err)
    }
    if reply.GetC() != 6 {
        log.Fatalf(`arith.Mul: expected = %d, got = %d`, 6, reply.GetC())
    }

    // Div
    args.A = proto.Int32(13)
    args.B = proto.Int32(5)
    if err = stub.Div(&args, &reply); err != nil {
        log.Fatalf(`arith.Div: %v`, err)
    }
    if reply.GetC() != 2 {
        log.Fatalf(`arith.Div: expected = %d, got = %d`, 2, reply.GetC())
    }

    // Div zero
    args.A = proto.Int32(1)
    args.B = proto.Int32(0)
    if err = stub.Div(&args, &reply); err.Error() != "divide by zero" {
        log.Fatalf(`arith.Error: expected = %s, got = %s`, "divide by zero", err.Error())
    }

    // Error
    args.A = proto.Int32(1)
    args.B = proto.Int32(2)
    if err = stub.Error(&args, &reply); err.Error() != "ArithError" {
        log.Fatalf(`arith.Error: expected = %s, got = %s`, "ArithError", err.Error())
    }

    log.Printf("Done")
}

然後就可以啟動服務, 並測試客戶端了.

PS: 看來圖靈社群這類地方不是很喜歡技術類的文章, 以後技術文章就不發這裡了. 以後來這裡只看別人八卦灌水 :)

補充: 目前專案已經移到googlecode, 可以直接用go命令安裝:

go get code.google.com/p/protorpc

更多Go文章請訪問: http://my.oschina.net/chai2010

相關文章