背景
在RPC大行其道的时候,很多人并不是很懂该如何设计一个RPC协议?本文将会深入浅出地讲一下设计过程。
一、RPC是什么?
RPC全称Remote Procedure Call,简单理解字面意思,即跨机器访问应用程序,结合HTTP简单通俗讲讲。
首先,它们都属于应用层协议,这里不展开讲网络模型,如果有需要评论区留言,后续单开一讲。其次,RPC没有行业标准,百花争艳,而HTTP有官方定义的一套标准。最后,大部分RPC是通过TCP协议实现的,当然也有个例,例如GRPC采用HTTP2。
二、为什么需要协议?
众所周知,网络中传输的二进制,所以任何请求包括RPC请求,它们在发送之前都需要转换成二进制,写入Socket,最终通过网卡发送至网络设备中。
发送的数据往往在传输的过程中会被TCP切分或者合并,那么接受方如何知道你一个完成的请求是哪块数据?聪明的你很快会想到,每次发送的时候告诉对方请求数据的结束位置不就好了。
这个大家都能想到的发送请求的时候设置结束位置(消息边界),接收方按照结束位置进行数据处理做法,其实就是协议。
三、如何设计RPC协议?
基础V1.0
协议本身是用来规定请求的内容的,这点大家应该没有异议。
前面我们说过消息边界,由于每个请求的大小都是不固定的,为了接收方能够正确读出请求,可以在协议头固定长度的存储请求的大小,这样在接收方收到数据时,根据协议头所存储的请求大小值,读取请求协议体的数据。
协议头 | 协议体 |
请求的数据是可以正常的获取到了,这时接收方拿到的是协议体的二进制数据,它不知道调用方具体序列化的方式是哪种,也就无法知道消息的真正含义。
基础V2.0
就如上节所说的,接收方需要知道请求的序列化方式,除此之外一般的协议中还会存放消息ID、协议版本、消息类型等固定长度存放的参数,这部分数据我们统称协议头;协议体则存放具体的请求内容,长度不固定。到这里,一个功能完善的RPC协议也就出来了。
length | message id | version | type | serialization | body |
0-9 | 10-14 | 15-19 | 20-24 | 25-29 | ... |
前0-9位属于请求长度、10-14位属于消息ID,以此类推body为不定长的协议体,当然这个不定长受到length的max value限制。
可扩展V3.0
上节说到的协议属于定长协议头,不支持新加参数,强行添加会造成线上兼容问题。例如新参数加在协议头末尾,接收方只会把新参数当成协议体的一部分,导致出错,这点相信大家也不难理解。
一般的操作是会把新参数加在协议体,但是一旦放入协议体中,那么就会涉及序列化。如果参数本身不和请求内容含义相关联,例如是一个请求超时时间,在这个场景下调用方设置的超时时间在被接收方收到后,如果在协议头就无需反序列化协议体得到超时时间直接返回给调用方,降低了CPU开销。
所以一个可扩展的协议,应该是包含三部分协议头固定长度、协议头扩展长度、协议体。
body-length | message id | version | type | serialization | head-length | 扩展字段 | body |
0-9 | 10-14 | 15-19 | 20-24 | 25-29 | 30-24 | ... | ... |
小结
设计一个完整的RPC协议并不复杂,难的是如何在迭代的过程中,新加特性之后还能够做到版本的向下兼容,这时候一个协议需要同时支持协议头和协议体的可扩展性显得尤为重要。
一个架构设计良好的系统往往是扩展性良好的,支持新特性的插装。