GRPC介绍

  • 简介
  • 概述
  • 特点
  • 使用场景
  • 接口定义
  • 定义消息
  • 定义服务


简介

gRPC 是由Google一个高性能、开源的RPC框架,面向移动和HTTP/2设计。目前由C、JAVA等语言版本。用一句话也就是说gRPC提供一套机制,使得应用程序之间能够进行通信,且遵从CS模型,在使用的时候调用Server端接口向本地方法一样。

摘自官网的一个典型gRPC结构图,如图。


gRPC 默认使用protocol buffers,这是Google开源的一套成熟的结构数据序列化机制,推荐使用叫 proto3 的新风格protocol buffers,更轻量简洁、支持语言较多,具有一些新功能。

概述

特点

1.强大的接口描述语言以及支持多语言
gRPC默认使用Protobuf来定义接口,同时为各种语言编写的服务自动生成相应语言的客户端和服务端接口(存根)。
2.基于HTTP2标准设计
基于HTTP2标准设计,有了双向流、流程控制、头部压缩、单TCP连接上的多路请求等待等特性

使用场景

1.对接口有严格约束要求
我们向外部提供了一个公共服务,很多人甚至公司外部人也可以访问这个服务,这时我们就希望对这个接口有更加严格的约束,考虑到安全性,我们也不希望客户端能够发送任意数据。gRPC就可以通过protobuf来提供严格的接口约束。
2.对性能有高要求
有时我们的服务需要传递大量数据,但是又不希望影响到性能,这个时候就可以考虑使用gRPC。gRPC通过protobuf我们可以将数据编码转化为二进制格式,传递的数量要小很多,而且可以使用HTTP2实现异步请求,可以大大提高通信效率。

  • 但是通常并不会单独使用gRPC,而是将gRPC作为一个部件进行使用,因为在实际生产环境中,面对大并发的情况我们要使用分布式系统进行处理,而gRPC就没有提供一些分布式系统相关的组件。实际的开发过程中很复杂例如负载均衡、限流熔断、监控报警、服务注册和发现这些都需要考虑到。

接口定义

定义消息

先看一个简单例子。

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

1. 版本号
对于一个pb文件而言,文件首个非空、非注释的行必须注明pb的版本,即syntax = “proto3”;,否则默认版本是proto2
2. message
一个message类型看上去很像一个Java class,由多个字段组成。每一个字段都由类型、名称组成,位于等号右边的值不是字段默认值,而是数字标签,可以理解为字段身份的标识符,类似于数据库中的主键,不可重复,标识符用于在编译后的二进制消息格式中对字段进行识别,一旦你的pb消息投入使用,字段的标识就不应该再改变。数字标签的范围是[1, 536870911],其中19000~19999是保留数字。
3. 类型
每个字段的类型(int32,string)都是scalar的类型,和java语言类型的对比如下:

proto类型

Java类型

默认值

备注

double

double

0

float

float

0

int32

int

0

使用可变长度编码。对负数进行编码时比较低效 – 如果你的字段要使用负数值,请使用sint32来代替

int64

long

0

使用可变长度编码。对负数进行编码时比较低效 – 如果你的字段要使用负数值,请使用sint64来代替

uint32

in

0

使用可变长度编码。无符号

uint64

long

0

使用可变长度编码。无符号

sint32

int

0

使用变长编码,这些编码在负值时比int32高效的多

sint64

long

0

使用变长编码,有符号的整型值。编码时比通常的int64高效。

fixed32

int

0

总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。

fixed64

long

0

总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效

sfixed32

int

0

总是4个字节

sfixed64

long

0

总是8个字节

bool

boolean

false

string

String

空字符串

bytes

ByteString

空的bytes

  • 修饰符
    如果一个字段被repeated修饰,则表示它是一个列表类型的字段,如下所示:
message SearchRequest {
repeated string args = 1; // 等价于java中的List<String> args
}
  • 默认值
  • string类型的默认值是空字符串
  • bytes类型的默认值是空字节
  • bool类型的默认值是false
  • 数字类型的默认值是0
  • enum类型的默认值是第一个定义的枚举值 message类型(对象,如上文的SearchRequest就是message类型)的默认值与编程语言相关
  • repeated修饰的字段默认值是空列表

如果一个字段的值等于默认值(如bool类型的字段设为false),那么它将不会被序列化,这样的设计是为了节省流量。
6. 枚举
每个枚举值有对应的数值,数值不一定是连续的。第一个枚举值的数值必须是0且至少有一个枚举值,否则编译报错。编译后编译器会为你生成对应语言的枚举类。

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}

一个数值可以对应多个枚举值,必须标明option allow_alias = true;

enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}

可以使用MessageType.EnumType的形式引用定义在其它message类型中的枚举。

由于编码原因,出于效率考虑,官方不推荐使用负数作为枚举值的数值

7.其他类型
除了上述基本类型,一个字段的类型也可以是其它的message类型:

syntax = "proto3";

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

还可以使用import导入其他文件中的message;可以使用嵌套类型类似Java内部类;可以使用Any类型包装message类型,使用pack()和unpack()进行装箱和拆箱;使用oneof关键字实现一个字段最多只有一个能被设置;
8.包
你可以用指定package以避免类型命名冲突:

package foo.bar;
message Open { ... }

然后可以用类型的全限定名来引用它:

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

9.JSON映射
pb支持和JSON互相转换。如果一个字段不存在JSON数据中或者为null,那么pb中会被赋为该字段的默认值,反之,如果一个字段在pb中是默认值,那么不会写到JSON数据中以节省空间。
10.选项

选项不对message的定义产生任何的效果,只会在一些特定的场景中起到作用,下面是一部分例子,完整的选项列表可以前往google/protobuf/descriptor.proto查看(Java语言可以在jar包中找到):

  • option java_package = “com.example.foo”; 编译器为以此作为生成的Java类的包名,如果没有该选项,则会以pb的package作为包名。
  • option javamultiplefiles = true; 该选项为true时,生成的Java类将是包级别的,否则会在一个包装类中。
  • option optimizefor = CODESIZE; 该选项会对生成的类产生影响,作用是根据指定的选项对代码进行不同方面的优化。
  • int32 old_field = 6 [deprecated=true]; 把字段标为过时的。

定义服务

要定义一个服务,你必须在你的 .proto 文件中指定 service

service SearchService {

}

然后在我们的服务中定义 rpc 方法,指定它们的请求的和响应类型。gRPC 允许你定义4种类型的service 方法。

  1. 简单 RPC
    客户端使用存根发送请求到服务器并等待响应返回,就像平常的函数调用一样。
rpc Search (SearchRequest) returns (SearchResponse);
  1. 服务器端流式 RPC
    客户端发送请求到服务器,拿到一个流去读取返回的消息序列。 客户端读取返回的流,直到里面没有任何消息。从例子中可以看出,通过在 响应 类型前插入 stream 关键字,可以指定一个服务器端的流方法。
rpc Search (SearchRequest) returns (stream  SearchResponse);
  1. 客户端流式 RPC
    客户端写入一个消息序列并将其发送到服务器,同样也是使用流。一旦 客户端完成写入消息,它等待服务器完成读取返回它的响应。通过在 请求 类型前指定 stream 关键字来指定一个客户端的流方法。
rpc upload(stream UploadRequest) returns (UploadResponse) {}
  1. 双向流式 RPC

是双方使用读写流去发送一个消息序列。两个流独立操作,因此客户端和服务器 可以以任意喜欢的顺序读写:比如, 服务器可以在写入响应前等待接收所有的客户端消息,或者可以交替 的读取和写入消息,或者其他读写的组合。 每个流中的消息顺序被预留。你可以通过在请求和响应前加 stream 关键字去制定方法的类型。

rpc fileConvert(stream UploadRequest) returns (stream DownloadResponse) {}