gRPC是什么,怎么用
RPC是什么
Remoting Process Call (RPC)是一种远程过程调用的技术,在这种架构中具体而言,在这种架构中, 两个节点分别运行在不同的物理机器上, 一个应用实例部署在节点A, 另一个运行在节点B. 当需要从节点B上的应用程序获取所需的服务功能时, 因为这两个节点位于不同的内存空间环境中而无法直接进行本地访问或操作, 因此必须借助网络进行功能交互的同时传递必要的数据信息.
一个RPC框架的基本构成:
序列化协议
一切事物本质上都是由二进制数据构成的。我们必须要找到一个高效的方法来将所有的事物转化为统一的二进制数据流进行在网络传输过程中的统一编码与解码操作。这种转换机制即为序列化协议体系,在现代网络通信领域已经形成一套成熟的技术方案系统。这些主流的技术方案包括但不限于Java自带的序列化机制、Thrift框架以及Hessian编码器等
传输层
就现有情况来看,主流应用多采用TCP/UDP协议作为传输基础。在Java生态系统中广泛采用NettyAPI以隐藏其内部机制。
动态代理层
屏蔽业务感知远程调用,等同于一个本地服务调用
1.1 远程过程调用要解决的新问题
1. Call ID映射
每个RPC中的函数都需要分配唯一的标识符。这些标识符在整个系统中是独一无二的。每当应用程序希望发起远程过程调用请求时,则必须携带相应的标识符。为了实现高效的通信机制,在客户端和服务端之间应分别建立两个索引映射表(Function <-> Call ID)。尽管这两个映射表可以存在差异性(比如不同系统的具体实现),但为了保证通信的一致性与可靠性,在同一个功能模块上两端生成的一致标识符是必要的。当应用程序希望发起远程过程调用请求时,则需通过查找本地映射表获取对应的Call ID,并将其传输至服务端节点完成操作流程中的相应步骤
2. 序列化和反序列化
如何在客户端实现将参数值传递至远程函数?本地环境中处理时可采用压栈的方式将参数传递给目标函数。然而,在远程过程调用场景下,客户端与服务端处于不同的进程环境中,则无法直接通过内存空间完成信息交互。即使有时候客户端与服务端所使用的编程语言并不一致,则必须借助特定的数据编码方式来实现信息传输需求。此时必须将调用所需的参数以二进制形式编码后传输至目标端,在接收端则需解码还原为原始数据对象。这一操作被称为数据序列化与反序列化。
3. 网络传输
远程调用通常依赖于网络介质,在这种模式下,客户端与服务端之间建立网络通信。所有操作的数据都需要经过网络传输处理,并且必须设计一个专门的网络传输层机制来完成数据交换的核心逻辑。为了确保数据正确传递到目标端点并及时反馈回来,在这个过程中需要将Call ID与参数进行序列化编码后发送至服务端,并返回经过解密处理后的结果至客户端。只要能够满足这两个基本功能需求的机制都可以作为传输层协议使用。例如基于TCP协议的设计方案已经成为主流选择;而基于UDP协议的实现方案在某些场景下也能发挥出良好的性能优势;采用HTTP/2协议实现了更高效率的数据传输则是近年来新兴的技术路线之一。例如Java Netty框架同样支持这一功能模块的设计与实现。
1.2 业界常用的 RPC 框架
- Dubbo: Dubbo 是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。目前 Dubbo 已经成为 Spring Cloud Alibaba 中的官方组件,支持多语言和多协议,如Java、.NET、C++、REST、HTTP等。
- gRPC :gRPC是Google开源的跨语言高性能RPC框架,支持多种语言,包括C、C++、Java、Python、Go、Ruby等。它可以通过可插拔的支持来有效地连接数据中心内和跨数据中心的服务,以实现负载平衡,跟踪,运行状况检查和身份验证。它也适用于分布式计算的最后一英里,以将设备,移动应用程序和浏览器连接到后端服务。
- Hessian: Hessian是一个轻量级的 remoting-on-http 工具,使用简单的方法提供了 RMI 的功能。 相比 WebService,Hessian 更简单、快捷。采用的是二进制 RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。
- Apache Thrift:Thrift是Apache开源的跨语言高效的RPC框架,支持多种语言,包括C++、Java、Python、Ruby等。
- Apache Avro:Avro是Apache开源的数据序列化系统,也是一个支持跨语言的RPC框架,支持多种语言,包括Java、C、C++、Python等。
1.3 gRPC是什么
grpc 由 google 开发的一种 rpc 调用协议,在其官方文档中被描述为基于 protobuf 标准定义了一套完善的 RPC 传输机制和数据定义体系。然而 gRPC 系统目前仍存在一些功能尚待完善的情况:它尚未推出专门的连接池支持,并未实现服务发现功能和负载均衡策略。
1.4 gRPC的特点
- 采用Google研发的技术方案作为API定义工具:gRPC利用Google Protocol Buffers实现接口定义功能。这种解决方案适用于C/C++/Java/Python/Go/Ruby等多种编程环境,并且具备良好的跨平台兼容性及扩展性。
- gRPC采用了高效的数据编码方式,在binary format下表现出色。相比于JSON和其他非二进制格式,在binary format下传输效率更高。
- 该技术应用了先进的HTTP/2传输层协议,并通过multiplexing技术提升了处理能力。这种设计使得数据传输更加高效可靠。
- 该系统集成多种身份验证与加密机制如AES encryption OAuth2等确保通信安全性。这些措施有效保障了数据在整个传输过程中的安全性与可靠性。
综合而言,gRPC以其高性能,快速响应,兼容性好,支持灵活的扩展和集成等显著优势,主要应用于微服务架构,分布式计算以及云计算环境等多个领域
二、proto语法
Protocol Buffers 是一种高效简洁的结构化数据存储格式,在实际应用中不仅能够将结构化的信息进行编码(编码),还能够实现解码(解码)。特别适用于作为通信协议中的RPC(远程过程调用)或作为数据库中使用。它无语言依赖地提供强大的序列化能力,并具备跨平台设计的特点,在高度可扩展的同时兼具功能强大。
Protocol Buffers 是一种具有灵活性、高效率以及自动化的机制来实现结构数据序列化的方法-类似于 XML 但其规模上是其约3至10倍的小,在速度上快20至100倍的同时相对而言更加简单。
2.1 如何使用ProtoBuf
创建pb文件
第一阶段, 请建立一个.proto文件, 用于制定数据格式, 以下是一个简化的案例: 假设你需要规划一项工作流程, 可以采用以下方法来设计你的工作计划书了
syntax = "proto3";
package attribution.sdk.test;
option java_package = "com.attribution.sdk.test.channelinfo";
option java_outer_classname = "AdIdInfoNewProto";
option java_multiple_files = true;
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
service AttributionService {
rpc Search(SearchRequest) returns (SearchResponse);
}
- 此文件的第一行指定了 proto3 格式:如果不这样做, protobuf 编译器将默认解析 proto2 格式代码,因此必须位于第一个非空且非注释行的位置.
- 包名指定了 proto 格式中服务所在的完整路径.
- Java 包名指定了 proto 格式中消息(message)生成的位置.
- Java 外部类名指定了输出 Java 文件中的相应类名称.
- 消息被视为一个数据结构,在 Java 中会被转换成一个实体类.
- 服务是被调用的对象,在此服务下定义了 RPC 方法.
- 此 SearchRequest 消息包含了三个字段信息:分别对应所需的内容.
指定字段类型
在之前的案例中,默认情况下,默认情况下所有字段都是基本数据类型的实例:例如有两个整数(page_number表示页面编号和result_per_page表示每页结果数量)以及一个字符串(query表示搜索查询)。然而,默认情况下您还可以自定义字段的数据结构形式,并支持定义合成数据类型的组合体包括枚举型和其他消息类别。
分配标识号
在消息定义部分,每个字段都分配了一个唯一的数字标识符。这些标识符用于在消息的二进制形式中识别各个字段的位置,并且一旦被使用就不能够更改其数值范围或者分配方式。注释:区间[1,15]内的标识号编码时占用一个字节;而区间[16,2047]内的则占用两个字节的空间。因此,在频繁使用的消息元素中应优先保留编号在[1,15]范围内的标识号。
再举一个例子
创建 .proto 文件,定义数据结构,如下例:
// 例1: 在 xxx.proto 文件中定义 Example1 message
message gps_data {
optional string stringVal = 1;
optional bytes bytesVal = 2;
message EmbeddedMessage {
int32 int32Val = 1;
string stringVal = 2;
}
optional EmbeddedMessage embeddedExample1 = 3;
repeated int32 repeatedInt32Val = 4;
repeated string repeatedStringVal = 5;
}
在上一例中我们定义了一个名为 gps_data 的消息。该消息采用了简单的语法结构,在 message 关键字后面紧跟的消息名称:
message xxx {
}
在Java开发中设计数据接口时需注意命名规范的一致性。具体而言,在构造数据接口时若采用下划线分隔符命名gps\_data变量,则系统会默认将其转换为驼峰式命名GpsData以适应后续开发需求。然而,在某些特殊情况下也可以通过配置参数或代码注释等方式灵活指定变量的具体命名规则。
option java_outer_classname = "BatteryData";
之后我们在其中定义了 message 具有的字段,形式为:
message xxx {
// 字段规则:required -> 字段必须出现且只能出现 1 次
// 字段规则:optional -> 字段可出现 0 次或1次
// 字段规则:repeated -> 字段可出现任意多次(包括 0)
// 类型:int32、int64、sint32、sint64、string、32-bit ....
// 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
字段规则 类型 名称 = 字段编号;
}
在上例中,我们定义了:
- 字段 stringVal 为可选类型 string。
- 字段 bytesVal 为 bytes 类型。
- 字段 embeddedExample1 为可选嵌入消息。
- 字段 repeatedInt32Val 为可重复出现的 int32 类型。
- 字段 repeatedStringVal 允许重复出现的字符串类型。
还可以指定改java代码的包路径,命令如下:
option java_package = "com.ks.test";
2.2 proto文件几种路径说明
syntax = "proto3";
package com.ks.infra.grpc.test;
option java_package = "com.ks.demo";
option java_outer_classname = "Demo";
option java_multiple_files = true;
message TestRequest {
string value = 1;
}
message TestResponse {
string value = 1;
}
service TestService {
rpc Test (TestRequest) returns (TestResponse);
rpc TestDeadline (TestRequest) returns (TestResponse);
}
service TestStreamService {
rpc Test (TestRequest) returns (stream TestResponse);
rpc TestFailed (TestRequest) returns (stream TestResponse);
rpc TestSlow (TestRequest) returns (stream TestResponse);
rpc TestUpStream(stream TestRequest) returns (TestResponse);
}
如上面proto文件所示,一共有三个路径:
- proto所在的本地位置通常是ks/demo这样的位置;
当其他.proto 文件需要引用当前.proto 文件时会依赖这个本地位置,
类似于导入"ks/demo/demo.proto"的方式;
这样就能访问该Proto定义的所有消息类型。 - package定位相当关键;
任何对proto结构的更改或迁移操作都不应该修改包的位置,
因为这决定了各个Service的位置;
一旦发生变动会导致gRPC调用时无法找到服务方法并抛出异常(Unimplemented)。 - option参数指定Java输出目录为com.ks.demo:;
这一设置相当重要;
建议在变更前谨慎处理以避免影响源代码的一致性和可编译性。
2.3 proto文件输出样式
proto文件输出样式由如下几个因素决定:
- proto文件的名称遵循以下规则:例如infra_demo.proto;若未指定相关参数,则按照驼峰命名策略生成对应的Java类(此处指明Java outer class名为InfraDemo)。
- 在proto文件中如果message名称与默认生成的Java类名相同的情况下(即本例中的InfraDemo),系统会在该Java类名称后附加OuterClass前缀(此处得到的结果即为InfraDemo OuterClass)。
- 若选择设置option java_outer_classname = "Demo"时,则输出文件的完整路径将包含指定的分类名 Demo。
- option java_multiple_files = true:当此选项被启用时,默认情况下每个message将独立编译成一个单独的Java源代码文件。
- 请特别提醒,在启用该选项后,请务必谨慎更改任何配置参数。
2.4 protoc 编译 .proto 文件会生成什么
当您借助Protoc工具编译一个proto文件时, 编译器将基于你在 proto 文件中声明的数据类型自动生成所需代码。所生成的代码集涵盖了获取设置接口、序列化功能以及反序列化接口。
- 针对 C++ 编程语言来说,在编译过程中会遵循以下规则:针对每个 proto 文件而言,在编译完成后会产生两个源代码文件——一个是.h 头文件和另一个是.cc 实现文件。这些源代码将根据您在 proto 文件中定义的消息类型自动生成相应的类。
- 同样地,在 Java 编程环境中进行编译时会遵循类似的规则:针对每个 .java 文件而言,在编译完成后会产生一个单独的 Java 类文件。这个类文件中包含了所有消息类型对应的 Java 类,并且同时提供特殊的 Builder 类来帮助构造这些消息实例。
为开发者及业务程序开发的 .proto 格式数据规范文档中包含了若干数据结构。这些规范文档主要供开发者及业务系统人员使用,在存储与传输方面并未有所涉及。
在处理这些数据的存储或传输时,必须对这些结构数据执行序列化处理,并完成反序列化操作以及实现读取和写入功能。Protocol Buffers(缩写为 Protobuf)由编译器protoc自动生成相应的接口代码。
可通过如下命令生成相应的接口代码:
// $SRC_DIR: .proto 文件所在的源目录
// --java_out: 生成 java 代码
// $DST_DIR: 生成java代码的目标目录
// xxx.proto: 要针对哪个 proto 文件生成接口代码
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/xxx.proto
