RPC定义
RPC(Remote Procedure Call)全称远程过程调用,它指的是通过网络,我们可以实现客户端调用远程服务端的函数并得到返回结果。这个过程就像在本地电脑上运行该函数一样,只不过系统本身隐藏了发送的过程和细节。
待解决的问题
(1)既然是远程调用服务端的服务,这就意味着服务端必须知道客户端传递过来函数名、参数名、以及表示方法的意思。这就引出了其中一个问题就是协议约定问题,在客户端和服务器进行服务调用之前,我们需要规定参数所代表的意思,怎么划分边界等等。
(2)客户端和服务端通过网络进行调用,而在网络中会存在错误、重传、丢包等问题,这些问题我们统称为传输问题。怎么解决这些问题也成为服务是否能实现的关键问题。
(3)服务发现问题,客户端怎么知道哪些服务可用?以及服务的端口号?这样可以避免对不可用的服务和错误的端口进行请求的时间浪费。
对于上面的问题,不同的人有不同的解决办法。后来一个叫Nelson的人写了一片论文来定义RPC问题的解决方式和框架。
从上图我们可以看到,在客户端中,当用户进行本地调用的时候,会将参数等数据传递给本地的Stub,Stub将参数封装成双方协议规定的模式,然后发送给PRCRuntime模块,该模块主要的任务是进行网络任务,将封装好的参数发送出去,在其中并处理其他会出现的网络异常情况。服务端的PRCRuntime在接收到请求之后将数据传递给Stub模块对数据按照约定的格式进行解封,然后按照客户端的意思调用相应的函数得到结果执行同样的流程进行发送。
RPC的早期(经典)应用
RPC最早的应用是在NFS(网络文件系统)中,在这个文件系统中我们需要启动两个服务端,一个是mount(挂载文件路径),一个是nfsd(读写文件)。 NFS主要作用是我们可以像操作本地文件一样操作远程服务端上文件的读写。其架构图如下:
在图中XDR(External Data Representation ,外部数据表示法)进行的是像Stub的工作,对数据按照协议进行相应的封装和解封(压缩成二进制)。例如下图:
socket进行数据网络的传输,当然这不是简单的socket编程,这里还包含了对于异常的处理,例如超时,重试等问题。这里有一个比较出名的RPC状态转换图。可以看出对应不同的状态有相应的处理方式。
对于服务发现问题,在NFS中通过portmapper实现的。portmapper 会启动在一个众所周知的端口上,RPC 程序由于是用户自己写的,会监听在一个随机端口上,但是 RPC 程序启动的时候,会向 portmapper 注册。客户端要访问 RPC 服务端这个程序的时候,首先查询 portmapper,获取 RPC 服务端程序的随机端口,然后向这个随机端口建立连接,开始 RPC 调用。从图中可以看出,mount 命令的 RPC 调用,就是这样实现的。
NFS存在的缺点:(1) 需要双方的压缩格式完全一致,多一位,少一位都可能造成无法解压缩。这一点可以用传输层的可靠性以及加入校验值等方式(冗余检验码)来减少传输过程中的差错。(2) 协议修改不灵活,如果不是传输过程中造成的差错,而是客户端添加或者删除了某些字段或者服务端添加或者删除了字段,而双方没有及时通知,就会造成解压缩不成功。(3)版本的问题,比如在服务端提供一个服务,参数的格式是版本一的,因为其中一个客户端需要对服务多加一个字段,如果服务端加上这一个字段,这会导致所有的客户端都必须加上这个字段。
基于XML的SOAP协议
在上面NFC的系统中网络传输这块使用的是二进制形式进行传输,并且参数的封装都是严格按照描述文件进行封装的,可读性也比较差。那既然这样我们不采用二进制的形式进行传输,使用文本形式。比如xml格式。一个比较著名的通信协议SOAP(Simple Object Access Protocol,简单对象访问协议)就是使用XML编写请求和回复,使用HTTP进行传输来解决传输问题。例如下面HTTP的请求头的格式,表示传输一个格式为application/soap+xml的XML文件给www.test.com。
POST /purchaseOrder HTTP/1.1
Host: www.test.com
Content-Type: application/soap+xml; charset=utf-8
Content-Length: nnn
这是application/soap+xml的文件内容
<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2001/12/soap-envelope"
soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">
<soap:Header>
<m:Trans xmlns:m="http://www.w3schools.com/transaction/"
soap:mustUnderstand="1">1234
</m:Trans>
</soap:Header>
<soap:Body xmlns:m="http://www.test.com/perchaseOrder">
<m:purchaseOrder">
<order>
<date>2018-07-01</date>
<className> SOAP </className>
<Author> GGG </Author>
</order>
</m:purchaseOrder>
</soap:Body>
</soap:Envelope>
协议约定问题:在这个过程中我们使用一种叫做Web服务描述语言,WSDL(Web Service Description Languages)也是一个XML文件。服务端对应的WSDL描述文件比较复杂,我们一般可以使用工具进行自动生成。然后客户端根据描述文件进行调用,这里一般的WSDL文件对于客户端的使用者来说晦涩难懂,同时也有相应的工具生成相应的Stub程序来进行协议的封装,让客户端直接调用。
服务发现问题:在SOAP中有一个UDDI(Universal Description, Discovery, and Integration,统一描述、发现和集成协议)。它其实是一个注册中心,服务提供方可以将上面的 WSDL 描述文件,发布到这个注册中心,注册完毕后,服务使用方可以查找到服务的描述,封装为本地的客户端进行调用。
缺点:基于XML的SOAP协议虽然使用了文本形式,但是WSDL对于用户来说还是太复杂,对于用户来说并不是很友好。另外,不管是SOAP还是NFS系统,服务端都需要记录不同客户端的状态,这样的情况下如果客户端数目不是很多还能承受,但是一旦客户端数目比较大的时候服务器的负担就会大大增加。
基于JSON的RESTful接口协议
RESTful指的是一种架构风格,同时也包含API的设计规范。他是由Fielding(HTTP协议的制定者)的博士论文《Architectural Styles and the Design of Network-based Software Architectures》(构风格与基于网络的软件架构设计)所提出来的。他在文中地论证了一个互联网应用应该有的设计要点,而这些设计要点成为后来我们能看到的所有高并发应用设计都必须要考虑的问题,再加上 REST API 比较简单直接,所以后来几乎成为互联网应用的标准接口。
传输协议问题:RESTful使用HTTP协议进行传输JSON序列化的数据,另外在HTTP中有相应操作的动词。例如GET、POST、DELETE等,来表示对相应的组员的对应操作。
协议约定问题:不同于SOAP协议服务端需要存储客户端的状态信息,RESTful采用客户端自己管理自己的状态,当需要请求时,客户端端带上必要的信息对服务器端相应的数据进行请求。这就是所谓的无状态服务,其实是服务端维护资源的状态,客户端维护会话的状态。对于服务端来讲,只有资源的状态改变了,客户端才调用 POST、PUT、DELETE 方法来找我;如果资源的状态没变,只是客户端的状态变了,就不用告诉我了,对于我来说都是统一的 GET。按照这种思路,对于 API 的设计,就慢慢变成了以资源为核心,而非以过程为核心。也就是说,客户端只要告诉服务端你想让资源状态最终变成什么样就可以了,而不用告诉我过程,不用告诉我动作。另外,这种 API 的设计需要实现幂等,因为网络不稳定,就会经常出错进行重试,但是一旦重试,就会存在幂等的问题,意思就是说同一个调用,多次调用的结果应该一样,不能一次支付调用,因为调用三次变成了支付三次。也不能一次扣减库存,调用了三次,就扣减三次库存。
服务发现问题:使用的是Eureka, 他是Netflix开发的服务发现框架,本身是一个基于REST的服务,用于达到负载均衡和中间层服务故障转移的目的。Eureka包含两个组件:Eureka Server和Eureka Client。Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也就是一个内置的、使用轮询(round-robin)负载算法的负载均衡器。