听故事,引入RMI

在了解什么是RMI 之前我们先来看一个场景(虚构场景):

产品经理:“小王,我需要一个这样的功能:在我本地调用RelaxHeart网的服务端数据统计的业务方法(方法的具体逻辑/实现)。” 我:“ok, 这个简单,马上实现给你”

然后我可能就直接在我的服务端上这样干了:

  1. /**
  2. * @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
  3. * @Date: 2019-7-20 0020 16:01
  4. * @Description: 服务监控Service
  5. */
  6. public interface RelaxHeartMonitorService {
  7. /**
  8. * 监控
  9. * @param data
  10. */
  11. void monitor(MonitorData data);
  12. /**
  13. * 根据指标取对应监控结果
  14. * @param idx 指标名称
  15. * @return
  16. */
  17. List<MonitorData> get(String idx);
  18. }
  19. /**
  20. * @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
  21. * @Date: 2019-7-20 0020 16:04
  22. * @Description: 服务客户端
  23. */
  24. public class ConsumerClient {
  25. RelaxHeartMonitorService relaxHeartMonitorService;
  26. public ConsumerClient(){}
  27. public ConsumerClient(RelaxHeartMonitorService relaxHeartMonitorService){
  28. this.relaxHeartMonitorService = relaxHeartMonitorService;
  29. }
  30. /**
  31. * 取指标
  32. * @param idx
  33. * @return
  34. */
  35. public String get(String idx) {
  36. return relaxHeartMonitorService.get(idx);
  37. }
  38. }
  39. /**
  40. * @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
  41. * @Date: 2019-7-20 0020 16:09
  42. * @Description: 提供给产品经理的调用入口
  43. */
  44. public class MonitorClient {
  45. /**
  46. * 我计划打个jar包给我的产品经理,args仅限一个入参,即是指标名称
  47. * @param args
  48. */
  49. public static void main(String[] args) {
  50. if (args.length != 1 || StringUtils.isEmpty(args[1])){
  51. System.out.println("入参不合法");
  52. return;
  53. }
  54. RelaxHeartMonitorService service = new RelaxHeartMonitorService();
  55. ConsumerClient client = new ConsumerClient(service);
  56. String value = client.get(args[0]);
  57. System.out.println(String.format("%s 对应的指标值为 :%s", args[0], value));
  58. }
  59. }

上述代码:RelaxHeartMonitorService是我的RelaxHeart网数据监控服务类;ConsumerClient 是我提供给出来的一个客户端调用程序;而MonitorClient 则是我最终提供给产品经理在客户端调用过程的入口;

然后我匆匆忙忙打了个jar包(假设是:monitor.jar)发给产品经理,并留言:“哥,请查收,使用方式,你在本地执行 java -jar monitor.jar idx(需要获取指标名)” 过了两分钟…… 产品经理回复:“小王呀,你怕是理解错了我的意思,你这样搞不就相当于把服务端的程序都一块给我了吗,万一后面有变动,我这边还需要改逻辑重新打包。。。。我的意思是我在客户端使用MonitorClient.java 直接调用你远程服务端的RelaxHeartMonitorService ,就像调用本地程序一样,而你这个RelaxHeartMonitorService 是在你服务端维护的,我不需要这个类及其底层的实现逻辑” 我懵逼了……什么意思?我理解错了?在客户端直接调用远程的逻辑,而不需要服务端对象的实现,还要像本地一样,这要怎么搞?这时不可能实现的吧? 我考虑了一会回复:“哥,你这个不可能实现。哪有…… …… ……” 产品经理:“你去了解一下 RMI 协议,在来跟我聊能不能实现吧~” 我:“哦,好的”

什么是 RMI

跟产品经理聊完后,赶紧上网百度RMI,什么是RMI?结果发现一大堆的文章,学习归纳如下:
正如上面故事中的,RMI可以做到客户端调用远程的方法,但是感觉跟调用本地过程一模一样,是不是有点神奇呢?
RMI:远程方法调用。即为实现调用远程方法跟调用本地一样简单。
下面是我为大家准备的RMI的实现原理,或者说调用链的图示:
【20200223】什么是RMI 协议Java中如何实现 - 图1
image.png
这里有一个非常重要的概念:代理。如果现在不理解代理可以先不用理会,就先简单的理解为 “替代品、管家、影子”等……都行。
根据上图的步骤我们来看RMI的执行流:
(1) 首先位于客户端(客户端JVM堆内)的客户端对象发请求到客户端辅助对象(其实就是远程对象的代理);注意客户端服务对象不是真正的远程服务,我们图示已经很清楚了,只是他操作起来很想远程对象(具体相同的方法)
(2)客户端服务对象通过网络/Socket发送请求到服务端服务对象
(3)服务端辅助对象通过socket接收到请求后,对请求调用信息进行解包
(4)服务端辅助对象调用服务端对象,执行真正的远程过程
(5)、(6)、(7)很显然是将服务端对象接执行结果一路传回客户端对象
这样就实现了客户端调用远程方法跟调用本地方法一样简单的要求了(即RMI)。

RMI的具体实现

一般我们需要五步来实现:

第一步:制作远程接口

  1. /**
  2. * @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
  3. * @Date: 2019-7-20 0020 16:01
  4. * @Description:
  5. * ------------------------------
  6. * RMI远程服务接口制作:
  7. * 1. 扩展java.rmi.Remote,即我们的远程服务接口需要继承Remote
  8. * 2. 所有声明的方法都会抛出RemoteException
  9. * 3. 确定方法的返回值必须是原语(primitive)类型或者可序列化类型(这个不难理解,因为返回值需要打包通过网络运送,这个过程需要序列化完成)
  10. * ------------------------------
  11. */
  12. public interface RelaxHeartMonitorService extends Remote {
  13. /**
  14. * 根据指标取对应监控结果
  15. * @param idx 指标名称
  16. * @return
  17. */
  18. List<MonitorData> get(String idx) throws RemoteException;
  19. }

RMI远程服务接口制作: A. 扩展java.rmi.Remote,即我们的远程服务接口需要继承Remote B. 所有声明的方法都会抛出RemoteException C. 确定方法的返回值必须是原语(primitive)类型或者可序列化类型(这个不难理解,因为返回值需要打包通过网络运送,这个过程需要序列化完成)

第二步:制作远程实现

  1. /**
  2. * @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
  3. * @Date: 2019-7-20 0020 16:51
  4. * @Description:
  5. * ---------------------------
  6. * 制造远程接口的实现:
  7. * A.实现远程接口
  8. * B.扩展UnicastRemoteObject
  9. * C.设计一个不带变量的构造器,并声明RemoteException
  10. * D.用RMI注册此服务
  11. * ---------------------------
  12. */
  13. public class RelaxHeartMonitorServiceImpl extends UnicastRemoteObject implements RelaxHeartMonitorService {
  14. private static final Logger LOGGER = LoggerFactory.getLogger(RelaxHeartMonitorServiceImpl.class);
  15. protected RelaxHeartMonitorServiceImpl() throws RemoteException { }
  16. @Override
  17. public String get(String idx) throws RemoteException {
  18. return "test";
  19. }
  20. //用RMI注册此服务
  21. public static void main(String[] args) {
  22. try {
  23. RelaxHeartMonitorService service = new RelaxHeartMonitorServiceImpl();
  24. Naming.rebind("monitorService", service);
  25. } catch (Exception e) {
  26. LOGGER.error("服务注册异常!");
  27. }
  28. }
  29. // 其他逻辑
  30. }

制造远程接口的实现: A.实现远程接口 B.扩展UnicastRemoteObject C.设计一个不带变量的构造器,并声明RemoteException D.用RMI注册此服务: 为我们的服务命名,好让客户在注册表中寻找它,并在RMI registry中注册此服务名和服务。当我们绑定(bind)服务对象时,RMI会把服务换成stub,然后把stub放进registry注册表中。

第三步:产生Stub和Skeleton

在远程实现类(不是远程接口)上执行rmic rmic是JDK自带的一个工具,用来为一个服务类产生stub和skeleton。rmic还有很多特性,感兴趣可以去了解下。

【20200223】什么是RMI 协议Java中如何实现 - 图2
image.png

第四步:启动rmiregistry

开启一个终端,启动rmiregistry

第五步:启动服务

开启另一个终端,启动服务

  1. java xxx.xx.xx.RelaxHeartMonitorServiceImpl

【20200223】什么是RMI 协议Java中如何实现 - 图3
启动服务

客户调用

  1. /**
  2. * @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
  3. * @Date: 2019-7-20 0020 17:31
  4. * @Description:
  5. * -----------------
  6. * 客户通过rmi发现服务,本地调用
  7. * A. 客户从rmi中寻找Naming.lookup(rmi://127.0.0.1/monitorService);
  8. * B. RMI registry返回stub对象:RMI会自动对stub进行反序列化,所以我们在客户端必须有stub类RelaxHeartMonitorService(由rmic生成),否则反序列化失败
  9. * C. 客户调用stub的方法,就像stub就是真正的服务对象一样
  10. * -----------------
  11. */
  12. public class ConsumerClient {
  13. // monitorService必须为我们服务端注册的服务名
  14. private static final String RMI_REGISTRY = "rmi://127.0.0.1/monitorService";
  15. public static void main(String[] args) throws Exception {
  16. RelaxHeartMonitorService service = (RelaxHeartMonitorService) Naming.lookup(RMI_REGISTRY);
  17. String value = service.get(args[0]);
  18. }
  19. }

客户通过rmi发现服务,本地调用 A. 客户从rmi中寻找Naming.lookup(rmi://127.0.0.1/monitorService); B. RMI registry返回stub对象:RMI会自动对stub进行反序列化,所以我们在客户端必须有stub类RelaxHeartMonitorService(由rmic生成),否则反序列化失败 C. 客户调用stub的方法,就像stub就是真正的服务对象一样

扩展

Java RMI与RPC有什么不同?

0:定义不同

RMI:远程方法调用 RPC:远程过程调用

1:方法调用方式不同:

RMI中是通过在客户端的Stub对象作为远程接口进行远程方法的调用。每个远程方法都具有方法签名。如果一个方法在服务器上执行,但是没有相匹配的签名被添加到这个远程接口(stub)上,那么这个新方法就不能被RMI客户方所调用。
RPC中是通过网络服务协议向远程主机发送请求,请求包含了一个参数集和一个文本值,通常形成“classname.methodname(参数集)”的形式。RPC远程主机就去搜索与之相匹配的类和方法,找到后就执行方法并把结果编码,通过网络协议发回。

2:适用语言范围不同:

RMI只用于Java;
RPC是网络服务协议,与操作系统和语言无关。

3:调用结果的返回形式不同:

Java是面向对象的,所以RMI的调用结果可以是对象类型或者基本数据类型;
RMI的结果统一由外部数据表示语言表示,这种语言抽象了字节序类和数据类型结构之间的差异。

作者:RelaxHeart
链接:https://www.jianshu.com/p/3d1fd849ed7c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。