RMI程序设计

相关概念

RPC是一种程序设计模式(框架、协议),RPC的主要目标是让构建分布式计算(应用)更容易、透明,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC需提供一种透明调用机制,让使用者不必显式的区分本地调用和远程调用。它通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

说白了,就是在服务器端定义远程对象并实现这些对象中的远程方法,然后对外宣称具有这些对象。客户端根据自己的需要可以远程调用这些方法,无需自己书写详细的实现代码。如服务端定义好购房、购车等费用的计算方法,股票的查询方法。例如:两台服务器A和B,有一个应用程序部署在A上,它想调用B上的应用提供的函数/方法,由于它们不在同一个内存空间,不能直接调用,需要通过网络来表达调用的语义和需要传输的数据。 RPC框架负责屏蔽底层的传输方式(TCP或者UDP)、序列化方式以及网络通信细节,具体来说,RPC需要做以下工作:

(1)通信:RPC 假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。RPC采用C/S架构,在客户机和服务器中建立一个TCP/UDP连接,则远程调用所交换的数据都在这个连接里传输;

(2)因为RPC的作用是让使用者调用远程函数/方法时,就像调用本地函数/方法,所以不管是本地客户还是远程服务端,需要使用一套统一的接口,然后两边分别实现自己的逻辑;

(3)序列化和反序列化:当A服务器的应用程序发起远程调用时,方法的参数要通过网络协议(如TCP)传输到B服务器,而网络协议是基于二进制的,所以A服务器内存中参数的值需要序列化为二进制后再发送给B服务器;B服务器接收到A服务器发送过来的二进制参数时,需要对其进行反序列化,将其恢复为内存能识别的表达方式,然后通过寻址找到对应的方法进行本地调用,得到其返回值,将返回值再序列化后发回A服务器上的应用,A服务器接收二进制返回值后再进行反序列化,最后将内存能识别的表达方式传给A中的应用程序。

实现原理

RMI为客户端B的对象和远程服务端A的对象分别提供了客户辅助对象(客户端stub,桩)和远程服务辅助对象(服务端stub,也称为skeleton,骨架),来帮助本地客户端的对象真正和远程服务对象进行沟通;

RMI在客户端B为stub创建和远程服务对象相同的方法,客户调用stub上的方法,仿佛stub就是真正的服务,stub再负责为我们转发这些请求。 stub会联系服务器A,传送方法调用信息(例如 方法名称、变量等),然后等待服务器A对应 远程方法 的返回;

在服务器端A,skeleton通过Socket连接从stub中接收请求,将调用的信息解包,然后调用真正服务对象上的真正方法。所以,对于服务对象来说,调用是本地的,来自skeleton,而不是远程客户。 skeleton从服务中得到返回值,将它打包,然后运回到客户端辅助对象stub(通过网络Socket的输出流),stub对信息解包,最后将返回值交给客户对象。 RMI的好处在于我们不必亲自写任何网络或I/O代码(Java 1.5后,也不需要用户来手动生成stub和skeleton,完全被隐藏了细节),客户程序调用远程方法(即真正的服务所在)就和在运行在客户自己的JVM上对对象进行正常方法调用一样。

整个过程示意图如图
在这里插入图片描述
注意:上面的示意图是A为B提供远程服务。其实远程对象不仅可以位于服务器端,也可以位于客户端。客户端也可以为服务端提供远程方法调用,这种调用过程被称为回调。在这一刻, B和A的角色就好像是临时互换了。

RMI实现

哪些方法是客户远程可以调用的?在远程接口中定义的方法就是可以被客户端远程调用的方法。一个远程接口中可以定义多个远程方法,远程接口本身也可以定义多个。任何远程接口都要满足以下四个要求:

(1)直接或间接继承java.rmi.Remote接口,这是RMI规范的要求;

(2)客户进行远程调用,底层用到了网络和I/O,而网络通信是不可靠的,比如一旦服务器或者客户端有一方突然断开连接,或者网络出现故障,通信就会失败。RMI规范要求远程接口中定义的方法都要声明抛出RemoteException异常,在客户端进行远程方法调用时,RMI框架会把遇到的网络通信失败转换为RemoteException,客户端可以捕获这种异常,并进行相应的处理;

(3)由于远程方法的变量必须被打包并通过网络运送,必须靠序列化完成,所以远程方法的变量和返回值必须属于基本类型或实现了Serializable接口的引用类型;

(4)服务端和客户端都必须各自拥有一份完全相同的的远程接口,不只是接口名字、内部定义的远程方法完全一样,所在的包名称也要一致。例如服务端的若干远程接口定义在rmi包中,那么远程的客户端也需要有定义在rmi包中完全相关的若干远程接口。(大家想一想,客户端调用远程方法要像在调用本地方法一样,而这些方法是定义在接口中的,所以客户端中肯定有一份完全远程接口,客户端就好像是在调用其本地接口中的方法)。

我们在自己的项目工程中,新建一个包,命名为rmi,然后在rmi包中创建一个远程接口HelloService:

package rmi;

public interface HelloService extends Remote {
	public String echo(String msg) throws RemoteException;
  	public Date getTime() throws RemoteException;
} 

由于我们现在是在自己的机器上模拟创建服务端和客户端,所以rmi中的接口就可以看作是通信双方共同拥有的一份远程接口,如果有更多的接口,也创建在这个包中。

远程接口实现类(以下都简称远程服务类)就是远程服务对象所属的类,其实现远程接口中定义的远程方法。其对象实例就是客户真正想要调用方法的对象。这个类除了必须实现远程接口,且为了成为远程服务对象,我们的对象需要某些“远程的”功能,最简单的方式是扩展java.rmi.server.UnicastRemoteObject,让超类帮我们完成这些工作(UnicastRemoteObject可以把实现了远程接口的类“导出”为远程对象,使它具有相应的服务端stub,并使它能够监听远程客户的方法调用请求,这些细节都被RMI框架很好地隐藏了),并且远程接口实现类的构造方法必须声明抛出RemoteException。

接下来我们就创建一个远程服务类:

在项目工程中新建chapter12包,然后在chapter12包中再创建一个server子包,在chapter12.server包中创建类HelloServerImpl:

package chapter12.server;
public class HelloServiceImpl extends UnicastRemoteObject
    implements HelloService {
  private String name;
  public HelloServiceImpl() throws RemoteException {
  }
  public HelloServiceImpl(String name) throws RemoteException {
    this.name = name;
  }
  @Override
  public String echo(String msg) throws RemoteException {  
    System.out.println("服务端完成一些echo方法相关任务......");
    return "echo: " + msg + " from " + name;
  }

  @Override
  public Date getTime() throws RemoteException {    
    System.out.println("服务端完成一些getTime方法相关任务......");
    return new Date();
  }
} 

远程服务类创建后,如何为客户端服务呢?不是简单地写个服务发布程序,仅仅实例化远程服务类就完事了,还要解决客户端如何寻找服务的问题。

RMI采用一种命名服务机制来使得客户程序可以找到服务器上的一个远程对象。RMI注册器会提供这种命名服务。不妨把RMI注册器比作日常生活中的114电话查询系统,那些希望对外公开联系方式的单位先到114查询系统登记,使得114查询系统记录该单位的名字和联系方式信息。当客户想知道某个单位的联系方式时,只需向114查询系统提供单位的名字,114查询系统就会返回该单位的联系方式。

远程服务发布程序的一大任务就是向RMI注册器注册(绑定)远程对象。远程对象注册到注册器后,就可以被客户端检索到。

我们来实现一个远程服务发布程序:
在chapter12.server包中新建类HelloServer:

package chapter12.server;

public class HelloServer {
  public static void main(String[] args) {
    try {
      //(1)启动RMI注册器,并监听在1099端口(这是RMI的默认端口,正如HTTP的默认端口是80)
      Registry registry = LocateRegistry.createRegistry(1099);

      //(2)实例化远程服务对象,如果有多个远程接口,只实例化自己实现的接口(为什么可能有没有实例化的接口?)
      HelloService helloService = new HelloServiceImpl("李四的远程服务");

      //(3)用助记符来注册发布远程服务对象,助记符建议和远程服务接口命名相同,这样更好起到”助记"效果
      registry.rebind("HelloService",helloService);
      //也可以用另外一种方式进行注册发布,建议用上面的方式
      //Naming.rebind("HelloService",helloService);

      System.out.println("发布了一个HelloService RMI远程服务");

    } catch (RemoteException e) {
      e.printStackTrace();
    }
  }
}

远程服务发布程序HelloServer启动后,即使main()方法执行完毕,程序仍然不会结束运行,因为它向注册器注册了远程对象,注册器一直引用这个远程对象,使得这个远程对象不会结束生命周期,因而HelloServer程序也不会结束运行,远程对象一直为客户端提供服务。

客户端程序首先要获得RMI注册器,然后在注册器中用助记符(别名)来查找远程服务,并获得远程服务在本地的stub,之后就可以像使用本地方法一样调用远程服务方法。

核心代码:

/**
 * 初始化rmi相关操作
 */
 package chapter12.client;
 
 public class HelloClientFX  {
	 private HelloService helloService
	 
	 public void rmiInit() {
	   try {
	     //(1)获取RMI注册器
	     Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
	     System.out.println("RMI远程服务别名列表:");
	     for (String name : registry.list()) {
	       System.out.println(name);
	     }
		 //(2)客户端(调用端)到注册器中使用助记符寻找并创建远程服务对象的客户端(调用端)stub,之后本地调用helloService的方法,实质就是调用了远程服务器上同名的远程接口下的同名方法
	     helloService = (HelloService)registry.lookup("HelloService");
	     //另外一种创建stub的方式
	     //helloService = (HelloService)Naming.lookup("rmi://127.0.0.1:1099/" + "HelloService");
	
	    } catch (Exception e) {
	      e.printStackTrace();
	    }
	  }
	}
	
	public static void main(String[] args) {
  	 	new Thread(()->{rmiInit();}).start();
		//调用本地接口中的方法,实质是调用远程服务器上同名的远程接口中的同名远程方法
		helloService.echo(msg)
 	}


}

RMI程序示意图
在这里插入图片描述

说明:在实际应用中,服务端和客户端一般是分别部署在不同主机,特别要注意的就是两端的远程接口要完全一致,所属的包也要同名(packet rmi)(这里是在本地模拟RMI的服务端和客户端之间的交互,所以是共享了一份rmi包中的远程接口)。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值