day16:RPC分层设计| 青训营

160 阅读5分钟

二、RPC分层设计

1. RPC框架主要核心有三层:编解码层、协议层和网络通信层

image.png

3.二进制编解码的实现原理和选型要点

3.1实现原理

TLV编码

  • Tag:标签,可以理解为类型
  • Lenght:长度
  • Value:值,Value也可以是个TLV结构
image.png image.png

3.2选型要点

A.通用性:

  • 第一、技术层面,序列化协议是否支持跨平台、跨语言。如果不支持,在技术层面上的通用性就大大降低了。
  • 第二、流行程度,序列化和反序列化需要多方参与,很少人使用的协议往往意味着昂贵的学习成本;另一方面, 流行度低的协议,往往缺乏稳定而成熟的跨语言、跨平台的公共包。

B.兼容性:

  • 移动互联时代,业务系统需求的更新周期变得更快,新的需求不断涌现,而老的系统还是需要继续维护。如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。

C.性能:

  • 第一、空间开销(Verbosity) ,序列化需要在原有的数据 上加上描述字段,以为反序列化解析之用。如果序列化过程引入的额外开销过高,可能会导致过大的网络,磁盘等各方面的压力。对于海量分布式存储系统,数据量往往以TB为单位,巨大的的额外空间开销意味着高昂的成本。
  • 第二、时间开销(Complexity) ,复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈。

4.协议的一般构造,以及框架协议解析的基本流程

  • 协议是双方确定的交流语义,比如:我们设计一个字符串传输的协议,它允许客户端发送一个字符串,服务端接收到对应的字符串。这个协议很简单,首先发送一个4字节的消息总长度,然后再发送1字节的字符集charset长度,接下来就是消息的payload,字符集名称和字符串。
  • 特殊结束符:过于简单,对于一个协议单元必须要全部读入才能够进行处理,除此之外必须要防止用户传输的数据不能同结束符相同,否则就会出现紊乱。
    image.png
  • HTTP协议头就是以回车(CR)加换行(LF)符号序列结尾。
  • 变长协议:一般都是自定义协议, 有header和payload组成,会以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度,使用比较广泛。
    image.png
4.1协议构造

LENGTH字段:32bits,包括数据包剩余部分的字节大小,不包含LENGTH自身长度。
HEADER MAGIC字段:16bits,值为0x1000,用于标识协议版本信息,协议解析的时候可以快速校验。
FLAGS字段:16bits,为预留字段,暂未使用,默认值为0x0000
SEQUENCE NUMBER字段:32bits,表示数据包的seqld,可用于多路复用,最好确保单个连接内递增。
HEADER SIZE字段:16bits,等于头部长度字节数/4,头部长度计算从第14个字节开始计算,一直到PAYLOAD前(备注: header 的最大长度为64K)。
PROTOCOL ID字段:uint8编码,取值有Binary和Compact两种。
TRANSFORM ID:压缩方式,如zlib和snappy。
INFO ID:传递一些定制的meta信息。
PAYLOAD:消息体。
image.png

4.2协议解析的一般流程

image.png 先解析MagicNumber采用哪个协议版本,再用协议解析出Payload。

5. Socket API的调用流程

image.png

  • 套接字编程中的客户端必须知道两个信息:服务器的IP地址,以及端口号。
  • socket函数创建一个套接字,bind将一个套接字绑定到一个地址上。listen监听进来的连接,backlog的含义有点复杂,这里先简单的描述:指定挂起的连接队列的长度,当客户端连接的时候,服务器可能正在处理其他逻辑而未调用accept接受连接,此时会导致这个连接被挂起,内核维护鼓起的连接队列,backlog则指定这个队列的长度,accept函数从队列中取出出连接请求并接收它,然后这个连接就从挂起队列移除。如果队列末满,客户端调用connect马上成功,如果满了可能会阻塞等待队列末满(实际上在Linux中测试并不是这样的结果,这后面再专门来研究)。Linux的backlog默认是128,通常情况下,我们也指定为128即可。
  • connect客户端向服务器发起连接,accept 接收一个连接请求,如果没有连接则会直阻塞直到有连接进来。得到客户端的fd之后,就可以调用read,write函数和客户端通讯,读写方式和其他I/0类似
  • read从fd读数据,socket默认是阻塞模式的,如果对方没有写数据,read会一直阻塞着: write写fd写数据,socket默认是阻塞模式的, 如果对方没有写数据,write会一 直阻塞着 :
  • socket关闭套接字,当另-端socket关闭后,这一端读写的情况: 尝试去读会得到一个EOF,并返回0。 尝试去写会触发一个SIGPIPE信号,并返回-1和errno=EPIPE, SIGPIPE的默认行为是终止程序,所以通常我们应该忽略这个信号,避免程序终止。 如果这一端不去读写,我们可能没有办法知道对端的socket关闭了。