一、gRPC 基础
包括 rpc
、gRPC
、Protobuf
等概念,网上已经有详细的介绍,此处不再过多说明。
关于 gRPC
的介绍,可以参考官方介绍文档。
以及微软官方文档介绍。
通俗的来说,gRPC
是一种 rpc
的具体实现,其利用 Protobuf
将数据进行序列化并用于传输。
二、开发前置准备
默认已经安装了 Golang
等相关内容,这里着重基于 Golang
的 gRPC
相关工具的安装。
主要需要 2 个工具的安装:
Protocol Buffer
编译器:protoc
protoc
的golang
插件
2.1 protoc
该工具的作用,是将 .proto
文件编译成目标语言的源码。
如 Golang
就会根据 .proto
文件编译为 xxx.go
文件。
共三种安装方法:通过包管理、预编译包安装和源码编译安装。
以下是详细说明。
2.1.1 包管理方式安装
# ubuntu
apt install -y protobuf-compiler
# mac
brew install protobuf
# 安装完毕记得检查版本号,保证版本 3 +
protoc --version # Ensure compiler version is 3+
2.1.2 预编译二进制安装
根据系统和处理器架构,选择合适的包,以 protoc-21.6-linux-x86_64.zip
为例进行操作。
可以提取下载链接,直接下载到开发环境中,也可以下载并拷贝。
以直接下载为例:
# ---------
# 第一步下载
# wget 下载 zip 包
wget https://github.com/protocolbuffers/protobuf/releases/download/v21.6/protoc-21.6-linux-x86_64.zip
# 或者 curl命令,二选一即可
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v21.6/protoc-21.6-linux-x86_64.zip
# ---------
# 第二步解压
# 下载完毕后解压
# 需要安装 unzip 并创建 ~/.local 文件夹
# apt install unzip zip; mkdir ~/.local
# 想放到其他目录也可自行配置
unzip protoc-21.6-linux-x86_64.zip -d ~/.local
# -----------------------------
# 第三步,将 protoc 加入环境变量,可[临时添加]或[永久添加],二选一即可
# 3.1 临时
export PATH="$PATH:~/.local/bin"
# 3.2 永久,根据使用的shell和使用习惯变更对应的shell配置文件
# /etc/profile, ~/.bashrc, ~/.zshrc xxx 等
# 此处以 /etc/profile 为例
vi /etc/profile
# 在末尾插入:
PATH="$PATH:~/.local/bin"
# 刷新环境变量
source /etc/profile
# ---------------
# 第四步,检验安装
# 查看 protoc version
protoc --version
# 出现:libprotoc 3.xx.x 即为安装成功
2.1.3 编译安装
略
2.2 Golang 插件
因为 protoc
工具本身并不支持 Go 语言的编译,所以需要安装 Golang 的插件以支持编译。
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
protoc-gen-go
生成将 protobuf 消息进行序列化/反序列化的 go 代码,也就是定义 go 语言版本的protobuf 协议。
protoc-gen-go-grpc
是根据这些 protobuf 协议,生成 gRPC 服务的 go 代码。
要想使用基于 Golang 的 gRPC,二者缺一不可。
两个工具的具体使用方法,见本文 4.1。
Tips:网上有些教程只安装了一个插件就可以使用了,因为使用的是旧版。新旧说明可以参考这里。
三、proto 文件语法
在这里,proto文件主要是为了定义一种 rpc 服务。
详细语法可以参考官方英文文档。
后文内容如出现语法不一致,以官方为准。
3.1 头部语法
syntax = "proto3"; // 版本声明
package hello; // 包名,proto 的包名,如果有多个 proto 文件,依此区分
option go_package="./;hello";
// option go_package = "[path];[name]";
// path: 输出存放地址
// name: 生成 go 文件所属 package name
// 还有这种写法:
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";
// 没有 ";[name]",会根据最后一个名称,也就是 'tutorialpb' 来作为 go 的 package name
// 个人不是很建议用这种方法
3.2 定义服务
语法如下:
service service_name{
rpc function_name (msg_type1) returns (msg_type2);
}
上述语法说明:
service_name
注册到 gRPC 中的具体服务function_name
提供调用的方法,server 端提供,client 端用来调佣msg_type1
,msg_type2
具体的消息类型,其实就是结构体,具体定义见3.3
3.3 定义消息类型
消息类型,其实就是传输的结构体,对应上面的 msg_type
。
// message 是关键字,msg_name 是自定义的消息名称
message msg_name {
[访问修饰符] [数据类型] [数据名称] = [num],
optional string test1= 1;
string test2= 2;
string test3= 3;
}
语法说明:
- 访问修饰符:官方称作“Field Rules”,常见的有
require
(表示该项必填,默认)optional
(选填)repeated
(重复项或数组) - 数据类型:参考官方文档
常见的有string
int32
int64
uint32
uint64
bool
bytes
- 数据名称:自定义即可
- num,这个数字是在传输的时候的排序,可以自定义排序,但不能重复。
num 取值范围 [1, 229-1]
其中,num 取值 [1, 15] 时占用 1 字节,[16, 2047] 占用 2 个字节,因此应将频繁传输的字段分配 1 - 15 ;
此外,19000 -19999 被预占用,不可使用,否则会报错。 - 同样的,message 还支持嵌套,类似于结构体嵌套。
综上,写一个 hello.proto
作为示例,以便后文说明使用。
// hello.proto
syntax = "proto3";
package hello; // 包名
option go_package="./;hello";
// The Hello service definition.
service Hello {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message
message HelloRequest {
string name = 1;
}
// The response message
message HelloReply {
string message = 1;
}
四、Golang gRPC 开发
可参考 grpc 官方文档中 Go Basics tutorial。
4.1 将 proto 文件编译为 go 文件
借助第二部分下载的工具,以及第三部分的 proto 文件,可以生成 go 源码文件,具体操作如下:
建议创建一个单独的文件夹,来保存 proto 文件和生成的 go 文件(在 go 项目中)。
操作命令:
# Tips:将--plugin= 后边的 protoc-gen-go 和 protoc-gen-go-rpc 的前缀路径换成自己实际的地址
protoc -I=./ ./*.proto --plugin=/go/bin/protoc-gen-go --go_out=./ --plugin=/go/bin/protoc-gen-go-grpc --go-grpc_out=./
简单说明一下上述命令中的内容:
protoc
命令主体-I=./ ./*.proto
前边-I
是指定 proto 文件所在目录,后边./*.proto
是说明要具体编译哪些 proto 文件,这里是指定文件夹内的所有 proto 文件--plugin=xxx --xxx_out=xxx
这两个是一个组合,分别是使用的插件和输出目标存放文件夹。
在这里,
protoc-gen-go
对应的是--go_out
protoc-gen-go-grpc
对应的是--go-rpc_out
,有所区别需要注意。- 两个插件最好直接写绝对路径,这样避免找不到这两个命令。
操作完成后,可以看到当前 proto 文件夹中,每一个 proto 文件生成了两个对应的 go 文件。
即由 xxx.proto
生成了 xxx.pb.go
和 xxx_grpc.pb.go
。
4.2 gRPC server 端
官方文档给出的 server 端流程如下:
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
var opts []grpc.ServerOption
...
grpcServer := grpc.NewServer(opts...)
pb.RegisterRouteGuideServer(grpcServer, newServer())
grpcServer.Serve(lis)
流程概况:
- 通过
net.Listen(...)
建立一个TCP监听,指定监听地址和端口; - 通过
grpc.NewServer(...)
建立一个 gRPC server; - 通过
pb.RegisterxxxServer(...)
注册 gRPC 服务方法的具体实现;
这里,pb 调用的具体方法,要看通过 protoc 生成的xxx_grpc.pb.go
文件里的具体方法名。
同时,第二个参数,是一个结构体,该结构体要包含xxx_grpc.pb.go
中以Unimplemented
开头的一个空结构体,示例如下:// 见 xxx_grpc.pb.go // UnimplementedHelloServer must be embedded to have forward compatible implementations. type UnimplementedHelloServer struct { }
- 通过
Server(...)
绑定net.Listen()
建立的listen,并阻塞监听。
所以这里的业务处理核心就是 pb.RegisterxxxServer(...)
的第二个参数,应该传入一个结构体 s
对象,该结构体应该包含 xxx_grpc.pb.go
中的 UnimplementedXXXServer
这个空结构体。
且该结构体应该实现具体的业务逻辑方法,即在 proto
文件中所定义的 service
,会在 xxx_grpc.pb.go
中对应 server 中的 interface 中看到应该实现的方法和限定参数。如下代码所示:
// 节选自 hello_grpc.pb.go 部分代码,展示应该实现的具体方法
// HelloServer is the server API for Hello service.
// All implementations must embed UnimplementedHelloServer
// for forward compatibility
type HelloServer interface {
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
mustEmbedUnimplementedHelloServer()
}
以第三部分的 proto
文件为例,定义一个server:
// myserver.go
package main
import (
"net"
pb "grpc_demo/proto"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
// 该结构体需要包含 hello_grpc.pb.go 中的 Unimplementedxxxx
type helloService struct {
pb.UnimplementedHelloServer
}
// 定义具体的方法,与业务相关的核心内容
// 限定传输参数见 hello_grpc.pb.go 中定义的 interface
func (s helloService) SayHello(c context.Context, r *pb.HelloRequest) (*pb.HelloReply, error) {
ret := new(pb.HelloReply)
ret.Message = "hello, " + r.Name
return ret, nil
}
// 展示了一个 grpc server 的创建监听、绑定grpc服务的全过程
func myServer() {
lis, err := net.Listen("tcp", ":3399")
if err != nil {
panic(err)
}
grpcServer := grpc.NewServer()
pb.RegisterHelloServer(grpcServer, &helloService{})
grpcServer.Serve(lis)
}
func main() {
myServer()
}
4.3 gRPC client 端
client 端首先需要建立一个 grpc 连接,流程如下:
var opts []grpc.DialOption
...
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
...
}
defer conn.Close()
其中:
serverAddr
即连接地址(IP + port);opts
是用来设置身份验证凭证,可以是TLS、GCE 或 JWT 等;本示例中通过insecure.NewCredentials()
关闭了安全传输,在服务端无需进行安全验证。- 如需配置进阶安全传输,请参考官方文档。
获得 grpc 的 conn
连接后,根据 xxx_grpc.pb.go
文件中的 NewxxxClient
方法来获得一个对应服务的客户端连接 client
。
client
获取后,再构建对应的 request
请求,具体的请求结构体,参照 xxx.pb.go
文件中根据 proto
文件生成的 Request 结构。
最后,通过 client
提供的 grpc 请求方法(参照 xxx_grpc.pb.go
),完成请求调用即可。
同样,以第三部分和 4.2 server 端代码为准,构造 client 端代码:
package main
import (
"fmt"
pb "grpc_learn/proto"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func myClient() {
conn, err := grpc.Dial(":3399", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
// handle error
panic(err)
}
defer conn.Close()
client := pb.NewHelloClient(conn)
req := pb.HelloRequest{
Name: "world",
}
reply, err := client.SayHello(context.Background(), &req)
if err != nil {
fmt.Println("client.SayHello error:", err)
return
}
fmt.Printf("get msg from server:[%v] \n", reply)
}
func main() {
myClient()
}
五、demo
直接给一个从 proto
文件,到编译出 go 文件,到 server 端和 client 端的例子,请保证前述 golang、protoc、go pkg 的安装成功。
前文已经给出了各代码,这里做个总结汇总。
5.1 各文件及编译
文件目录:
./
├── myclient.go
├── myserver.go
└── proto
├── hello.pb.go
├── hello.proto
└── hello_grpc.pb.go
hello.proto 文件
// proto/hello.proto
syntax = "proto3";
package hello; // 包名
option go_package="./;hello";
// The Hello service definition.
service Hello {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message
message HelloRequest {
string name = 1;
}
// The response message
message HelloReply {
string message = 1;
}
使用以下命令,将 proto
文件编译为 go 源码
(注意protoc-gen-go和protoc-gen-go-grpc的具体路径):
protoc -I=./ ./*.proto --plugin=/go/bin/protoc-gen-go --go_out=./ --plugin=/go/bin/protoc-gen-go-grpc --go-grpc_out=./
服务端:
// myserver.go
package main
import (
"net"
pb "grpc_demo/proto"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
type helloService struct {
pb.UnimplementedHelloServer
}
func (s helloService) SayHello(c context.Context, r *pb.HelloRequest) (*pb.HelloReply, error) {
ret := new(pb.HelloReply)
ret.Message = "hello, " + r.Name
return ret, nil
}
func myServer() {
lis, err := net.Listen("tcp", ":3399")
if err != nil {
panic(err)
}
grpcServer := grpc.NewServer()
pb.RegisterHelloServer(grpcServer, &helloService{})
grpcServer.Serve(lis)
}
func main() {
myServer()
}
客户端:
// client.go
package main
import (
"fmt"
pb "grpc_learn/proto"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func myClient() {
conn, err := grpc.Dial(":3399", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
// handle error
panic(err)
}
defer conn.Close()
client := pb.NewHelloClient(conn)
req := pb.HelloRequest{
Name: "world",
}
reply, err := client.SayHello(context.Background(), &req)
if err != nil {
fmt.Println("client.SayHello error:", err)
return
}
fmt.Printf("get msg from server:[%v] \n", reply)
}
func main() {
myClient()
}
5.2 结果
一个 shell go run myserver.go
开启服务后,再启动另一个 shell 执行 go run myclient.go
。
// myclient.go 响应
get msg from server:[message:"hello, world"]
写在最后:
由于时间限制和本人水平不足,本文可能存在错误和不足,欢迎指正。