RPC -| 青训营

319 阅读5分钟

分层设计

以Apache Thrift这个轻量级远程调用框架为例 image (1).png

自顶向下的各个分层

  1. 编解码层(紫+绿)

    编解码层作用就是客户端和服务端通过一个IDL文件生成不同语言 image (2).png

    其中IDL数据格式有以下三种

    1. 特定语言的格式:
      这种编码方便,可以用很少的额外代码实现内存对象的保存与恢复。但是与特定语言深度绑定,其他语言很难读取。所以存储或传输数据就和这个语言绑定了,易引发安全和兼容性问题。

    2. 文本格式:

      这种具有人类可读性,但是数字的编码有很多歧义之处。如:XML和CSV不能区分数字和字符串;JSON虽能区分,但是不区分整数和浮点数,而且不能指定精度。在处理大量数据时没有强制模型约束,实际上只能采用文档方式约定,从而带来调试不便,而且JSON的序列化和反序列化采用反射机制导致性能较差。

    3. 二进制编码:

      具备跨语言高性能优点,常见有Thrift框架的BinaryProtocol,Protobuf...(其中有TLV编码或Varint编码实现)

1692197310427.png BinaryProtocol用的就是TLV编码,左下角为其IDL文件。 TLV编码结构简单清晰,扩展性好,但是由于增加了Type和Length两个冗余信息,有额外的内存开销,特别是在大部分字段都是基本类型的情况下有较大的空间浪费得。

该怎么选择

  • 兼容性

    • 由于系统更新周期快,需要支持自动增加新的字段,而且不影响老的服务,这将提高系统的灵活性
  • 通用性

    • 技术层面

      序列化协议是否支持跨平台、跨语言

    • 流行程度

      序列化和反序列化需要多方参与,很少人用的协议意味着学习成本,而且流行性低的协议缺乏稳定成熟的跨语言、跨平台的公共包。

  • 性能

    • 空间开销

    序列化需在原数据上加描述字段,便于反序列化解析。若序列化过程引入的额外开销过高,可能会导致过大的网络,磁盘等各方面的压力。对于海量分布式存储系统,数据量往往以TB为单位,巨大的的额外空间开销意味着高昂的成本。

    • 时间开销

    复杂的序列化协议会导致较长的解析时间

  1. 协议层(浅粉)

1692198707976.png

协议是双方确定的交流语义

比如:假设一个字符串传输的协议,允许客户端发一个字符串,服务端收到后。首先发一个4字节的消息总长度,然后再发送1字节的字符集charset长度,接下来就是消息的payload,字符集名称和字符串正文。

  • 特殊结束符:过于简单,对于一个协议单元必须要全部读入才能够进行处理,除此之外必须要防止用户传输的数据不同结束符相同,否则就会出现紊乱(HTTP 协议头就是以回车(CR)加换行(LD)符号序列结尾。)

  • 变长协议:一般都是自定义协议,有 header 和 payload 组成,会以定长加不定长的部分组成,其中定长的部分需要描述不定长的内容长度,使用比较广泛

协议构造

1692199442721.png

协议解析

1692199519489.png

  1. 网络通信层(粉)

1692199546480.png

套接字编程中的客户端必须知道两个信息: 服务器的 IP 地址,以及端口号

简单流程

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

网络库

实际开发时候,用网络库操作

  • 提供易用API
    • 封装底层Socket API
    • 连接管理和事件分发
  • 功能
    • 协议支持:TCP、UDP、UDS等
    • 优雅退出、异常处理
  • 性能
    • 应用层buffer减少copy
    • 高性能定时器、对象池等