前言
RPC 框架允许客户端通过网络在远程服务器上请求服务,其中包含服务发现、负载、网络传输和序列化等过程。
RPC,Remote Procedure Call,远程过程调用
RPC 的功能模块及调用过程
功能模块
RPC 主要由客户端、客户端存根、网络传输模块、服务端存根、服务端五个模块组成。
Client Stub:负责存储服务器地址信息,并将请求参数打包为网络消息。
Server Stub:接受和解码客户端的请求消息,服务寻址后本地服务调用消息包中的数据进行处理。
打包和解包执行相对应的序列化和反序列化
Network Service:网络服务,不同的 RPC 框架中网络服务会使用不同的网络通信协议。
例如 TCP、UDP、HTTP/1.X、HTTP/2.0
调用流程
从客户端请求调用服务开始,一次 RPC 调用流程如下:
- Client Stub 接收到调用请求后将方法、入参等信息序列化(打包);
- Client Stub 根据远程服务器地址,给服务器发送请求消息;
- Server Stub 对接收的请求消息进行反序列化解码,并调用本地服务进行处理;
- Server Stub 序列化本地服务得到的结果,然后发送至客户端;
- Client Stub 反序列化响应消息,得到结果;
RPC 的三个重要过程
实现 RPC 中的三个重要过程:
- 网络通信协议
- 实现服务寻址
- 序列化和反序列化
网络通信
RPC 通过网络通信实现客户端和服务器之间的数据交换,使用的协议主要为 TCP 和 HTTP。
基于 TCP 的连接主要分为三种:
- 按需连接:需要调用是建立连接,调用结束后断开连接;
- 长连接:无论有无数据报的发送,客户端和服务器之间都保持连接(可以配合心跳检测机制判断连接是否存活。
- 共享连接:多个远程过程调用共享一个 TCP 连接。
服务寻址
服务寻址:RPC 框架通过被调用方法的端口和方法名等信息完成对该方法的调用。
实现
客户端和服务端维护一个方法和 Call ID 的映射表。当客户端需要进行远程调用时,根据映射表找出相应的 Call ID 发送给服务端,服务端根据 Call ID 查表确定客户端需要调用的函数,然后执行对应的方法。
Call ID:服务端每个函数都有一个在所有进程中唯一的 ID,可以通过该 ID 调用具体方法。
实例:RMI
RMI(远程方法调用)借助 JNDI 实现服务寻址和 RPC。当一个 RMI 服务创建后,服务端通过 JNDI 将服务名称和对象关联。每当客户端发送服务名称,服务端就可以通过服务名称直接访问对象及方法。
JNDI,Java Naming and Directory Interface,是 Java 的一种目录服务应用程序接口。JNDI 提供一个目录系统将服务名称与对象关联起来,开发人员在开发过程中可以使用名称来访问对象。
序列化和反序列化
序列化
客户端的请求信息通过网络底层传输到服务端,所以传输的参数数据都需要先序列化为二进制的字节流才能进行传输。
反序列化
服务端需要将接受到的信息反序列化为内存中可以使用的数据,然后本地调用对应的方法。
本地调用一般是通过生成代理 Proxy 去调用。通常会有 JDK 动态代理、CGLIB 动态代理、Javassist 生成字节码技术等
Protocol Buffers
gRPC 默认使用 Protocol Buffers,可以使用 Proto files 创建 gRPC 服务,或用 Protocol Buffers 消息类型来定义方法参数和返回类型。
Protocol Buffers,简称 ProtoBuf,是 Google 的开源跨平台序列化数据结构协议。
不同网络传输协议的 RPC 实现
在 RPC 中可以选择 TCP 协议、UDP 协议以及 HTTP 协议。不同的协议有不同的传输效率和性能。
基于 TCP 协议
客户端和服务端之间建立 Socket 连接,客户端将需要调用的接口名称、方法名称和参数序列化后通过 Socket 连接传递给服务端,服务端反序列化得到数据后通过反射调用方法,得到的结果序列化后将返回给客户端。
1 | public class ConsumerDemo { |
1 | /** |
基于 HTTP 协议
- HTTP 协议更像是访问网页一样,返回的结果单一简单。
- 客户端向服务端发送请求,这种请求的方式可能是
GET
、POST
、PUT
、DELETE
等。服务端根据不同的请求方式做出不同的处理,或者某个方法只允许某种请求方式。 - 服务端方法所需要的参数可能是客户端传输的 XML 或者 JSON 格式的数据,同样服务端方法计算后的结果也会以 XML 或者 JSON 的格式传输回客户端。
1
2
3
4
5<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>1
2
3
4
5
6
7
8
9
10
11
12
public void testHttpService() throws UnsupportedEncodingException {
System.out.println("测试 http 请求开始~");
// 封装请求参数
Map map = new HashMap<String, String>();
map.put("reqData","Hello World, 世界你好~");
// http://localhost/testHttpService 请求的服务器地址 URL
String resp = HttpUtil.post("http://localhost/testHttpService", map);
System.out.println("http 服务返回结果为:"+ com.alibaba.fastjson.JSON.toJSON(resp));
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// 服务端需要提供一个请求路径
public void testHttpService(HttpServletRequest request,HttpServletResponse response) throws IOException {
logger.info("测试 HTTP 请求 服务端开始~");
String reqData=request.getParameter("reqData");
// 模拟相关业务逻辑处理
logger.info("处理相关业务~reqData="+reqData);
/*
调用业务相关的方法
*/
// 方法调用结束,返回结果
logger.info("业务处理完成,返回结果~");
Map mapResult=new HashMap();
mapResult.put("success",true);
mapResult.put("code","0000");
mapResult.put("msg","http 请求测试成功~");
// 防止 Http 请求中文乱码
response.setHeader("Content-Type", "text/html;charset=utf-8");
PrintWriter printWriter=response.getWriter();
printWriter.write(com.alibaba.fastjson.JSON.toJSONString(mapResult));
printWriter.flush();
logger.info("测试 HTTP 请求 服务端结束~");
}
两种协议的对比
TCP 协议:
- 能够灵活定制协议字段,减少网络开销,提高性能,实现更大的吞吐量和并发数。
- 需要关注底层复杂的实现细节,代价高。
- 不同平台需要重新开发工具包进行请求发送和解析。
HTTP 协议:
- 使用 JSON 和 XML 格式请求或响应数据。这两种数据格式的解析工具成熟,二次开发简单。
- 在同等网络下传输相同的内容,HTTP 占用的字节数比 TCP 高,信息传输所占用的时间更长。
gRPC 采用了 HTTP/2.0 协议,避免相同信息的重复传输,并对首部字段进行压缩。
基于 RPC 的框架
基于 RPC 产生了很多框架:Dubbo、Netty、gRPC、BRPC、Thrift、JSON-RPC 。这里主要介绍最常用的三种:
- gRPC:是 Google 公布的开源软件,基于 HTTP/2.0 协议,并支持常见的众多编程语言,底层使用 Netty 框架。
- Thrift:是 Facebook 的开源 RPC 框架,主要是一个跨语言的服务开发框架。用户只要在其之上进行二次开发就行,应用对于底层的 RPC 通讯等都是透明的。
- Dubbo:是阿里集团开源的一个极为出名的 RPC 框架,在很多互联网公司和企业应用中广泛使用。协议和序列化框架都可以插拔是极其鲜明的特色。