使用 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 
#include 
#include 
#include 

#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)
      : 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 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 返回给客户端。

全文完

你可能感兴趣的:(使用 grpc-go 编写 Client/Server)