RPC通信协议

 协议大家平时都会遇到,只是没有特别注意。
 像平时大家阅读文章的时候都是从上到下、从左往右 按行阅读,这可以看做一种阅读(通信)协议。 ( 备注: 古人在竹简上写的文字则是从上到下、从右往左 按列阅读。)
 阅读作文时第一行是标题,段首要空两格的是一个自然段。遇到一个句号是一句话。
这个就是一种阅读时的通信协议

在计算机远程方法调用时,传输的都是二进制的01,调用方(写数据)和被调用方(读数据)怎么约定通信协议的?

(1) 协议是什么

协议的作用就类似于文字中的符号,作为应用拆解请求消息的边界,保证二进制数据经过网络传输后,还能被正确地还原语义。

那么,服务端收到二进制数据后怎么根据协议解析出数据呢?

假如让你设计,你怎么设计一个协议

(1.1) 协议和序列化的区别

序列化后的二进制数据是协议的子集

RPC其实是把拦截到的方法参数,转成可以在网络中传输的二进制,并保证在服务提供方能正确地还原出语义,最终实现像调用本地一样地调用远程的目的。

那么客户端、服务端如何在二进制流里区分出想要的数据?


(2) 传输协议的作用(职责)

首先要有协议长度,比如用int类型表示,放在二进制数据的前4个字节
需要标识是什么协议
协议可能会有多个版本,需要表示协议版本,用byte类型表示
需要消息Id,用来标识唯一,调用方根据消息Id来区分同一个方法的不同请求,用int类型标识,占4字节
需要消息类型,用来标识是 调用、响应、异常,用byte类型标识,占1字节
需要序列化方式 用byte类型,占用1字节
还需要预留协议扩展字段,不定长,约定协议扩展字段的前2个字节标识扩展字段的长度
协议头长度,除协议体的部分的长度,用2字节表示
剩下的是协议体内容,存放序列化后的二进制数据 (协议体的长度可以根据协议长度-协议头长度获得)

问题一:如何规定远程调用的语法?

客户端如何告诉服务端,我是一个加法,而另一个是乘法。 是用字符串“add”传给你,还是传给你一个整数,比如 1 表示加法,2 表示乘法? (方法描述)
服务端该如何告诉客户端,这个加法,目前只能加整数,不能加小数,不能加字符串; (类型描述)
而另一个加法“add1”,它能实现小数和整数的混合加法。 (类型区分)
返回值是什么?正确的时候返回什么,错误的时候又返回什么? (返回值描述)

问题二:如果传递参数?

是先传两个整数,后传一个操作符“add”,还是先传操作符,再传两个整数?
如果都是 UDP,想要实现一个逆波兰表达式,放在一个报文里面还好,如果是 TCP,是一个流,在这个流里面,如何将两次调用进行分界?什么时候是头,什么时候是尾?
把这次的参数和上次的参数混了起来,TCP 一端发送出去的数据,另外一端不一定能一下子全部读取出来。所以,怎么才算读完呢?

问题三:如何表示数据?

1、如果是变长的类型,是一个结构体,甚至是一个类,应该怎么处理呢?
2、如果是 int,不同的平台上长度也不同,该怎么处理呢?
3、在网络上传输超过一个 Byte 的类型,还有大端 Big Endian 和小端 Little Endian 的问题。假设我们要在 32 位四个 Byte 的一个空间存放整数 1,很显然只要一个 Byte 放 1,其他三个 Byte 放 0 就可以了。那问题是,最后一个 Byte 放 1 呢,还是第一个 Byte 放 1 呢?或者说 1 作为最低位,应该是放在 32 位的最后一个位置呢,还是放在第一个位置呢?最低位放在最后一个位置,叫作 Little Endian,最低位放在第一个位置,叫作 Big Endian。
TCP/IP 协议栈是按照 Big Endian 来设计的,而 X86 机器多按照 Little Endian 来设计的,因而发出去的时候需要做一个转换。

问题四:如何知道一个服务端都实现了哪些远程调用?

从哪个端口可以访问这个远程调用?
假设服务端实现了多个远程调用,每个可能实现在不同的进程中,监听的端口也不一样,而且由于服务端都是自己实现的,不可能使用一个大家都公认的端口,而且有可能多个进程部署在一台机器上,大家需要抢占端口,为了防止冲突,往往使用随机端口,那客户端如何找到这些监听的端口呢?

问题五:发生了错误、重传、丢包、性能等问题怎么办?

本地调用没有这个问题,但是一旦到网络上,这些问题都需要处理,因为网络是不可靠的,虽然在同一个连接中,我们还可通过 TCP 协议保证丢包、重传的问题,但是如果服务器崩溃了又重启,当前连接断开了,TCP 就保证不了了,需要应用自己进行重新调用,重新传输会不会同样的操作做两遍,远程调用性能会不会受影响呢?

协议体里面的内容都是经过序列化出来的,也就是说你要获取到你参数的值,就必须把整个协议体里面的数据经过反序列化出来。但在某些场景下,这样做的代价有点高啊!

(3) 协议核心要素

XID 唯一标识一对请求和回复。请求为 0,回复为 1。
RPC 有版本号,两端要匹配 RPC 协议的版本号。如果不匹配,就会返回 Deny,原因就是 RPC_MISMATCH。
程序有编号。如果服务端找不到这个程序,就会返回 PROG_UNAVAIL。
程序有版本号。如果程序的版本号不匹配,就会返回 PROG_MISMATCH。
一个程序可以有多个方法,方法也有编号,如果找不到方法,就会返回 PROC_UNAVAIL。
调用需要认证鉴权,如果不通过,则 Deny。
参数列表,如果参数无法解析,则返回 GABAGE_ARGS。

协议长度 RPC每次发请求发的大小都是不固定的,所以我们的协议必须能让接收方正确地读出不定长的内容。
序列化方式
协议标示
消息 ID
消息类型

断句,双工通信,配合专用的序列化方法,可以实现一套高性能的网络通信协议。

参考资料

[1] 趣谈网络协议 - 第32讲 | RPC协议综述:远在天边,近在眼前
[2] RPC 实战与核心原理 - 02 | 协议:怎么设计可扩展且向后兼容的协议?
[3] 消息队列高手课 - 13 | 传输协议:应用程序之间对话的语言