1. 开篇 gRPC 环境&启动
1. 开篇 gRPC 环境&启动

1. 开篇 gRPC 环境&启动

从这篇开始,《深入微服务》系列就开始了,该系列从微服务的“应用”与“原理”两条线路进行推进,让咱大家伙明白微服务内部到底是个啥。好了不吹牛了,烂尾就打脸了。
 
该篇代码:

学到什么

  1. 什么是 gRPC ?
  1. 什么是 Protobuf ?
  1. 如何搭建 gRPC 环境?
  1. 如何应用 gRPC ?
 

什么是 gRPC

gRPC 是基于 Http/2 协议的开源远程过程调用系统。
 
远程过程调用,即 RPC (Remote Procedure Calls) ,它实现了像本地一样调用远程的方法,而不考虑底层的具体实现细节。
 
而为了远程调用时数据传输的更高效,选择了 Protobuf,平常我们使用较多的就是 JSON、XML。
 

gRPC 调用模型

notion image
一个完整的调用过程如下:
  1. 假如客户端 (Client) 调用服务端 (Server) 上的 A 方法,则发起 RPC 调用。
  1. 对请求信息使用 Protobuf 进行对象序列化压缩。
  1. 服务端接收到请求后,解码请求体,进行业务逻辑处理并返回。
  1. 对响应结果使用 Protobuf 进行对象序列化压缩。
  1. 客户端接受到服务端响应,解码请求体,并唤醒正在等待 A 方法响应的客户端调用。

什么是 Protobuf

Protobuf (Protocol buffers) 和 Go 都是一个亲妈,来自于 Google。它用于序列结构化数据,提供了一种相比 XML、JSON 格式,会更小、更快、操作更简单的方法。
 
先简单对比下。
JSON
{ "name": "laomiao", "id": 1, "email": "2825873215@qq.com" }
 
Protobuf
message Person { string name = 1; int32 id = 2; string email = 3; }
该信息存储于后缀为 proto 的文件中,它只提供格式,而不存储数据。在使用时,用 Protobuf 编译工具将该文件编译成你所使用的语言。
 
例如,在 Go 语言中,会根据该信息生成编译后的 Go 文件,message Person 会对应到 struct Person 上,并提供了序列化结构体的方法。
 
本篇不会详细讲解 Protobuf 定义语法,只会把用到的进行简单描述,详细的前往:
 

编译工具

将 Proto 文件编译成 Go 代码需要两个工具:
  • protoc 工具,c++ 语言实现。
  • protoc-gen-go 插件,go 语言实现,给 protoc 工具提供插件,生成 go 代码。
 
gRPC 系统也要基于 Proto 文件生成相关代码,因此还需要一个工具:
  • protoc-gen-go-grpc 插件,go 语言实现,配合上面两个工具。
 
下来开始安装这些工具。

protoc 工具安装

1. 下载

根据自己的系统选择对应的版本,我这块选择了 Win64。
notion image

2. 环境变量

先解压,将整个目录放置到你想要的位置,再把 bin 目录加入环境变量,完成后在控制台输入命令。
$ protoc --version libprotoc 3.15.8
成功搞定!
 

插件安装

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
安装完成后,会在 $GOPATH/bin 目录下生成两个文件,记得这个目录也要加入环境变量。

编写 Proto

创建 "helloword.proto" 文件。
syntax = "proto3"; option go_package = "github.com/miaogaolin/gofirst/example/helloworld"; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; }
解释如下:
  • 第一行,proto3 声明版本号为 3,不写时,默认版本为 2。
  • go_package 定义包路径。
  • Greeter 为 rpc 服务,远程调用的方法为 SayHello。
  • HelloRequest 用于 SayHello 方法参数。
  • HelloReply 用于 SayHello 方法返回。
  • name 和 message 后的数字 1 是给字段分配的唯一编号,如果新增加字段就给分配 2、3、4 等等。
 

生成代码

工具装好、proto 文件编写好,下来就开始使用工具生成代码。
protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ helloworld.proto
  • go_out:生成 go 文件(不含 rpc 代码)的输出目录。
  • go_opt=paths=source_relative:生成的 go 文件和 proto 源文件在同一目录,如果 go_out 定义为其它目录,该设置无效。
  • go-grpc_out:rpc 的 go 文件输出目录。
  • go-grpc_opt=paths=source_relative:生成 rpc 的 go 文件和 proto 源文件在同一目录,如果 go-grpc_out 定义为其它目录,该设置无效。
 
运行后,会在当前目录下产生两个文件:
  • helloworld.pb.go:数据类型代码。
  • helloworld_grpc.pb.go:rpc 相关代码,其中会产生 GreeterClientGreeterServer 两个接口,一会要实现。

远程调用

要实现 gRPC 的远程调用,就要分别实现 “客户端” 和 “服务端” 代码,下来我会给出关键性代码的说明,完整的前往 Github。

1. 服务端

// example/hellworld/server/main.go // 实现服务端 GreeterServer 接口 type server struct { // 必须嵌套 // 包含了 GreeterServer 接口其它方法实现 pb.UnimplementedGreeterServer } // 远程调用方法的具体逻辑 func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } func main() { // 监听端口,用于接受客户端的请求 lis, err := net.Listen("tcp", port) ... // 服务端实例化 s := grpc.NewServer() // 将具体的实现注册到服务端 pb.RegisterGreeterServer(s, &server{}) // 阻塞等待 if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
主线流程:
  1. 监听端口;
  1. 实例化服务端;
  1. 将具体实现进行注册;
  1. 阻塞等待。
 

2. 客户端

实现好了服务端,下来客户端只需要远程调用就行。
// example/hellworld/client/main.go func main() { // 连接服务端 // grpc.WithInsecure() 跳过安全验证,即明文传输 // grpc.WithBlock() 等待与服务端握手成功 conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock()) ... // 客户端实例化 c := pb.NewGreeterClient(conn) ... // 连接超时设置 ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // 远程调用 r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) if err != nil { log.Fatalf("could not greet: %v", err) } // 返回消息 log.Printf("Greeting: %s", r.GetMessage()) }
主线流程:
  1. 连接服务端;
  1. 实例化客户端;
  1. 开始调用;
  1. 获取返回消息。

3. 运行

先执行服务端代码,再执行客户端代码。
服务端
$ go run main.go 2021/11/04 11:28:19 server listening at [::]:50051
客户端
$ go run main.go 2021/11/04 11:30:17 Greeting: Hello world
客户端接收到了服务端的响应,一切完成!

参考