网络协议趣谈RPC协议综述

Posted sysu_lluozh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络协议趣谈RPC协议综述相关的知识,希望对你有一定的参考价值。

服务之间的互相调用该怎么实现呢?使用Socket协议,服务之间分调用方和被调用方,建立一个TCP或者UDP的连接,不就可以通信了?

仔细想一下,这事儿没这么简单。拿最简单的场景,客户端调用一个加法函数,将两个整数加起来返回它们的和

如果放在本地调用,那是简单的不能再简单了,但是一旦变成远程调用,门槛一下子就上去了
首先要会Socket编程,然后再看Socket程序设计的书学会几种Socket程序设计的模型,而且搞定了Socket程序设计,才是万里长征的第一步,后面还有很多问题呢

一、如何解决这五个问题?

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

客户端如何告诉服务端:

  • 这是一个加法,而另一个是乘法,比如1表示加法,2表示乘法?
  • 传过去的是一个字符串"add",还是一个整数

服务端该如何告诉客户端:

  • 这个加法只能加整数,不能加小数,不能加字符串,另一个加法add1能实现小数和整数 的混合加法
  • 返回值是什么?正确的时候返回什么,错误的时候返回什么?

1.2 问题二:如果传递参数

先传两个整数,后传一个操作符add,还是先传操作符add,再传两个整数?

如果都是UDP,放在一个报文里面还好
如果是TCP是一个流,在这个流里如何将两次调用进行分界?什么时候是头,什么时候是尾?
这次的参数和上次的参数混起来时,TCP一端发送出去的数据,另外一端不一定能一下子全部读取出来。所以,怎么才算读完呢?

1.3 问题三:如何表示数据

  • 数据类型

在上面例子中传递的是一个固定长度的int值,这种情况还好
如果是变长的类型,比如是一个结构体,甚至是一个类,应该怎么办呢?
如果是int,不同的平台上长度也不同,该怎么办呢?

  • 超过一个Byte的类型

在网络上传输超过一个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来设计的,因而发出去的时候需要做一个转换

1.4 问题四:如何知道服务端提供了哪些服务

如何知道一个服务端都实现了哪些远程调用?从哪个端口可以访问这个远程调用?

  • 假设服务端实现了多个远程调用,每个可能实现在不同的进程中,监听的端口也不一样
  • 由于服务端都是自己实现的,不可能使用一个都公认的端口
  • 有可能多个进程部署在一台机器上,各进程需抢占端口,为防止冲突往往使用随机端口

那客户端如何找到这些监听的端口呢?

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

本地调用没有这个问题,但是一旦到网络上这些问题都需要处理,因为网络是不可靠的

虽然在同一个连接中,还可通过TCP协议保证丢包、重传的问题,但是如果服务器崩溃了又重启,当前连接断开后TCP就保证不了了,需要应用自己进行重新调用,重新传输会不会同样的操作做两遍,远程调用性能会不会受影响呢?

二、协议约定问题

看一下下面这张图:

本地调用函数里有很多问题,比如词法分析、语法分析、语义分析等这些编译器本来已处理的逻辑,但在远程调用中这些问题都需要重新操心

很多公司的解决方法是弄一个核心通信组,里面都是Socket编程的大牛,实现一个统一的库让其他业务组的人来调用,业务的人不需要知道中间传输的细节
通信双方的语法、语义、格式、端口、错误处理等,都需要调用方和被调用方开会商量,双方达成一致。一旦有一方改变要及时通知对方,否则通信就会有问题

但并不是每一个公司都有这种大牛团队,往往只有大公司才配得起,那有没有已经实现好的框架可以使用呢?

当然有
一个大牛Bruce Jay Nelson写了一篇论文Implementing Remote Procedure Calls 定义了RPC的调用标准,后面所有RPC框架都是按照这个标准模式来的


当客户端应用发起一个远程调用时:

  1. 通过本地调用本地调用方的Stub,负责将调用的接口、方法和参数,通过约定的协议规范进行编码
  2. 通过本地的RPCRuntime进行传输,将调用网络包发送到服务器

当服务器收到一个请求时:

  1. 服务器端的RPCRuntime收到请求
  2. 交给提供方Stub进行解码
  3. 调用服务端的方法,服务端执行方法,返回结果
  4. 提供方Stub将返回结果编码后
  5. RPCRuntime发送给客户端

当客户端接收到响应时:

  1. 客户端的RPCRuntime收到结果
  2. 发给调用方Stub解码得到结果
  3. 返回给客户端

这里面分了三个层次:

  1. 对于客户端和服务端,都像是本地调用一样,专注于业务逻辑的处理即可
  2. 对于Stub层,处理双方约定好的语法、语义、封装、解封装
  3. 对于RPCRuntime,主要处理高性能的传输,以及网络的错误和异常

最早的RPC的一种实现方式称为Sun RPC或ONC RPC
Sun公司是第一个提供商业化RPC库和RPC编译器的公司,这个RPC框架是在NFS协议中使用的

NFS(Network File System)是网络文件系统
要使NFS成功运行,要启动两个服务端:

  • 一个是mountd,用来挂载文件路径
  • 一个是nfsd,用来读写文件

NFS可以在本地mount一个远程的目录到本地的一个目录,从而本地的用户在这个目录里面写入、读出任何文件的时候,其实操作的是远程另一台机器上的文件

操作远程和远程调用的思路是一样的,就像操作本地一样。所以NFS协议就是基于RPC实现的。当然无论是什么RPC,底层都是Socket编程

XDR(External Data Representation,外部数据表示法)是一个标准的数据压缩格式,可以表示基本的数据类型,也可以表示结构体

这是几种基本的数据类型:

在RPC的调用过程中,所有的数据类型都要封装成类似的格式。而且RPC的调用和结果返回也有严格的格式

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


为了可以成功调用RPC,在客户端和服务端实现RPC时,首先要定义一个双方都认可的程序、版本、方法、参数等

如果还是上面的加法,则双方约定为一个协议定义文件,同理NFS、mount和读写也有类似的定义

有了协议定义文件,ONC RPC会提供一个工具,根据这个文件生成客户端和服务器端的Stub 程序

最下层的是XDR文件,用于编码和解码参数。这个文件是客户端和服务端共享的,因为只有双方一致才能成功通信

  1. 在客户端调用clnt_create创建一个连接

  2. 调用Stub函数add_1,感觉在调用本地一样

  3. Stub函数函数发起了一个RPC调用,通过调用clnt_call来调用ONC RPC的类库来真正发送请求

  4. 服务端的Stub程序监听客户端的请求,当调用到达时判断如果是add,则调用真正的服务端逻辑,即将两个数加起来

  5. 服务端将结果返回服务端的Stub

  6. Stub程序发送结果给客户端

  7. 客户端的Stub程序正在等待结果,当结果到达时客户端Stub接收结果

  8. 将结果返回给客户端的应用程序,从而完成整个调用过程

有了这个RPC的框架,前面五个问题中的前三个"如何规定远程调用的语法","如何传递参 数"以及"如何表示数据"基本解决,这三个问题统称为协议约定问题

三、传输问题

错误、重传、丢包、性能等问题还没有解决,这些问题统称为传输问题
传输问题就不需要Stub操心,而是由ONC RPC的类库来实现。这是大牛们实现的,只需调用即可

在这个类库中,为了解决传输问题,对于每一个客户端都会创建一个传输管理层,而每一次RPC调用都会是一个任务,在传输管理层,可以看到熟悉的队列机制、拥塞窗口机制等

由于在网络传输时经常需要等待,所以同步的方式往往效率比较低,因此也就有Socket的异步模型
为了能够异步处理,对于远程调用往往是通过状态机来实现的。只有当满足某个状态时才进行下一步,如果不满足状态,不是在那里等,而是将资源留出来处理其他的RPC调用

从上面这个图可以看出,整个状态的转换图还是很复杂的

首先,进入起始状态,查看RPC的传输层队列中有没有空闲的位置可以处理新的RPC任务

  • 如果没有,直接结束或重试
  • 如果申请成功,就可以分配内存、获取服务的端口号,然后连接服务器

连接的过程需要一段时间,因此要等待连接的结果

  • 如果连接失败,直接结束或重试
  • 如果连接成功,开始发送RPC请求

等待获取RPC结果,这个过程也需要一定的时间

  • 如果发送出错,可以重新发送
  • 如果连接断了,可以重新连接
  • 如果超时,可以重新传输
  • 如果获取到结果,可以解码并正常结束

处理连接失败、重试、发送失败、超时、重试等场景非常复杂,因而实现一个RPC的框架其实很有难度

四、服务发现问题

传输问题解决了,还遗留问题四"如何找到RPC服务端的那个随机端口"未解决,这个问题称为服务发现问题
在ONC RPC中,服务发现是通过portmapper实现的

portmapper启动在一个众所周知的端口上,RPC程序由于是用户自己实现的,会监听在一个随机端口上,所以RPC程序启动时向portmapper注册
客户端要访问RPC服务端这个程序时,首先查询portmapper,获取RPC服务端程序的随机端口,然后向这个随机端口建立连接,开始RPC调用

从图中可以看出,mount命令的RPC调用就是这样实现的

五、小结

总结一下上面的内容:

  • 远程调用看起来用Socket编程就实现,其实需要解决协议约定问题、传输问题和服务发现问题,整套逻辑还是非常复杂的 Bruce Jay
  • Nelson的论文、早期ONC RPC框架,以及NFS的实现,给出了解决这三大问题的示范性实现,即协议约定要公用协议描述文件,并通过这个文件生成Stub程序,RPC的传输一般需要一个状态机,需要另外一个进程专门做服务发现

以上是关于网络协议趣谈RPC协议综述的主要内容,如果未能解决你的问题,请参考以下文章

网络协议趣谈什么是网络协议

网络协议趣谈什么是网络协议

趣谈网络协议

网络协议趣谈UDP协议

网络协议趣谈UDP协议

网络协议趣谈HTTP协议内容和传输