深刻理解 RPC 消息协议设计

本节咱们开始讲解 RPC 的消息协议设计背后的基本原理,了解 RPC 的协议开发背后有哪些须要考虑的基本点。在通晓原理以后,咱们就能够本身设计一套协议来开发属于本身的 RPC 系统。算法

本节主要涉及的知识点和它们之见的关系以下图:json

对于一串消息流,咱们必须能肯定消息边界,提取出单条消息的字节流片断,而后对这个片断按照必定的规则进行反序列化来生成相应的消息对象。数组

消息表示指的是序列化后的消息字节流在直观上的表现形式,它看起来是对人类友好仍是对计算机友好。文本形式对人类友好,二进制形式对计算机友好。性能优化

每一个消息都有其内部字段结构,结构构成了消息内部的逻辑规则,程序要按照结构规则来决定字段序列化的顺序。服务器

接下来,咱们初步详细拆解。网络

消息边界架构

RPC 须要在一条 TCP 连接上进行屡次消息传递。在连续的两条消息之间必须有明确的分割规则,以便接收端能够将消息分割开来,这里的接收端能够是 RPC 服务器接收请求,也能够是 RPC 客户端接收响应。并发

基于 TCP 连接之上的单条消息若是过大,就会被网络协议栈拆分为多个数据包进行传送。若是消息太小,网络协议栈可能会将多个消息组合成一个数据包进行发送。对于接收端来讲它看到的只是一串串的字节数组,若是没有明确的消息边界规则,接收端是无从知道这一串字节数组到底是包含多条消息仍是只是某条消息的一部分。app

比较经常使用的两种分割方式是特殊分割符法和长度前缀法。分布式

消息发送端在每条消息的末尾追加一个特殊的分割符,而且保证消息中间的数据不能包含特殊分割符。好比最为常见的分割符是 。当接收端遍历字节数组时发现了 ,就当即能够判定 以前的字节数组是一条完整的消息,能够传递到上层逻辑继续进行处理。HTTP 和 Redis 协议就大量使用了 分割符。此种消息通常要求消息体的内容是文本消息。

消息发送端在每条消息的开头增长一个 4 字节长度的整数值,标记消息体的长度。这样消息接受者首先读取到长度信息,而后再读取相应长度的字节数组就能够将一个完整的消息分离出来。此种消息比较经常使用于二进制消息。

基于特殊分割符法的优势在于消息的可读性比较强,能够直接看到消息的文本内容,缺点是不适合传递二进制消息,由于二进制的字节数组里面很容易就冒出连续的两个字节内容正好就是 分割符的 ascii 值。若是须要传递的话,通常是对二进制进行 base64 编码转变成普通文本消息再进行传送。

基于长度前缀法的优势和缺点同特殊分割符法正好是相反的。长度前缀法由于适用于二进制协议,因此可读性不好。可是对传递的内容自己没有特殊限制,文本和内容皆能够传输,不须要进行特殊处理。HTTP 协议的 Content-Length 头信息用来标记消息体的长度,这个也能够当作是长度前缀法的一种应用。

HTTP 协议是一种基于特殊分割符和长度前缀法的混合型协议。好比 HTTP 的消息头采用的是纯文本外加 分割符,而消息体则是经过消息头中的 Content-Type 的值来决定长度。HTTP 协议虽然被称之为文本传输协议,可是也能够在消息体中传输二进制数据数据的,例如音视频图像,因此 HTTP 协议被称之为「超文本」传输协议。

消息的结构

每条消息都有它包含的语义结构信息,有些消息协议的结构信息是显式的,还有些是隐式的。好比 json 消息,它的结构就能够直接经过它的内容体现出来,因此它是一种显式结构的消息协议。

json 这种直观的消息协议的可读性很是棒,可是它的缺点也很明显,有太多的冗余信息。好比每一个字符串都使用双引号来界定边界,key/value 之间必须有冒号分割,对象之间必须使用大括号分割等等。这些还只是冗余的小头,最大的冗余还在于连续的多条 json 消息即便结构彻底同样,仅仅只是 value 的值不同,也须要发送一样的 key 字符串信息。

消息的结构在同一条消息通道上是能够复用的,好比在创建连接的开始 RPC 客户端和服务器之间先交流协商一下消息的结构,后续发送消息时只须要发送一系列消息的 value 值,接收端会自动将 value 值和相应位置的 key 关联起来,造成一个完成的结构消息。在 Hadoop 系统中普遍使用的 avro 消息协议就是经过这种方式实现的,在 RPC 连接创建之处就开始交流消息的结构,后续消息的传递就能够节省不少流量。

消息的隐式结构通常是指那些结构信息由代码来约定的消息协议,在 RPC 交互的消息数据中只是纯粹的二进制数据,由代码来肯定相应位置的二进制是属于哪一个字段。好比下面的这段代码

若是纯粹看消息内容是没法知道节点消息内容中的哪些字节的含义,它的消息结构是经过代码的结构顺序来肯定的。这种隐式的消息的优势就在于节省传输流量,它彻底不须要传输结构信息。

消息压缩

若是消息的内容太大,就要考虑对消息进行压缩处理,这能够减轻网络带宽压力。可是这同时也会加剧 CPU 的负担,由于压缩算法是 CPU 计算密集型操做,会致使操做系统的负载加剧。因此,最终是否进行消息压缩,必定要根据业务状况加以权衡。

若是肯定压缩,那么在选择压缩算法包时,务必挑选那些底层用 C 语言实现的算法库,由于 Python 的字节码执行起来太慢了。比较流行的消息压缩算法有 Google 的 snappy 算法,它的运行性能很是好,压缩比例虽然不是最优的,可是离最优的差距已经不是很大。阿里的 SOFA RPC 就使用了 snappy 做为协议层压缩算法。

流量的极致优化

开源的流行 RPC 消息协议每每对消息流量优化到了极致,它们经过这种方式来打动用户,吸引用户来使用它们。好比对于一个整形数字,通常使用 4 个字节来表示一个整数值。

可是通过研究发现,消息传递中大部分使用的整数值都是很小的非负整数,若是所有使用 4 个字节来表示一个整数会很浪费。因此就发明了一个类型叫变长整数varint。数值很是小时,只须要使用一个字节来存储,数值稍微大一点可使用 2 个字节,再大一点就是 3 个字节,它还能够超过 4 个字节用来表达长整形数字。

其原理也很简单,就是保留每一个字节的最高位的 bit 来标识是否后面还有字节,1 表示还有字节须要继续读,0 表示到读到当前字节就结束。

那若是是负数该怎么办呢?-1 的 16 进制数是 0xFFFFFFFF,若是要按照这个编码那岂不是要 6 个字节才能存的下。-1 也是很是常见的整数啊。

因而 zigzag 编码来了,专门用来解决负数问题。zigzag 编码将整数范围一一映射到天然数范围,而后再进行 varint 编码。

zigzag 将负数编码成正奇数,正数编码成偶数。解码的时候遇到偶数直接除 2 就是原值,遇到奇数就加 1 除 2 再取负就是原值。

小结

如今咱们知道了 RPC 消息结构的设计原理,遵循这些基本方法,就能够创造出一个又一个不一样的消息协议。

在此我向你们推荐一个Java高级群 :725633148 里面会分享一些资深架构师录制的视频录像:(有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构)等这些成为架构师必备的知识体系 进群立刻免费领取,目前受益良多!

相关文章
相关标签/搜索