# 前言
RPC,Remote Procedure Call,远程过程调用
RPC 框架允许客户端通过网络在远程服务器上请求服务,其中包含服务发现、负载、网络传输和序列化等过程。
# 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 中的三个重要过程:
基于 TCP 的连接主要分为三种:
- 按需连接:需要调用是建立连接,调用结束后断开连接;
- 长连接:无论有无数据报的发送,客户端和服务器之间都保持连接(可以配合心跳检测机制判断连接是否存活。
- 共享连接:多个远程过程调用共享一个 TCP 连接。
| 服务寻址
服务寻址:RPC 框架通过被调用方法的端口和方法名等信息完成对该方法的调用。* 实现
Call ID:服务端每个函数都有一个在所有进程中唯一的 ID,可以通过该 ID 调用具体方法。
客户端和服务端维护一个方法和 Call ID 的映射表。当客户端需要进行远程调用时,根据映射表找出相应的 Call ID 发送给服务端,服务端根据 Call ID 查表确定客户端需要调用的函数,然后执行对应的方法。
* 实例:RMI
RMI(远程方法调用)借助 JNDI 实现服务寻址和 RPC。当一个 RMI 服务创建后,服务端通过 JNDI 将服务名称和对象关联。每当客户端发送服务名称,服务端就可以通过服务名称直接访问对象及方法。
JNDI,Java Naming and Directory Interface,是 Java 的一种目录服务应用程序接口。 JNDI 提供一个目录系统将服务名称与对象关联起来,开发人员在开发过程中可以使用名称来访问对象。
| 序列化和反序列化
* 序列化
客户端的请求信息通过网络底层传输到服务端,所以传输的参数数据都需要先序列化为二进制的字节流才能进行传输。
* 反序列化
服务端需要将接受到的信息反序列化为内存中可以使用的数据,然后本地调用对应的方法。
本地调用一般是通过生成代理 Proxy 去调用。 通常会有 JDK 动态代理、CGLIB 动态代理、Javassist 生成字节码技术等
* Protocol Buffers
Protocol Buffers,简称 ProtoBuf,是 Google 开源的跨平台序列化数据结构的协议。
gRPC 默认使用 Protocol Buffers,可以使用 Proto files 创建 gRPC 服务,或用 Protocol Buffers 消息类型来定义方法参数和返回类型。
# 不同网络传输协议的 RPC 实现
在 RPC 中可以选择 TCP 协议、UDP 协议以及 HTTP 协议。不同的协议有不同的传输效率和性能。
|基于 TCP 协议
客户端和服务端之间建立 Socket 连接,客户端将需要调用的接口名称、方法名称和参数序列化后通过Socket 连接传递给服务端,服务端反序列化得到数据后通过反射调用方法,得到的结果序列化后将返回给客户端。
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!"};// 客户端和服务端建立socketSocket 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());}}
/*** 服务提供者接口,用于暴露给服务消费者进行消费*/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); // 得到接口ClassObject 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 协议更像是访问网页一样,返回的结果单一简单。
- 客户端向服务端发送请求,这种请求的方式可能是
GET、POST、PUT、DELETE等。服务端根据不同的请求方式做出不同的处理,或者某个方法只允许某种请求方式。 服务端方法所需要的参数可能是客户端传输的 XML 或者 JSON 格式的数据,同样服务端方法计算后的结果也会以 XML 或者 JSON 的格式传输回客户端。
<dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.2</version></dependency>
@Testpublic void testHttpService() throws UnsupportedEncodingException {System.out.println("测试http请求开始~");// 封装请求参数Map map = new HashMap<String, String>();map.put("reqData","Hello World,世界你好~");// http://localhost/testHttpService 请求的服务器地址URLString resp = HttpUtil.post("http://localhost/testHttpService", map);System.out.println("http服务返回结果为:"+ com.alibaba.fastjson.JSON.toJSON(resp));}
```java // 服务端需要提供一个请求路径 @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 。这里主要介绍最常用的三种:
