网络协议 19 - RPC 协议:远在天边近在眼前

【前五篇】系列文章传送门:html

  1. 网络协议 14 - 流媒体协议:要说爱你不容易
  2. 网络协议 15 - P2P 协议:小种子大学问
  3. 网络协议 16 - DNS 协议:网络世界的地址簿
  4. 网络协议 17 - HTTPDNS:私人定制的 DNS 服务
  5. 网络协议 18 - CDN:家门口的小卖铺

    这几年微服务很火,想必各位博友或多或少的都接触过。微服务概念中,
各服务间的相互调用是不可或缺的一环。你知道微服务之间是经过什么方式相互调用的吗?编程

    你可能说,这还不简单,用 socket 呗。服务之间分调用方和被调用方,咱们就创建一个 TCP 或者 UDP 链接进行通讯就行了。服务器

    说着说着,你可能就会发现,这事儿没那么简单。网络

    咱们就拿最简单的场景:app

客户端调用一个加法函数,将两个整数加起来,返回它们的和。

    若是放在本地调用,那是简单的不能再简单,可是一旦变成了远程调用,门槛一会儿就上去了。框架

    首先,你要会 socket 编程,至少要先了解我们这个系列的全部协议 ,而后再看 N 本砖头厚的 socket 程序设计的书,学会我们了解过的几种 socket 程序设计的模型。异步

    这就使得原本大学毕业就能干的一项工做,变成了一件五年工做经验都不必定干好的工做,并且,搞定了 socket 程序设计,才是万里长征的第一步,后面还有不少问题呢。socket

存在问题

问题一:如何规定远程调用的语法?
    客户端如何告诉服务端,我是一个加法,而另外一个是减法。是用字符串 “add” 传给你,仍是传给你一个整数,好比 1 表示加法,2 表示减法?函数

    服务端又该若是告诉客户端,我这个是加法,目前只能加整数,不能加小数和字符串。而另外一个加法 “add1”,它能实现小数和整数的混合加法,那返回值是什么?正确的时候返回什么,错误的时候又返回什么?微服务

问题二:如何传递参数?
    是先传两个整数,后传一个操做数 “add”,仍是先传操做符,再传两个整数?

    另外,若是咱们是用 UDP 传输,把参数放在一个报文里还好,但若是是 TCP,是一个流,在这个流里面如何区分先后两次调用?

问题三:如何表示数据?
    在咱们的加法例子中,传递的就是一个固定长度的 int 值,这种状况还好,若是是变长的类型,是一个结构体,甚至是一个类,应该怎么办呢?即便是 int,在不一样的平台上长度也不一样,该怎么办呢?

问题四:如何知道一个服务端都实现了哪些远程调用?从哪一个端口能够访问这个远程调用?
    假设服务端实现了多个远程调用,每一个实现可能都不在一个进程中,监听的端口也不同,并且因为服务端都是本身实现的,不可能使用一个你们都公认的端口,并且有可能多个进程部署在一台机器上,你们须要抢占端口,为了防止冲突,每每使用随机端口,那客户端如何找到这些监听的端口呢?

问题五:发生了错误、重传、丢包、性能等问题怎么办?
    本地调用没有这个问题,可是一旦到网络上,这些问题都须要处理,由于网络是不可靠的,虽然在同一个链接中,咱们还能够经过 TCP 协议保证丢包、重传的问题,可是若是服务器崩溃了又重启,当前链接断开了,TCP 就保证不了了,须要应用本身进行从新调用,从新传输会不会一样的操做作两遍,远程调用性能会不会受影响呢?

解决问题

    看到这么多问题,是否是很头疼?还记得我们了解 http 的时候,认识的协议三要素吗?

    本地调用函数里不少问题,好比词法分析、语法分析、语义分析等待,这些问题编译器基本上都帮咱们解决了,可是在远程调用中,这些问题咱们都要本身考虑。

协议约定问题

    不少公司对于这个问题,是弄一个核心通讯组,里面都是 socket 编程的大牛,实现一个统一的库,让其余业务组的人来调用,业务的人不须要知道中间传输的细节。

    通讯双方的语法、语义、格式、端口、错误处理等,都须要调用方和被调用方开会商量,双方达成一致。一旦有一方改变,要及时通知对方,不然就会出现问题。

    可是,不是每一个公司都能经过这种大牛团队解决问题的,而是使用已经实现好的框架。

    有一个大牛(Bruce Jay Nelson)经过一篇论文,定义了 RPC 的调用标准。后面全部 RPC 框架都是按照这个标准模式来的。

整个过程以下:

  1. 客户端的应用想发起一个远程调用时,它其实是经过本地调用方的 Stub。它负责将调用的接口、方法和参数,经过约定的协议规范进行编码,并经过本地 RPCRuntime 进行传输,将调用网络包发送到服务器;
  2. 服务端的 RPCRuntime 收到请求后,交给提供方 Stub 进行编码,而后调用服务端的方法,获取结果,并将结果编码后,发送给客户端;
  3. 客户端的 RPCRuntime 收到结果,发给调用方 Stub 解码获得结果,返回给客户端。

    上面过程当中分了三个层次:客户端、Stub 层、服务端。

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

    最先的 RPC 的一种实现方式称为 Sun RPCONC 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;
  • 一个程序能够有多个方法,方法也有编号,若是找不到方法,就会返回 PROG_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 编程就能够了,实际上是很复杂的,要解决协议约定问题、传输问题和服务发现问题;
  • ONC RPC 框架以及 NFS 的实现,给出了解决上述三大问题的示范性实现,也就是公用协议描述文件,并经过这个文件生成 Stub 程序。RPC 的传输通常须要一个状态机,须要另一个进程专门作服务发现。

参考:

  1. 刘超-趣谈网络协议系列课;
  2. 如何给老婆解释什么是RPC;
相关文章
相关标签/搜索