前言

RPC 框架允许客户端通过网络在远程服务器上请求服务,其中包含服务发现、负载、网络传输和序列化等过程。

RPC,Remote Procedure Call,远程过程调用

image.png

RPC 的功能模块及调用过程

功能模块

RPC 主要由客户端、客户端存根、网络传输模块、服务端存根、服务端五个模块组成。

  • Client Stub:负责存储服务器地址信息,并将请求参数打包为网络消息。

  • Server Stub:接受和解码客户端的请求消息,服务寻址后本地服务调用消息包中的数据进行处理。

    打包和解包执行相对应的序列化和反序列化

  • Network Service:网络服务,不同的 RPC 框架中网络服务会使用不同的网络通信协议。

    例如 TCP、UDP、HTTP/1.X、HTTP/2.0

image.png

调用流程

从客户端请求调用服务开始,一次 RPC 调用流程如下:

  • Client Stub 接收到调用请求后将方法、入参等信息序列化(打包);
  • Client Stub 根据远程服务器地址,给服务器发送请求消息;
  • Server Stub 对接收的请求消息进行反序列化解码,并调用本地服务进行处理;
  • Server Stub 序列化本地服务得到的结果,然后发送至客户端;
  • Client Stub 反序列化响应消息,得到结果;
image.png

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 提供一个目录系统将服务名称与对象关联起来,开发人员在开发过程中可以使用名称来访问对象。

image.png

序列化和反序列化

序列化

客户端的请求信息通过网络底层传输到服务端,所以传输的参数数据都需要先序列化为二进制的字节流才能进行传输。

反序列化

服务端需要将接受到的信息反序列化为内存中可以使用的数据,然后本地调用对应的方法。

本地调用一般是通过生成代理 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
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
29
30
public class ConsumerDemo {

public static void main(String[] args) throws NoSuchMethodException, IOException, ClassNotFoundException {
//获取服务提供者的接口名,一般 RPC 框架都是暴露服务提供者的接口定义
String providerInterface = ProviderDemo.class.getName();

//需要远程执行的方法,其实就是消费者调用生产者的方法
Method method = ProviderDemo.class.getMethod("printMsg", java.lang.String.class);

//需要传递的参数
Object[] rpcArgs = {"Hello RPC!"};

// 客户端和服务端建立 socket
Socket consumer = new Socket("127.0.0.1", 8899);

//将方法名称和参数序列化后传递给服务生产者
ObjectOutputStream output = new ObjectOutputStream(consumer.getOutputStream());
output.writeUTF(providerInterface);
output.writeUTF(method.getName());
output.writeObject(method.getParameterTypes());
output.writeObject(rpcArgs);

//从生产者读取返回的结果
ObjectInputStream input = new ObjectInputStream(consumer.getInputStream());
Object result = input.readObject();

System.out.println(result.toString());

}
}
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
服务提供者接口,用于暴露给服务消费者进行消费
*/
public interface ProviderDemo {
/**
服务提供者打印 Msg 方法
*/
public String printMsg(String msg);
}

/**
服务提供者实现类
*/
public class ProviderDemoImpl implements ProviderDemo {

public String printMsg(String msg) {
System.out.println("----" + msg + "----");
return "Ni Hao " + msg;
}
}

/**
生产者运行的服务器
*/
public class ProviderServer {

public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {

//用于存放生产者服务接口的 Map, 实际的框架中会有专门保存服务端接口的数据结构
Map<String, Object> serviceMap = new HashedMap();
serviceMap.put(ProviderDemo.class.getName(), new ProviderDemoImpl());

//服务器,因为基于 TCP,所以采用 socket 通信
ServerSocket server = new ServerSocket(8899);

while (true) {
Socket socket = server.accept();
// 接收到序列化方式的数据流,然后反序列化得到需要的参数
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
// 获取服务消费者需要消费服务的接口名
String interfaceName = input.readUTF();
// 获取服务消费者需要消费服务的方法名
String methodName = input.readUTF();

//参数的类型
Class<?>[] parameterTypes = (Class<?>[]) input.readObject();
//参数的对象
Object[] rpcArgs = (Object[]) input.readObject();

//执行调用过程
Class providerInteface = Class.forName(interfaceName); // 得到接口 Class
Object provider = serviceMap.get(interfaceName); // 取得服务实现的对象

//获取需要执行的方法
Method method = providerInteface.getMethod(methodName, parameterTypes);
//通过反射进行调用
Object result = method.invoke(provider, rpcArgs);

//返回给客户端即服务消费者数据
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.writeObject(result);

}
}
}

基于 HTTP 协议

  • HTTP 协议更像是访问网页一样,返回的结果单一简单。
  • 客户端向服务端发送请求,这种请求的方式可能是 GETPOSTPUTDELETE 等。服务端根据不同的请求方式做出不同的处理,或者某个方法只允许某种请求方式。
  • 服务端方法所需要的参数可能是客户端传输的 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
    @Test
    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
    // 服务端需要提供一个请求路径
    @RequestMapping("/testHttpService")
    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 框架,在很多互联网公司和企业应用中广泛使用。协议和序列化框架都可以插拔是极其鲜明的特色。

参考

  1. 什么是 RPC?
  2. RPC 框架实现原理
  3. 维基百科-JNDI
  4. 基于 http 协议实现 RPC 远程调用
  5. 基于 TCP 和 HTTP 协议的 RPC 简单实现
  6. 花了一个星期,我终于把 RPC 框架整明白了!
  7. RPC 是通信协议吗?
  8. Protocol Buuffers; wikipedia