去调用远程的函数或方法,一个健壮的微服务集

作者: 编程  发布:2019-10-02

目前很多应用应该都不是one in all模式了,避免不了发生远程调用,和同事聊了下RPC之后,发现大家对RPC的概念还是比较模糊的,虽然一直在用,但是不太明白其含义,在知乎上搜了下RPC HTTP这俩个关键词,发现还是有很多误解的。

当我们找到了微服务边界,将应用分解成了多个微服务,那么接下来一个重要的问题就是这些微服务的集成了。

很长时间以来都没有怎么好好搞清楚RPC(即Remote Procedure Call,远程过程调用)和HTTP调用的区别,不都是写一个服务然后在客户端调用么?这里请允许我迷之一笑~Naive!本文简单地介绍一下两种形式的C/S架构,先说一下他们最本质的区别,就是RPC主要是基于TCP/IP协议的,而HTTP服务主要是基于HTTP协议的,我们都知道HTTP协议是在传输层协议TCP之上的,所以效率来看的话,RPC当然是要更胜一筹啦!下面来具体说一说RPC服务和HTTP服务。

远程过程调用(Remote Procedure Call, RPC)是从一台机器上通过参数传递的方式调用另一台机器上的一个函数或方法并得到返回的结果。这样的方法又可以称为服务。RPC隐藏了底层的通讯细节,不需要直接处理Socket通讯或Http通讯,并将这些细节封装成一个服务(Service)。RPC是一个请求响应模型。客户端发起请求,服务器返回响应。

  • 问题一:既然有http 请求,为什么还要用rpc调用?
  • 问题二:请问rpc协议和http协议的关系和区别?

一个健壮的微服务集成环境需要考虑多方面要素:

OSI网络七层模型

在进行详细的阐述之前,RPC的请求响应模型可以简单的理解为类似于Http的工作方式。

之后就萌生了写一篇关于RPC的文章。

  • 通信协议
  • 接口协议
  • 服务注册、发现
  • 服务版本控制
  • 负载均衡
  • 服务可用性
  • 服务幂等性
  • 服务扩展性
  • 服务安全性
  • 服务弹性伸缩
  • 服务降级、熔断
  • 调用链可追溯

在说RPC和HTTP的区别之前,我觉的有必要了解一下OSI的七层网络结构模型(虽然实际应用中基本上都是五层),它可以分为以下几层:

在使用形式上,RPC像调用本地函数或方法一样,去调用远程的函数或方法。

RPC,即 Remote Procedure Call,说得通俗一点就是:调用远程计算机上的服务,就像调用本地服务一样。RPC的实现包含了两部分,一部分是客户端,一部分是服务端,服务的调用方发送RPC请求到服务提供方,服务提供方根据参数执行方法,响应客户端,一次RPC请求结束。这篇文章解释的不错:通俗的语言解释什么是 RPC 框架

头大了?别愁,其实也不是一定要面面俱到,还是那句话:“架构不是设计出来的,而是演进出来的”,所以只要满足当下的需要就够了,适合的就是最好的,过早优化往往只会带来更多痛苦。

第一层:应用层。定义了用于在网络中进行通信和传输数据的接口;

为什么需要RPC

在设计一个非分布式应用时,程序各个模块之间的交互都通过方法调用进行数据传递。例如,dao模块通过定义接口的方式暴露一些方法,供给上层调用,业务逻辑层调用这些方法,传入一些参数,就可以对持久层的数据进行读写访问。这种单台主机就能完成的数据交互就叫做集中式运算(centralized computing)。当数据交互的需求转移到分布式运算(distributed computing)系统中时,由于本模块所需要的数据可能不在本地的内存或者持久层中,所以要通过网络调用的方式进行数据传输,网络传输的原始解决方案是使用socket。

Socket是Client/Server模型网络的基本组成部分。它们为程序提供了一个相对简单的机制,可以在远程或本地机器上建立与另一个程序的连接并来回发送消息。甚至可以使用Socket进行系统调用。但是原始的Socket必须使用输入/输出接口(input/output)来对分布式系统进行数据交互,这和传统的通过接口暴露方法服务的方式有很大差距,也不利于服务提供模块对底层服务的管理。为了解决这个局限性,1984年,Birrell和Nelson设计了一个机制,允许程序在其他机器上调用程序。主机A上的进程可以对主机B的过程进行调用,此时A上的进程被暂停并且继续执行B,当B返回时传递返回值给A并继续执行A中的进程。这种机制被称为远程过程调用(RPC)。RPC的主要目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。

图片 1image.png

微服务通信

此篇我想先谈谈在通信方面我的一些思考,因为通信是集成的基础,所以选择合适的通信方案至关重要。

第二层:表示层。定义不同的系统中数据的传输格式,编码和解码规范等;

本地方法调用的实现

由于编译器和系统结构的不同,本地方法的调用的过程可能会有不同。JVM使用基于栈的指令系统,这里主要分析基于栈的指令系统来分析。

处理器会提供某种类型的call指令,这条指令会将堆栈中下一条指令的地址压入操作数栈,并将处理器的访问控制转移到由调用指定的地址。当调用结束后,通过一个return指令,目标地址从栈顶弹出,处理器释放对该地址的控制权。在这个过程中,编译器会执行类似识别参数、压参数入栈、执行调用指令等细节。在被调用的函数中,编译器存储可能被标记为clobbered的寄存器的值,为本地变量分配栈帧,然后在返回之前恢复寄存器和释放栈空间。

指令系统共有四种分类:堆栈型,累加器型,寄存器-存储器型和寄存器-寄存器型。分类的依据是操作数的来源。

RPC 可基于 HTTP 或 TCP 协议,Web Service 就是基于 HTTP 协议的 RPC,它具有良好的跨平台性,但其性能却不如基于 TCP 协议的 RPC。会两方面会直接影响 RPC 的性能,一是传输方式,二是序列化。

通信方案选型

可能有人会说服务提供者完全可以提供多种通信方案,由服务的消费者根据其偏好自行选择。那么我想问问,你见过一部手机提供了多种数据线接口么?即便真的有过这种手机,你觉得它更好用更合理么?可能这个比方并不很恰当,你明白我要表达的意思就好。

同时提供多种通信方案可能带来哪些问题,我大概列了一下:

  • 开发和维护成本增加
  • 可能带来潜在的性能问题
  • 加大了系统耦合性

所以选择合适的通信方案是要从多角度多层面来考量的,

  • 首先,前篇说过好的微服务是松耦合高内聚的,在选择通信方案时也要围绕这一核心
  • 其次,服务的消费者在调用服务时不应该有大量工作要做,所以要选择一个易于使用的通信方案
  • 此外,微服务由于是高度自治的,允许技术异构的,所以通信方案也不应该受到具体技术的限制,即技术无关

所以,选用什么通信方案最好?答案是没有。但是可以将上述几点作为参考,结合自己的实际情况,比如业务特点、服务规模、性能压力、团队结构等,来找寻合适的方案。所以本文中只有思考过程,没有现成的答案。

针对具体的通信方案,我们可能要进一步考虑其细分的一些方面,包括通信协议、接口风格、数据封装、同步异步等,下面说说我的理解。

第三层:会话层。管理用户的会话,控制用户间逻辑连接的建立和中断;

RPC通用架构及典型流程

本地方法调用没有一个步骤是涉及到网络的,为了令编译器模拟在不同系统之间的方法调用过程,需要一个比Socket更高级别的定义。它需要一个操作系统级别(operating system level)的协议构造,使远程过程不必通过Socket调用,而是通过一个语言级别(language-level)的架构。

远程过程调用协议的架构看起来是这个样子:

图片 2

RPC

图片来源:UNIX网络编程(UNIX Network Programming)英文版第693页,作者W. Richard Steven。

进行RPC的核心便是Stub中的函数。在客户端上,Stub中的函数看起来真的是本地调用的函数,例如dao定义的暴露给业务逻辑层调用的接口,但实际上包含通过网络发送和接收消息的代码。

什么是Stub?Stub是一段部署在分布式系统Client端的代码,一方面接收应用层的参数,并对其包装(pakage)后通过socket发送到Server端,另一方面接收Server端序列化后的结果数据,解除包装后交给Client端应用层。

Skeleton功能与Client Stub相反,部署在Server端,从传输层接收序列化参数,将包装的参数再转换后交给Server端的应用层,并将应用层的执行结果包装后最终传送给Client端Stub。

所以客户端调用一个client stub的方法并获取返回结果,就完成了远程过程调用。在这之间的步骤大概如下。

  1. 客户端调用client stub中的过程(procedure),因为client stub部署在客户端,所以调用起来和调用本地方法看上去是一样的。
  2. client stub对客户端传入的参数进行包装(package),例如进行特定RPC框架的标准格式转换,并包装后的数据构建成一个或者多个网络消息。这个过程成为编组(marshaling),需要将数据序列化为平面化的字节数组。
  3. 通过使用Socket接口对本地内核进行系统调用,client stub将网络消息发送到远程系统。
  4. 网络消息由内核通过某种协议(例如无连接的UDP,面向连接的TCP)传输到远程系统。
  5. skeleton对来自消息的参数进行解包装,将它们从标准网络格式转换成特定于机器的形式。
  6. skeleton使用这些参数调用server端的函数或者方法(对客户端来说即远程过程)。
  7. skeleton获取返回结果,并重新包装、编组为消息发送回客户端。
  8. client stub从本地内核读取网络中的结果并进行转换,返回给客户端。客户端代码继续执行,调用结束。

众所周知,TCP 是传输层协议,HTTP 是应用层协议,而传输层较应用层更加底层,在数据传输方面,越底层越快,因此,在一般情况下,TCP 一定比 HTTP 快。就序列化而言,Java 提供了默认的序列化方式,但在高并发的情况下,这种方式将会带来一些性能上的瓶颈,于是市面上出现了一系列优秀的序列化框架,比如:Protobuf、Kryo、Hessian、Jackson 等,它们可以取代 Java 默认的序列化,从而提供更高效的性能。

通信协议

通信协议有很多,常用的比如 HTTP、TCP、UDP等,此外还有如 WebSockets、XMPP、MQTT等,这些都是网络通信,非网络通信的还包括 串口通信、USB等等。

现今除非是极为特殊的环境,一般应该都是使用网络通信协议的,所以就不说非网络通信了。要说网络通信协议就离不开TCP/IP四层模型(现已基本淘汰OSI七层模型),总不能连 HTTP 和 TCP 有什么区别都不知道就做选择吧。对四层模型我们一般只需要关注上面两层:应用层传输层,应用层协议(HTTP、XMPP等)是建立在传输层协议(TCP、UDP)上的。

  • 应用层协议:针对一些特定应用采用特定的格式进行数据传输,数据被编码成标准协议,并传送到下一层。该层的协议相比下层协议提供了更多特性,如QoS、安全性等都要比下层丰富,这些特性为我们提供了很多方便。
  • 传输层协议:目前主要使用的就是TCPUDP两个协议。前者是一个较可靠的、面向连接的传输机制,它提供一种可靠的字节流保证数据完整、无损并且按顺序到达。后者是一个无连接的数据报协议,它是一个“不可靠”协议,因为它不检查数据包是否已经到达目的地,并且不保证它们按顺序到达。相比应用层协议,该层缺少了很多特性,因此需要一些特性的时候就只能自己实现。

所以通信协议的选择最好是结合自己的应用场景所需的特性,考虑是否有较为匹配的协议,如无匹配则考察是否有一些其他工具或技术可以提供相关特性,或者自己是否能够轻松实现这些特性,总之要做到:

  1. 清楚了解自己在通信层面需要什么样的特性
  2. 对各种主要的网络通信协议的特点和特性有所掌握

第四层:传输层。管理着网络中的端到端的数据传输;

RPC相对于Socket的优势

RPC相对于原始的Socket接口,将所有的网络代码隐藏到Stub的函数或方法中,应用程序不必担心Socket、端口号、数据转换和解析等底层细节。带来的优势主要体现在以下两点:

  • 使用过程调用语义来调用远程函数并获得响应
  • 降低编写分布式应用程序的开发成本

针对对象序列化,有各种方式的性能对比,Github地址:

接口风格

应用最广泛的接口风格当属 RPC(远程过程调用)了,从当年的基于SOAP和WSDL的WebServices,到现在的Thrift、Dubbo等,RPC 技术和工具在 SOA 生态中一直扮演着重要的角色。RPC 技术和工具帮助我们解决了网络通信和数据序列化/反序列化的问题,让我们像调用本地方法一样调用远程服务,有些 RPC 框架会帮你生成服务端和客户端桩代码(Thrift等),让你可以快速开发,有些 RPC 框架甚至提供了服务注册、发现能力(Dubbo等),便于你对服务进行水平伸缩。但是 RPC 多多少少会增加一定的耦合性,比如有时服务端的修改也会引起客户端的修改,而且修改后要求服务提供者和消费者要同时发布。

随着万维网技术的日新月异,REST 架构风格也越来越被推崇,但我发现实际上大部分人(包括我)都并没有真正理解 REST 精髓并加以应用。很多人以为使用 HTTP + JSON 并使用了 PUT、DELETE 等方法就是 RESTful 服务了,实际上这只是一种表面形式。首先 REST 并没有说一定是要用 HTTP 协议,也没有说要用 JSON 数据格式,只不过 HTTP 的特性为 REST 提供了更好的支持,所以使用 HTTP 对 REST 来说是一种较好的实践。REST 包含了非常多内容,我也尚在学习中,所以要深入了解 REST 还是建议读读 Roy Fielding 博士论文 以及 Richardson 成熟度模型。不过在使用 REST + HTTP 风格时,相比一些 RPC 方案,自己可能要做更多的工作。

异步通信也是现在的一个主流技术,因为它使得系统资源利用率更高,并有助于降低耦合性,提高系统的扩展性。一些 RPC 框架和 REST 框架也同时提供了同步和异步通信的能力,事件驱动架构(EDA)则能够借助异步技术(如消息系统)实现更加松耦合且易于扩展的应用系统。

在选择接口风格时我认为需要考虑:

  1. 去调用远程的函数或方法,一个健壮的微服务集成环境需要考虑多方面要素。自己当前最需要什么,如易用性、开发速度、熟悉程度、通信性能、系统扩展性、水平伸缩性、同步异步、高可用等等。
  2. 尽可能多的了解各种接口风格及相关技术和工具提供的能力,学习曲线,优缺点等,筛选较为满足自己需要的方案。
  3. 评估筛选出来的技术和工具如果应用到自己的系统中,需要做些什么样的工作,花费多大成本,带来的效果和好处是否值得,有必要的话做一些基准测试,用数据说话。

第五层:网络层。定义网络设备间如何传输数据;

RPC协议在协议分层体系中的位置

计算机网络中主流的协议分层体系即OSI和TCP/IP。在OSI参考模型上,RPC跨越会话层和展示层(session and presentation layers, 层5和层6)。TCP/IP协议族中,RPC属于应用层的内容。

图片 3image.png

数据封装

如果你是用的是 RPC,十有八九你都不需要考虑数据如何封装,因为 RPC 框架基本都已经实现了数据的序列化/反序列化,例如 Protocol buffers、Hession 等。不同的框架在不同场景下的性能表现也各不相同,相关的文章有很多,这里不多赘述。

若你需要自己来选取一种数据封装方案,比如使用 REST + HTTP 时,面对 JSON、XML 或 二进制 等格式该如何选择?我认为可以从以下几方面思考:

  1. 技术无关性。JSON 和 XML 这类格式天生是技术无关的,如果使用 二进制 格式则要避免使用特定技术,比如Java自带的序列化/反序列化。
  2. 数据大小。相比 JSON,XML 可能会略大一点,如果传输的数据量较大,那么不同数据格式封装的结果可能会相差很多。
  3. 性能。不同数据格式各自都有多种序列化/反序列化工具,它们的实现方式使得性能也存在很大差别,有些对较小的数据量处理很高效,对较大数据量则性能低下,有些则恰恰相反。
  4. 使用场景。如果有的客户端可能不对数据反序列化,而是直接取用其中的某个特定部分数据,则使用 XML、JSON 这类格式会更方便,实际上消费方如何使用数据是它自己的事情,服务提供者理应提供这种便利。

第六层:链路层。将上面的网络层的数据包封装成数据帧,便于物理层传输;

RPC的优势

  • 不用考虑端口号的选择问题,RPC封装了对可用端口号的选择并绑定。
  • 独立于运输层,由于skeleton是代码自动生成的,所以会自动兼容传输层的协议。客户端也可以动态选择协议,因为发送和接收消息的代码是自动生成的,业务层无需关心这些问题。
  • 客户端上的应用程序只需要知道一个传输地址:负责告诉应用程序在哪里连接一组给定的服务器功能的名称服务器。
  • 使用函数调用模型代替Socket接口

通过对比可知:

同步异步

何时用同步何时用异步?我觉得这个问题现今已经没有多少意义了。因为同步能做到的,异步也都能做到,例如在异步调用中注册一个回调,或者使用 Future + Listener 这种模型,而相比异步,同步会产生大量阻塞,带来延迟,对系统资源的利用率低下。

那么为什么很多系统仍然在使用同步?因为简单。同步的开发能够很快上手,需要做的工作也相对较少,而异步技术对很多开发者尚存在一定的壁垒,如果完全自己来实现异步要做的工作也不少。

前篇提到了领域驱动设计(DDD),我们可以使用其中的思想来将单块应用分解为多个微服务,在 DDD 中较为推崇的一种协作方式是事件驱动架构(EDA),在微服务中这种架构更是如鱼得水。基于事件的协作能让微服务更加松耦合,业务完全内聚在各个微服务中,不需要有一个集中控制业务逻辑的中枢,从而大大提高系统的可扩展性。

所以我的想法是,

  1. 如果开发团队了解EDA那么尽可能使用EDA。
  2. 如果开发团队不了解EDA但是熟悉异步,那么结合业务的实际情况合理使用异步技术。
  3. 如果开发团队即不了解EDA也不熟悉异步,那么可以用同步快速开发,然后逐步建立异步的知识体系,随着业务的开展针对系统中对低延迟、低耦合要求高的地方用异步一步步进行重构。

第七层:物理层。这一层主要就是传输这些二进制数据。

RPC实现要点

实现一个RPC,一般有如下几点需要考虑。

  • Google的Protostuff性能最好
  • JSON/XML性能比较差

结语

本篇主要针对微服务集成中的基础——通信——该如何选择合适方案谈了我的思路,指出了一些方面是我在做选择时会去考量的,不是十分全面,更不敢说都是正确的,有什么不妥之处欢迎和我交流。

实际应用过程中,五层协议结构里面是没有表示层和会话层的。应该说它们和应用层合并了。我们应该将重点放在应用层和传输层这两个层面。因为HTTP是应用层协议,而TCP是传输层协议。好,知道了网络的分层模型以后我们可以更好地理解为什么RPC服务相比HTTP服务要Nice一些!

参数传递

一般来说参数传递可以有引用传递和值传递。由于远端和本地端内存位置可能有差异,所以引用传递是没有意义的。如果是引用型数据结构,必须转换成平面结构。例如树必须转换成扁平化树(flattened tree)。

但是JSON/XML方式在互联网领域应用比较广泛,第三方的解析包也比较容易使用,所以在效率要求不是很高的情况下是一种不错的选择。

RPC服务

数据表示

在本地系统上不存在数据不兼容问题,因为数据格式总是一样的。而网络调用中,远程机器可能具有不同的字节顺序,不同的整数大小以及不同的浮点表示。所以在网络上传输的数据需要进行序列化

IP协议强制对header中的所有16位和32位字段使用大端字节排序。而RPC中需要一个标准的数据结构。一般有隐式(implicit typing)和显式(explicit typing)两种

  • 隐式:只传输值,而不是变量的名称或类型。
  • 显式:每个字段的类型与值一起传输。

JSON、Protocol Buffers、XML都是显式格式。

dubbo作为一种服务治理框架,RPC作为其中的内部通信方式,使用也是非常简单:

从三个角度来介绍RPC服务:分别是RPC架构,同步异步调用以及流行的RPC框架。

传输协议

某些RPC实现只允许使用一个传输层协议(例如TCP)。大多数RPC支持实现多个并允许用户选择。

@Componentpublic class CityDubboConsumerService { @Reference(version = "1.0.0") CityDubboService cityDubboService; public void printCity() { String cityName="xx"; City city = cityDubboService.findCityByName; System.out.println(city.toString; }}

RPC架构

主机和端口绑定

我们需要找到一台远程主机,并在主机上找到合适的进程(端口或传输地址)。一个解决方案是维护一个中央数据库,可以找到一个提供服务类型的主机。

此外还需要考虑错误重试、性能、安全性问题。

  • 在不理解RPC概念的情况下,会认为RPC就只有这种应用,其实开发中经常使用的HTTPClient调用也是属于RPC的一种方式。

先说说RPC服务的基本架构吧。允许我可耻地盗一幅图哈~我们可以很清楚地看到,一个完整的RPC架构里面包含了四个核心的组件,分别是Client,Server,Client Stub以及Server Stub,这个Stub大家可以理解为存根。分别说说这几个组件:

Stub代码生成

很多主流的开发语言并没有生成Stub代码的语法,所以不能直接支持RPC。一个比较通用的解决办法是提供一个编译器把Client/Server Stub代码编译出来:

图片 4

Stub

由于RPC的跨平台特性,所以需要一个平台无关的语言来描述这种RPC接口的特性,这样的语言就叫做接口描述语言(interface definition language, IDL)。加入IDL后,整个流程大概就是这个样子:

图片 5

图片来自美团点评技术团队:序列化和反序列化

1、基于TCP的远程调用

  • 服务消费者
public class Consumer { public static void main(String[] args) throws UnknownHostException, IOException, SecurityException, NoSuchMethodException, ClassNotFoundException{ //接口名称 String interfacename= SayHelloService.class.getName(); //需要远程执行的方法 Method method = SayHelloService.class.getMethod("sayHello", java.lang.String.class); //需要传递到远端的参数 Object[] arguments = {"hello"}; Socket socket = new Socket("127.0.0.1", 1234); //将方法名称和参数传递到远端 ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream; output.writeUTF(interfacename); //接口名称 output.writeUTF(method.getName; //方法名称 output.writeObject(method.getParameterTypes; output.writeObject(arguments); //从远端读取方法执行结果 ObjectInputStream input = new ObjectInputStream(socket.getInputStream; Object result = input.readObject(); //使用代理对象来处理,直接返回string类型 System.out.println; }}
  • 服务提供者
public class Provider { //所有的服务 private static Map<String,Object> services = new HashMap<String,Object>(); static{ services.put(SayHelloService.class.getName(), new SayHelloServiceImpl; } public static void main(String[] args) throws IOException, ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException{ ServerSocket server = new ServerSocket; while { Socket socket = server.accept(); //读取服务信息 ObjectInputStream input = new ObjectInputStream(socket.getInputStream; String interfacename = input.readUTF(); //接口名称 String methodName = input.readUTF(); //方法名称 Class<?>[] parameterTypes = (Class<?>[])input.readObject(); //参数类型 Object[] arguments = input.readObject(); //参数对象 //执行调用 Class serviceinterfaceclass = Class.forName(interfacename);//得到接口的class Object service = services.get(interfacename);//取得服务实现的对象 Method method = serviceinterfaceclass.getMethod(methodName, parameterTypes);//获得要调用的方法 Object result = method.invoke(service, arguments); ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream; output.writeObject; } }}
  • 接口
public interface SayHelloService { /** * 问好的接口 * @param helloArg 参数 * @return */ public String sayHello(String helloArg);}
  • 实现类
public class SayHelloServiceImpl implements SayHelloService { @Override public String sayHello(String helloArg) { if(helloArg.equals{ return "hello"; }else{ return "bye bye"; } }}

客户端,服务的调用方。

接口描述语言

接口描述语言用于描述接口定义,接口定义就像是Java接口的方法声明,RPC编译后,这些声明就会织入端中。WebService的WSDL(XML为基础)是一个IDL的例子。

很多数据表示格式本身也作为RPC中序列化的数据结构,也能作为IDL编译Stub代码。比如Yaml、Json、xml、PB等等,这些都可以作为接口描述语言。

关于IDL的选择参考Api 体系架构分享(上)

2、基于HTTP的远程调用

  • 基础服务接口
public interface BaseService { public Object execute(Map<String,Object> args);}
  • JSON结果集
public class JsonResult { //结果状态码 private int resultCode; //状态码解释消息 private String message; //结果 private Object result; public int getResultCode() { return resultCode; } public void setResultCode(int resultCode) { this.resultCode = resultCode; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Object getResult() { return result; } public void setResult(Object result) { this.result = result; }}
  • JSON帮助类
public class JsonUtil { private static final ObjectMapper mapper = new ObjectMapper(); public static Object jsonToObject(String json, Class cls) { try{ //允许json串里面的key value不带双引号 mapper.configure(org.codehaus.jackson.JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); // 允许制定的object中的属性没有json串中某个key mapper.configure(org.codehaus.jackson.map.DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false); return mapper.readValue(json, cls); }catch(Exception e){} return null; } public static String getJson(Object object) { try{ String json = null; StringWriter sw = new StringWriter(); JsonGenerator gen = new JsonFactory().createJsonGenerator; mapper.writeValue(gen, object); gen.close(); json = sw.toString(); return json; }catch(Exception e){} return null; }}

public class SayHelloService implements BaseService{ public Object execute(Map<String, Object> args) { //request.getParameterMap() 取出来为array,此处需要注意 String[] helloArg =  args.get; if("hello".equals(helloArg[0])){ return "hello"; }else{ return "bye bye"; } }}
  • 服务消费者
public class ServiceConsumer extends HttpServlet{ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //参数 String service = "com.http.sayhello"; String format = "json"; String arg1 = "hello"; String url = "http://localhost:8080//testhttprpc/provider.do?"+"service=" + service + "&format=" + format + "&arg1=" + arg1; //组装请求 HttpClient httpClient = new DefaultHttpClient(); HttpGet httpGet = new HttpGet; //接收响应 HttpResponse response = httpClient.execute; HttpEntity entity = response.getEntity(); byte[] bytes = EntityUtils.toByteArray; String jsonresult = new String(bytes, "utf8"); JsonResult result = (JsonResult)JsonUtil.jsonToObject(jsonresult, JsonResult.class); resp.getWriter().write(result.getResult().toString; }}
  • 服务提供者
public class ServiceProvider extends HttpServlet{ private Map<String,BaseService> serviceMap ; @Override public void init() throws ServletException { //服务map初始化 serviceMap = new HashMap<String,BaseService>(); serviceMap.put("com.http.sayhello", new SayHelloService; } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //基本参数 String servicename = req.getParameter("service"); String format = req.getParameter; Map parameters = req.getParameterMap(); BaseService service = serviceMap.get(servicename); Object result = service.execute(parameters); //生成json结果集 JsonResult jsonResult = new JsonResult(); jsonResult.setResult; jsonResult.setMessage("success"); jsonResult.setResultCode; String json = JsonUtil.getJson(jsonResult); resp.getWriter().write; }}

服务端,真正的服务提供者。

早期的RPC

第一代RPC不支持对象的传递,比较典型的有ONC RPC,OSF RPC。

3、URL风格

  • RPC风格的URL
  • RESTFUL风格的URL

RPC风格的URLhttp://hostname/provider.do?service=com.http.sayhello&format=json&timest amp=2017-04-07-13-22-09&arg1=arg1&arg2=arg2

  • hostname表示服务提供方的主机名
  • service表示远程调用的服务接口名称
  • format表示返回参数的格式
  • timestamp表示客户端请求的时间戳
  • arg1和 arg2表示服务所需要的参数
  • 备注:淘宝开放平台的API以这种形式的URL提供

RESTFUL风格的URL

POST http://hostname/people 创建name为zhangsan的people记录GET http://hostname/people/zhangsan 返回name为zhangsan的people记录PUT http://hostname/people/zhangsan 提交name为zhangsan的people记录更新 DELETE http://hostname/people/zhangsan 删除name为zhangsan的people记录
  • 本文内容部分摘自《大型分布式网站架构》

客户端存根,存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。

面向对象的RPC

随着面向对象语言的流行,现有的RPC机制和框架实现虽然能够完成需求,但是不支持面向对象的操作。例如从远程类中实例化一个对象、跟踪对象实例、多态支持等。加入这些面向对象的支持成为一种趋势。

服务端存根,接收客户端发送过来的消息,将消息解包,并调用本地的方法。

微软的COM和DCOM

COM,包括后来的DCOM、COM+,都并没有真正实现跨平台,它们主要用于Windows,是微软实现的RPC框架。COM的序列化的原理利用了编译器中虚表,使得其学习成本巨大。由于序列化的数据与编译器紧耦合,扩展属性非常麻烦。

想一下这个场景,工程师需要是简单的序列化协议,但却要先掌握语言编译器,可想而知。

和大多数早期RPC系统一样,DCOM也不能很好地跨越防火墙,因此防火墙必须允许流量在ORPC和DCOM使用的某些端口之间流动。

图片 6

CORBA

CORBA是早期比较好的实现了跨平台,跨语言的序列化协议(RPC框架)。CORBAR为了解决异构平台的 RPC,首先使用IDL来定义远程接口,并将其映射到特定的平台语言中。但是CORBA太复杂,其参与方过多带来的版本过多,版本之间兼容性又差,最终导致COBRA的消亡。

J2SE 1.3之后的版本提供了基于CORBA协议的RMI-IIOP技术,这使得Java开发者可以采用纯粹的Java语言进行CORBA的开发。

CORBA的IDL看起来像这个样子:

Module StudentObject {
    Struct StudentInfo {
        String name;
        int id;
        float gpa;
    };
    exception Unknown {};
    interface Student {
        StudentInfo getinfo(in string name)
            raises(unknown);
        void putinfo(in StudentInfo data);
    };
};

RPC主要是用在大型企业里面,因为大型企业里面系统繁多,业务线复杂,而且效率优势非常重要的一块,这个时候RPC的优势就比较明显了。实际的开发当中是这么做的,项目一般使用maven来管理。比如我们有一个处理订单的系统服务,先声明它的所有的接口(这里就是具体指Java中的interface),然后将整个项目打包为一个jar包,服务端这边引入这个二方库,然后实现相应的功能,客户端这边也只需要引入这个二方库即可调用了。为什么这么做?主要是为了减少客户端这边的jar包大小,因为每一次打包发布的时候,jar包太多总是会影响效率。另外也是将客户端和服务端解耦,提高代码的可移植性。

Java RMI

Java RMI没有IDL,所以也就不支持跨平台,但是对于Java程序员而言显得更直接简单,降低使用的学习成本。

除了跨平台,Java RMI还有一个问题是其序列化机制。使用Java序列化写出元数据(meta-data)是非常昂贵的。Java Serializable序列化不仅写入完整类名,也包含整个类的定义,包含所有被引用的类。类定义可以是相当大的,可能会构成性能和效率的问题,当然这是编写一个单一的对象。如果序列化大量相同的类的对象,这时类定义的开销通常不是一个大问题。除此之外,如果对象有一个类的引用,那么Java序列化将写入整个类的定义,不只是类的名称,

同步调用与异步调用

支持Web的RPC

传统的RPC协议也可以在Web下工作,但是它的Socket端口是动态选择的。但是防火墙可能为了安全性限制大部分端口的访问,只允许开启某些协议端口,并检查协议格式是否正确,例如是否是一个正确的HTTP请求。

什么是同步调用?什么是异步调用?同步调用就是客户端等待调用执行完成并返回结果。异步调用就是客户端不等待调用执行完成返回结果,不过依然可以通过回调函数等接收到返回结果的通知。如果客户端并不关心结果,则可以变成一个单向的调用。这个过程有点类似于Java中的callable和runnable接口,我们进行异步执行的时候,如果需要知道执行的结果,就可以使用callable接口,并且可以通过Future类获取到异步执行的结果信息。如果不关心执行的结果,直接使用runnable接口就可以了,因为它不返回结果,当然啦,callable也是可以的,我们不去获取Future就可以了。

XML-RPC

XML-RPC是1998年设计的一种RPC消息传递协议,用于将程序请求和响应编入人可读的XML中。XML-RPC中,参数使用XML格式作为数据结构,并通过HTTP协议传输,不必为RPC服务器应用程序打开其他端口,解决了传统的企业防火墙的端口限制问题。

XML每个字段的类型与值一起传输,是显式传输的数据结构。

流行的RPC框架

SOAP和Web Service

SOAP(Simple Object Access Protocol)是随着XML-RPC的流行而发展起来的一种规范化的对象传输协议。

由于该协议一点儿也不简单,并且不限于访问对象,因此该首字母缩略词已被丢弃。

SOAP使用XML格式作为无状态消息交换格式,支持包括RPC式的过程调用以及multipart响应。但是SOAP只是提供一个标准的消息传递结构,为了正确创建SOAP消息,还要一种描述服务的方法。Web Service使用WSDL(Web Services Description Language)来描述Web Service的服务。这是一个XML文档,可以被送入一个程序,该程序将生成将发送和接收SOAP消息的软件。WSDL就是一个IDL,用于生成Stub。

SOAP和XML-RPC的区别:

  • SOAP设计更复杂,功能更强大
  • XML-RP不需要对参数命名,按传入顺序传入和读出;SOAP通过参数名确定参数,无顺序。
  • XML-RPC更适配Python

SOAP和XML-RPC的区别可以查看Difference Between RPC and SOAP

目前流行的开源RPC框架还是比较多的。下面重点介绍三种:

REST

REST(Representational State Transfer)遵循Web原则,使用HTTP作为协议的核心部分。并将HTTP中的PUT/GET/POST/DELETE操作和对资源的insdert/select/updaate/delete操作对应。

REST的思想是使用HTTP的命令来对数据进行获取和操作。REST使用URL来引用资源和相应操作。URL作为HTTP的一部分,提供了分层命名(hierachical naming)格式和参数属性值列表(attribute-value lists)。

REST不是RPC,但有一个类似的请求-响应模型。生成请求、消息生成和解析响应不是REST的一部分。但是REST对面向资源的服务很有意义,例如使用HTTP发送如下URL请求:

HTTP GET //www.xxxx.com/parts

返回一个包含请求数据的某种格式的数据结构,如JSON:

{"data":"null"}

gRPC是Google最近公布的开源软件,基于最新的HTTP2.0协议,并支持常见的众多编程语言。 我们知道HTTP2.0是基于二进制的HTTP协议升级版本,目前各大浏览器都在快马加鞭的加以支持。 这个RPC框架是基于HTTP协议实现的,底层使用到了Netty框架的支持。

JSON

使用JSON作为数据格式来发送数据已经十分流行。由于它不是二进制数据格式,所以更适合作为HTTP消息的载体。

但是JSON受JavaScript语言子集的限制,可表示的数据类型不够多,而且无法表示数据内的复杂引用,如自引用,互引用和循环引用。另外,某些语言具有多种JSON版本的实现,但在类型影射上没有统一标准,存在兼容性问题。

JSON只是一种消息传递格式,JSON不会尝试提供RPC库并支持服务发现、绑定、托管和垃圾回收。使用JSON作为数据转换格式的RPC成为JSON-RPC。JSON-RPC 虽然有规范,但是却没有统一的实现。在不同语言中的各自实现存在兼容性问题,无法真正互通。

Thrift是Facebook的一个开源项目,主要是一个跨语言的服务开发框架。它有一个代码生成器来对它所定义的IDL定义文件自动生成服务代码框架。用户只要在其之前进行二次开发就行,对于底层的RPC通讯等都是透明的。不过这个对于用户来说的话需要学习特定领域语言这个特性,还是有一定成本的。

无需IDL?

JSON来源于JavaScript中的"Associative array",由于"Associative array"在弱类型语言中本身就是类的概念,所以在这些弱语言如JavaScript、PHP中得到了良好的支持。并且,因为JSON中的字段一般可以和类中的属性名称和值一一对应,所以对于Java这强类型语言可以通过反射操作统一转换。

Dubbo是阿里集团开源的一个极为出名的RPC框架,在很多互联网公司和企业应用中广泛使用。协议和序列化框架都可以插拔是及其鲜明的特色。同样 的远程接口是基于Java Interface,并且依托于spring框架方便开发。可以方便的打包成单一文件,独立进程运行,和现在的微服务概念一致。

Google Protocol Buffers

Google Protocol Buffers本质上只是一种序列化机制,并不是完整的RPC。它仅仅简化了网络传输中编组(marshaling)和解组(unmarshaling)的流程。protobuf为结构化数据的序列化提供了一种高效的机制,使得将数据编码到网络上并解码接收到的数据变得容易。Protobuf定义了一种平台独立的数据结构类型,使得其序列化后的数据十分紧凑,解析非常高效。与很多IDL类似,protobuf的消息结构以高级格式定义,包含名称、类型和值。protobuf既可用于类似RPC的消息传递,也可用于持久性存储,您需要将数据转换为标准的串行形式以将其写入文件。一个例子是:

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

和XML相比,protobuf生成的字节码短了2.5倍,解析速度快50倍左右。

其他常见的RPC框架优缺点可以参考什么是RESTful?到底REST和SOAP、RPC有何区别?中其中一个回答。

偷偷告诉你集团内部已经不怎么使用dubbo啦,现在用的比较多的叫HSF,又名“好舒服”。后面有可能会开源,大家拭目以待。

文献

RPC的拆解和实现可以查阅RPC的概念模型与实现解析

HTTP服务

引用

Remote Procedure Calls - Paul Krzyzanowski
Difference Between RPC and SOAP
序列化和反序列化 - 美团点评技术团队
Api 体系架构分享(上)

其实在很久以前,我对于企业开发的模式一直定性为HTTP接口开发,也就是我们常说的RESTful风格的服务接口。的确,对于在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。利用现成的http协议进行传输。我们记得之前本科实习在公司做后台开发的时候,主要就是进行接口的开发,还要写一大份接口文档,严格地标明输入输出是什么?说清楚每一个接口的请求方法,以及请求参数需要注意的事项等。比如下面这个例子:

POST

接口可能返回一个JSON字符串或者是XML文档。然后客户端再去处理这个返回的信息,从而可以比较快速地进行开发。但是对于大型企业来说,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了,首先就是长链接,不必每次通信都要像http一样去3次握手什么的,减少了网络开销;其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。

总结

RPC服务和HTTP服务还是存在很多的不同点的,一般来说,RPC服务主要是针对大型企业的,而HTTP服务主要是针对小企业的,因为RPC效率更高,而HTTP服务开发迭代会更快。总之,选用什么样的框架不是按照市场上流行什么而决定的,而是要对整个项目进行完整地评估,从而在仔细比较两种开发框架对于整个项目的影响,最后再决定什么才是最适合这个项目的。一定不要为了使用RPC而每个项目都用RPC,而是要因地制宜,具体情况具体分析。

本文由9159.com发布于编程,转载请注明出处:去调用远程的函数或方法,一个健壮的微服务集

关键词:

上一篇:没有了
下一篇:没有了