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