「连载二」gRPC Client and Server

前言

本章节将使用 Go 来编写 gRPC Server 和 Client,让其互相通讯。在此之上会使用到如下库:

  • google.golang.org/grpc
  • github.com/golang/protobuf/protoc-gen-go

安装

gRPC

  1. go get-u google.golang.org/grpc

Protocol Buffers v3

  1. wget https://github.com/google/protobuf/releases/download/v3.5.1/protobuf-all-3.5.1.zip
  2. unzip protobuf-all-3.5.1.zip
  3. cd protobuf-3.5.1/
  4. ./configure
  5. make
  6. make install

检查是否安装成功

  1. protoc --version

若出现以下错误,执行 ldconfig 命名就能解决这问题

  1. protoc: error while loading shared libraries: libprotobuf.so.15: cannot open shared object file:No such file or directory

Protoc Plugin

  1. go get-u github.com/golang/protobuf/protoc-gen-go

安装环境若有问题,可参考我先前的文章 《介绍与环境安装》 内有详细介绍,不再赘述

gRPC

本小节开始正式编写 gRPC 相关的程序,一起上车吧 😄

图示

image

目录结构

  1. $ tree go-grpc-example
  2. go-grpc-example
  3. ├── client
  4. ├── proto
  5. └── search.proto
  6. └── server.go

IDL

编写

在 proto 文件夹下的 search.proto 文件中,写入如下内容:

  1. syntax ="proto3";
  2. package proto;
  3. service SearchService{
  4. rpc Search(SearchRequest) returns (SearchResponse){}
  5. }
  6. message SearchRequest{
  7. string request =1;
  8. }
  9. message SearchResponse{
  10. string response =1;
  11. }

生成

在 proto 文件夹下执行如下命令:

  1. $ protoc --go_out=plugins=grpc:.*.proto
  • plugins=plugin1+plugin2:指定要加载的子插件列表

我们定义的 proto 文件是涉及了 RPC 服务的,而默认是不会生成 RPC 代码的,因此需要给出 plugins 参数传递给 protoc-gen-go,告诉它,请支持 RPC(这里指定了 gRPC)

  • –go_out=.:设置 Go 代码输出的目录

该指令会加载 protoc-gen-go 插件达到生成 Go 代码的目的,生成的文件以 .pb.go 为文件后缀

  • : (冒号)

冒号充当分隔符的作用,后跟所需要的参数集。如果这处不涉及 RPC,命令可简化为:

  1. $ protoc --go_out=.*.proto

注:建议你看看两条命令生成的 .pb.go 文件,分别有什么区别

生成后

执行完毕命令后,将得到一个 .pb.go 文件,文件内容如下:

  1. type SearchRequeststruct{
  2. Requeststring`protobuf:"bytes,1,opt,name=request" json:"request,omitempty"`
  3. XXX_NoUnkeyedLiteral struct{}`json:"-"`
  4. XXX_unrecognized []byte`json:"-"`
  5. XXX_sizecache int32 `json:"-"`
  6. }
  7. func (m *SearchRequest)Reset(){*m =SearchRequest{}}
  8. func (m *SearchRequest)String()string{return proto.CompactTextString(m)}
  9. func (*SearchRequest)ProtoMessage(){}
  10. func (*SearchRequest)Descriptor()([]byte,[]int){
  11. return fileDescriptor_search_8b45f79ee13ff6a3,[]int{0}
  12. }
  13. func (m *SearchRequest)GetRequest()string{
  14. if m !=nil{
  15. return m.Request
  16. }
  17. return""
  18. }

通过阅读这一部分代码,可以知道主要涉及如下方面:

  • 字段名称从小写下划线转换为大写驼峰模式(字段导出)
  • 生成一组 Getters 方法,能便于处理一些空指针取值的情况
  • ProtoMessage 方法实现 proto.Message 的接口
  • 生成 Rest 方法,便于将 Protobuf 结构体恢复为零值
  • Repeated 转换为切片
  1. type SearchRequeststruct{
  2. Requeststring`protobuf:"bytes,1,opt,name=request" json:"request,omitempty"`
  3. }
  4. func (*SearchRequest)Descriptor()([]byte,[]int){
  5. return fileDescriptor_search_8b45f79ee13ff6a3,[]int{0}
  6. }
  7. type SearchResponsestruct{
  8. Responsestring`protobuf:"bytes,1,opt,name=response" json:"response,omitempty"`
  9. }
  10. func (*SearchResponse)Descriptor()([]byte,[]int){
  11. return fileDescriptor_search_8b45f79ee13ff6a3,[]int{1}
  12. }
  13. ...
  14. func init(){ proto.RegisterFile("search.proto", fileDescriptor_search_8b45f79ee13ff6a3)}
  15. var fileDescriptor_search_8b45f79ee13ff6a3 =[]byte{
  16. // 131 bytes of a gzipped FileDescriptorProto
  17. 0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00,0x02,0xff,0xe2,0xe2,0x29,0x4e,0x4d,0x2c,
  18. 0x4a,0xce,0xd0,0x2b,0x28,0xca,0x2f,0xc9,0x17,0x62,0x05,0x53,0x4a,0x9a,0x5c,0xbc,
  19. 0xc1,0x60,0xe1,0xa0,0xd4,0xc2,0xd2,0xd4,0xe2,0x12,0x21,0x09,0x2e,0xf6,0x22,0x08,
  20. 0x53,0x82,0x51,0x81,0x51,0x83,0x33,0x08,0xc6,0x55,0xd2,0xe1,0xe2,0x83,0x29,0x2d,
  21. 0x2e,0xc8,0xcf,0x2b,0x4e,0x15,0x92,0xe2,0xe2,0x28,0x82,0xb2,0xa1,0x8a,0xe1,0x7c,
  22. 0x23,0x0f,0x98,0xc1,0xc1,0xa9,0x45,0x65,0x99,0xc9,0xa9,0x42,0xe6,0x5c,0x6c,0x10,
  23. 0x01,0x21,0x11,0x88,0x13,0xf4,0x50,0x2c,0x96,0x12,0x45,0x13,0x85,0x98,0xa3,0xc4,
  24. 0x90,0xc4,0x06,0x16,0x37,0x06,0x04,0x00,0x00,0xff,0xff,0xf3,0xba,0x74,0x95,0xc0,
  25. 0x00,0x00,0x00,
  26. }

而这一部分代码主要是围绕 fileDescriptor 进行,在这里 fileDescriptor_search_8b45f79ee13ff6a3 表示一个编译后的 proto 文件,而每一个方法都包含 Descriptor 方法,代表着这一个方法在 fileDescriptor 中具体的 Message Field

Server

这一小节将编写 gRPC Server 的基础模板,完成一个方法的调用。对 server.go 写入如下内容:

  1. package main
  2. import(
  3. "context"
  4. "log"
  5. "net"
  6. "google.golang.org/grpc"
  7. pb "github.com/EDDYCJY/go-grpc-example/proto"
  8. )
  9. type SearchServicestruct{}
  10. func (s *SearchService)Search(ctx context.Context, r *pb.SearchRequest)(*pb.SearchResponse, error){
  11. return&pb.SearchResponse{Response: r.GetRequest()+" Server"},nil
  12. }
  13. const PORT ="9001"
  14. func main(){
  15. server := grpc.NewServer()
  16. pb.RegisterSearchServiceServer(server,&SearchService{})
  17. lis, err := net.Listen("tcp",":"+PORT)
  18. if err !=nil{
  19. log.Fatalf("net.Listen err: %v", err)
  20. }
  21. server.Serve(lis)
  22. }
  • 创建 gRPC Server 对象,你可以理解为它是 Server 端的抽象对象
  • 将 SearchService(其包含需要被调用的服务端接口)注册到 gRPC Server 的内部注册中心。这样可以在接受到请求时,通过内部的服务发现,发现该服务端接口并转接进行逻辑处理
  • 创建 Listen,监听 TCP 端口
  • gRPC Server 开始 lis.Accept,直到 Stop 或 GracefulStop

Client

接下来编写 gRPC Go Client 的基础模板,打开 client/client.go 文件,写入以下内容:

  1. package main
  2. import(
  3. "context"
  4. "log"
  5. "google.golang.org/grpc"
  6. pb "github.com/EDDYCJY/go-grpc-example/proto"
  7. )
  8. const PORT ="9001"
  9. func main(){
  10. conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure())
  11. if err !=nil{
  12. log.Fatalf("grpc.Dial err: %v", err)
  13. }
  14. defer conn.Close()
  15. client := pb.NewSearchServiceClient(conn)
  16. resp, err := client.Search(context.Background(),&pb.SearchRequest{
  17. Request:"gRPC",
  18. })
  19. if err !=nil{
  20. log.Fatalf("client.Search err: %v", err)
  21. }
  22. log.Printf("resp: %s", resp.GetResponse())
  23. }
  • 创建与给定目标(服务端)的连接交互
  • 创建 SearchService 的客户端对象
  • 发送 RPC 请求,等待同步响应,得到回调后返回响应结果
  • 输出响应结果

验证

启动 Server

  1. $ pwd
  2. $GOPATH/github.com/EDDYCJY/go-grpc-example
  3. $ go run server.go

启动 Client

  1. $ pwd
  2. $GOPATH/github.com/EDDYCJY/go-grpc-example/client
  3. $ go run client.go
  4. 2018/09/2311:06:23 resp: gRPC Server

总结

在本章节,我们对 Protobuf、gRPC Client/Server 分别都进行了介绍。希望你结合文中讲述内容再写一个 Demo 进行深入了解,肯定会更棒 🤔

参考

本系列示例代码

「连载二」gRPC Client and Server - 图2