网络协议趣谈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框架都是按照这个标准模式来的
当客户端应用发起一个远程调用时:
- 通过本地调用本地调用方的Stub,负责将调用的接口、方法和参数,通过约定的协议规范进行编码
- 通过本地的RPCRuntime进行传输,将调用网络包发送到服务器
当服务器收到一个请求时:
- 服务器端的RPCRuntime收到请求
- 交给提供方Stub进行解码
- 调用服务端的方法,服务端执行方法,返回结果
- 提供方Stub将返回结果编码后
- RPCRuntime发送给客户端
当客户端接收到响应时:
- 客户端的RPCRuntime收到结果
- 发给调用方Stub解码得到结果
- 返回给客户端
这里面分了三个层次:
- 对于客户端和服务端,都像是本地调用一样,专注于业务逻辑的处理即可
- 对于Stub层,处理双方约定好的语法、语义、封装、解封装
- 对于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的调用和结果返回也有严格的格式
- XID唯一标识一对请求和回复。请求为0,回复为1
- RPC有版本号,两端要匹配RPC协议的版本号。如果不匹配就返回
Deny
,原因是RPC_MISMATCH
- 程序有编号,如果服务端找不到这个程序就返回
PROG_UNAVAIL
- 程序有版本号,如果程序的版本号不匹配就返回
PROG_MISMATCH
- 一个程序可以有多个方法,方法也有编号,如果找不到方法就返回
PROC_UNAVAIL
- 调用需要认证鉴权,如果不通过则
Deny
- 解析参数列表,如果参数无法解析则返回
GABAGE_ARGS
为了可以成功调用RPC,在客户端和服务端实现RPC时,首先要定义一个双方都认可的程序、版本、方法、参数等
如果还是上面的加法,则双方约定为一个协议定义文件,同理NFS、mount和读写也有类似的定义
有了协议定义文件,ONC RPC会提供一个工具,根据这个文件生成客户端和服务器端的Stub 程序
最下层的是XDR文件,用于编码和解码参数。这个文件是客户端和服务端共享的,因为只有双方一致才能成功通信
-
在客户端调用
clnt_create
创建一个连接 -
调用Stub函数add_1,感觉在调用本地一样
-
Stub函数函数发起了一个RPC调用,通过调用
clnt_call
来调用ONC RPC的类库来真正发送请求 -
服务端的Stub程序监听客户端的请求,当调用到达时判断如果是add,则调用真正的服务端逻辑,即将两个数加起来
-
服务端将结果返回服务端的Stub
-
Stub程序发送结果给客户端
-
客户端的Stub程序正在等待结果,当结果到达时客户端Stub接收结果
-
将结果返回给客户端的应用程序,从而完成整个调用过程
有了这个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协议综述的主要内容,如果未能解决你的问题,请参考以下文章