RPC(Remote Procedure Call)协议

RPC协议是一种通过网络从远程计算机上请求服务, 而不需要了解底层网络技术的协议, 在OSI模型中处在应用层和网络层.

作为一个规范, 使用RPC协议的框架有很多, Dubbo,Hessian等均使用这个协议, RMI也使用该协议实现.

RMI(Remote Method Invocation) 远程方法调用

RMI使用Java远程消息交换协议JRMP(Java Remote Messaging Protocol)进行通信,JRMP是纯java的.

  1. 定义接口, 使其extends Remote接口, 方法需要抛出异常RemoteException, Remote是一个标记接口
  1. public interface IRmiTest extends Remote {
  2. String hello() throws RemoteException;
  3. }
  1. 实现接口, 使其extends UnicastRemoteObject, 需要有构造方法, 并抛出异常RemoteException
  1. public class RmiTest extends UnicastRemoteObject implements IRmiTest {
  2. public RmiTest() throws RemoteException {
  3. }
  4. @Override
  5. public String hello() {
  6. return "Hello ....";
  7. }
  8. }
  1. 定义服务端, 注册和绑定
  1. public class TestServer {
  2. public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
  3. IRmiTest rmiTest = new RmiTest();
  4. LocateRegistry.createRegistry(8888);
  5. Naming.bind("rmi://localhost:8888/hello",rmiTest);
  6. System.out.println("server started");
  7. }
  8. }
  1. 定义客户端, lookup方法的参数url与服务端bind的必须一致. 接口需要定义为与服务端一致.
  1. public class TestClient {
  2. public static void main(String[] args) throws RemoteException, MalformedURLException, NotBoundException {
  3. IRmiTest rmiTest = (IRmiTest) Naming.lookup("rmi://localhost:8888/hello");
  4. System.out.println(rmiTest.hello());
  5. }
  6. }

RMI实现机制

RMI屏蔽了底层复杂的网络调用, 使得远程对象的方法调用变得透明, 就像调用本地方法一样方便.

下面深入探究下jdk中rmi的实现原理, 看看底层是如何实现远程调用的.

首先, 需要了解下比较重要的两个角色stub和skeleton, 这两个角色封装了与网络相关的代码. 原始的交互式这样的,客户端--网络--服务器--具体服务. 有了这两个角色之后的模型变为: 客户端--stub--网络--skeleton--服务器--服务.可以参考的图维基百科

下面来看源码...

一.实例化RegistryImpl,初始化

LocateRegistry.createRegistry(8888);这句代码启动了一个注册器(其中有个Map对象来存储名称和服务的映射,这个后面再细看)

  1. public static Registry createRegistry(int port) throws RemoteException {
  2. return new RegistryImpl(port);
  3. }

这个方法实例化了一个RegistryImpl的实例,RegistryImpl实现了Registry.

  1. public RegistryImpl(final int var1) throws RemoteException {
  2. if(var1 == 1099 && System.getSecurityManager() != null) {
  3. try {
  4. AccessController.doPrivileged(new PrivilegedExceptionAction() {
  5. public Void run() throws RemoteException {
  6. LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
  7. RegistryImpl.this.setup(new UnicastServerRef(var1x));
  8. return null;
  9. }
  10. }, (AccessControlContext)null, new Permission[]{new SocketPermission("localhost:" + var1, "listen,accept")});
  11. } catch (PrivilegedActionException var3) {
  12. throw (RemoteException)var3.getException();
  13. }
  14. } else {
  15. LiveRef var2 = new LiveRef(id, var1);
  16. this.setup(new UnicastServerRef(var2));
  17. }
  18. }

两个分支最终都调用了setup()方法, 主要关注该方法.if分支中var1=1099是指默认端口并且存在安全管理器的时候不做校验, 这是为了性能考虑.

  1. private void setup(UnicastServerRef var1) throws RemoteException {
  2. this.ref = var1; // UnicastServerRef继承了RemoteRef,this.ref的类型就是RemoteRef
  3. var1.exportObject(this, (Object)null, true);
  4. }

setup方法的参数是包装后的UnicastServerRef对象, UnicastServerRef继承了RemoteRef因此可以赋值给ref变量. 该方法将调用委托给UnicastServerRef的方法exportObject()

如果是拿文章开头的代码进行调试, 会发现这个方法会走两次, 除了RegistryImpl, 还有一次是RmiTest也会走这个方法.不同的是RegistryImpl会走下面代码中的if(var5 instanceof RemoteStub)分支语句, 这个语句最终将生成一个Skeleton实例并设置给当前实例的域变量skel, 不过自jdk1.2之后skeleton就没什么用了.

  1. public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
  2. Class var4 = var1.getClass();
  3. Remote var5;
  4. try {
  5. var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
  6. } catch (IllegalArgumentException var7) {
  7. throw new ExportException("remote object implements illegal remote interface", var7);
  8. }
  9. if(var5 instanceof RemoteStub) {
  10. // 生成Skeleton实例并设置给当前实例的域变量skel
  11. this.setSkeleton(var1);
  12. }
  13. Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
  14. this.ref.exportObject(var6); //ref是实例化UnicastServerRef的时候传入的
  15. this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
  16. return var5;
  17. }

上面方法首先根据Remote的参数var1创建了一个代理对象var5, var1是RegistryImpl类的实例. 然后实例化一个Target的实例, 从参数可以看到,Target对象包含了几乎之前代码的所有对象.然后将这个对象作为参数,调用LiveRef实例ref的exportObject()方法.

二. 网络连接和对象传输

  1. public void exportObject(Target var1) throws RemoteException {
  2. this.ep.exportObject(var1);
  3. }

接上一步, RemoteRef的方法最终委托给TCPEndpoint的同名方法(委托模式), 到此代码将控制权传递给传输层.

  1. public void exportObject(Target var1) throws RemoteException {
  2. synchronized(this) {
  3. this.listen();
  4. ++this.exportCount;
  5. }
  6. boolean var2 = false;
  7. boolean var12 = false;
  8. try {
  9. var12 = true;
  10. super.exportObject(var1);
  11. var2 = true;
  12. var12 = false;
  13. } finally {
  14. if (var12) {
  15. if (!var2) {
  16. synchronized(this) {
  17. this.decrementExportCount();
  18. }
  19. }
  20. }
  21. }
  22. if (!var2) {
  23. synchronized(this) {
  24. this.decrementExportCount();
  25. }
  26. }
  27. }

这个方法实现了网络通信, 首先linsten()启动了一个ServerSocket的线程,并开始监听端口. 然后调用父类的方法将Target对象暴露出去, 此时服务端的初始化就完成了.

三. 注册服务

Naming.bind("rmi://localhost:8888/hello",rmiTest); 完成名称和服务对象的绑定.

  1. public static void bind(String name, Remote obj)
  2. throws AlreadyBoundException,
  3. java.net.MalformedURLException,
  4. RemoteException
  5. {
  6. ParsedNamingURL parsed = parseURL(name);
  7. Registry registry = getRegistry(parsed);
  8. if (obj == null)
  9. throw new NullPointerException("cannot bind to null");
  10. registry.bind(parsed.name, obj);
  11. }

上面代码Naming类, 调用的是注册器Registrybind()方法

  1. public void bind(String var1, Remote var2) throws RemoteException, AlreadyBoundException, AccessException {
  2. Hashtable var3 = this.bindings;
  3. synchronized(this.bindings) {
  4. Remote var4 = (Remote)this.bindings.get(var1);
  5. if (var4 != null) {
  6. throw new AlreadyBoundException(var1);
  7. } else {
  8. this.bindings.put(var1, var2);
  9. }
  10. }
  11. }

注册使用的容器是一个HashTable, 最终服务的名称和服务会被注册到这个map容器中.

到此为止, 服务端的初始化完成. 首先实例化了一个实现Register注册器的实例, 通过层层组装, 最终生成一个Target对象, 其中包含了组装过程中生成的全部状态, 最后调用RemoteRef的方法将对象转交给传输层对象TCPEndpoint的实例, 最终由这个对象启动Socket开启通信连接. 注册服务是通过Naming的方法委托调用Register注册器的方法实现, 并将结果最终注册到Register域的map对象中.

四. 客户端远程调用

IRmiTest rmiTest = (IRmiTest) Naming.lookup("rmi://localhost:8888/hello"); 客户端通过Naming的方法获取服务的实例

  1. public static Remote lookup(String name)
  2. throws NotBoundException,
  3. java.net.MalformedURLException,
  4. RemoteException{
  5. ParsedNamingURL parsed = parseURL(name);
  6. Registry registry = getRegistry(parsed);
  7. if (parsed.name == null)
  8. return registry;
  9. return registry.lookup(parsed.name);
  10. }

与服务端注册时候使用Naming.bind()方法一样, 这里lookup()最终也会委托给Registry的实例. 这个实例的实现不是用的服务端的Register_Impl, 而是使用RegistryImpl_Stub, 下面代码是lookup()的实现, 可以看出这里封装了网络io的一些逻辑.

  1. public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
  2. try {
  3. RemoteCall var2 = this.ref.newCall(this, operations, 2, 4905912898345647071L);
  4. try {
  5. ObjectOutput var3 = var2.getOutputStream();
  6. var3.writeObject(var1);
  7. } catch (IOException var17) {
  8. throw new MarshalException("error marshalling arguments", var17);
  9. }
  10. this.ref.invoke(var2);
  11. Remote var22;
  12. try {
  13. ObjectInput var4 = var2.getInputStream();
  14. var22 = (Remote)var4.readObject();
  15. } catch (IOException var14) {
  16. throw new UnmarshalException("error unmarshalling return", var14);
  17. } catch (ClassNotFoundException var15) {
  18. throw new UnmarshalException("error unmarshalling return", var15);
  19. } finally {
  20. this.ref.done(var2);
  21. }
  22. return var22;
  23. } catch (RuntimeException var18) {
  24. throw var18;
  25. } catch (RemoteException var19) {
  26. throw var19;
  27. } catch (NotBoundException var20) {
  28. throw var20;
  29. } catch (Exception var21) {
  30. throw new UnexpectedException("undeclared checked exception", var21);
  31. }
  32. }

至此, 服务端和客户端的连接完成, 可以开始通信了.

RMI自JDK1.1就已经提供了, 它提供了Java语言自己的RPC调用方式, 虽然有些老旧, 但依然经典. 目前有很多跨语言的技术或框架, 如后来的WebService, 再到目前的netty,shrift等基本已经取代了这种原始的调用方式, 他们是非阻塞的,且还能跨语言调用. 但熟悉RMI的实现方式对了解分布式系统的通信的实现原理有很大帮助.

分布式系列五: RMI通信的更多相关文章

  1. 分布式系列六: WebService简介

    WebSerice盛行的时代已经过去, 这里只是简单介绍下其基本概念, 并用JDK自带的API实现一个简单的服务. WebSerice的概念 WebService是一种跨平台和跨语言的远程调用(RPC ...

  2. Netty4.x中文教程系列(五)编解码器Codec

    Netty4.x中文教程系列(五)编解码器Codec 上一篇文章详细解释了ChannelHandler的相关构架设计,版本和设计逻辑变更等等. 这篇文章主要在于讲述Handler里面的Codec,也就 ...

  3. 分布式系列四: HTTP及HTTPS协议

    分布式系列四: HTTP及HTTPS协议 非常全面的一篇HTTP的文章: 关于HTTP协议,一篇就够了 还有一个帮助理解HTTPS的文章: 也许,这样理解HTTPS更容易 本文的一些描述摘自这篇文章 ...

  4. [时序图笔记] 步步为营UML建模系列五、时序图(Squence diagram)【转】

    概述 顺序图是一种详细表示对象之间以及对象与参与者实例之间交互的图,它由一组协作的对象(或参与者实例)以及它们之间可发送的消息组成,它强调消息之间的顺序. 顺序图是一种详细表示对象之间以及对象与系统外 ...

  5. WCF开发实战系列五:创建WCF客户端程序

    WCF开发实战系列五:创建WCF客户端程序 (原创:灰灰虫的家http://hi.baidu.com/grayworm) 在前面的三篇文章中我们分别介绍了WCF服务的三种载体:IIS.Self-Hos ...

  6. CSS 魔法系列:纯 CSS 绘制各种图形《系列五》

    我们的网页因为 CSS 而呈现千变万化的风格.这一看似简单的样式语言在使用中非常灵活,只要你发挥创意就能实现很多比人想象不到的效果.特别是随着 CSS3 的广泛使用,更多新奇的 CSS 作品涌现出来. ...

  7. WCF编程系列(五)元数据

    WCF编程系列(五)元数据   示例一中我们使用了scvutil命令自动生成了服务的客户端代理类: svcutil http://localhost:8000/?wsdl /o:FirstServic ...

  8. JVM系列五:JVM监测&工具

    JVM系列五:JVM监测&工具[整理中]  http://www.cnblogs.com/redcreen/archive/2011/05/09/2040977.html 前几篇篇文章介绍了介 ...

  9. SQL Server 2008空间数据应用系列五:数据表中使用空间数据类型

    原文:SQL Server 2008空间数据应用系列五:数据表中使用空间数据类型 友情提示,您阅读本篇博文的先决条件如下: 1.本文示例基于Microsoft SQL Server 2008 R2调测 ...

随机推荐

  1. 手把手教你实现Android RecyclerView上拉加载功能

    摘要 一直在用到RecyclerView时都会微微一颤,因为一直都没去了解怎么实现上拉加载,受够了每次去Github找开源引入,因为感觉就为了一个上拉加载功能而去引入一大堆你不知道有多少BUG的代码, ...

  2. Filebeat命令参考

     Filebeat命令参考: Filebeat提供了一个命令行界面,用于启动Filebeat并执行常见任务,例如测试配置文件和加载仪表板.命令行还支持用于控制全局行为的全局标志. 命令: export ...

  3. spring+struts2+hibernate框架搭建(Maven工程)

    搭建Spring 1.porm.xml中添加jar包 <!-- spring3 --> <dependency> <groupId>org.springframew ...

  4. iOS开发基础-Plist实现嵌套模型

    一.plist文件结构图 说明: title 属性表示该 item 下汽车名字的首字母, cars 属性存放首字母为 title 的汽车, icon 属性存放图片的名称, name 属性存放汽车的名字 ...

  5. AttributeError: Got AttributeError when attempting to get a value for field `password2` on serializer ` UserSerializer`...

    Error_msg: AttributeError: Got AttributeError when attempting to get a value for field `password2` o ...

  6. springboot 学习进度

    1 hello world --------------ok 主启动程序必须在层次结构的最上面. 2 配置 3.日志 4.Web开发 1)SpringBoot集成JSP的方法 配置applicatio ...

  7. hadoop用户写入文件权限不够的问题

    问题: 普通用户echo写入文件,提示权限不够. 解决方式: sudo tee test.txt <<< "要插入内容"

  8. 2019-04-03 研究EasyWeb有感

    今天从往常睡到11点多才起床的状态中一下子转回9点前起床,起床第一件事就是开始研究这框架 1. 根据这框架的说明,首先搭建IDEA开发环境,下载.破解:当从EasyWeb官网下载了两个框架(一个是前端 ...

  9. Tensorflow集成接口TensorLayer、Keras

    https://www.zhihu.com/question/50030898 https://zhuanlan.zhihu.com/p/25296966 https://www.jiqizhixin ...

  10. Hive 执行作业时报错 [ Diagnostics: File file:/ *** reduce.xml does not exist FileNotFoundException: File file:/ ]

    2019-03-10 本篇文章旨在阐述本人在某一特定情况下遇到 Hive 执行 MapReduce 作业的问题的探索过程与解决方案.不对文章的完全.绝对正确性负责. 解决方案 Hive 的配置文件  ...