Go 中的 gRPC 入门详解
Posted 面向云技术架构
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go 中的 gRPC 入门详解相关的知识,希望对你有一定的参考价值。
目录
Go GRPC 入门
1,安装包
2,gRPC 服务端
3,gRPC 客户端
4,编译运行
5,其它
GRPC
编译 proto
字段类型
字段规则
Protobuf buffer
Protobuf
gRPC 四种服务方法
Go GRPC 入门
1,安装包
grpc
golang-grpc 包提供了 gRPC 相关的代码库,通过这个库我们可以创建 gRPC 服务或客户端,首先需要安装他。
go get -u google.golang.org/grpc
协议插件
要玩 gRPC,自然离不开 proto 文件,需要安装两个包,用于支持 protobuf 文件的处理。
go get -u github.com/golang/protobuf
go get -u github.com/golang/protobuf/protoc-gen-go
注:GOPATH/bin
下有个 protoc-gen-go.exe
文件,然而这个只是 protoc 的插件,他本身不是 protoc 工具。。。
Protocol Buffers
Protocol Buffers 是一个与编程语言无关、与平台无关的可拓展机制,用于序列化结构数据,是一种数据交换格式,gRPC 使用 protoc 作为协议处理工具。
学习 Go 的 gRPC 时,有个坑,很多文章里面都没有说到要安装这个,执行命令提示不存在 protoc 命令。
首先到 https://github.com/protocolbuffers/protobuf/releases 下载 相应的包,例如笔者下载的是 protoc-3.15.6-win64.zip
。
解压后,复制里面的 bin\protoc.exe
文件,复制到 GOPATH\bin
命令,跟 protoc-gen-go.exe
放一起。
测试
以上都妥当后,我们在一个新的目录,创建一个 test.proto 文件,其内容示例如下如下:
注:protoc-3.15.6-win64\include\google\protobuf
目录也有很多示例。
syntax = "proto3";
// 包名
package test;
// 指定输出 go 语言的源码到哪个目录以及文件名称
// 最终在 test.proto 目录生成 test.pb.go
// 也可以只填写 "./"
option go_package = "./;test";
// 如果要输出其它语言的话
// option csharp_package="MyTest";
service Tester{
rpc MyTest(Request) returns (Response){}
}
// 函数参数
message Request{
string jsonStr = 1;
}
// 函数返回值
message Response{
string backJson = 1;
}
然后在 proto 所在目录,执行命令将 proto 转换为相应的编程语言文件。
protoc --go_out=plugins=grpc:. *.proto
会发现在当前目录输出了 test.pb.go
文件。
2,gRPC 服务端
创建一个 go 程序,把 test.pb.go 复制放到在 main.go 目录,在 main.go 引入 grpc:
import (
"context"
"fmt"
"google.golang.org/grpc"
// test.pb.go 默认包名是 package 为 main,不需要在这里引入
"google.golang.org/grpc/reflection"
"log"
"net"
)
在 test.pb.go
中,生成了两个个 Tester
的接口,我们来看一下这两个接口的定义:
type TesterServer interface {
MyTest(context.Context, *Request) (*Response, error)
}
type TesterClient interface {
MyTest(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
}
要实现 proto 中的服务,则需要我们实现 TesterServer
接口,要编写 客户端,则需要实现 TesterClient
。
这里我们先实现 Server。
// 用于实现 Tester 服务
type MyGrpcServer struct{}
func (myserver *MyGrpcServer) MyTest(context context.Context, request *Request) (*Response, error) {
fmt.Println("收到一个 grpc 请求,请求参数:", request)
response := Response{BackJson: `{"Code":666}`}
return &response, nil
}
接着我们创建 gRPC 服务。
func main() {
// 创建 Tcp 连接
listener, err := net.Listen("tcp", ":8028")
if err != nil {
log.Fatalf("监听失败: %v", err)
}
// 创建gRPC服务
grpcServer := grpc.NewServer()
// Tester 注册服务实现者
// 此函数在 test.pb.go 中,自动生成
RegisterTesterServer(grpcServer, &MyGrpcServer{})
// 在 gRPC 服务上注册反射服务
// func Register(s *grpc.Server)
reflection.Register(grpcServer)
err = grpcServer.Serve(listener)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
3,gRPC 客户端
创建一个新的 go 项目,把 test.pb.go 复制放到 main.go 同级目录,main.go 的代码:
package main
import (
"bufio"
"context"
"google.golang.org/grpc"
"log"
"os"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:8028", grpc.WithInsecure())
if err != nil {
log.Fatal("连接 gPRC 服务失败,", err)
}
defer conn.Close()
// 创建 gRPC 客户端
grpcClient := NewTesterClient(conn)
// 创建请求参数
request := Request{
JsonStr: `{"Code":666}`,
}
reader := bufio.NewReader(os.Stdin)
for {
// 发送请求,调用 MyTest 接口
response, err := grpcClient.MyTest(context.Background(), &request)
if err != nil {
log.Fatal("发送请求失败,原因是:", err)
}
log.Println(response)
reader.ReadLine()
}
}
4,编译运行
由于创建的时候,test.pb.go 使用的包名是 main,所以在编译时,需要把多个 go 文件一起编译:
go build .\main.go .\test.pb.go
然后分别启动 server 和 client,在 client 每按下一次回车键,便发送一次 gRPC 消息。
到这里,我们学习了一个完整的 gRPC 从创建协议到创建服务和客户端的过程,下面将接着学习一些相关的知识,了解一些细节。
5,其它
proto.Marshal
可以对请求的参数进行序列化,如:
// 创建请求参数
request := Request{
JsonStr: `{"Code":666}`,
}
out,err:= proto.Marshal(&request)
if err != nil {
log.Fatalln("Failed to encode address book:", err)
}
if err := ioutil.WriteFile("E:/log.txt", out, 0644); err != nil {
log.Fatalln("Failed to write address book:", err)
}
而 proto.Unmarshal
则可以反序列化。
我们还可以自定义如何序列化反序列化消息,代码示例:
b, err := MarshalOptions{Deterministic: true}.Marshal(m)
感兴趣的读者可访问 https://pkg.go.dev/google.golang.org/protobuf/proto#MarshalOptions
GRPC
Protobuf buffer
Protobuf buffer 是一种数据格式,而 Protobuf 是 gRPC 协议,这里需要区分一下。
protobuf buffer 是 Google 用于序列化结构化数据的开源机制,要定义一个 protobuf buffer,需要使用 message 定义。
message Person {
string name = 1;
int32 id = 2;
bool has_ponycopter = 3;
}
开源看到,每个字段都有一个 数字,= 1
这个不是赋值,而是编号。一个 message 中,每个字段都有唯一的编号,这些数字用于标识二进制格式的字段(数据传输时会被压缩等),当编号范围是 1-15 时,存储编号需要一个字节,也就是说 message 中的字段尽量不超过 15 个,1-15 编号用来定义频繁出现的消息元素。当然,也可以使用16-2047
之间的数字作为编号,此时存储编号需要两个字节。
详细的说可以参考官方文档:
https://developers.google.com/protocol-buffers/docs/overview
字段类型
字段类型就不详细列表了,读者可以参考官方文档,这里列一下常用的数据类型:
double、float、int32、int64、bool、string、bytes、枚举。
由于 gRPC 需要考虑兼容 C 语言、C#、Java、Go 语言等,所以 gRPC 中的类型不等同于编程语言中的相关类型。这些类型都是 gRPC 中定义的,并且如果要转换为编程语言中的类型,需要一些转换机制,而这有时会十分麻烦。
字段规则
每个字段都可以指定一个规则,在定义字段类型的开头使用规则标识。
有以下三种规则:
required
:格式正确的消息必须恰好具有此字段之一,即必填字段。optional
:格式正确的消息可以包含零个或一个此字段(但不能超过一个,即值是可选的。repeated
:在格式正确的消息中,此字段可以重复任意次(包括零次),重复值的顺序将保留,表示该字段可以包含0~N个元素。
由于历史原因,repeated
标量数字类型的字段编码效率不高。新代码应使用特殊选项[packed=true]
来获得更有效的编码。例如:
repeated int32 samples = 4 [packed=true];
在可选字段中 optional 中,我们可以为其设置一个默认值,当传递消息时如果没有填写此字段,则使用其默认值:
optional int32 result_per_page = 3 [default = 10];
Protobuf
接下来将介绍 gRPC 的协议格式(protobuf),下面是官方文档的一个示例:
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
syntax 指明协议的版本;
package 指明该 .proto 的名称;
import 关键字可以在当前 .proto 中引入其它 .proto 文件,gRPC 基本数据类型中不包含时间格式,可以引入 timestamp.proto
。
不同编程语言引入包/库的方式是不同的,C++ 和 C# 都是使用命名空间区分代码位置;Java 以目录、公共类严格区别包名;go 则是以一个 .go 文件任意设置 package 名称。
前面提到了 protoc,可以将协议文件转为为具体的代码。
为了兼容各种编程语言,我们协议设置 _package
,这样可以支持生成不同语言代码时设置包/库名称。
例如 :
option go_package = "Test"; // ...
option csharp_package = "MyGrpc.Protos"; // 生成命名空间 namespace MyGrpc.Protos{}
option java_paclage = "MyJava.Protos"; // ...
gRPC 四种服务方法
protobuf 中除了可以定义 message,也可以定义流式接口。
gRPC使您可以定义四种服务方法:
一元 RPC,客户端向服务器发送单个请求并获得单个响应,就像普通的函数调用一样。前面我们提到的都是一元 gRPC。
rpc SayHello(HelloRequest) returns (HelloResponse);
服务器流式RPC,客户端在其中向服务器发送请求,并获取流以读取回一系列消息。客户端从返回的流中读取,直到没有更多消息为止。gRPC保证在单个RPC调用中对消息进行排序。
客户端 -> 服务端 -> 返回流 -> 客户端 -> 接收流
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
客户端流式RPC,客户端在其中编写消息序列,然后再次使用提供的流将其发送到服务器。客户端写完消息后,它将等待服务器读取消息并返回其响应。gRPC再次保证了在单个RPC调用中的消息顺序。
客户端 -> 发送流 -> 服务端 -> 接收流 ->
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
双向流式RPC,双方都使用读写流发送一系列消息。这两个流独立运行,因此客户端和服务器可以按照自己喜欢的顺序进行读写:例如,服务器可以在写响应之前等待接收所有客户端消息,或者可以先读取消息再写入消息,或读写的其他组合。每个流中的消息顺序都会保留。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
编译 proto
前面我们用 protoc 来编译 .proto 文件为 go 语言,为了支持编译为 go,需要安装 protoc-gen-go
插件,C# 可以安装 protoc-gen-zsharp
插件。
需要注意的是,转换 .proto 为编程语言,不一定要安装 protoc。
例如 C# 只需要把 .proto 文件放到项目中,通过包管理器安装一个库,就会自动转换为相应的代码。
回归正题,聊一下 protoc 编译 .proto 文件的命令。
protoc 常用的参数如下:
--proto_path=. #指定proto文件的路径,填写 . 表示就在当前目录下
--go_out=. #表示编译后的文件存放路径;如果编译的是 csharp,则 --csharp_out
--go_opt={xxx.proto}={xxx.proto的路径}
# 示例:--go_opt=Mprotos/bar.proto=example.com/project/protos/foo
最简单的编译命令:
protoc --go_out=. *.proto
--{xxx}_out
指令是必须的,因为要输出具体的编程语言代码。
这个输出文件的路径是执行命令的路径,如果我们不在 .proto 文件目录下执行命令,则输出的代码便不是相同位置了。为了解决这个问题,我们可以使用:
--go_opt=paths=source_relative
这样在别的地方执行命令,生成的代码会跟 .proto 文件放在相同的位置。
以上是关于Go 中的 gRPC 入门详解的主要内容,如果未能解决你的问题,请参考以下文章
《Go-micro微服务框架入门教程》学习笔记 | gRPC