RPC是什么
RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
一个RPC框架的基本构成:
序列化协议
万物皆字节,我们需要一种途径将万物转化为字节序列在网络传输,这个转化器便是序列化协议,常见如Java原生序列化协议、Thrift、Hession、Json/XML、ProtoBuf。
传输层
目前而言主要是TCP/UDP,对于Java生态而言大多使用NettyAPI来屏蔽底层实现细节
动态代理层
屏蔽业务感知远程调用,等同于一个本地服务调用
1.1 远程过程调用要解决的新问题
1. Call ID映射
在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。还需要在客户端和服务端分别维护一个 {函数 <–> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
2. 序列化和反序列化
客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言。这时候就需要将调用参数序列化成二进制格式进行传输,并在接收端进行反序列化还原成参数对象,这个过程叫序列化和反序列化。
3. 网络传输
远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2。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 调用方式,它基于 google 的 protobuf 定义方式,提供了一整套数据定义和 rpc 传输的方式。但是gRPC有些功能尚不完善,GRPC尚未提供连接池,也尚未提供“服务发现”、“负载均衡”机制。
1.4 gRPC的特点
- 基于标准协议:gRPC使用Google开发的Protocol Buffers作为接口定义语言(IDL),支持多种语言,包括C、C++、Java、Python、Go、Ruby等。Protocol Buffers具有良好的跨语言和跨平台支持,易于扩展和维护。
- 支持多种序列化格式:gRPC支持多种序列化格式,包括二进制、JSON和Protobuf(Protocol Buffers的二进制格式),二进制格式的性能最佳,而Protobuf格式则更为高效。
- 高性能和效率:gRPC使用HTTP/2作为传输协议,支持多路复用、头部压缩、流控等特性,可以减少网络开销,提高性能和效率。
- 支持多种认证和安全机制:gRPC支持多种认证和安全机制,包括SSL/TLS、OAuth2等,可以保障数据的安全性和可靠性。
综上所述,gRPC具有高效、高性能、跨平台、易于扩展和集成等优点,广泛应用于微服务、分布式系统、云计算等领域。
二、proto语法
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
2.1 如何使用ProtoBuf
创建pb文件
第一步,创建一个.proto文件,定义数据结构,下面是一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.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。这必须是文件的第一个非空的非注释行。
- package决定了proto文件内部service的全路径名称。
- java_package决定了proto中message类的生成路径。
- java_outer_classname决定了输出文件的类名。
- message可以理解为一个结构体,转化为java之后为一个类。
- service为被调用的服务,rpc为该服务下的一个方法。
- 所述SearchRequest消息定义了三个字段,对应着需要的三个消息内容。每个字段都有一个名称和类型。
指定字段类型
在上面的示例中,所有字段都是标量类型:两个整数(page_number和result_per_page)和一个字符串(query)。但是,您还可以为字段指定合成类型,包括枚举和其他消息类型。
分配标识号
正如上述文件格式,在消息定义中,每个字段都有唯一的一个数字标识符,通俗的说也就是这个字段所分配的序号。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [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 {
}
gps_data 的名称格式与为生成的java文件的名称是有关系的,如果加入了下划线,则默认生成的是GpsData 这个驼峰格式的名称。当然你也可以在文件里自定义java文件的名称:
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 之间的数字)
字段规则 类型 名称 = 字段编号;
}
在上例中,我们定义了:
- 类型 string,名为 stringVal 的 optional 可选字段,字段编号为 1,此字段可出现 0 或 1 次
- 类型 bytes,名为 bytesVal 的 optional 可选字段,字段编号为 2,此字段可出现 0 或 1 次
- 类型 EmbeddedMessage(自定义的内嵌 message 类型),名为 embeddedExample1 的 optional 可选字段,字段编号为 3,此字段可出现 0 或 1 次
- 类型 int32,名为 repeatedInt32Val 的 repeated 可重复字段,字段编号为 4,此字段可出现 任意多次(包括 0)
- 类型 string,名为 repeatedStringVal 的 repeated 可重复字段,字段编号为 5,此字段可出现 任意多次(包括 0)
还可以指定改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文件时依赖这个路径,形如 import “ks/demo/demo.proto”。这样就可以使用该文件内部定义的所有message了
- proto文件内部package指定路径:com.ks.infra.grpc.test;这个路径非常的核心,任何迁移或者修改proto文件的操作都不应该修改这个package路径,因为它决定了文件内部service的生成路径。一旦发生变化就会造成在实际gRPC调用时找不到服务方法而抛异常(Unimplemented)
- option参数指定java输出路径:com.ks.demo;这个路径也比较核心,定义好之后也不要轻易修改,因为它决定了proto中message类的生成路径,如果修改会造成源码不兼容编译阻塞
2.3 proto文件输出样式
proto文件输出样式由如下几个因素决定:
- proto文件的名称,例如infra_demo.proto;在没指定2的前提下,会默认按照驼峰生成OuterClass,对于本例为InfraDemo.java;有意思的是当proto文件中的message跟默认生成java类名重名的时候,会在生成类名后面加上OuterClass,对于本例为InfraDemoOuterClass
- option java_outer_classname = "Demo"如果指定此选项,则输出文件名称为指定名称Demo
- option java_multiple_files = true:指定此选项proto文件中的message会生成独立的java文件,此选项定义好后也不要轻易修改,否则也会造成编译阻塞,推荐开启这个选项
2.4 protoc 编译 .proto 文件会生成什么
当你使用protoc 来编译一个.proto文件的时候,编译器将利用你在文件中定义的类型生成你打算使用的语言的代码文件。生成的代码包括getting setting 接口和序列化,反序列化接口。
- 对于C ++,编译器会从每个.proto文件生成一个.h和一个.cc文件,并为您文件中描述的每种消息类型提供一个类。
- 对于Java,编译器生成一个.java文件,其中包含每种消息类型的类,以及Builder用于创建消息类实例的特殊类。
我们在 .proto 文件中定义了数据结构,这些数据结构是面向开发者和业务程序的,并不面向存储和传输。\
当需要把这些数据进行存储或传输时,就需要将这些结构数据进行序列化、反序列化以及读写。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