一、介绍
RPC
(
Remote Procedure Call
):远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想。
RPC
是一种技术思想而非一种规范或协议,常见
RPC
技术和框架有:
-
应用级的服务框架:阿里的
Dubbo/Dubbox
、
Google gRPC
、
Spring Boot/Spring Cloud
。 -
远程通信协议:
RMI
、
Socket
、
SOAP(HTTP XML)
、
REST(HTTP JSON)
。 -
通信框架:
MINA
和
Netty
。
下面重点介绍三种:
-
gRPC
:是
Google
公布的开源软件,基于最新的
HTTP 2.0
协议,并支持常见的众多编程语言。
RPC
框架是基于
HTTP
协议实现的,底层使用到了
Netty
框架的支持。 -
Thrift
:是
Facebook
的开源
RPC
框架,主要是一个跨语言的服务开发框架。 用户只要在其之上进行二次开发就行,应用对于底层的
RPC
通讯等都是透明的。不过这个对于用户来说需要学习特定领域语言这个特性,还是有一定成本的。 -
Dubbo
:是阿里集团开源的一个极为出名的
RPC
框架,在很多互联网公司和企业应用中广泛使用。协议和序列化框架都可以插拔是极其鲜明的特色。
二、完整的
RPC
框架
RPC
在一个典型
RPC
的使用场景中,包含了服务发现、负载、容错、网络传输、序列化等组件,其中“
RPC
协议”就指明了程序如何进行网络传输和序列化。
1、
RPC
核心功能
RPC
RPC
的核心功能是指实现一个
RPC
最重要的功能模块,就是上图中的”
RPC
协议”部分:
2、
RPC
组成部分
RPC
一个
RPC
的核心功能主要有
5
个部分组成,分别是:
客户端、客户端
Stub
、网络传输模块、服务端
Stub
、服务端等。
下面分别介绍核心
RPC
框架的重要组成:
-
客户端(
Client
):服务调用方。 -
客户端存根(
Client Stub
):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。 -
服务端存根(
Server Stub
):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。 -
服务端(
Server
):服务的真正提供者。 -
Network Service
:底层传输,可以是
TCP
或
HTTP
。
如果网络传输中使用的是
HTTP
协议,建立
HTTP
协议之间有
TCP
三次握手,断开
HTTP
协议时有
TCP
四次挥手:
3、详细调用过程
RPC
实现流程和分工角色可以用下图来表示:
一次
RPC
调用流程如下:
-
服务消费者(
Client
客户端)通过本地调用的方式调用服务。 -
客户端存根(
Client Stub
)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体。 -
客户端存根(
Client Stub
)找到远程的服务地址,并且将消息通过网络发送给服务端。 -
服务端存根(
Server Stub
)收到消息后进行解码(反序列化操作)。 -
服务端存根(
Server Stub
)根据解码结果调用本地的服务进行相关处理 -
服务端(
Server
)本地服务业务处理。 -
处理结果返回给服务端存根(
Server Stub
)。 -
服务端存根(
Server Stub
)序列化结果。 -
服务端存根(
Server Stub
)将结果通过网络发送至消费方。 -
客户端存根(
Client Stub
)接收到消息,并进行解码(反序列化)。 - 服务消费方得到最终结果。
4、
RPC
核心之功能实现
RPC
RPC
的核心功能主要由
5
个模块组成,如果想要自己实现一个
RPC
,最简单的方式要实现三个技术点,分别是:
- 服务寻址
- 数据流的序列化和反序列化
- 网络传输
4.1、服务寻址
服务寻址可以使用
Call ID
映射。在本地调用中,函数体是直接通过函数指针来指定的,但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。
所以在
RPC
中,所有的函数都必须有自己的一个
ID
。这个
ID
在所有进程中都是唯一确定的。
客户端在做远程过程调用时,必须附上这个
ID
。然后我们还需要在客户端和服务端分别维护一个函数和
Call ID
的对应表。
当客户端需要进行远程调用时,它就查一下这个表,找出相应的
Call ID
,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
实现方式:服务注册中心。
要调用服务,首先你需要一个服务注册中心去查询对方服务都有哪些实例。
Dubbo
的服务注册中心是可以配置的,官方推荐使用
Zookeeper
。
实现案例:
RMI
(
Remote Method Invocation
,远程方法调用)也就是
RPC
本身的实现方式。
Registry(服务发现):借助
JNDI
发布并调用了
RMI
服务。实际上,
JNDI
就是一个注册表,服务端将服务对象放入到注册表中,客户端从注册表中获取服务对象。
RMI
服务在服务端实现之后需要注册到
RMI Server
上,然后客户端从指定的
RMI
地址上
Lookup
服务,调用该服务对应的方法即可完成远程方法调用。
Registry
是个很重要的功能,当服务端开发完服务之后,要对外暴露,如果没有服务注册,则客户端是无从调用的,即使服务端的服务就在那里。
4.2、序列化和反序列化
客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。
但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。
这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。
- 对象转换为有序字节流的过程叫做序列化
- 将二进制流转换成对象的过程叫做反序列化
这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
4.3、网络传输
远程调用往往用在网络上,客户端和服务端是通过网络连接的。
所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把
Call ID
和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。
只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。
尽管大部分
RPC
框架都使用
TCP
协议,但其实
UDP
也可以,而
gRPC
干脆就用了
HTTP2
。
TCP 的连接是最常见的,简要分析基于 TCP 的连接:通常
TCP
连接可以是按需连接(需要调用的时候就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。
所以,要实现一个
RPC
框架,只需要把以下三点实现了就基本完成了:
-
Call ID
映射:可以直接使用函数字符串,也可以使用整数
ID
。映射表一般就是一个哈希表。 -
序列化反序列化:可以自己写,也可以使用
Protobuf
或者
FlatBuffers
之类的。 -
网络传输库:可以自己写
Socket
,或者用
Asio
,
ZeroMQ
,
Netty
之类。
RPC
核心之网络传输协议
要实现一个
RPC
,需要选择网络传输的方式。
在
RPC
中可选的网络传输方式有多种,可以选择
TCP
协议、
UDP
协议、
HTTP
协议。
每一种协议对整体的性能和效率都有不同的影响,如何选择一个正确的网络传输协议呢?首先要搞明白各种传输协议在
RPC
中的工作方式。
基于
TCP
协议的
RPC
调用
由服务的调用方与服务的提供方建立
Socket
连接,并由服务的调用方通过
Socket
将需要调用的接口名称、方法名称和参数序列化后传递给服务的提供方,服务的提供方反序列化后再利用反射调用相关的方法。
最后将结果返回给服务的调用方,整个基于
TCP
协议的
RPC
调用大致如此。
但是在实例应用中则会进行一系列的封装,如
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
协议的数据效率要低,信息传输所占用的时间也会更长,当然压缩数据,能够缩小这一差距。