>

介绍

这篇文章介绍使用 grpc-go 编写 Client/Server 程序

一、proto 文件定义及 Go 代码生成

gRpc 使用 protobuf 作为默认的 wire-format 传输,在 .proto 文件中,我们需要定义传输过程中需要使用的各种 Message 类型,同时我们还需要定义 service,并在 service 中提供远程调用的各种方法。

(1)传输过程中的数据格式

提供 service 的 .proto 文件中定义两个 message 类型,一个用于指定请求的参数类型 HelloRequest,一个用于返回值的类型 HelloReply

1
2
3
4
5
6
7
message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

(2)提供的服务 Service
除了定义消息类型,.proto 文件中还需要定义一个 service 用于供客户端远程调用, 在这个 service 中声明 RPC 服务所要提供的各种方法的函数签名。
注意:这里只是函数声明,其指明了需要接收的参数类型和返回的类型。函数的实现需要我们根据 proto 生成的 Go 语言文件,在 Server 端编写代码来自行实现。

1
2
3
4
5
// 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 代码

1
2
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 结构体,如下:

1
2
3
4
5
6
7
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)为上述结构体生成一些默认的方法,比如

1
2
3
4
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 定义,如下:

1
2
3
4
5
6
7
8
9
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 服务端提供的服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)注册服务

1
2
3
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 服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#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 返回给客户端。

全文完


如果你对我的文章感兴趣,欢迎留言或者关注我的专栏。

微信公众号:“知辉”

搜索“deliverit”或

扫描二维码