gRPC
gRPC
gPRC是一个由Goggle开源、基于HTTP/2、可跨语言的高性能RPC框架,它使客户端和服务器应用程序能够透明地进行通信,并简化了构建分布式系统的过程。
gRPC基于HTTP/2协议进行传输,使用Protocol Buffers(Protobuf)作为接口定义语言(IDL)和数据序列化协议,所有的数据传输都会在底层进行序列化及反序列化,而无需手动进行,解决了使用RPC原生开发的一些痛点。
gRPC版本
- grpc for C,支持编写C、C++、C#、NodeJS、Python、Ruby等。
- grpc-java for Java。
- grpc-go for Go。
Protobuf基础
Protobuf即Protocol Buffers,是Google成熟的开源的数据结构序列化机制,是一种序列化和反序列化协议,类似于JSON、XML的数据结构化。
在传输数据的过程中,使用Protobuf代替JSON等协议,称为Proto Request或Proto Response。
优缺点
- 优点
- 缺点
- 序列化与反序列化效率高。
- 可自动序列化和反序列化。
- 压缩性能强。相比之下,JSON基本没什么压缩。
- 传输速度快。由于压缩体积小,因此传输很快。
- 加密性强。一般不去反序列化是看不到原始数据的。
- 跨平台跨语言。支持多种操作系统,提供相应的程序。
- 轻量级,维护性好。有单独的
.proto文件。
- 需要单独的库、工具、依赖支持。
- 需要对
.proto文件进行维护、编译。
因此,可在均支持Protobuf的语言之间使用Protobuf,其他情况可选择考虑使用JSON。
安装与配置
- 安装
- 环境配置
brew install protobuf@3
protoc --version
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
// go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go env GO111MODULE on
go env GOPROXY https://proxy.golang.org,direct
proto文件
.proto文件能帮助你使用简单的代码生成复杂的Go文件,服务端和客户端只需定义一套数据就可被定义成Go语言所认识的代码。.proto文件中最主要的是定义数据,其次才是定义服务。.proto文件需要进行编译才能生成真正的Go文件,生成的文件名为xxx.pb.go。
// 指定语法版本
syntax = "proto3";
// 每行的末尾必须要有分号,否则会报错
// Go语言中的package就是文件夹
/**
* ./ 表示编译后生成的目录放到哪个目录
* proto 表示编译后文件的包名
*/
option go_package = "./;proto";
文件编译
- .proto文件编译
- info.proto
- info.pb.go
protoc -I . info.proto --go_out=plugins=grpc:.
// protoc -I . info.proto --go-grpc_out=.
// 编译后会生成`xxx.pb.go`Go源码文件。
syntax = "proto3";
option go_package = "/";
message Person {
// 字段编号,用于Proto Compiler进行字段顺序编译
int32 id = 1;
string name = 2;
bool isMarriage = 3;
}
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.33.0
// protoc v3.20.3
// source: info.proto
package __
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// {
// "id": 1,
// "name": "Tom",
// "is_marriage": false
// }
type Person struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// 字段编号,用于Proto Compiler进行字段顺序编译
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
IsMarriage bool `protobuf:"varint,3,opt,name=isMarriage,proto3" json:"isMarriage,omitempty"`
}
func (x *Person) Reset() {
*x = Person{}
if protoimpl.UnsafeEnabled {
mi := &file_info_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Person) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Person) ProtoMessage() {}
func (x *Person) ProtoReflect() protoreflect.Message {
mi := &file_info_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Person.ProtoReflect.Descriptor instead.
func (*Person) Descriptor() ([]byte, []int) {
return file_info_proto_rawDescGZIP(), []int{0}
}
func (x *Person) GetId() int32 {
if x != nil {
return x.Id
}
return 0
}
func (x *Person) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Person) GetIsMarriage() bool {
if x != nil {
return x.IsMarriage
}
return false
}
var File_info_proto protoreflect.FileDescriptor
var file_info_proto_rawDesc = []byte{
0x0a, 0x0a, 0x69, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x4c, 0x0a, 0x06,
0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x73,
0x4d, 0x61, 0x72, 0x72, 0x69, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a,
0x69, 0x73, 0x4d, 0x61, 0x72, 0x72, 0x69, 0x61, 0x67, 0x65, 0x42, 0x03, 0x5a, 0x01, 0x2f, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_info_proto_rawDescOnce sync.Once
file_info_proto_rawDescData = file_info_proto_rawDesc
)
func file_info_proto_rawDescGZIP() []byte {
file_info_proto_rawDescOnce.Do(func() {
file_info_proto_rawDescData = protoimpl.X.CompressGZIP(file_info_proto_rawDescData)
})
return file_info_proto_rawDescData
}
var file_info_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_info_proto_goTypes = []interface{}{
(*Person)(nil), // 0: Person
}
var file_info_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_info_proto_init() }
func file_info_proto_init() {
if File_info_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_info_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Person); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_info_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_info_proto_goTypes,
DependencyIndexes: file_info_proto_depIdxs,
MessageInfos: file_info_proto_msgTypes,
}.Build()
File_info_proto = out.File
file_info_proto_rawDesc = nil
file_info_proto_goTypes = nil
file_info_proto_depIdxs = nil
}
编码结果对比
- protobuf.test.go
- json.test.go
package main
import (
__ "basic3/proto"
"encoding/json"
"fmt"
"google.golang.org/protobuf/proto"
)
func main() {
phInfo := __.Person{
Id: 1,
Name: "Tom",
IsMarriage: true,
}
phResp, _ := proto.Marshal(&phInfo)
fmt.Println(phResp)
fmt.Println(string(phResp))
}
/**
[8 1 18 3 84 111 109 24 1]
Tom
*/
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Id int32 `json:"id"`
Name string `json:"name"`
IsMarriage bool `json:"isMarriage"`
}
func main() {
jsonInfo := Person{
Id: 1,
Name: "Tom",
IsMarriage: true,
}
jsonResp, _ := json.Marshal(&jsonInfo)
fmt.Println(jsonResp)
fmt.Println(string(jsonResp))
}
/**
[123 34 105 100 34 58 49 44 34 110 97 109 101 34 58 34 84 111 109 34 44 34 105 115 77 97 114 114 105 97 103 101 34 58 116 114 117 101 125]
{"id":1,"name":"Tom","isMarriage":true}
*/
可见,Protobuf相比于json序列化后体积更小,且加密性更好。
案例
计算器
- calculator.proto
- calculator.server.go
- calculator.client.go
syntax = "proto3";
option go_package="./;proto";
// proto中的枚举
enum CalculatorType {
// 必须从0开始
PLUS = 0;
MINUS = 1;
MULTIPLY = 2;
DIVIDE = 3;
}
// 请求
message CalculatorRequest {
int32 num1 = 1;
int32 num2 = 2;
CalculatorType type = 3;
}
// 响应
message CalculatorResponse {
int32 num1 = 1;
int32 num2 = 2;
CalculatorType type = 3;
int32 result = 4;
}
// 定义服务端的接口
service Calculator {
// 要调用的方法
// rpc Method(Request) returns (Response)
rpc Calculator(CalculatorRequest) returns (CalculatorResponse);
}
package main
import (
"basic4/proto"
"context"
"net"
"google.golang.org/grpc"
)
type Server struct{}
func (s *Server) Calculator(ctx context.Context, req *proto.CalculatorRequest) (*proto.CalculatorResponse, error) {
var result int32 = 0
switch req.Type {
case 0:
result = req.Num1 + req.Num2
case 1:
result = req.Num1 - req.Num2
case 2:
result = req.Num1 * req.Num2
case 3:
result = req.Num1 / req.Num2
}
return &proto.CalculatorResponse{
Num1: req.Num1,
Num2: req.Num2,
Type: req.Type,
Result: result,
}, nil
}
func main() {
// 创建一个grpc新服务
gServer := grpc.NewServer()
// 使用proto注册该服务
proto.RegisterCalculatorServer(gServer, &Server{})
// 监听服务
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err.Error())
}
// 将服务与监听关联
err = gServer.Serve(listener)
if err != nil {
panic(err.Error())
}
}
package main
import (
"basic4/proto"
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// 创建grpc通道
conn, err := grpc.NewClient(":8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err.Error())
}
// 关闭grpc通道
defer func(conn *grpc.ClientConn) {
err := conn.Close()
if err != nil {
panic(err.Error())
}
}(conn)
// 创建一个新Calculator客户端
client := proto.NewCalculatorClient(conn)
// 发起gpc调用
resp, err := client.Calculator(context.Background(), &proto.CalculatorRequest{
Num1: 25,
Num2: 4,
Type: proto.CalculatorType_MULTIPLY,
})
if err != nil {
panic(err.Error())
}
fmt.Println(resp)
}
跨语言
grpc支持跨语言通信,以下以Node为例实现一个TodoList:Go作为服务端,而Node作为客户端并给前端提供接口:
- proto/todolist.proto
- server/server.go
- client/app.js
- client/rpc.js
- client/package.json
syntax = "proto3";
option go_package = "./;proto";
message AddingTodoRequest {
string content = 1;
}
message TogglingTodoRequest {
int64 id = 1;
}
message RemovingTodoRequest {
int64 id = 1;
}
message TodoResponse {
int64 id = 1;
string content = 2;
bool completed = 3;
}
service TodoList {
rpc AddTodo (AddingTodoRequest) returns (TodoResponse);
rpc ToggleTodo (TogglingTodoRequest) returns (TodoResponse);
rpc RemoveTodo (RemovingTodoRequest) returns (TodoResponse);
}
package main
import (
"basic5/proto"
"context"
"net"
"time"
"google.golang.org/grpc"
)
type Todo struct {
Id int64
Content string
Completed bool
}
type TodoList struct {
list []Todo
}
func (td *TodoList) AddTodo(ctx context.Context, req *proto.AddingTodoRequest) (*proto.TodoResponse, error) {
todo := Todo{
Id: time.Now().Unix(), // 传到其它语言为long类型
Content: req.Content,
Completed: false,
}
td.list = append(td.list, todo)
return &proto.TodoResponse{
Id: todo.Id,
Content: todo.Content,
Completed: todo.Completed,
}, nil
}
func (td *TodoList) ToggleTodo(ctx context.Context, req *proto.TogglingTodoRequest) (*proto.TodoResponse, error) {
var target *Todo
for i := 0; i < len(td.list); i++ {
if td.list[i].Id == req.Id {
// td.list[i].Completed = !td.list[i].Completed
// target = td.list[i]
target = &td.list[i]
target.Completed = !target.Completed
break
}
}
return &proto.TodoResponse{
Id: target.Id,
Content: target.Content,
Completed: target.Completed,
}, nil
}
func (td *TodoList) RemoveTodo(ctx context.Context, req *proto.RemovingTodoRequest) (*proto.TodoResponse, error) {
var target Todo
for i := 0; i < len(td.list); i++ {
if td.list[i].Id == req.Id {
target = td.list[i]
td.list = append(td.list[0:i], td.list[i+1:]...)
break
}
}
return &proto.TodoResponse{
Id: target.Id,
Content: target.Content,
Completed: target.Completed,
}, nil
}
func main() {
// 创建grpc server
gServer := grpc.NewServer()
// 注册todolist server
// 参数1: gRPC的服务对象指针
// 参数2: 需要注册的服务对象的引用
proto.RegisterTodoListServer(gServer, &TodoList{
list: make([]Todo, 0),
})
// 创建服务器监听对象,开启8080端口的监听
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err.Error())
}
// 使用8080端口监听对象进行gServer的运行
err = gServer.Serve(listener)
if err != nil {
panic(err.Error())
}
}
const express = require('express')
const bodyParser = require('body-parser')
// grpc客户端实例
const gClient = require('./rpc')
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.all('*', (req, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', '*')
next()
})
let list = []
app.post('/add_todo', (req, res) => {
const { content } = req.body
gClient.AddTodo({ content }, (err, resp) => {
res.json(resp)
})
})
app.post('/toggle_todo', (req, res) => {
const { id } = req.body
gClient.ToggleTodo({ id }, (err, resp) => {
res.json(resp)
})
})
app.post('/remove_todo', (req, res) => {
const { id } = req.body
gClient.RemoveTodo({ id }, (err, resp) => {
res.json(resp)
})
})
app.listen(3000, () => {
console.log('Server is running on', 3000)
})
// grpc的js版本
const grpc = require('@grpc/grpc-js')
// 解析proto文件
const protoLoader = require('@grpc/proto-loader')
// proto文件路径
const PROTO_PATH = '../proto/todolist.proto'
// 定义一个包 protoLoader.loadSync(protoPath, [options])
// JavaSript版本的proto
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
longs: Number, // 如何解析long类型
defaults: true, // 参数类型默认值
keepCase: true // 保持驼峰
})
// 加载包 => TodoList => grpc.loadPackageDefiniton()
const TodoProto = grpc.loadPackageDefinition(
packageDefinition
).TodoList
// 实例化服务客户端
/**
返回值: => {
AddTodo(reqBody, (err, resp) => {})
ToggleTodo(reqBody, (err, resp) => {})
RemoveTodo(reqBody, (err, resp) => {})
}
*/
module.exports = new TodoProto(
'localhost:8080',
grpc.credentials.createInsecure() // 安全设置
)
{
"dependencies": {
"@grpc/grpc-js": "^1.10.8",
"@grpc/proto-loader": "^0.7.13",
"express": "^4.19.2"
}
}
gRPC通信模式
简单模式
Simple RPC Mode,一次请求与一次响应。
服务端数据流模式
Server-Side Streaming RPC Mode,服务端向客户端持续性传输数据流。
- readme.md
- proto/file.proto
- server/main.go
- client/main.go
/*
本案例以流式传输一张图片为例:
1. 客户端向服务端发起请求。
2. 服务端针对请求持续不断地进行流式传输,直到传输完毕。
3. 客户端持续不断地接收数据,并将最终的数据写入一个文件中,生成一张图片。
4. 流式传输完毕。
*/
syntax = "proto3";
option go_package = "./;proto";
message FileRequest {
string filename = 1;
}
message FileResponse {
string slice = 1;
float progress = 2;
}
service File {
// 流模式需要对响应体指明 stream
rpc GetStream (FileRequest) returns (stream FileResponse);
}
package main
import (
"basic6/data"
"basic6/proto"
"net"
"time"
"google.golang.org/grpc"
)
type File struct {
}
func (f *File) GetStream(req *proto.FileRequest, res proto.File_GetStreamServer) error {
// 每次传输的长度
const sliceLen = 1000
// 接收客户端的文件名
filename := req.Filename
fileBase64 := data.GetBase64(filename)
// 要传输的字符串总长度
fileLen := len(fileBase64)
// 标记位
flag := 0
// 最后索引
finalIndex := fileLen - 1
for {
// 超过最大长度结束传输
if flag+sliceLen >= finalIndex {
_ = res.Send(&proto.FileResponse{
Slice: fileBase64[flag:],
Progress: 100,
})
break
}
// 向客户端发送
_ = res.Send(&proto.FileResponse{
Slice: fileBase64[flag : flag+sliceLen],
Progress: (float32(flag + 1)) / float32(fileLen) * 100,
})
// 更新标记位
flag += sliceLen
time.Sleep(time.Second / 10)
}
return nil
}
func main() {
gServer := grpc.NewServer()
proto.RegisterFileServer(gServer, &File{})
listener, _ := net.Listen("tcp", ":8080")
_ = gServer.Serve(listener)
}
package main
import (
"basic6/proto"
"context"
"encoding/base64"
"fmt"
"io"
"os"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
const FILENAME = "avatar"
const EXT = ".jpg"
const PATH = "./"
func main() {
fileBase64 := ""
conn, _ := grpc.NewClient(":8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
defer func() {
_ = conn.Close()
}()
client := proto.NewFileClient(conn)
stream, _ := client.GetStream(context.Background(), &proto.FileRequest{
Filename: FILENAME,
})
// 创建文件
file, _ := os.Create(PATH + FILENAME + EXT)
for {
// 接收消息
msg, _ := stream.Recv()
// 拼接消息
fileBase64 += msg.Slice
fmt.Printf("Received: %f%%\r\n", msg.Progress)
// 进度为100时写入文件并结束
if msg.Progress == 100 {
// base64字符串有效开始索引
inValidIndex := strings.Index(fileBase64, ",")
// 编码
decRes := base64.NewDecoder(
base64.StdEncoding,
strings.NewReader(fileBase64[inValidIndex+1:]),
)
// 写入文件
_, _ = io.Copy(file, decRes)
break
}
}
}
客户端数据流模式
Client-Side Streaming RPC Mode,客户端向服务端持续性传输数据流。
客户端数据流传输与服务端模式正好相反:
syntax = "proto3";
option go_package = "./;proto";
message FileRequest {
string slice = 1;
float progress = 2;
}
message FileResponse {
string filename = 1;
}
service File {
rpc PostStream (stream FileRequest) returns (FileResponse);
}
双端数据流模式
Bidirectional Streaming RPC Mode,客户端与服务端双向持续性传输数据流。