一、RPC基本概念
RPC(Remote Procedure Call):远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想。
RPC 是一种技术思想而非一种规范或协议,常见 RPC 技术和框架有:
● 应用级的服务框架:阿里的 Dubbo/Dubbox、Google gRPC、Spring Boot/Spring Cloud、Facebook 的 Thrift、Twitter 的 Finagle 等。
● 远程通信协议:RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)。
● 通信框架:MINA 和 Netty。
● ps: Google gRPC 框架是基于 HTTP2 协议实现的,底层使用到了 Netty 框架的支持。
- RPC 框架
一个典型 RPC 的使用场景中,包含了服务发现、负载、容错、网络传输、序列化等组件,其中“RPC 协议”就指明了程序如何进行网络传输和序列化。
图 1:完整 RPC 架构图 - RPC 核心功能
RPC 的核心功能是指实现一个 RPC 最重要的功能模块,就是上图中的”RPC 协议”部分:
一个 RPC 的核心功能主要有 5 个部分组成,分别是:客户端、客户端 Stub、网络传输模块、服务端 Stub、服务端等。
图 2:RPC 核心功能图
下面分别介绍核心 RPC 框架的重要组成: - 客户端(Client):服务调用方。
- 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
- 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
- 服务端(Server):服务的真正提供者。
- Network Service:底层传输,可以是 TCP 或 HTTP。
一次 RPC 调用流程如下: - 服务消费者(Client 客户端)通过本地调用的方式调用服务。
- 客户端存根(Client Stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体。
- 客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端。
- 服务端存根(Server Stub)收到消息后进行解码(反序列化操作)。
- 服务端存根(Server Stub)根据解码结果调用本地的服务进行相关处理
- 服务端(Server)本地服务业务处理。
- 处理结果返回给服务端存根(Server Stub)。
- 服务端存根(Server Stub)序列化结果。
- 服务端存根(Server Stub)将结果通过网络发送至消费方。
- 客户端存根(Client Stub)接收到消息,并进行解码(反序列化)。
- 服务消费方得到最终结果。
RPC的目标就是要2~10这些步骤都封装起来,让用户对这些细节透明。
二、gRPC简介
官网
https://www.grpc.io/ gRPC 官方文档中文版
RPC 框架的目标就是让远程服务调用更加简单、透明,其负责屏蔽底层的传输方式(TCP/UDP)、序列化方式(XML/Json)和通信细节。服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程。
gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.
在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。
gRPC 客户端和服务端可以在多种环境中运行和交互 - 从 google 内部的服务器到你自己的笔记本,并且可以用任何 gRPC 支持的语言来编写。所以,你可以很容易地用 Java 创建一个 gRPC 服务端,用 Go、Python、Ruby 来创建客户端。
gRPC 的功能优点:
● 高兼容性、高性能、使用简单
gRPC 的组成部分:
● 使用 http2 作为网络传输层
● 使用 protobuf 这个高性能的数据包序列化协议
● 通过 protoc gprc 插件生成易用的 SDK
三、使用 protobuf序列化协议
什么是ProtoBuf
ProtoBuf(Protocol Buffers)是一种跨平台、语言无关、可扩展的序列化结构数据的方法,可用于网络数据交换及存储。
在序列化结构化数据的机制中,ProtoBuf是灵活、高效、自动化的,相对常见的XML、JSON,描述同样的信息,ProtoBuf序列化后数据量更小 (在网络中传输消耗的网络流量更少)、序列化/反序列化速度更快、更简单。
一旦定义了要处理的数据的数据结构之后,就可以利用ProtoBuf的代码生成工具生成相关的代码。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言(proto3支持C++, Java, Python, Go, Ruby, Objective-C, C#)或从各种不同流中对你的结构化数据轻松读写。
如何使用 ProtoBuf
ProtoBuf 协议的工作流程
在开发 gRPC 应用程序时,先要定义服务接口,其中应包含如下信息:消费者消费服务的方式、消费者能够远程调用的方法以及调用这些方法所使用的参数和消息格式等。在服务定义中所使用的语言叫作接口定义语言(interface definition language,IDL)。
借助服务定义,可以生成服务器端代码,也就是服务器端骨架 (这里的“骨架”和“存根”都是代理。服务器端代理叫作“骨架”(skeleton),客户端代理叫作“存根”(stub)。),它通过提供低层级的通信抽象简化了服务器端的逻辑。同时,还可以生成客户端代码,也就是客户端存根,它使用抽象简化了客户端的通信,为不同的编程语言隐藏了低层级的通信。就像调用本地函数那样,客户端能够远程调用我们在服务接口定义中所指定的方法。底层的 gRPC 框架处理所有的复杂工作,通常包括确保严格的服务契约、数据序列化、网络通信、认证、访问控制、可观察性等。为了理解 gRPC 的基本概念,我们来看一个使用 gRPC 实现微服务的实际场景。假设我们正在构建一个在线零售应用程序,该应用程序由多个微服务组成。
如图 1-1 所示,假设我们要构建一个微服务来展现在线零售应用程序中可售商品的详情。例如,将 ProductInfo 服务建模为 gRPC 服务,通过网络对外暴露。
服务定义是在 ProductInfo.proto 文件中声明的,服务器端和客户端都会使用该文件来生成代码。这里假设 ProductInfo 服务使用 Go 语言来实现,消费者使用 Java 语言来实现,两者之间的通信则通过 HTTP/2 来进行。
典型的序列化和反序列化过程往往需要如下组件:
● IDL(Interface description language)文件:参与通讯的各方需要对通讯的内容需要做相关的约定(Specifications)。为了建立一个与语言和平台无关的约定,这个约定需要采用与具体开发语言、平台无关的语言来进行描述。这种语言被称为接口描述语言(IDL),采用IDL撰写的协议约定称之为IDL文件。
● IDL Compiler:IDL文件中约定的内容为了在各语言和平台可见,需要有一个编译器,将IDL文件转换成各语言对应的动态库。
● Stub/Skeleton Lib:负责序列化和反序列化的工作代码。Stub是一段部署在分布式系统客户端的代码,一方面接收应用层的参数,并对其序列化后通过底层协议栈发送到服务端,另一方面接收服务端序列化后的结果数据,反序列化后交给客户端应用层;Skeleton部署在服务端,其功能与Stub相反,从传输层接收序列化参数,反序列化后交给服务端应用层,并将应用层的执行结果序列化后最终传送给客户端Stub。
● Client/Server:指的是应用层程序代码,他们面对的是IDL所生存的特定语言的class或struct。
● 底层协议栈和互联网:序列化之后的数据通过底层的传输层、网络层、链路层以及物理层协议转换成数字信号在互联网中传递。
可以看到,对于序列化协议来说,使用方只需要关注业务对象本身,即 idl (Interface description language)定义,序列化和反序列化的代码只需要通过工具生成即可。
ProtoBuf 消息定义
在Java 中,构建一个Person类的数据结构,包含成员变量name、id、email等等
// Java类
public class Person
{
private String name;
private Int id;
private String email;
...
}
根据上述数据结构的需求,在demo.proto里 通过 Protocol Buffer 语法写入对应 .proto对象模型的代码
proto2版本
package protocobuff_Demo;
// 关注1:包名,防止不同 .proto 项目间命名 发生冲突
option java_package = "com.chenj.protobuf"; 作用:指定生成的类应该放在什么Java包名下
option java_outer_classname = "Demo";//作用:生成对应.java 文件的类名(不能跟下面message的类名相同)
// 关注2:option选项,作用:影响 特定环境下 的处理方式
// 关注3:消息模型 作用:真正用于描述 数据结构
// 下面详细说明
// 生成 Person 消息对象(包含多个字段,下面详细说明)
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
message AddressBook {
repeated Person person = 1;
}
proto3版本
syntax = "proto3"; // 协议版本(proto3中,在第一行非空白非注释行,必须写:syntax = "proto3";)
package protocobuff_Demo;
// 关注1:包名,防止不同 .proto 项目间命名 发生冲突
option java_package = "com.chenj.protobuf"; 作用:指定生成的类应该放在什么Java包名下
option java_outer_classname = "Demo";//作用:生成对应.java 文件的类名(不能跟下面message的类名相同)
// 关注2:option选项,作用:影响 特定环境下 的处理方式
// 关注3:消息模型 作用:真正用于描述 数据结构
// 下面详细说明
// 生成 Person 消息对象(包含多个字段,下面详细说明)
message Person {
string name = 1;//(proto3消息定义时,移除了 “required”、 “optional” :)
int32 id = 2;//(proto3消息定义时,移除了 “required”、 “optional” :)
string email = 3;//(proto3消息定义时,移除了 “required”、 “optional” :)
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2 ;//(proto3消息定义时,移除了 default 选项:)
}
repeated PhoneNumber phone = 4;
}
message AddressBook {
repeated Person person = 1;
}
- 消息对象
在 ProtocolBuffers 中:
● 一个消息对象(Message) = 一个 结构化数据
● 消息对象用 修饰符 message 修饰
● 消息对象 含有 字段:消息对象(Message)里的 字段 = 结构化数据 里的成员变量