从这篇开始,《深入微服务》系列就开始了,该系列从微服务的“应用”与“原理”两条线路进行推进,让咱大家伙明白微服务内部到底是个啥。好了不吹牛了,烂尾就打脸了。
该篇代码:
https://github.com/miaogaolin/gofirst/tree/main/example/helloworld
学到什么
什么是 gRPC ?
什么是 Protobuf ?
如何搭建 gRPC 环境?
如何应用 gRPC ?
什么是 gRPC
gRPC 是基于 Http/2 协议的开源远程过程调用系统。
远程过程调用,即 RPC (Remote Procedure Calls) ,它实现了像本地一样调用远程的方法,而不考虑底层的具体实现细节。
而为了远程调用时数据传输的更高效,选择了 Protobuf,平常我们使用较多的就是 JSON、XML。
gRPC 调用模型
一个完整的调用过程如下:
假如客户端 (Client) 调用服务端 (Server) 上的 A 方法,则发起 RPC 调用。
对请求信息使用 Protobuf 进行对象序列化压缩。
服务端接收到请求后,解码请求体,进行业务逻辑处理并返回。
对响应结果使用 Protobuf 进行对象序列化压缩。
客户端接受到服务端响应,解码请求体,并唤醒正在等待 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 定义语法,只会把用到的进行简单描述,详细的前往:
https://developers.google.com/protocol-buffers/docs/proto3
编译工具
将 Proto 文件编译成 Go 代码需要两个工具:
protoc 工具,c++ 语言实现。
protoc-gen-go 插件,go 语言实现,给 protoc 工具提供插件,生成 go 代码。
gRPC 系统也要基于 Proto 文件生成相关代码,因此还需要一个工具:
- protoc-gen-go-grpc 插件,go 语言实现,配合上面两个工具。
下来开始安装这些工具。
protoc 工具安装
1. 下载
地址:https://github.com/protocolbuffers/protobuf/releases。
根据自己的系统选择对应的版本,我这块选择了 Win64。
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 相关代码,其中会产生
GreeterClient
和GreeterServer
两个接口,一会要实现。
远程调用
要实现 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)
}
}
主线流程:
监听端口;
实例化服务端;
将具体实现进行注册;
阻塞等待。
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())
}
主线流程:
连接服务端;
实例化客户端;
开始调用;
获取返回消息。
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
客户端接收到了服务端的响应,一切完成!