这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天 gRPC
1 微服务架构
1.1 代码冗余问题
服务未拆分之前,公共的功能有统一的实现,比如认证,授权,限流等,但是服务拆分之后,每一个服务都要实现一遍。
解决方案:
- 由于为了保持对外提供服务的一致性,引入了网关的概念。由于网关根据不同的需求将其转发到不同的服务。由于入口的一致性,可以在网关上实现公共的一些功能。不
- 可以将公共的功能抽取出来,形成一个新的服务,比如统一认证中心。
1.2 服务之间调用
服务拆分之后,服务和服务之间发生的是进程和进程之间的调用。服务器和服务器之间的调用。
那么就需要发起网络调用,网络调用我们能力往想象到的就是HTTP但是在微服务架构中,HTTP虽然方便快捷,但是性能很低。这时候就需要引入RPC(远程过程调用)。通过自定义协议发起tcp调用,来加快传输。
问题以及解决方案:
2 gRPC简介
数据在网络传输。需要进行序列化。序列化的协议有很多。比如xml, json, Protobuf,G PC默认使用Protocol buffers。
序列化:将数据结构和独享转换成二进制的过程。
反序列化:将在序列化过程中所产生的二进制串转换成数据结构或对象的过程。
3 protobuf
优势:
- 序列化后体积很小,适合网络传输。
- 支持跨平台多语言。
- 序列化和反序列化速度很快。
3.1 安装
- 第一步:下载通用编译器
- 地址:github.com/protocolbuf…
- 根据不同的操作系统,下载不同的包,我是windows电脑,解压出来是
protoc.exe - 第二步:配置环境变量
- 第三步:安装go专用的protoc的生成器(最新)
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
如何使用protobuf呢?
- 定义了一种源文件,扩展名为
.proto,使用这种源文件,可以定义存储类的内容(消息类型) - protobuf有自己的编译器
protoc,可以将.proto编译成对应语言的文件,就可以进行使用了
3.2 hello world
假设,我们现在需要传输用户信息,其中有username和age两个字段
// profile/user.proto
// 指定的当前proto语法的版本,有2和3 syntax = "proto3" ; //option go_package = "path;name"; ath 表示生成的go文件的存放地址,会自动生成目录的 // name 表示生成的go文件所属的包名 option go_package= "../service" ; // 指定等会文件生成出来的package package service; message User { string username = 1 ; int32 age = 2 ; }
运行protoc命令编译成go中间文件
# 编译user.proto之后输出到service文件夹
protoc --go_out=./ --go-grpc_out=./ .\user.proto
测试代码
package main
import (
"fmt"
"google.golang.org/protobuf/proto"
"hello_world/service"
)
func main() {
user := &service.User{
Username: "hanling",
Age: 12,
}
marshal, err := proto.Marshal(user)
if err != nil {
panic(err)
}
newUser := &service.User{}
err = proto.Unmarshal(marshal, newUser)
if err != nil {
panic(err)
}
fmt.Println(newUser)
}
3.3 proto文件介绍
3.3.1 message介绍
message:protobuf中定义一个消息类型是通过关键字message字段指定的。
消息就是需要传输的数据格式的定义。
message关键字类似于C++中的class,Java中的class,go中的struct
例如:
message User {
string username = 1;
int32 age = 2;
}
在消息中承载的数据分别对应于每一个字段。
其中每个字段都有一个名字和一种类型 。
3.3.2 字段规则
required:消息体中必填字段,不设置会导致编解码异常。(例如位置1)optional: 消息体中可选字段。(例如位置2)repeated: 消息体中可重复字段,重复的值的顺序会被保留(例如位置3)在go中重复的会被定义为切片。
message User {
string username = 1;
int32 age = 2;
optional string password = 3;
repeated string address = 4; //一个用户有多个地址
}
3.3.3 字段映射
| .proto Type | Notes | C++ Type | Python Type | Go Type |
|---|---|---|---|---|
| double | double | float | float64 | |
| float | float | float | float32 | |
| int32 | 使用变长编码,对于负值的效率很低,如果你的域有 可能有负值,请使用sint64替代 | int32 | int | int32 |
| uint32 | 使用变长编码 | uint32 | int/long | uint32 |
| uint64 | 使用变长编码 | uint64 | int/long | uint64 |
| sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int32 | int | int32 |
| sint64 | 使用变长编码,有符号的整型值。编码时比通常的 int64高效。 | int64 | int/long | int64 |
| fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这 个类型会比uint32高效。 | uint32 | int | uint32 |
| fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这 个类型会比uint64高效。 | uint64 | int/long | uint64 |
| sfixed32 | 总是4个字节 | int32 | int | int32 |
| sfixed32 | 总是4个字节 | int32 | int | int32 |
| sfixed64 | 总是8个字节 | int64 | int/long | int64 |
| bool | bool | bool | bool | |
| 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文 本。 | string | str/unicode | string | |
| bytes | 可能包含任意顺序的字节数据。 | string | str | []byte |
3.3.4 默认值
protobuf3 删除了 protobuf2 中用来设置默认值的 default 关键字,取而代之的是protobuf3为各类型定义的默认值,也就是约定的默认值,如下表所示:
| 类型 | 默认值 |
|---|---|
| bool | FALSE |
| 整型 | 0 |
| string | 空字符串"" |
| 枚举enum | 第一个枚举元素的值,因为Protobuf3强制要求第一个枚举元素的值必须是0,所以枚举的默认值就是0; |
| message | 不是null,而是DEFAULT_INSTANCE |
3.3.5 标识号
标识号:在消息体的定义中,每个字段都必须要有一个唯一的标识号,标识号是[0,2^29-1]范围内的一个整数。
message Person {
string name = 1; // (位置1)
int32 id = 2;
optional string email = 3;
repeated string phones = 4; // (位置4)
}
以Person为例,name=1,id=2, email=3, phones=4 中的1-4就是标识号。
3.3.6 定义多个消息类型
一个proto文件中可以定义多个消息类型
message UserRequest {
string username = 1;
int32 age = 2;
optional string password = 3;
repeated string address = 4;
}
message UserResponse {
string username = 1;
int32 age = 2;
optional string password = 3;
repeated string address = 4;
}
3.3.7 嵌套消息
可以在其他消息类型中定义、使用消息类型,在下面的例子中,Person消息就定义在PersonInfo消息内,如 :
message PersonInfo {
message Person {
string name = 1;
int32 height = 2;
repeated int32 weight = 3;
}
repeated Person info = 1;
}
如果你想在它的父消息类型的外部重用这个消息类型,你需要以PersonInfo.Person的形式使用它,如:
message PersonMessage {
PersonInfo.Person info = 1;
}
当然,你也可以将消息嵌套任意多层,如 :
message Grandpa { // Level 0
message Father { // Level 1
message son { // Level 2
string name = 1;
int32 age = 2;
}
}
message Uncle { // Level 1
message Son { // Level 2
string name = 1;
int32 age = 2;
}
}
}
3.3.8 定义服务(Service)
如果想要将消息类型用在RPC系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer 编译器将会根据所选择的不同语言生成服务接口代码及存根。
service SearchService {
//rpc 服务的函数名 (传入参数)返回(返回参数)
rpc Search (SearchRequest) returns (SearchResponse);
}
上述代表表示,定义了一个RPC服务,该方法接收SearchRequest返回SearchResponse