赞
踩
写作时间:2019-11-28
Spring Boot: 2.2 ,JDK: 1.8, IDE: IntelliJ IDEA
RPC 是远程过程调用(Remote Procedure Call)的缩写形式。RPC 这个概念术语在上世纪 80 年代由 Bruce Jay Nelson 提出。Birrell 和 Nelson 在 1984 发表于 ACM Transactions on Computer Systems 的论文《Implementing remote procedure calls》对 RPC 做了经典的诠释。这里我们追溯下当初开发 RPC 的原动机是什么?在 Nelson 的论文 “Implementing Remote Procedure Calls” 中他提到了几点:
RPC 是指计算机 A 上的进程,调用另外一台计算机 B 上的进程,其中 A 上的调用进程被挂起,而 B 上的被调用进程开始执行,当值返回给 A 时,A 进程继续执行。调用方可以通过使用参数将信息传送给被调用方,而后可以通过传回的结果得到信息。而这一过程,对于开发人员来说是透明的。
图1 描述了数据报在一个简单的RPC传递的过程
注:上述论文,可以在线阅读 https://www.cs.princeton.edu/courses/archive/fall03/cs518/papers/rpc.pdf
远程过程调用采用客户机/服务器(C/S)模式。请求程序就是一个客户机,而服务提供程序就是一台服务器。和常规或本地过程调用一样,远程过程调用是同步操作,在远程过程结果返回之前,需要暂时中止请求程序。使用相同地址空间的低权进程或低权线程允许同时运行多个远程过程调用。
Nelson 的论文中指出实现 RPC 的程序包括 5 个部分:
这里 user 就是 client 端,当 user 想发起一个远程调用时,它实际是通过本地调用 user-stub。user-stub 负责将调用的接口、方法和参数通过约定的协议规范进行编码并通过本地的 RPCRuntime 实例传输到远端的实例。远端 RPCRuntime 实例收到请求后交给 server-stub 进行解码后发起本地端调用,调用结果再返回给 user 端。
以上是粗粒度的 RPC 实现概念结构,接下来我们进一步细化它应该由哪些组件构成,如下图所示。
RPC 服务方通过 RpcServer 去导出(export)远程接口方法,而客户方通过 RpcClient 去引入(import)远程接口方法。客户方像调用本地方法一样去调用远程接口方法,RPC 框架提供接口的代理实现,实际的调用将委托给代理RpcProxy 。代理封装调用信息并将调用转交给RpcInvoker 去实际执行。在客户端的RpcInvoker 通过连接器RpcConnector 去维持与服务端的通道RpcChannel,并使用RpcProtocol 执行协议编码(encode)并将编码后的请求消息通过通道发送给服务方。
RPC 服务端接收器 RpcAcceptor 接收客户端的调用请求,同样使用RpcProtocol 执行协议解码(decode)。解码后的调用信息传递给RpcProcessor 去控制处理调用过程,最后再委托调用给RpcInvoker 去实际执行并返回调用结果。如下是各个部分的详细职责:
1. RpcServer 负责导出(export)远程接口 2. RpcClient 负责导入(import)远程接口的代理实现 3. RpcProxy 远程接口的代理实现 4. RpcInvoker 客户方实现:负责编码调用信息和发送调用请求到服务方并等待调用结果返回 服务方实现:负责调用服务端接口的具体实现并返回调用结果 5. RpcProtocol 负责协议编/解码 6. RpcConnector 负责维持客户方和服务方的连接通道和发送数据到服务方 7. RpcAcceptor 负责接收客户方请求并返回请求结果 8. RpcProcessor 负责在服务方控制调用过程,包括管理调用线程池、超时时间等 9. RpcChannel 数据传输通道
RPC的设计由Client,Client stub,Network ,Server stub,Server构成。 其中Client就是用来调用服务的,Cient stub是用来把调用的方法和参数序列化的(因为要在网络中传输,必须要把对象转变成字节),Network用来传输这些信息到Server stub, Server stub用来把这些信息反序列化的,Server就是服务的提供者,最终调用的就是Server提供的方法。
异步和同步的区分在于是否等待服务端执行完成并返回结果。
RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC 框架需提供一种透明调用机制,让使用者不必显式的区分本地调用和远程调用,在之前给出的一种实现结构,基于 stub 的结构来实现。下面我们将具体细化 stub 结构的实现。
RPC的目的是让你在本地调用远程的方法,而对你来说这个调用是透明的,你并不知道这个调用的方法是部署哪里。通过RPC能解耦服务,这才是使用RPC的真正目的。
传递引用参数相对来说比较困难。单纯传递参数的引用(也包含指针)是完全没有意义的,因为引用地址传递给远程计算机,其指向的内存位置可能跟远程系统上完全不同。如果你想支持传递引用参数,你必须发送参数的副本,将它们放置在远程系统内存中,向他们传递一个指向服务器函数的指针,然后将对象发送回客户端,复制它的引用。如果远程过程调用必须支持引用复杂的结构,比如树和链表,他们需要将结构复制到一个无指针的表示里面(比如,一个扁平的树),并传输到在远程端来重建数据结构。
在本地系统上不存在数据不相容的问题,因为数据格式总是相同的。而在分布式系统中,不同远程机器上可能有不同的字节顺序,不同大小的整数,以及不同的浮点表示。对于 RPC,如果想与异构系统通信,我们就需要想出一个“标准”来对所有数据类型进行编码,并可以作为参数传递。例如,ONC RPC 使用 XDR (eXternal Data Representation) 格式 。这些数据表示格式可以使用隐式或显式类型。隐式类型,是指只传递值,而不传递变量的名称或类型。常见的例子是 ONC RPC 的 XDR 和 DCE RPC 的 NDR。显式类型,指需要传递每个字段的类型以及值。常见的例子是 ISO 标准 ASN.1 (Abstract Syntax Notation)、JSON (JavaScript Object Notation)、Google Protocol Buffers、以及各种基于 XML 的数据表示格式。
有些实现只允许使用一个协议(例如 TCP )。大多数 RPC 实现支持几个,并允许用户选择。
相比于本地过程调用,远程过程调用出错的机会将会更多。由于本地过程调用没有过程调用失败的概念,项目使用远程过程调用必须准备测试远程过程调用的失败或捕获异常。
调用一个普通的过程语义很简单:当我们调用时,过程被执行。远程过程完全一次性调用成功是非常难以实现。执行远程过程可以有如下结果:
如果服务器崩溃或进程在运行服务器代码之前就死了,那么远程过程会被执行0次;
如果一切工作正常,远程过程会被执行1次;
如果服务器返回服务器存根后在发送响应前就奔溃了,远程过程会被执行1次或者多次。客户端接收不到返回的响应,可以决定再试一次,因此出现多次执行函数。如果没有再试一次,函数执行一次;
如果客户机超时和重新传输,那么远程过程会被执行多次。也有可能是原始请求延迟了。两者都可能会执行或不执行。
RPC 系统通常会提供至少一次或最多一次的语义,或者在两者之间选择。如果需要了解应用程序的性质和远程过程的功能是否安全,可以通过多次调用同一个函数来验证。如果一个函数可以运行任何次数而不影响结果,这是幂等(idempotent)函数的,如每天的时间、数学函数、读取静态数据等。否则,它是一个非幂等(nonidempotent)函数,如添加或修改一个文件)。
毫无疑问,一个远程过程调用将会比常规的本地过程调用慢得多,因为产生了额外的步骤以及网络传输本身存在延迟。然而,这并不应该阻止我们使用远程过程调用。
使用 RPC,我们必须关注各种安全问题:
远程过程调用有诸多的优点:
任何 RPC 实现都需要提供一组支持库。这些包括:
所以,判断一种通信方式是否是 RPC,就看它是否提供上述的 API。
展示了一个简单 RPC 进行远程计算的例子。其中,远程过程 add(i,j) 有两个参数 i 和 j, 其结果是返回 i 和 j 的算术和。
通过 RPC 进行远程计算的步骤有:
当然,这里只是做了简单的演示,在实际分布式系统中,还需要考虑其他情况,因为不同的机器对于数字、字符和其他类型的数据项的表示方式常有差异。比如整数型,就有 Big Endian 和 Little Endian 之分。
参照教程【SpringBoot 2.1 | 第一篇:构建第一个SpringBoot工程】新建一个Spring Boot项目,名字叫RPCClient, 在目录src/main/java/resources
下找到配置文件application.properties
,重命名为application.yml
。
Spring Boot 版本选择2.2.1,依赖勾选Developer Tools > Lombok.
计算接口
package com.zgpeace.rpc.common;
public interface Calculator {
int add(int a, int b);
}
RPC请求类,包括请求参数,和请求方法。
package com.zgpeace.rpc.common; import lombok.Data; import java.io.Serializable; @Data public class CalculateRPCRequest implements Serializable { private String method; private int a; private int b; @Override public String toString() { return "CalculateRPCRequest{" + "method='" + method + '\'' + ", a=" + a + ", b=" + b + "}"; } }
客户端请求service
package com.zgpeace.rpc.client.service; import com.zgpeace.rpc.common.Calculator; import com.zgpeace.rpc.common.CalculateRPCRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Socket; import java.util.ArrayList; import java.util.List; public class CalculatorRemoteImpl implements Calculator { public static final int PORT = 9090; private static Logger logger = LoggerFactory.getLogger(CalculatorRemoteImpl.class); @Override public int add(int a, int b) { List<String> addressList = lookupProvider("Calculator.add"); String address = chooseTarget(addressList); try { Socket socket = new Socket(address, PORT); // 将请求序列化 CalculateRPCRequest calculateRPCRequest = generateRqeust(a, b); ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); // 将请求发给服务提供方 objectOutputStream.writeObject(calculateRPCRequest); // 将响应体反序列化 ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); Object response = objectInputStream.readObject(); logger.info("response is {}", response); if (response instanceof Integer) { return (Integer)response; } else { throw new InternalError(); } } catch (Exception e) { logger.error("fail", e); throw new InternalError(); } } private CalculateRPCRequest generateRqeust(int a, int b) { CalculateRPCRequest calculateRPCRequest = new CalculateRPCRequest(); calculateRPCRequest.setA(a); calculateRPCRequest.setB(b); calculateRPCRequest.setMethod("add"); return calculateRPCRequest; } private String chooseTarget(List<String> providers) { if (null == providers || providers.size() == 0) { throw new IllegalArgumentException(); } return providers.get(0); } private List<String> lookupProvider(String name) { List<String> strings = new ArrayList<>(); strings.add("127.0.0.1"); return strings; } }
解析:
因为没有远程socket开启,所以是建立连接失败。
09:54:49.405 [main] ERROR com.zgpeace.rpc.client.service.CalculatorRemoteImpl - fail java.net.ConnectException: Connection refused (Connection refused) at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188) at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) at java.net.Socket.connect(Socket.java:589) at java.net.Socket.connect(Socket.java:538) at java.net.Socket.<init>(Socket.java:434) at java.net.Socket.<init>(Socket.java:211) at com.zgpeace.rpc.client.service.CalculatorRemoteImpl.add(CalculatorRemoteImpl.java:23) at com.zgpeace.rpc.client.controller.ClientController.main(ClientController.java:14) Exception in thread "main" java.lang.InternalError at com.zgpeace.rpc.client.service.CalculatorRemoteImpl.add(CalculatorRemoteImpl.java:45) at com.zgpeace.rpc.client.controller.ClientController.main(ClientController.java:14) Process finished with exit code 1
参照教程【SpringBoot 2.1 | 第一篇:构建第一个SpringBoot工程】新建一个Spring Boot项目,名字叫RPCProvider, 在目录src/main/java/resources
下找到配置文件application.properties
,重命名为application.yml
。
Spring Boot 版本选择2.2.1,依赖勾选Developer Tools > Lombok.
拷贝在Client的公用接口Calculator
和类CalculateRPCRequest
, 按住option键(笔者是MacBook)把包common从Client工程拖过来即可。
真正实现加法实现
package com.zgpeace.rpc.provider.service;
import com.zgpeace.rpc.common.Calculator;
public class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
}
启动ServerSocket长连接的Controller
package com.zgpeace.rpc.provider.controller; import com.zgpeace.rpc.common.Calculator; import com.zgpeace.rpc.provider.service.CalculatorImpl; import com.zgpeace.rpc.common.CalculateRPCRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.ServerSocket; import java.net.Socket; public class ProviderController { private static Logger logger = LoggerFactory.getLogger(ProviderController.class); private Calculator calculator = new CalculatorImpl(); public static void main(String[] args) throws IOException { logger.info("ProviderController running..."); new ProviderController().run(); } private void run() throws IOException { ServerSocket listener = new ServerSocket(9090); try { while (true) { Socket socket = listener.accept(); try { // 将请求反序列化 ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); Object object = objectInputStream.readObject(); logger.info("common is {}", object); System.out.println("common is {}" + object); //调用服务 int result = 0; if (object instanceof CalculateRPCRequest) { CalculateRPCRequest calculateRPCRequest = (CalculateRPCRequest)object; logger.info("CalculateRPCRequest is {}", calculateRPCRequest.toString()); if ("add".equals(calculateRPCRequest.getMethod())) { result = calculator.add(calculateRPCRequest.getA(), calculateRPCRequest.getB()); logger.info("calculate success > {}", result); } else { throw new UnsupportedOperationException(); } } // 返回结果 ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); objectOutputStream.writeObject(new Integer(result)); } catch (Exception e) { logger.error("fail", e); } finally { socket.close(); } } } finally { listener.close(); } } }
解析:
先启动Provider Server
com.zgpeace.rpc.provider.controller.ProviderController - ProviderController running...
再启动客户端Client
com.zgpeace.rpc.client.service.CalculatorRemoteImpl - response is 3
com.zgpeace.rpc.client.controller.ClientController - result is 3
https://github.com/zgpeace/Spring-Boot2.1/tree/master/rpc/SimpleRPC
恭喜你!学完了RPC的入门教程。要想深入了解RPC推荐学习Spring Cloud 和
https://zhuanlan.zhihu.com/p/36528189
https://dubbo.apache.org/zh-cn/blog/rpc-introduction.html
https://waylau.com/remote-procedure-calls/
https://www.cs.princeton.edu/courses/archive/fall03/cs518/papers/rpc.pdf
https://zhuanlan.zhihu.com/p/36427583
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。