使用 grpc-go 編寫 Client/Server

介紹

這篇文章介紹使用 grpc-go 編寫 Client/Server 程序

一、proto 文件定義及 Go 代碼生成

gRpc 使用 protobuf 作為默認的 wire-format 傳輸,在 .proto 文件中,我們需要定義傳輸過程中需要使用的各種 Message 類型,同時我們還需要定義 service,并在 service 中提供遠程調用的各種方法。

(1)傳輸過程中的數據格式

提供 service 的 .proto 文件中定義兩個 message 類型,一個用于指定請求的參數類型 HelloRequest,一個用于返回值的類型 HelloReply

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

(2)提供的服務 Service
除了定義消息類型,.proto 文件中還需要定義一個 service 用于供客戶端遠程調用, 在這個 service 中聲明 RPC 服務所要提供的各種方法的函數簽名。
注意:這里只是函數聲明,其指明了需要接收的參數類型和返回的類型。函數的實現需要我們根據 proto 生成的 Go 語言文件,在 Server 端編寫代碼來自行實現。

// The request message containing the user's name.
service Greeter {
  rpc SayHello (HelloRequest) returns(HelloReply){}
  rpc SayGoodby (GoodByRequest) returns(GoodByReplay) {}
}

我們可以把上述(1)和(2)的內容放在兩個單獨的 .proto 文件中(當然,必須使用同一個包名)。也可以放在同一個 .proto 文件中。通常,為了方便,我們都會放在同一個 .proto 文件中。這里我們把上述內容放在 helloworld.proto 文件中。

現在,調用 protoc 來生成 go 代碼

protoc -I ghello/ ghello/ghello.proto --go_out=plugins=grpc:ghello
protoc -I helloworld/helloworld/ helloworld/helloworld/helloworld.proto --go_out=plugins=grpc:helloworld

執行上述語句,protoc 會生成一個叫 helloworld.pb.go 的文件,這里面就包含了 Go 語言表述的相關代碼。

二、生成的 helloworld.pb.go 代碼解析

理解 protoc 所生成的 helloworld.pb.go 代碼有助于我們理解整個 gRpc 的調用過程。

(1)首先,按照 proto 文件中所申明的各種不同消息類型,會生成對應名稱的 struct 結構體,如下:

type HelloRequest struct {
    Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}

type HelloReply struct {
    Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
}

(2)為上述結構體生成一些默認的方法,比如

func (m *HelloRequest) Reset()                    { *m = HelloRequest{} }
func (m *HelloRequest) String() string            { return proto.CompactTextString(m) }
func (*HelloRequest) ProtoMessage()               {}
func (*HelloRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }

(3)為 Service 分別生成 Client 端和 Server 端的 interface 定義,如下:

type GreeterClient interface {
    // Sends a greeting
    SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}

type GreeterServer interface {
    // Sends a greeting
    SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

這里要特別注意,雖然兩者都是 interface 類型,但是 Client 端的對象是由 protoc 自動生成的,其實現了 GreeterClient 接口,見(4)。而 Server 端的對象則需要我們自己去手動編寫了。因為我們是服務提供方嘛,提供什么具體的服務當然是由我們決定的。

(4)生成默認的 Client 類型,以便 gRpc 的客戶端可以使用它來連接及調用 gRpc 服務端提供的服務。

type greeterClient struct {
    cc *grpc.ClientConn
}

func NewGreeterClient(cc *grpc.ClientConn) GreeterClient {
    return &greeterClient{cc}
}

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
    out := new(HelloReply)
    err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, c.cc, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

注意到上述 Client 默認實現的 SayHello 函數,這個叫做 Client Stub,其實就是相當于本地實現了一個 SayHello 函數,當 grpc 的客戶端調用 SayHello 函數的時候,其調用的就是這個本地的 SayHello 函數,這個函數在內部通過 grpc.Invoke() 的方式實現了遠程調用。

(5)注冊服務

func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
    s.RegisterService(&_Greeter_serviceDesc, srv)
}

這里的邏輯很簡單,我們需要在我們的服務端,自己去定義一個 struct 對象,實現 .pb.go 中所聲明的 GreeterServer 接口,然后把那個 struct 注冊到 grpc 服務上。

三、啟動 gRpc 服務端和客戶端

理解了上述所生成的 pb.go 的代碼內容,現在我們就需要來編寫 gRpc 的 Server 端代碼了。先來看 Server 端代碼怎么寫才能提供 gRpc 服務。

為了簡單,我分成了如下如下步驟:

  • (1) 指定需要提供服務的端口,本地未被使用的任意端口都可以,比如 50051。
  • (2) 監聽端口,調用net.Listen("tcp", port)
  • (3) 定義一個 server struct 來實現 proto 文件中 service 部分所聲明的所有 RPC 方法。 這步最關鍵!
  • (4) 調用 grpc.NewServer() 來創建一個 Server
  • (5) 把上述 struct 的實例注冊到 gprc 上
  • (6)調用 s.Serve(lis) 提供 gRpc 服務
package main

import (
    "log"
    "net"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    pb "google.golang.org/grpc/examples/helloworld/helloworld"
    "google.golang.org/grpc/reflection"
)

const (
    port = ":50051"
)

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    // Register reflection service on gRPC server.
    reflection.Register(s)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

有了服務端提供服務,我們就要想辦法訪問它了,這就需要我們編寫 gRpc 的客戶端了,來看看客戶端代碼怎么寫。
我也詳細分成了如下步驟:

  • (1)連接服務端, 調用 conn = grpc.Dial("localhost:50051", grpc.WithInsecure())
  • (2)創建 GreeterClient(conn),把連接成功后返回的 conn 傳入
  • (3)調用具體的方法 SayHello()
package main

import (
    "log"
    "os"

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
    address     = "localhost:50051"
    defaultName = "world"
)

func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    name := defaultName
    if len(os.Args) > 1 {
        name = os.Args[1]
    }
    r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.Message)
}

這里,只需要把正確類型的參數傳給 c.SayHello() 就能完成 grpc 調用了。如:&pb.HelloRequest{Name: name}

注:要連接 gRpc 的服務端,使用上述 Go 語言版本的 gRpc client 可以完成,使用其他任何 grpc 支持的語言的 client 都可以完成。比如,我們使用 cpp 版本的 client

#include <iostream>
#include <memory>
#include <string>
#include <grpc++/grpc++.h>

#ifdef BAZEL_BUILD
#include "examples/protos/helloworld.grpc.pb.h"
#else
#include "helloworld.grpc.pb.h"
#endif

using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
using helloworld::HelloRequest;
using helloworld::HelloReply;
using helloworld::Greeter;

class GreeterClient {
 public:
  GreeterClient(std::shared_ptr<Channel> channel)
      : stub_(Greeter::NewStub(channel)) {}

  // Assembles the client's payload, sends it and presents the response back
  // from the server.
  std::string SayHello(const std::string& user) {
    // Data we are sending to the server.
    HelloRequest request;
    request.set_name(user);

    // Container for the data we expect from the server.
    HelloReply reply;

    // Context for the client. It could be used to convey extra information to
    // the server and/or tweak certain RPC behaviors.
    ClientContext context;

    // The actual RPC.
    Status status = stub_->SayHello(&context, request, &reply);

    // Act upon its status.
    if (status.ok()) {
      return reply.message();
    } else {
      std::cout << status.error_code() << ": " << status.error_message()
                << std::endl;
      return "RPC failed";
    }
  }

 private:
  std::unique_ptr<Greeter::Stub> stub_;
};

int main(int argc, char** argv) {
  // Instantiate the client. It requires a channel, out of which the actual RPCs
  // are created. This channel models a connection to an endpoint (in this case,
  // localhost at port 50051). We indicate that the channel isn't authenticated
  // (use of InsecureChannelCredentials()).
  GreeterClient greeter(grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials()));
  std::string user("world");
  std::string reply = greeter.SayHello(user);
  std::cout << "Greeter received: " << reply << std::endl;

  return 0;
}

在服務端,查詢 db,并且賦值給 HelloReply 中的 fields。 然后把 HelloReply message 返回給客戶端。

全文完

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容