learn-tech/专栏/趣谈网络协议/32RPC协议综述:远在天边,近在眼前.md
2024-10-16 11:00:45 +08:00

13 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        32 RPC协议综述远在天边近在眼前
                        前面我们讲了容器网络如何实现跨主机互通,以及微服务之间的相互调用。

网络是打通了那服务之间的互相调用该怎么实现呢你可能说咱不是学过Socket吗。服务之间分调用方和被调用方我们就建立一个TCP或者UDP的连接不就可以通信了

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

如果放在本地调用,那是简单的不能再简单了,只要稍微学过一种编程语言,三下五除二就搞定了。但是一旦变成了远程调用,门槛一下子就上去了。

首先你要会Socket编程至少先要把咱们这门网络协议课学一下然后再看N本砖头厚的Socket程序设计的书学会咱们学过的几种Socket程序设计的模型。这就使得本来大学毕业就能干的一项工作变成了一件五年工作经验都不一定干好的工作而且搞定了Socket程序设计才是万里长征的第一步。后面还有很多问题呢

如何解决这五个问题?

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

客户端如何告诉服务端我是一个加法而另一个是乘法。我是用字符串“add”传给你还是传给你一个整数比如1表示加法2表示乘法服务端该如何告诉客户端我的这个加法目前只能加整数不能加小数不能加字符串而另一个加法“add1”它能实现小数和整数的混合加法。那返回值是什么正确的时候返回什么错误的时候又返回什么

问题二:如果传递参数?

我是先传两个整数后传一个操作符“add”还是先传操作符再传两个整数是不是像咱们数据结构里一样如果都是UDP想要实现一个逆波兰表达式放在一个报文里面还好如果是TCP是一个流在这个流里面如何将两次调用进行分界什么时候是头什么时候是尾把这次的参数和上次的参数混了起来TCP一端发送出去的数据另外一端不一定能一下子全部读取出来。所以怎么才算读完呢

问题三:如何表示数据?

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

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

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

假设服务端实现了多个远程调用,每个可能实现在不同的进程中,监听的端口也不一样,而且由于服务端都是自己实现的,不可能使用一个大家都公认的端口,而且有可能多个进程部署在一台机器上,大家需要抢占端口,为了防止冲突,往往使用随机端口,那客户端如何找到这些监听的端口呢?

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

本地调用没有这个问题但是一旦到网络上这些问题都需要处理因为网络是不可靠的虽然在同一个连接中我们还可通过TCP协议保证丢包、重传的问题但是如果服务器崩溃了又重启当前连接断开了TCP就保证不了了需要应用自己进行重新调用重新传输会不会同样的操作做两遍远程调用性能会不会受影响呢

协议约定问题

看到这么多问题,你是不是想起了我[第一节]讲过的这张图。

本地调用函数里有很多问题,比如词法分析、语法分析、语义分析等等,这些编译器本来都能帮你做了。但是在远程调用中,这些问题你都需要重新操心。

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

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

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

当客户端的应用想发起一个远程调用时它实际是通过本地调用本地调用方的Stub。它负责将调用的接口、方法和参数通过约定的协议规范进行编码并通过本地的RPCRuntime进行传输将调用网络包发送到服务器。

服务器端的RPCRuntime收到请求后交给提供方Stub进行解码然后调用服务端的方法服务端执行方法返回结果提供方Stub将返回结果编码后发送给客户端客户端的RPCRuntime收到结果发给调用方Stub解码得到结果返回给客户端。

这里面分了三个层次对于用户层和服务端都像是本地调用一样专注于业务逻辑的处理就可以了。对于Stub层处理双方约定好的语法、语义、封装、解封装。对于RPCRuntime主要处理高性能的传输以及网络的错误和异常。

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

NFSNetwork File System就是网络文件系统。要使NFS成功运行要启动两个服务端一个是mountd用来挂载文件路径一个是nfsd用来读写文件。NFS可以在本地mount一个远程的目录到本地的一个目录从而本地的用户在这个目录里面写入、读出任何文件的时候其实操作的是远程另一台机器上的文件。

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

XDRExternal 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创建一个连接然后调用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的传输一般需要一个状态机需要另外一个进程专门做服务发现。

最后,给你留两个思考题。

在这篇文章中mount的过程是通过系统调用最终调用到RPC层。一旦mount完毕之后客户端就像写入本地文件一样写入NFS了这个过程是如何触发RPC层的呢

ONC RPC是早期的RPC框架你觉得它有哪些问题呢

我们的专栏更新到第32讲不知你掌握得如何每节课后我留的思考题你都有没有认真思考并在留言区写下答案呢我会从已发布的文章中选出一批认真留言的同学赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议,我们下期见!