gRPC
介绍
gRPC是google开发的一个RPC框架,跨语言,跨平台,基于Protobuf序列化协议。简单的说就是:是一个RPC框架,使用Protobuf序列化数据。本文以实战为主,一步步走完gRPC入门教程。
首先gRPC是以Protobuf序列化的,因此先来介绍Protobuf。
Protobuf
介绍
protobuf的全名是Google Protocol Buffers,是google开发的,与语言无关,平台无关,可扩展的系列化结构数据的方法,可用于数据通信,数据存储等。简单点说,就是和json或者xml类似的结构数据序列化方法。其特点:
- 跨平台,跨语言
- 序列化后体积更小,二进制形式,速度更快
- 兼容好,protobuf的设计有很好的向下或者向上兼容
编写
.proto文件
Protobuf文件以 .proto作为文件后缀,在.proto文件里定义好数据结构之后,就可以用工具将这个文件翻译成具体的代码。来看看.proto文件怎么写的
message
syntax = "proto3"; // 使用proto3版本语法,2和3版本的语法有些差异,这里使用3
message Person {
string name = 1;
int32 age = 2;
repeated string address = 3;
}
来解释一下这个文件的含义
- messaage: 结构定义的关键词,与之相同的还有service,enum,后面接一个名字,例子里是Person,即定义里一个Person的消息结构
- repeated 限定修饰词,即表示该字段可以出现多次(包括0),在2的版本里,还有required,optional两个限定修饰词,不过这里使用的是3,因此不在这样演示
- string, int32都是数据类型
- name,age, address 自定义的字段名字
- 1,2,3是字段编码值,有了这个值,通信双方才能互相识别对方的字段,范围为1~2^23,同一个message里不能有相同的字段编码值,其中1~15的编码时间和空间效率是最高的,编码值越大,效率就越低,通常建议设置在1~15之间
具体字段定义时的格式为:[限定修饰词] 数据类型 字段名称 = 字段编码值
service
message是用来定义消息结构体的,而service是定义服务的。比如想要定义一个RPC服务并具有一个方法,该方法有Person参数,返回Person,则可以如下定义
service PersonService {
rpc getPersonInfo (Person) returns (Person) {
}
}
安装
编写完的.proto文件并不能直接使用,需要用protoc工具,将文件翻译成对应的代码。
安装的方式有多种,我采用的是最简单的方法:
- 在该项目的仓库上,下载 protoc-3.11.4-osx-x86_64.zip 这个版本(写这篇文章时,最新版本是3.11.4,mac系统),根据自己的环境下载对应的版本
- 下载解压之后,目录里bin文件夹里有个protoc执行文件,把该文件放到 /usr/local/bin/目录下就可以了
- 在命令行里,执行
protoc --version可以查看当前版本,我的是libprotoc 3.11.4。当然也可以下源码编译安装 ╮(╯▽╰)╭
编译
安装完之后,就可以进行编译,自动的将写好的.proto文件转为相对应的代码,由于原生的protoc只能生成c++,python等代码,要生成go语言,需要安装protoc-gen-go插件,如果你能科学上网,直接go get -u github.com/golang/protobuf/protoc-gen-go 安装。不行的话,可以使用下面这个方法:
- 到protoc-gen-go的github仓库,选择一个版本,下载Source code(zip)
- 解压zip包,进入目录,进入protoc-gen-go文件夹里,执行
go build,就会生成一个protoc-gen-go的可执行文件 - 把生成的protoc-gen-go可执行文件,放到 /usr/local/go/bin/ 目录下即可。
插件安装好了之后,就可以执行命令了,在编写好的.proto文件目录里,执行protoc --go_out=plugins=grpc:. ./person.proto 命令。就会在目录下生成一个person.pb.go文件,这个文件就是根据.proto文件,生成的go代码。
命令的通用格式
protoc --go_out=plugins=grpc:生成的目录 定义好的.proto文件路径
其中plugins=grpc:是使用grpc的形式生成,会把.proto里定义的service生成对应的接口。如果去掉,即命令变成protoc --go_out=. ./person.proto,只会生成message定义的,service定义的就不会生成。
protobuf的入门教程就介绍到这里,如果有兴趣深入研究的话,可以去看官方文档╮(╯▽╰)╭
编写gRPC服务端和客户端
分析
先来分析一下person.pb.go文件,这个文件大致可以分为三个部分,
一是结构体的定义
type Person struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
Address []string `protobuf:"bytes,3,rep,name=address,proto3" json:"address,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
这里就是将.proto文件的message转化成了结构体。
二是服务端定义
// PersonServiceServer is the server API for PersonService service.
type PersonServiceServer interface {
GetPersonInfo(context.Context, *Person) (*Person, error)
}
这里将.proto文件里的service转化为interface,而我们在服务端的文件里,就是要实现这个interface。
下面这个函数,则是将PersonServiceServe注册到gRPC中,需要我们在服务端调用。
func RegisterPersonServiceServer(s *grpc.Server, srv PersonServiceServer) {
s.RegisterService(&_PersonService_serviceDesc, srv)
}
三是客户端定义
type PersonServiceClient interface {
GetPersonInfo(ctx context.Context, in *Person, opts ...grpc.CallOption) (*Person, error)
}
type personServiceClient struct {
cc grpc.ClientConnInterface
}
func NewPersonServiceClient(cc grpc.ClientConnInterface) PersonServiceClient {
return &personServiceClient{cc}
}
func (c *personServiceClient) GetPersonInfo(ctx context.Context, in *Person, opts ...grpc.CallOption) (*Person, error) {
out := new(Person)
err := c.cc.Invoke(ctx, "/person.PersonService/getPersonInfo", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
这一部分是在编写客户端时使用的。
服务端
开始编写的时候,需要用到google.golang.org/grpc这个包,我使用的go module方式管理包,因此十分的简单,如果不清楚go module可以看看我之前写的总结~
先看看目录结构:
├── client.go
├── go.mod
├── go.sum
├── proto
│ ├── person.pb.go
│ └── person.proto
└── server.go
服务端分为四个步骤
- 监听端口
- 实例化gRPC
- 注册服务
- 启动服务
// server.go
package main
import (
"context"
pb "gRPC/proto" // 导入刚刚生成的person.pb.go包
"google.golang.org/grpc"
"log"
"net"
)
// 定义空结构体,用于实现PersonServiceServer接口
type PersonService struct {
}
// GetPersonInfo 按照person.pb.go里的方法签名,实现这个方法,使得PersonService实现PersonServiceServer接口
func (p *PersonService) GetPersonInfo(ctx context.Context, req *pb.Person) (*pb.Person, error) {
// 具体逻辑实现
if req.Name == "张三" {
return &pb.Person{
Name: "张三",
Age: 18,
Address: []string{"广东", "上海"},
}, nil
}
return nil, nil
}
func main() {
//1、监听
listener, err := net.Listen("tcp", ":6000")
if err != nil {
log.Fatal(err)
}
//2、实例化gRPC
s := grpc.NewServer()
//3、gRPC上注册服务
p := PersonService{}
pb.RegisterPersonServiceServer(s, &p) // 空结构体 PersonService 就是在这里使用的
//4、启动服务端
s.Serve(listener)
}
服务端的实现,最主要的就是用一个结构体(一般是空结构体),来实现生成的xxxServer 接口,也就是为.proto里定义的service里的方法实现具体逻辑。然后再把这个结构体注册到gRPC上,绑定这些方法。
客户端
客服端也可以分为四个步骤
- 连接服务端
- 实例化一个客户端
- 组装请求参数
- 调用服务
// client.go
package main
import (
"context"
"fmt"
pb "gRPC/proto"
"google.golang.org/grpc"
"log"
)
func main() {
//1、连接服务端
conn, err := grpc.Dial(":6000", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
//2、实例化一个客户端
client := pb.NewPersonServiceClient(conn)
//3、组装请求参数
body := pb.Person{Name: "张三"}
//4、调用接口
resp, err := client.GetPersonInfo(context.Background(), &body)
if err != nil {
fmt.Println(err)
}
fmt.Println(resp) // 这里会打印 name:"\345\274\240\344\270\211" age:18 address:"\345\271\277\344\270\234" address:"\344\270\212\346\265\267" 因为是中文,所以序列化之后变成了数字的形式
fmt.Println(resp.Name) // 单独打印一个属性则是正常的 显示 "张三"
}
客户端的逻辑就比较简单一些,基本就是调用person.pb.go里的函数。
总结
基本上整个gRPC的入门过程就是这样,当然这只是最简单的应用,不过流程基本是这样啦~
Thanks!