RPC与REST

07-13 1145阅读

RPC与REST

        • 访问远程服务
          • 1远程服务调用(Remote Procedure Call,RPC):RPC 解决什么问题?如何解决的?为什么要那样解决?
            • 1.1 先解决两个进程间如何交换数据的问题,也就是进程间通信(Inter-Process Communication,IPC)。可以考虑的办法有以下几种:
            • 1.2 通信的成本
            • 1.3 三个基本问题
            • 1.4 统一的 RPC 与 分裂的 RPC
            • 2 REST 设计风格
              • 2.1 理解 REST
              • 2.2 RESTful 的系统
                访问远程服务

                远程服务将计算机的工作范围从单机扩展到网络,从本地延伸至远程,是构建分布式系统的首要基础。

                RPC与REST
                (图片来源网络,侵删)

                而远程服务又不仅仅是为了分布式系统服务的,在网络时代,浏览器、移动设备、桌面应用和服务端的程序,普遍都有与其他设备交互的需求,所以今天很难找到没有开发过远程服务的人,但是没有正确理解远程服务的coder比比皆是。

                1远程服务调用(Remote Procedure Call,RPC):RPC 解决什么问题?如何解决的?为什么要那样解决?

                进程间通信:尽管今天的大多数RPC 已经不再追求这个目标了,但无可否认,在最初的时候,这是先要解决的:让调用远程方法和本地方法一样。

                1.1 先解决两个进程间如何交换数据的问题,也就是进程间通信(Inter-Process Communication,IPC)。可以考虑的办法有以下几种:
                1. 管道或具名管道:管道类似于两个进程间的桥梁,可通过管道在进程间传递 少量的字符流或字节流。普通管道只用于有亲缘关系的进程(由一个进程启动的另外一个进程)间通信。具名管道拜托了普通管道没有名字的限制,除具有普通管道的所有功能外,它还允许无亲缘关系进程间的通信。管道典型的应用就是命令行中的 | 操作符,如:ps -ef | grep java ps 与 grep 都有独立的进程,以上命令就是将ps 的标准输出链接到grep 的标准输入上。
                2. 信号(signal):用于通知目标进程有某事发生,除了用于进程间通信外,进程还可以发送信号给进程自身。信号的典型命令就是 kill 命令:kill -9 pid
                3. 信号量(Semaphore):信号量用于两个进程间同步协作手段,它相当于操作系统提供的一个特殊变量,程序可以在上面进行 wait() notify() 操作。
                4. 消息队列:以上三种方式只适合传递少量信息,消息队列用于进程间数据量较多的通信。进程可以向队列添加消息,被赋予读权限的进程可以消费消息,MQ 克服了信号承载量小,管道只能用于无格式字节流以及缓冲区大小受限的缺点,但实时性相对受限。
                5. 共享内存(Shared Memory) :允许多个进程访问同一块公共的内存空间,这是效率最高的进程间通信方式。当一块进程被多进程共享时,各个进程往往会与其他通信机制,如信号量结合使用。
                6. 套接字接口(Socket):消息队列和共享内存只适合于单机多进程间的通信,套接字接口是更为普适的进程间通信机制,可用于不同机器之间的进程通信。套接字(socket)当仅限于本机进程间通信时,套接字接口是优化过的,不会经过网络协议栈,不需要打包拆包、计算校验和等,只是简单的将应用层数据从一个进程拷贝到另一个进程。这种进程间通信叫 IPC Socket。
                1.2 通信的成本

                IPC Socket 不仅适用于本地相同机器间不同进程间的通信,由于 Socket 网络栈的统一接口,它也理所应当的能支持基于网络的跨机器进程间通信。这么做有一个看起来无比诱人的好处,由于 Socket 是各个操作系统都有提供的标准接口,完全有可能把远程方法调用的通信细节隐藏在操作系统底层,从应用层面商看来可以做到远程调用与本地的进程间通信在编码上完全一致。事实上,在原始分布式时代的确是这么用的,但这种透明的调用形式却反问滥用以至于显著降低了分布式系统的性能。

                所以,对 RPC 提出一系列的疑问:

                1. 两个进程间通信,谁作为客户端,谁作为服务端?
                2. 怎样进行异常处理,如何让调用者获知?
                3. 服务端出现多线程竞争后改怎么办?
                4. 如何提高网络利用的效率?比如连接是否可以被多个请求复用以减少开销,是否支持多播?
                5. 参数、返回值如何表示?应该有怎样的字节序?
                6. 如何保证网络的可靠性?
                7. 发送的请求服务端收不到回复怎么办?

                这里的中心观点是:本地调用与远程调用当做一样处理,这是犯了方向性的错误。

                Sun 公司的一众大佬们总结了通过网络记性分布式运算的八宗罪:

                1. 网路是可靠的,安全的,同质化的;(这是三点)
                2. 延迟是不存在的;
                3. 带宽是无限的;
                4. 拓扑结构是一成不变的;
                5. 总会有一个管理员;
                6. 不必考虑传输成本。

                以上八大问题潜台词就是说如果远程服务调用要弄透明化的话,就必须为这些罪过埋单(就是RPC不能透明化)。这算是给 RPC 是否能等同于 IPC 正式定下了一个具有公信力的结论。至此,RPC 应该是一种高层次的或者说语言层次的特征,而不是像 IPC 那样,是低层次的货系统层次的特征成为工业界、学术界的主流观点。

                RPC 的定义:远程服务调用是指位于互不重合的内存地址空间中的两个程序,在语言层面上,以同步的方式使用有限带宽的信道来传输程序控制信息。

                1.3 三个基本问题

                RPC 协议无外乎变着花样使用各种手段来解决以下三个基本问题:

                • 如何表示数据:这里的数据包含了传递给方法的参数,以及方法执行后的返回值。不同进程间数据读取问题。且 RPC 完全可能绵连交互双方各自使用不同程序语言的情况:即使是用了相同语言的 RPC 协议,在不同硬件指令集、不同OS下,同样的数据类型也完全可能有不一样的表现细节,如数据宽度、字节差异等。有效的做法是:将交互双方所涉及的数据转换为某种事先约定好的中立数据流格式来进行传输,将数据流转换回不同语言中对应的数据类型来进行使用,就是序列化与反序列化。每种 RPC 协议都要有对应的序列化协议,如 gRPC Protocol Buffers,其他众多轻量级 RPC 的 JSON Serialization。
                • 如何传输数据:指如何通过网路,在两个服务的 Endpoint 间相互操作、交换数据。这里的“交换数据”通常指的是应用层协议,实际传输一般是基于标准的 TCP、UDP 等标准的传输层协议来完成的。两个服务交互不只是扔个序列化数据就行,还需有异常、超时、安全、认证、授权、事务等,这些都可能是双方需交换的。这类数据叫:Wire Protocol。常见的有:Java RMI 的 JRMP,Web Service 的 SOAP,如果要求足够简单,双方都是 HTTP Endpoint,就可直接使用 HTTP 协议(如 JSON-RPC)。
                • 如何确定方法:这在本地方法调用中不是太大问题,编译器会根据语言规范,将调用的方法签名转换为进程空间中子过程入口位置的指针。不过一旦要考虑不同语言,事情又麻烦起来,每门语言的方法前面可能不同,所以“如何表示同一个方法”,“找到对应的方法”还是得弄个跨语言的统一标准。这个标准非常简单,如直接给程序的每个方法都规定一个唯一的、在任何机器上都绝不重复的编号,直接找对应方法。这是当初DCE 的解决方案,就是 UUID,虽然此后DCE 还是弄了一套语言无关的接口描述语言(Interface Description Language,IDL),但 UUID 却广为流传。用于表示方法的协议:Web Service 的 WSDL,JSON-RPC 的JSON-WSP。
                  1.4 统一的 RPC 与 分裂的 RPC

                  刚开始的时候,大家总是尝试 RPC 可以解决所有分布式问题,如刚开始的时候既要支持面向对象,又要支持多种语言,功能还齐全。但是没有一家成功的,简单、普适、高性能这三点,似乎真的难以同时满足。所以现在的 RPC 仍处于百家争鸣的时代,大家都不再去追求大而全的“完美”,而是有自己的针对性作为主要的发展方向。如:

                  1. 面向对象发展的 RMI等;
                  2. 朝着性能发展的,如 gRPC 和 Thrift,决定 RPC 性能的主要两个因素:序列化效率和信息密度。序列化效率指输出结果容量小,速度快,效率自然高;信息密度则取决于协议中有效负载(Payload)所占总传输数据的比例大小,使用传输协议的层次越高,信息密度越低,SOAP 使用XML拙劣的性能表现就是前车之鉴。gRPC 和 Thrift 都有自己优秀的专有序列化器,而传输协议方面,gRPC 是基于 HTTP/2 的,支持多路复用和 Header 压缩,Thrift 则直接基于传输层的 TCP 协议来实现,省去了额外应用层协议的开销。
                  3. 朝着简化发展:代表为 JSON-RPC,要说功能最强、速度最快的 RPC 可能会很有争议,但选功能弱的、速度慢的,JSON-RPC 肯定会在候选人之一。牺牲了功能和效率,换来的是协议的简单轻便,接口与格式都更为通用,尤其适合用于 Web 浏览器这类一般不会有额外协议支持、额外客户端支持的应用场合。

                  到了最近几年,RPC 框架有明显的朝着更高层次(不仅负责调用远程服务,还要管理远程服务)与插件化方向发展的趋势,不再追求独立地解决 RPC 的全部三个问题(表示数据、传递数据、表示方法),而是将一部分功能设计成扩展点,让用户自己去选择。框架聚焦于提供核心的、更高层次的能力,如提供负载均衡、服务注册、可观察性等方面的支持。这一类框架的代表有 Facebook 的 Thrift 和阿里的 Dubbo。尤其是断更多年后重启的 Dubbo 表现得更为明显,它默认有自己的传输协议(Dubbo协议),同时也支持其他协议;默认采用 Hessian2 作为序列化器,如果你有 JSON 需求,可以替换为 Fastjson,如你对性能有更高的追求,可以替换为 Kryo、Protocol Buffers 等效率更好的序列化器,如果你不想依赖其他组件库,直接使用 JDK 自带的序列化器也是可以的。

                  开发一个分布式系统,是不是一定要用 RPC 呢?

                  2 REST 设计风格

                  很多人会拿 REST 与 RPC 互相比较,其实,REST 无论是在思想上、概念上,还是适用范围上,与 RPC 都不尽相同,充其量只能算有些相似,但本质上并不是同一类型的东西。

                  2.1 理解 REST

                  个人会有好恶偏爱,但计算机科学是务实的,有了RPC还会提出REST,有了面向过程编程还能产生面向资源编程,后者总会有些前者没有的闪光点,我们先理解 REST,在谈论评价它。

                  比较容易理解 REST 思想的途径是先理解什么是 HTTP,再配合一些实际例子来进行类比,你会发现REST(Representational State Transfer)实际是 HTT(Hypertext Transfer)的进一步抽象,两者就如同接口与实现类的关系一般。

                  Hypertext 超文本一词已被普遍接受,它指的是能够进行分支判断和差异响应的文本,相应地,超媒体指的是能够进行分支判断和差异响应的图形、电影和声音(也包括文本)的复合体。就是指文字可以点击、可以出发脚本等。

                  下面从“超文本”或“超媒体”的含义来理解 REST 中的相关概念:

                  • 资源(Resource)比如你在阅读的一篇文章,其内容本身就是资源;

                  • 表征(Representation)当你通过电脑浏览阅读此文章时,浏览器向服务端发出请求“我需要这个资源的HTML 格式”,服务端向浏览器返回的这个 HTML 就是表征,或者是文本的 PDF、Markdown等都是,即表现形式。

                  • 状态(State)当你读完了这篇文章,想看后面是什么内容时,你想服务器发送“下一篇文章”的请求,但服务端不知道下一篇是哪一篇,就得根据你现在的上下文信息来判断,以来的就是状态;

                  • 转移(Transfer)无论状态是由服务端还是客户端来提供,“取下一篇文章”这个行为必然只能由服务端来提供,服务端通过某种形式,把当前文章转变为下一篇,这就叫“表征状态转移”。

                  • 统一接口(Uniform Interface)前面提到的下一篇的点击跳转,这是一种让表征状态发生转移的方式,但是 URI 的含义是统一资源标识符,是一个名词,如何表达出“转移”的含义的呢?答案是 HTTP 协议中已经提前约定好了一套“统一接口”,它包括:GET、HEAD POST PUT DELETE TRACE OPTIONS七种基本操作,任何一个支持 HTTP协议的服务器都会遵守这套规定,对特定的 URI 采取这些操作,服务器就会触发相应的表征状态转移的动作。

                  • 超文本驱动(Hypertext Driven)任何网站的导航(状态转移)行为都不可能是预置于浏览器代码之中,而是由服务器发出的请求响应信息(超文本)来驱动的。

                  • 自描述信息 消息的类型以及应如何处理这条消息。如 Content-Type:application/json;charset=utf-8

                    2.2 RESTful 的系统

                    理解了上面的概念,我们就可以开始讨论面向资源的呢编程思想与几个具体的软件架构设计原则了。一套完整的、完全满足 REST 风格的系统应满足以下六大原则:

                    1. 服务端与客户端分离(Client-Server)将用户界面所关注的逻辑和数据存储所关注的逻辑分离开来,有助于提高用户界面的跨平台的可移植性。
                    2. 无状态(Stateless)无状态是 REST 的一条核心原则,部分开发者在做接口规划时,觉得 REST 风格的服务怎么设计都感觉别怒,很有可能的一种原因是在服务端持有着比较重的状态。应由客户端承担状态维护职责,但目前大多数系统都达不到这个要求。
                    3. 可缓存(Cacheability)无状态服务虽然提升了系统的可见性、可靠性和可伸缩性,但降低了系统的网络性。“降低网络性”是指某个功能如果使用有状态的设计只需一次(或少量)请求即可完成,而无状态的得多次。为了缓解这个矛盾,REST 希望软件系统能入万维网一样,允许客户端和中间的通讯传递者(如代理)将部分服务端的应答缓存起来。
                    4. 分层系统(Layered System)这里的分层并不是表示层、服务层、持久层这种,而是客户端一般不用知道是否直接连接到了最终的服务器,或中间服务器。中间服务器可以通过负载均衡和共享缓存的机制提高系统的可扩展性,这样也便于缓存、伸缩和安全策略的部署。该原则的典型应用是内容分发网络。如我们浏览某个网站,并不是直接访问源服务器,而是访问了某个 CDN 服务器。
                    5. 统一接口(Uniform Interface)这是 REST 的另一条核心原则,REST 希望开发者面向资源编程,希望软件系统设计的重点放在抽象系统该有哪些资源上,而不是抽象系统该有哪些行为上。这条原则可以类比计算机中对文件管理的操作来理解,管理文件的操作是可数的,而且对所有文件都是固定的,统一的。如果面向资源来设计系统,同样会有类似的操作特征,所有这些操作都借用了HTTP协议中固有的操作命令来完成。因为面向资源编程的抽象程度更高。想要在架构设计中合理恰当的利用统一接口,系统应能做到每次请求中都包含资源的ID,所有操作均通过资源ID 来进行;建议每个资源都是自描述的消息;建议通过超文本来驱动应用状态的转移。
                    6. 按需代码(Code-On-Demand)可选原则。

                    本节开篇提出的 REST 与 RPC 在思想上的差异?

                    REST 的基本思想是面向资源来抽象问题,它与此前流行的面向过程的编程在抽象主体上有本质区别。在 REST 提出以前,人们设计分布式系统的唯一方案就只有 RPC,开发者是围绕着“远程方法”去设计两个系统交互的,这样做的坏处不仅是“如何在不同系统间表示一个方法”、“如何获得接口能够提供的方法清单”都成了需要专门协议去解决的问题,更在于服务的每个方法都是独立的,服务者必须熟悉每个方法才能使用,而REST抽象为了几类

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]