【rpc通信框架】之rpc协议的实现和原理知识概括
一、rpc概念
RPC 的全称是 Remote Procedure Call,即远程过程调用。
RPC 是帮助我们屏蔽网络编程细节,实现调用远程方法就跟调用本地(同一个项目中的方法)一样的体验。
RPC的价值:
- 屏蔽远程调用跟本地调用的区别,让我们感觉就是调用项目内的方法;
- 隐藏底层网络通信的复杂性,让我们更专注于业务逻辑
二、rpc通信流程
三、rpc协议
1、什么是协议
- RPC 请求在发送到网络中之前,他需要把方法调用的请求参数转成二进制;转成二进制后,写入本地 Socket 中,然后被网卡发送到网络设备中。
- tcp通信会出现粘包,拆包问题。(调用方发送 AB、CD、EF 3 个消息,如果没有边界的话,接收端就可能收到 ABCDEF 或者 ABC、DEF 这样的消息,会导致接收的语义跟发送的时候不一致了)
- 为了避免语义不一致的事情发生,我们就需要在发送请求的时候设定一个边界,然后在收到请求的时候按照这个设定的边界进行数据分割。这个边界语义的表达,就是我们所说的协议。
2、一种支持可扩展的协议设计格式
服务提供方收到一个过期请求,这个过期是说服务提供方收到的这个请求的时间大于调用方发送的时间和配置的超时时间,既然已经过期,就没有必要接着处理,直接返回一个超时就好了。那要实现这个功能,就要在协议里面传递这个配置的超时时间,那如果之前协议里面没有加超时时间参数的话,我们现在把这个超时时间加到协议体里面是不是就有点重了呢?显然,会加重 CPU 的消耗。
整体协议就变成了三部分内容:固定部分、协议头内容、协议体内容,前两部分我们还是可以统称为“协议头”,具体协议如下:
- 设计一个简单的 RPC 协议并不难,难的就是怎么去设计一个可“升级”的协议
- 设计可扩展的、向后兼容的协议,其关键点就是利用好 Header 中的扩展字段
四、序列化
1、网络传输数据,必须是二进制数据,分为序列化,反序列化
2、常见的序列化框架
- JDK 原生序列化
- JSON
- Hessian
- Protobuf
(1)序列化框架
实际上任何一种序列化框架,核心思想就是设计一种序列化协议,将对象的类型、属性类型、属性值一一按照固定的格式写到二进制字节流中来完成序列化,再按照固定的格式一一读出对象的类型、属性类型、属性值,通过这些信息重新创建出一个新的对象,来完成反序列化。
(2)JDK 原生序列化
import java.io.*; public class Student implements Serializable { //学号 private int no; // 姓名 private String name; public int getNo() { return no; } public void setNo(int no) { this.no = no; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Student{" + "no=" + no + ", name='" + name + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { String home = System.getProperty("user.home"); String basePath = home + "/Desktop"; FileOutputStream fos = new FileOutputStream(basePath + "student.dat"); Student student = new Student(); student.setNo(100); student.setName("TEST_STUDENT"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(student); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream(basePath + "student.dat"); ObjectInputStream ois = new ObjectInputStream(fis); Student deStudent = (Student) ois.readObject(); ois.close(); System.out.println(deStudent); } }
(3)JSON
JSON 可能是我们最熟悉的一种序列化格式了,JSON 是典型的 Key-Value 方式,没有数据类型,是一种文本型序列化框架。
他在应用上还是很广泛的,无论是前台 Web 用 Ajax 调用、用磁盘存储文本类型的数据,还是基于 HTTP 协议的 RPC 框架通信,都会选择 JSON 格式。
JSON 进行序列化有这样两个问题,你需要格外注意:
- JSON 进行序列化的额外空间开销比较大,对于大数据量服务这意味着需要巨大的内存和磁盘开销;
- JSON 没有类型,但像 Java 这种强类型语言,需要通过反射统一解决,所以性能不会太好。
(4)Hessian
Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节数也更小。
public static void main(String[] args) throws IOException, ClassNotFoundException { Student student = new Student(); student.setNo(101); student.setName("HESSIAN"); //把student对象转化为byte数组 ByteArrayOutputStream bos = new ByteArrayOutputStream(); Hessian2Output output = new Hessian2Output(bos); output.writeObject(student); output.flushBuffer(); byte[] data = bos.toByteArray(); bos.close(); //把刚才序列化出来的byte数组转化为student对象 ByteArrayInputStream bis = new ByteArrayInputStream(data); Hessian2Input input = new Hessian2Input(bis); Student deStudent = (Student) input.readObject(); input.close(); System.out.println(deStudent); }
Hessian 本身也有问题,官方版本对 Java 里面一些常见对象的类型不支持,比如:
- Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展 CollectionDeserializer 类修复;
- Locale 类,可以通过扩展 ContextSerializerFactory 类修复;
- Byte/Short 反序列化的时候变成 Integer。
(5)Protobuf
Protobuf 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。
Protobuf 使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL 编译器,生成序列化工具类,它的优点是:
- 序列化后体积相比 JSON、Hessian 小很多;
- IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器;
- 序列化反序列化速度很快,不需要通过反射获取类型;
- 消息格式升级和兼容性不错,可以做到向后兼容。
Protobuf 非常高效,但是对于具有反射和动态能力的语言来说,这样用起来很费劲,这一点就不如 Hessian。
比如用 Java 的话,这个预编译过程不是必须的,可以考虑使用 Protostuff。
Protostuff 不需要依赖 IDL 文件,可以直接对 Java 领域对象进行反 / 序列化操作,在效率上跟 Protobuf 差不多,生成的二进制格式和 Protobuf 是完全相同的,可以说是一个 Java 版本的 Protobuf 序列化框架。
但在使用过程中,我遇到过一些不支持的情况,也同步给你:
- 不支持 null;
- ProtoStuff 不支持单纯的 Map、List 集合对象,需要包在对象里面。
3、rpc框架序列化框架选型的原则
- 我们首选的还是 Hessian 与 Protobuf,因为他们在性能、时间开销、空间开销、通用性、兼容性和安全性上,都满足了我们的要求。
- 其中 Hessian 在使用上更加方便,在对象的兼容性上更好;
- Protobuf 则更加高效,通用性上更有优势
关于rpc框架使用注意点
- 对象要尽量简单,没有太多的依赖关系,属性不要太多,尽量高内聚;
- 入参对象与返回值对象体积不要太大,更不要传太大的集合;
- 尽量使用简单的、常用的、开发语言原生的对象,尤其是集合类;
- 对象不要有复杂的继承关系,最好不要有父子类的情况。