gRPC
gRPC
gRPC入门
gRPC简介
gRPC来自Google,它是一个开源的框架;它同时也是Cloud Native Computation基金会(CNCF)的一部分,就像Docker和Kubernetes一样。
gRPC允许你为RPC(Remote Procedure call)定义请求和响应,然后gRPC会帮你处理一切剩余问题。
它速度快,执行效率高,基于HTTP/2构建,低延迟,支持流,与开发语言无关,并且可以很简单的插入身份认证、负载均衡、日志和监控等功能。
gRPC它是对RPC一种非常简洁的实现并且解决了很多RPC的问题。
gRPC结构
设计步骤
生命周期
如何学习gRPC
首先,你得学习Protocol Buffers(https://developers.google.com/protocol-buffers/),简单的说,它可以用来定义消息和服务。
然后,你只需要实现服务即可,剩余的gRPC代码将会自动为你生成。
.proto这个文件可以适用于十几种开发语言(包括服务端和客户端),并且它允许你使用同一个框架来支持每秒百万级以上的RPC调用。
开发模式
gPRC使用的是合约优先的API开发模式,它默认使用Protocol buffers(protobuf)作为接口设计语言(IDL),这个.proto文件包括两部分:
- gRPC服务的定义
- 服务端和客户端之间传递的消息
为什么使用Protocol Buffers
它和开发语言无关
可以生成所有主流开发语言的代码
数据是二进制格式的,串行化的效率高,Payload比较小
也很适合传递大量的数据
通过设定某些规则,使得API的进化也很简单
Hello gRPC
新建一个以 .proto
结尾的文件first.proto:
1 | syntax = "proto3"; //使用proto3语法 默认是proto2 |
消息定义
标量类型
- 数值型:数值型有很多种形式:double,float,int32,int64,uint32,uint64,sint32,sint64,fixed32,fixed64,sfixed32,sfixed64。
根据需要选择对应的数值类型。 - 布尔型:bool型可以有True和False两个值。
- 字符串:string表示任意长度的文本,但是它必须包含的是UTF-8编码或7位ASCII的文本,长度不可超过232。
- 字节型:bytes可表示任意的byte数组序列,但是长度也不可以超过232,最后是由你来决定如何解释这些bytes。例如你可以使用这个类型来表示一个图片。
1 | syntax = "proto3"; |
字段的数值(Tag)
在Protocol Buffers里面,字段的名其实没那么重要,但是写C#/go等语言代码的时候,字段名还是很重要的。
对于protobuf来说,这个tag是更为重要的。
可以使用的最小的tag数值是1,最大值是2^29-1,或者536,870,911。但是你不可以使用19000到19999之间的数,这部分数是保留的。
从1到15的Tag数只占用1个字节的空间,所以它们应该被用在频繁使用的字段上。而从16到2047,则占用两个字节,它们可以用在不频繁使用的字段上。
字段规则
protobuf的字段必须满足以下两个规则之一
- 单数字段(Singular)
大概意思就是指这个字段只能出现0或1次(不能超过一次),这也是proto3的默认字段规则。 - 重复字段(Repeated)
与singular相对的就是repeated。如果你想做一个list或数组的话,你可以使用重复字段这个概念。这个list可以有任何数量(包括0)的元素。它里面的值的顺序将会得到保留。
1 | syntax = "proto3"; |
保留的字段
如果你对你定义的消息类型进行了更新,例如删除某个字段或者注释掉某个字段,那么其它开发者在以后更新这个消息类型的时候可能会重新使用被你删除/注释掉的字段的数值(tag)。如果以后还需要使用这个消息类型的老版本的proto文件,那么这将会引起严重的问题,例如数据损坏、隐私漏洞等等。
一种避免此类事情发生的解决办法就是将你删除/注释掉的这些字段的数值(或/并且包括字段名,因为字段名也可引起JSON序列化的问题)标记为reserved,如果其他人再使用这个数值作为字段标识符,那么编译器就会有错误提示。
1 | syntax = "proto3"; |
字段的默认值
当消息被解析的时候,如果编码的消息里不含有特定的一个singular元素,那么在被解析对象里相应的字段就会被设为默认值。
常用类型的默认值如下:
- string:空字符串
- bytes:空的byte数组
- bool:false
- 数值型:0
- 枚举enum:枚举里定义的第一个枚举值,值必须是0
- repeated:通常是相应开发语言里的空list
- 还有个消息类型的字段,它的默认值和开发语言有关,这个以后再说。
默认值在更新Protocol Buffer消息定义的时候有很重要的作用,它可以防止对现有代码/新代码造成破坏性影响。它们也可以保证字段永远不会有null值。
但是,默认值还是非常危险的:
你无法区分这个默认值到底是来白一个去失的字段还是字段的实际值正好等于默认值。
应该怎么办?
需要保证这个默认值对于业务来说是一个毫无意义的值。例如int32pop(入口)默认值就可以设置为-1.
再就是,可能需要在你的代码里来做一些对默认值的判断,从而进行处理。
枚举
枚举里面定义的第一个值就是这个枚举的默认值。
Enum的tag必须从0开始,所以0就是枚举的数值默认值。
1 | syntax = "proto3"; |
为枚举值起别名
枚举值是可以起别名的,起别名的作用就是允许两个枚举值拥有同一个数值。
要想起别名,首先需要设置allow_alias这个option为true
然后我们为FEMALE这个枚举值起了一个别名叫做WOMAN,它们的数值是一样的。同样的MAN是MALE的数值也是一样的。
1 | syntax = "proto3"; |
枚举里面的常量的值必须不能超过32位整型的数值,不建议使用负数。
枚举可以定义在message里面,也可以在外边单独定义以便复用。如果另一个消息想使用Person里面这个Gender枚举,那么可以使用Person.Gender这这种形式。
使用其它的信息类型
可以使用其它的信息类型作为字段的类型。
新建date.proto
1 | syntax = "proto3"; |
在person.proto中使用
1 | syntax = "proto3"; |
消息嵌套
1 | syntax = "proto3"; |
打包
你可以向proto文件添加可选的打包(package)说明符,以避免消息类型间的名称冲突,打包是很有必要的。
1 | syntax = "proto3"; |
设置ProtocolBuffers编译器
protoc编译器主要就是用来生成代码的,它的下载地址目前是:https://github.com/protocolbuffers/protobuf/releases/
解压完后添加bin文件夹到环境变量中:
1 | vim ~/.bashrc |
查看是否成功:
1 | protoc |
出现内容说明安装成功。
生成源文件
csharp:
1 | protoc first.proto --csharp_out=csharp |
需要提前创建csharp文件夹
发现在csharp文件下生成了一个First.cs文件。
这个文件我们不要修改,可以把它看成是一个库,我们只需要调用就行
一次性生成:
1 | protoc *.proto --csharp_out=csharp |
go语言例子
创建项目
安装依赖
1 | go get -u github.com/golang/protobuf/protoc-gen-go |
编写proto文件
位置 src->proto->person.proto
:
1 | syntax = "proto3"; |
生成go代码
在项目根目录下运行:
1 | protoc --proto_path src/ --go_out=./ src/proto/person.proto |
发现生成了 person.pb.go
文件
1 | // Code generated by protoc-gen-go. DO NOT EDIT. |
该文件为生成文件,不要修改
编写go代码
main.go
1 | package main |
枚举使用
enum.proto
1 | syntax = "proto3"; |
生成go代码
1 | protoc --proto_path src/ --go_out=./ src/proto/enum.proto |
使用
1 | package main |
复杂类型使用
complex.proto
1 | syntax = "proto3"; |
生成go代码
1 | protoc --proto_path src/ --go_out=./ src/proto/complex.proto |
使用
1 | package main |
更新消息类型
需求会发生变化
有一些字段可能会发生变化,可能会添加一些字段,也可能会删除一些字段。
但是可能有很多程序正在使用/读取你的Protocol Buffer的消息,但是它们没法都随着需求进行更新。
所以,在你对源数据进行演进的时候,一定不要引起破坏性变化,否则其它的程序可能就无法正常工作了。
两种变更情景
向前兼容变更:使用新的.proto文件来写数据----从旧的.proto文件读取数据
向后兼容变更:使用旧的.proto文件来写数据----从新的.proto文件读取数据
更新消息类型的规则
不要修改任何现有字段的数字(tag)
你可以添加新的字段,那些使用旧的消息格式的代码仍然可以将消息序列化,您应该注意这些元素的默认值,以便新代码可以与旧代码生成的消息正确交互。类似的,新代码所创建的消息也可以被旧代码解析:旧的二进制在解析的时候会忽略新的字段。
字段可以被删除,只要它们的数字(tag)在更新后的消息类型中不再使用即可。你也可以把字段名改为使用“OBSOLETE_"前而不是删除字段,或者把这些字段的数字(tag)进行保留(reserved),以免未来其它开发者不消息使用了删除字段的数字。
对于数据类型的变化,例如int32到int64,string到bytes等等,可以参考官方文档:
https://developers.google.com/protocol-buffers/docs/proto3#updating。但是建议还是尽量不要去修改字段的数据类型。
身份认证
这里指的不是用户的身份认证,而是指多个server和client之间,它们如何识别出来谁是谁,并且能安全的进行消息传输。
在身份认证这方面,gRPC一共有4种身份认证的机制:
- 不采取任何措施的连接,也就是不安全的连接。
- TLS/SSL连接
- 基于GoogleToken 的身份认证。
- 自定义的身份认证提供商。
消息传输类型
gRPC的消息传输类型有4种
第一种是一元的消息,就是简单的请求–响应。
第二种是server streaming(流),server会把数据streaming回给client。
第三种是client streaming,也就是client会把数据streaming给server。
最后是双向streaming。
go Server
创建proto文件
新建protos->messages.proto
1 | syntax = "proto3"; |
新建protos->enums.proto
1 | syntax = "proto3"; |
添加依赖
1 | go get -u google.golang.org/grpc |
生成pb代码
1 | protoc ./protos/*.proto --go_out=./ |
编写go代码
模拟数据库:data.go
1 | package main |
生成证书:
1 | openssl req -newkey rsa:4096 -nodes -sha256 -keyout server_private.key -x509 -days 36500 -out server.pem -addext "subjectAltName =DNS:www.yuanyuan.blog" |
没安装openssl的可以使用 sudo apt-get install openssl
安装一下(ubuntu),其他平台自己搜素一下。
编写main.go:
1 | package main |
go client
创建项目,复制protos文件夹,证书文件,安装依赖,生成pb代码
编写main.go:
1 | package main |
测试
server:
client: