• RPC:设计可扩展且向后兼容的协议


    协议:怎么设计可扩展且向后兼容的协议?

    浏览器收到命令后会封装一个请求,并把请求发送到 DNS 解析出来的 IP 上,通过抓包工具我们可以抓到请求的数据包,如下图所示:

    协议的作用

    RPC 请求在发送到网络中之前,他需要把方法调用的请求参数转成二进制;转成二进制后,写入本地 Socket 中,然后被网卡发送到网络设备中。
    但在传输过程中,RPC 并不会把请求参数的所有二进制数据整体一下子发送到对端机器上,中间可能会拆分成好几个数据包,也可能会合并其他请求的数据包(合并的前提是同一个 TCP 连接上的数据),至于怎么拆分合并,这其中的细节会涉及到系统参数配置和 TCP 窗口大小。对于服务提供方应用来说,他会从 TCP 通道里面收到很多的二进制数据,那这时候怎么识别出哪些二进制是第一个请求的呢?

    为了避免语义不一致的事情发生,我们就需要在发送请求的时候设定一个边界,然后在收到请求的时候按照这个设定的边界进行数据分割。这个边界语义的表达,就是我们所说的协议。

    如何设计协议

    从 RPC 的作用说起,相对于 HTTP 的用处,RPC 更多的是负责应用间的通信,所以性能要求相对更高。但** HTTP 协议的数据包大小相对请求数据本身要大很多,又需要加入很多无用的内容,比如换行符号、回车符等;还有一个更重要的原因是,HTTP 协议属于无状态协议**,客户端无法对请求和响应进行关联,每次请求都需要重新建立连接,响应完成后再关闭连接。因此,对于要求高性能的 RPC 来说,HTTP 协议基本很难满足需求,所以 RPC 会选择设计更紧凑的私有协议。

    那怎么设计一个私有 RPC 协议呢?

    RPC 每次发请求发的大小都是不固定的,所以我们的协议必须能让接收方正确地读出不定长的内容。
    先设计一个固定长度的:先固定一个长度(比如 4 个字节)用来保存整个请求数据大小,读取固定长度的位置里面的值,值的大小就代表协议体的长度,接着再根据值的大小来读取协议体的数据,整个协议可以设计成这样:
    不定长协议:

    但这种协议只是实现了正确的断句效果,在RPC中是行不通的,因为服务提供方并不知道它的序列化方式是什么,也就不能将二进制数据还原成对象。因此,需要将序列化方式单独拿出来,类似协议长度一样用固定的长度存放,这些需要固定长度存放的参数统称为“协议头”,整个协议就被拆分为:协议头协议体两部分。

    在协议头里面,我们除了会放协议长度、序列化方式,还会放一些像协议标示、消息 ID、消息类型这样的参数,而协议体一般只放请求接口方法、请求的业务参数值和一些扩展属性。协议头是由一堆固定的长度参数组成,而协议体是根据请求接口和参数构造的,长度属于可变的,具体协议如下图所示:
    定长协议:

    1:Bit Offset——标识协议的其实位置
    2:魔术位——标识是什么协议
    3:整体长度——标识整个协议有多长,减去协议头长度就是协议体长度
    4:头长度——标识协议头的长度,因为头是可扩展的,所以具体长度不固定,需要标识一下
    5:协议版本——标识当前协议的版本,用于协议兼容性控制
    6:消息类型——标识消息的类型,对于文本的需要,这里也需要嘛?协议类型可能是对象?可能是XML文件?可能是JSON码?正常来讲应该都是对象才对,让用于反序列化,猜测是为了扩展预留的
    7:序列化方式——用于消息的序列化和反序列化
    8:消息ID——用于表示请求和响应的关系
    9:协议头扩展字段——用于扩展协议头,是协议具有扩展性,更加的灵活可控
    10协议体——协议的内容,一堆堆的二进制数据,双方沟通的东西

    协议头——规定信息转换的规则
    协议体——信息真正的内容,由于在传输层对人不友好对应用程序也不友好需要转换一下

    可扩展的协议

    上面的协议为定长协议,那如果如果想在协议体重放一些扩展属性怎么办?
    协议体里面是可以加新的参数,但这里有一个关键点,就是协议体里面的内容都是经过序列化出来的,也就是说你要获取到你参数的值,就必须把整个协议体里面的数据经过反序列化出来。但在某些场景下,这样做的代价有点高啊!

    所以为了保证能平滑地升级改造前后的协议,我们有必要设计一种支持可扩展的协议。其关键在于让协议头支持可扩展,扩展后协议头的长度就不能定长了。
    那要实现读取不定长的协议头里面的内容,在这之前肯定需要一个固定的地方读取长度,所以需要一个固定的写入协议头的长度。整体协议就变成了三部分内容:固定部分、协议头内容、协议体内容,前两部分我们还是可以统称为“协议头”,具体协议如下:
    可扩展协议:

    设计一个简单的 RPC 协议并不难,难的就是怎么去设计一个可“升级”的协议。不仅要让我们在扩展新特性的时候能做到向下兼容,而且要尽可能地减少资源损耗,所以我们协议的结构不仅要支持协议体的扩展,还要做到协议头也能扩展。

    PS:
    RPC 不直接用 HTTP 协议的一个原因是无法实现请求跟响应关联,每次请求都需要重新建立连接,响应完成后再关闭连接,所以我们要设计私有协议。那么在 RPC 里面,我们是怎么实现请求跟响应关联的呢?

    一般RPC为了性能,会采用异步通信的方式,请求响应对应关联,就需要一个类似身份证号的ID,消息ID

  • 相关阅读:
    162 基于UDP协议的socket套接字编程
    161 解决粘包问题
    160 粘包问题
    159 模拟ssh远程执行命令
    158 Socket抽象层
    157 基于TCP协议的socket套接字编程
    C++:查找字符串字串并替换
    C++:查找字符串字串并替换
    请问c++中的#include "stdafx.h"是什么意思?
    请问c++中的#include "stdafx.h"是什么意思?
  • 原文地址:https://www.cnblogs.com/whiteBear/p/15857855.html
Copyright © 2020-2023  润新知