美文网首页
使用 grpc-go 编写 Client/Server

使用 grpc-go 编写 Client/Server

作者: juniway | 来源:发表于2017-12-16 17:08 被阅读675次

    介绍

    这篇文章介绍使用 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 返回给客户端。

    全文完

    相关文章

      网友评论

          本文标题:使用 grpc-go 编写 Client/Server

          本文链接:https://www.haomeiwen.com/subject/veaswxtx.html