RPC(Remote Procedure Call):远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想。
RPC 是一种技术思想而非一种规范或协议,常见 RPC 技术和框架有:
- 应用级的服务框架:阿里的 Dubbo/Dubbox、Google gRPC、Spring Boot/Spring Cloud、Facebook 的 Thrift、Twitter 的 Finagle 等。
- 远程通信协议:RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)。
- 通信框架:MINA 和 Netty。
- ps: Google gRPC 框架是基于 HTTP2 协议实现的,底层使用到了 Netty 框架的支持。
1. RPC 框架
一个典型 RPC 的使用场景中,包含了服务发现、负载、容错、网络传输、序列化等组件,其中“RPC 协议”就指明了程序如何进行网络传输和序列化。
图 1:完整 RPC 架构图
2. RPC 核心功能
一个 RPC 的核心功能主要有 5 个部分组成,分别是:客户端、客户端 Stub、网络传输模块、服务端 Stub、服务端等。
图 4:RPC 核心功能图
下面分别介绍核心 RPC 框架的重要组成:
- 客户端(Client):服务调用方。
- 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
- 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
- 服务端(Server):服务的真正提供者。
- Network Service:底层传输,可以是 TCP 或 HTTP。
一次 RPC 调用流程如下:
- 服务消费者(Client 客户端)通过本地调用的方式调用服务。
- 客户端存根(Client Stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体。
- 客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端。
- 服务端存根(Server Stub)收到消息后进行解码(反序列化操作)。
- 服务端存根(Server Stub)根据解码结果调用本地的服务进行相关处理
- 服务端(Server)本地服务业务处理。
- 处理结果返回给服务端存根(Server Stub)。
- 服务端存根(Server Stub)序列化结果。
- 服务端存根(Server Stub)将结果通过网络发送至消费方。
- 客户端存根(Client Stub)接收到消息,并进行解码(反序列化)。
- 服务消费方得到最终结果。
RPC的目标就是要2~10这些步骤都封装起来,让用户对这些细节透明。
3. RPC 核心功能实现技术点
- 透明化远程服务调用:字节码生成,JDK动态代理
- 编码与解码
- 服务寻址:Call ID 映射,可以直接使用函数字符串,也可以使用整数 ID。映射表一般就是一个哈希表。
- 数据流的序列化和反序列化:可以自己写,也可以使用 Protobuf 或者 FlatBuffers 之类的。
- 网络传输:可以自己写 Socket,或者用 Asio,ZeroMQ,Netty 之类。在 RPC 中可选的网络传输方式有多种,可以选择 TCP 协议、UDP 协议、HTTP 协议
3.1 透明化远程服务调用(代理)
- jdk 动态代理:更多使用动态代理
- 字节码生成:更为强大和高效,但代码维护不易
3.2 服务寻址(服务注册中心)
实现方式:服务注册中心。
Call ID 映射
/** 1) 服务寻址可以使用 Call ID 映射。在本地调用中,函数体是直接通过函数指针来指定的,但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。 2) 所以在 RPC 中,所有的函数都必须有自己的一个 ID。这个 ID 在所有进程中都是唯一确定的。 3) 客户端在做远程过程调用时,必须附上这个 ID。然后我们还需要在客户端和服务端分别维护一个函数和Call ID的对应表。 4) 当客户端需要进行远程调用时,它就查一下这个表,找出相应的 Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。 **/
实现案例:RMI(Remote Method Invocation,远程方法调用)也就是 RPC 本身的实现方式。
图 9:RMI 架构图
Registry(服务发现):借助 JNDI 发布并调用了 RMI 服务。实际上,JNDI 就是一个注册表,服务端将服务对象放入到注册表中,客户端从注册表中获取服务对象。
3.3 编码与解码
客户端的请求消息结构一般需要包括以下内容: /** 1)接口名称:服务端就调用的那个接口; 2)方法名:一个接口内可能有很多方法,如果不传方法名服务端也就不知道调用哪个方法; 3)参数类型&参数值:参数类型有很多,比如有bool、int、long、double、string、map、list,甚至如struct(class);以及相应的参数值; 4)超时时间 5)requestID,标识唯一请求id */ 服务端返回的消息结构一般包括以下内容。 /** 1)返回值 2)状态code 3)requestID */
3.4 序列化与反序列化
客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。
但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。
这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。
只有二进制数据才能在网络中传输,序列化和反序列化的定义是:
- 将对象转换成二进制流的过程叫做序列化
- 将二进制流转换成对象的过程叫做反序列化
这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
目前互联网公司广泛使用Protobuf、Thrift、Avro等成熟的序列化解决方案来搭建RPC框架,这些都是久经考验的解决方案。
3.5 网络传输
网络传输:远程调用往往用在网络上,客户端和服务端是通过网络连接的。
所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把 Call ID 和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。
网络协议 1. 尽管大部分 RPC 框架都使用 TCP 协议,但其实 UDP 也可以,而 gRPC 干脆就用了 HTTP2。 2. 在 RPC 中可选的网络传输方式有多种,可以选择 TCP 协议、UDP 协议、HTTP 协议 网络框架 可以自己写 Socket,或者用 Asio,ZeroMQ,Netty 之类。
网络通信
目前有两种常用IO通信模型:1)BIO;2)NIO。一般RPC框架需要支持这两种IO模型。
如何实现RPC的IO通信框架呢?
1. 使用java nio方式自研,这种方式较为复杂,而且很有可能出现隐藏bug,但也见过一些互联网公司使用这种方式;
2. 基于mina,mina在早几年比较火热,不过这些年版本更新缓慢;
3. 基于netty,现在很多RPC框架都直接基于netty这一IO通信框架,省力又省心,比如阿里巴巴的HSF、dubbo,Twitter的finagle等。
基于 TCP 协议的 RPC 调用 /** 由服务的调用方与服务的提供方建立 Socket 连接,并由服务的调用方通过 Socket 将需要调用的接口名称、方法名称和参数序列化后传递给服务的提供方,服务的提供方反序列化后再利用反射调用相关的方法。 但是在实例应用中则会进行一系列的封装,如 RMI 便是在 TCP 协议上传递可序列化的 Java 对象。 */ 基于 HTTP 协议的 RPC 调用 /** 该方法更像是访问网页一样,只是它的返回结果更加单一简单。 其大致流程为:由服务的调用者向服务的提供者发送请求,这种请求的方式可能是 GET、POST、PUT、DELETE 等中的一种,服务的提供者可能会根据不同的请求方式做出不同的处理,或者某个方法只允许某种请求方式。 而调用的具体方法则是根据 URL 进行方法调用,而方法所需要的参数可能是对服务调用方传输过去的 XML 数据或者 JSON 数据解析后的结果,返回 JOSN 或者 XML 的数据结果。 由于目前有很多开源的 Web 服务器,如 Tomcat,所以其实现起来更加容易,就像做 Web 项目一样。 */ 两种方式对比 /** 基于 TCP 的协议实现的 RPC 调用,由于 TCP 协议处于协议栈的下层,能够更加灵活地对协议字段进行定制,减少网络开销,提高性能,实现更大的吞吐量和并发数。 但是需要更多关注底层复杂的细节,实现的代价更高。同时对不同平台,如安卓,iOS 等,需要重新开发出不同的工具包来进行请求发送和相应解析,工作量大,难以快速响应和满足用户需求。 基于 HTTP 协议实现的 RPC 则可以使用 JSON 和 XML 格式的请求或响应数据。 而 JSON 和 XML 作为通用的格式标准(使用 HTTP 协议也需要序列化和反序列化,不过这不是该协议下关心的内容,成熟的 Web 程序已经做好了序列化内容),开源的解析工具已经相当成熟,在其上进行二次开发会非常便捷和简单。 但是由于 HTTP 协议是上层协议,发送包含同等内容的信息,使用 HTTP 协议传输所占用的字节数会比使用 TCP 协议传输所占用的字节数更高。 因此在同等网络下,通过 HTTP 协议传输相同内容,效率会比基于 TCP 协议的数据效率要低,信息传输所占用的时间也会更长,当然压缩数据,能够缩小这一差距。 */
4. 摘录网址
- 花了一个星期,我终于把RPC框架整明白了!
- RPC原理及RPC实例分析