系统间通信方式之(Java之RMI初步使用详解)(八)

系统管理员 2022-06-05 00:58 541阅读 0赞

1、概述

在概述了数据描述格式的基本知识、IO通信模型的基本知识后。我们终于可以进入这个系列博文的重点:系统间通信管理。在这个章节我将通过对RMI的详细介绍,引出一个重要的系统间通信的管理规范RPC,并且继续讨论一些RPC的实现;再通过分析PRC的技术特点,引出另一种系统间通信的管理规范ESB,并介绍ESB的一些具体实现。最后我们介绍SOA:面向服务的软件架构。

2、RMI基本使用

RMI(Remote Method Invocation,远程方法调用),是JAVA早在JDK 1.1中提供的JVM与JVM之间进行 对象方法调用的技术框架的实现(在JDK的后续版本中,又进行了改进)。通过RMI技术,某一个本地的JVM可以调用存在于另外一个JVM中的对象方法,就好像它仅仅是在调用本地JVM中某个对象方法一样。例如RMI客户端中的如下调用:

List< UserInfo > users = remoteServiceInterface.queryAllUserinfo();

看似remoteServiceInterface对象和普通的对象没有区别,但实际上remoteServiceInterface对象的具体方法实现却不在本地的JVM中,而是在某个远程的JVM中(这个远程的JVM可以是RMI客户端同属于一台物理机,也可以属于不同的物理机)

我在写这篇博客的时候,查阅了一些网络资料。发现将RMI讲透彻的文章很少。有不少的文章提出“RMI技术已经过时”的观点,“性能不好”的观点等。更有甚者甚者将JAVA 原生的Socket框架和RMI技术框架做性能比较(说明作者没有弄清楚Socket框架和RMI框架的关系)。描述RMI底层IO模型的文章更是没有找到,只找到一篇寥寥几字的文章居然说RMI和NIO没有任何关联。

1-1、RMI使用场景

RMI是基于JAVA语言的,也就是说在RMI技术框架的描述中,只有Server端使用的是JAVA语言并且Client端也是用的JAVA语言,才能使用RMI技术(目前在codeproject.com中有一个开源项目名字叫做“RMI for C++”,可以实现JAVA To C++的RMI调用。但是这是一个第三方的实现,并不是java的标准RMI框架定义,所以并不在我们的讨论范围中)。

RMI适用于两个系统都主要使用JAVA语言进行构造,不需要考虑跨语言支持的情况。并且对两个JAVA系统的通讯速度有要求的情况。

RMI 是一个良好的、特殊的RPC实现:使用JRMP协议承载数据描述,可以使用BIO和NIO两种IO通信模型。RMI框架是可以在大规模集群系统中使用的,当然是不是使用RMI技术,还要看您的产品的技术背景、团队的技术背景、公司的业务背景甚至客户的非技术背景等。(但如果您说您自己写的分布式系统性能优于RMI,那说明我肤浅了:原来在我国像Bill Joy、Ann Wollrath这样的大师竟然是一抓一大把!!!)

1-2、RMI框架的基本组成

虽然RMI早在JDK.1.1版本中就开放了。但是在JDK1.5的版本中RMI又进行改进。所以我们后续的代码示例和原理讲解都基于最新的RMI框架特性。

要定义和使用一套基于RMI框架工作的系统,您至少需要做一下几个工作:

1、定义RMI Remote接口
2、实现这个RMI Remote接口
3、生成Stub(桩)和 Skeleton(骨架)。这一步的具体操作视不同的JDK版本而有所不同(例如JDK1.5后,Skeleton不需要手动);“RMI注册表”的工作方式也会影响“Stub是否需要命令行生成”这个问题。
4、向“RMI注册表”注册在第2步我们实现的RMI Remote接口。
5、创建一个Remote客户端,通过java“命名服务”在“RMI注册表”所在的IP:PORT寻找注册好的RMI服务。
6、Remote客户端向调用存在于本地JVM中对象那样,调用存在于远程JVM上的RMI接口。

下图描述了上述几个概念名称间的关系,呈现了JDK.5中RMI框架其中一种运行方式(注意,是其中一种工作方式。也就是说RMI框架不一定都是这种运行方式,后文中我们还将描述另外一种RMI的工作方式):

这里写图片描述

1-3、代码示例一

在这个代码中,我们将使用“本地RMI注册表”(LocateRegistry),让RMI服务的具体提供者和RMI注册表工作在同一个JVM上,向您介绍最基本的RMI服务的定义、编写、注册和调用过程:

首先我们必须定义RMI 服务接口,代码如下:

  1. package testRMI;
  2. import java.rmi.Remote;
  3. import java.rmi.RemoteException;
  4. import java.util.List;
  5. import testRMI.entity.UserInfo;
  6. public interface RemoteServiceInterface extends Remote {
  7. /** * 这个RMI接口负责查询目前已经注册的所有用户信息 */
  8. public List<UserInfo> queryAllUserinfo() throws RemoteException;
  9. }
  10. 1
  11. 2
  12. 3
  13. 4
  14. 5
  15. 6
  16. 7
  17. 8
  18. 9
  19. 10
  20. 11
  21. 12
  22. 13
  23. 14
  24. 15

很简单的代码,应该不用多解释什么了。这个定义的接口方法如果放在某个业务系统A中,您可以理解是查询这个系统A中所有可用的用户资料。注意这个接口所继承的java.rmi.Remote接口,是“RMI服务接口”定义的特点。

那么有接口定义了,自然就要实现这个接口:

  1. package testRMI;
  2. import java.rmi.RemoteException;
  3. import java.rmi.server.UnicastRemoteObject;
  4. import java.util.ArrayList;
  5. import java.util.List;
  6. import testRMI.entity.UserInfo;
  7. /** * RMI 服务接口RemoteServiceInterface的具体实现<br> * 请注意这里继承的是UnicastRemoteObject父类。 * 继承于这个父类,表示这个Remote Object是“存在于本地”的RMI服务实现 * (这句话后文会解释) * @author yinwenjie * */
  8. public class RemoteUnicastServiceImpl extends UnicastRemoteObject implements RemoteServiceInterface {
  9. /** * 注意Remote Object没有默认构造函数 * @throws RemoteException */
  10. protected RemoteUnicastServiceImpl() throws RemoteException {
  11. super();
  12. }
  13. private static final long serialVersionUID = 6797720945876437472L;
  14. /* (non-Javadoc) * @see testRMI.RemoteServiceInterface#queryAllUserinfo() */
  15. @Override
  16. public List<UserInfo> queryAllUserinfo() throws RemoteException {
  17. List<UserInfo> users = new ArrayList<UserInfo>();
  18. UserInfo user1 = new UserInfo();
  19. user1.setUserAge(21);
  20. user1.setUserDesc("userDesc1");
  21. user1.setUserName("userName1");
  22. user1.setUserSex(true);
  23. users.add(user1);
  24. UserInfo user2 = new UserInfo();
  25. user2.setUserAge(21);
  26. user2.setUserDesc("userDesc2");
  27. user2.setUserName("userName2");
  28. user2.setUserSex(false);
  29. users.add(user2);
  30. return users;
  31. }
  32. }
  33. 1
  34. 2
  35. 3
  36. 4
  37. 5
  38. 6
  39. 7
  40. 8
  41. 9
  42. 10
  43. 11
  44. 12
  45. 13
  46. 14
  47. 15
  48. 16
  49. 17
  50. 18
  51. 19
  52. 20
  53. 21
  54. 22
  55. 23
  56. 24
  57. 25
  58. 26
  59. 27
  60. 28
  61. 29
  62. 30
  63. 31
  64. 32
  65. 33
  66. 34
  67. 35
  68. 36
  69. 37
  70. 38
  71. 39
  72. 40
  73. 41
  74. 42
  75. 43
  76. 44
  77. 45
  78. 46
  79. 47
  80. 48
  81. 49
  82. 50
  83. 51

还有我们定义的Userinfo信息,就是一个普通的POJO对象:

  1. package testRMI.entity;
  2. import java.io.Serializable;
  3. import java.rmi.RemoteException;
  4. public class UserInfo implements Serializable {
  5. /** * */
  6. private static final long serialVersionUID = -377525163661420263L;
  7. private String userName;
  8. private String userDesc;
  9. private Integer userAge;
  10. private Boolean userSex;
  11. public UserInfo() throws RemoteException {
  12. }
  13. /** * @return the userName */
  14. public String getUserName() {
  15. return userName;
  16. }
  17. /** * @param userName the userName to set */
  18. public void setUserName(String userName) {
  19. this.userName = userName;
  20. }
  21. /** * @return the userDesc */
  22. public String getUserDesc() {
  23. return userDesc;
  24. }
  25. /** * @param userDesc the userDesc to set */
  26. public void setUserDesc(String userDesc) {
  27. this.userDesc = userDesc;
  28. }
  29. /** * @return the userAge */
  30. public Integer getUserAge() {
  31. return userAge;
  32. }
  33. /** * @param userAge the userAge to set */
  34. public void setUserAge(Integer userAge) {
  35. this.userAge = userAge;
  36. }
  37. /** * @return the userSex */
  38. public Boolean getUserSex() {
  39. return userSex;
  40. }
  41. /** * @param userSex the userSex to set */
  42. public void setUserSex(Boolean userSex) {
  43. this.userSex = userSex;
  44. }
  45. }
  46. 1
  47. 2
  48. 3
  49. 4
  50. 5
  51. 6
  52. 7
  53. 8
  54. 9
  55. 10
  56. 11
  57. 12
  58. 13
  59. 14
  60. 15
  61. 16
  62. 17
  63. 18
  64. 19
  65. 20
  66. 21
  67. 22
  68. 23
  69. 24
  70. 25
  71. 26
  72. 27
  73. 28
  74. 29
  75. 30
  76. 31
  77. 32
  78. 33
  79. 34
  80. 35
  81. 36
  82. 37
  83. 38
  84. 39
  85. 40
  86. 41
  87. 42
  88. 43
  89. 44
  90. 45
  91. 46
  92. 47
  93. 48
  94. 49
  95. 50
  96. 51
  97. 52
  98. 53
  99. 54
  100. 55
  101. 56
  102. 57
  103. 58
  104. 59
  105. 60
  106. 61
  107. 62
  108. 63
  109. 64
  110. 65
  111. 66
  112. 67
  113. 68
  114. 69
  115. 70
  116. 71
  117. 72
  118. 73
  119. 74
  120. 75
  121. 76

RMI Server 的接口定义和RMI Server的实现都有了,那么编写代码的最后一步是将这个RMI Server注册到“RMI 注册表”中运行。这样 RMI的客户端就可以调用这个 RMI Server了。下面的代码是将RMI Server注册到“本地RMI 注册表”中:

  1. package testRMI;
  2. import java.rmi.Naming;
  3. import java.rmi.registry.LocateRegistry;
  4. public class RemoteUnicastMain {
  5. public static void main(String[] args) throws Exception {
  6. /* * Locate registry,您可以理解成RMI服务注册表,或者是RMI服务位置仓库。 * 主要的作用是维护一个“可以正常提供RMI具体服务的所在位置”。 * 每一个具体的RMI服务提供者,都会讲自己的Stub注册到Locate registry中,以表示自己“可以提供服务” * * 有两种方式可以管理Locate registry,一种是通过操作系统的命令行启动注册表; * 另一种是在代码中使用LocateRegistry类。 * * LocateRegistry类中有一个createRegistry方法,可以在这台物理机上创建一个“本地RMI注册表” * */
  7. LocateRegistry.createRegistry(1099);
  8. // 以下是向LocateRegistry注册(绑定/重绑定)RMI Server实现。
  9. RemoteUnicastServiceImpl remoteService = new RemoteUnicastServiceImpl();
  10. // 通过java 名字服务技术,可以讲具体的RMI Server实现绑定一个访问路径。注册到LocateRegistry中
  11. Naming.rebind("rmi://127.0.0.1:1099/queryAllUserinfo", remoteService);
  12. /* * 在“已经拥有某个可访问的远程RMI注册表”的情况下。 * 下面这句代码就是向远程注册表注册RMI Server, * 当然远程RMI注册表的JVM-classpath中一定要有这个Server的Stub存在 * * (运行在另外一个JVM上的RMI注册表,可能是同一台物理机也可能不是同一台物理机) * Naming.rebind("rmi://192.168.61.1:1099/queryAllUserinfo", remoteService); * */
  13. }
  14. }
  15. 1
  16. 2
  17. 3
  18. 4
  19. 5
  20. 6
  21. 7
  22. 8
  23. 9
  24. 10
  25. 11
  26. 12
  27. 13
  28. 14
  29. 15
  30. 16
  31. 17
  32. 18
  33. 19
  34. 20
  35. 21
  36. 22
  37. 23
  38. 24
  39. 25
  40. 26
  41. 27
  42. 28
  43. 29
  44. 30
  45. 31
  46. 32
  47. 33
  48. 34

这样我们后续编写的Client端就可以调用这个RMI Server了。但是在给出Client端的代码前,关于前面几个类的代码还要进行一些细节的说明:

  • 由于我们使用LocateRegistry创建了一个“本地RMI注册表”,所以不需要使用rmic命令生成Stub了(注意是“不需要手工生成”而不是“不需要”了),这是因为RMI Sever真实服务的JVM和RMI 注册表的JVM是同一个JVM。
  • 那么RMI Sever真实服务的JVM和RMI注册表的JVM可以是两个不同的JVM吗?当然可以。而且这才是RMI框架灵活性、健壮性的提现。
  • 请注意RemoteUnicastServiceImpl的定义,它继承了UnicastRemoteObject。一般来说RMI Server的实现可以继承两种父类:UnicastRemoteObject和Activatable(下篇文章就会讲到Activatable)。
  • 前者的意义是,RMI Server真实的服务提供者将工作在“本地JVM”上;后者的意义是,RMI Server的真是的服务提供者,不是在“本地JVM”上运行,而是可以通过“RMI Remote Server 激活”技术,被序列化到“远程JVM”(即远程RMI注册表所在的JVM上),并适时被“远程JVM”加载运行。
  • 再注意一下“Naming.rebind”和“Naming.bind”的区别。前置是指“重绑定”,如果“重绑定”时“RMI 注册表”已经有了这个服务name的存在,则之前所绑定的Remote Object将会被替换;而后者在执行时如果“绑定”时“RMI注册表”已经有这个服务name的存在,则系统会抛出错误。所以除非您有特别的业务要求,那么建议使用rebind方法进行Remote Object绑定。
  • 还要注意registry.rebind和Naming.rebind绑定的区别。前者是使用RMI注册表绑定,所以不需要写完整的RMI URL了;后者是通过java的名称服务进行绑定,由于名称服务不止为RMI框架提供查询服务,所以在绑定是要书写完成的RMI URL。

下面的代码是RMI Client的代码:

  1. package testRMI;
  2. import java.rmi.Naming;
  3. import java.util.List;
  4. import org.apache.commons.logging.Log;
  5. import org.apache.commons.logging.LogFactory;
  6. import org.apache.log4j.BasicConfigurator;
  7. import testRMI.entity.UserInfo;
  8. /** * 客户端调用RMI测试 * @author yinwenjie * */
  9. public class RemoteClient {
  10. static {
  11. BasicConfigurator.configure();
  12. }
  13. /** * 日志 */
  14. private static final Log LOGGER = LogFactory.getLog(RemoteClient.class);
  15. public static void main(String[] args) throws Exception {
  16. // 您看,这里使用的是java名称服务技术进行的RMI接口查找。
  17. RemoteServiceInterface remoteServiceInterface = (RemoteServiceInterface)Naming.lookup("rmi://192.168.61.1/queryAllUserinfo");
  18. List<UserInfo> users = remoteServiceInterface.queryAllUserinfo();
  19. RemoteClient.LOGGER.info("users.size() = " +users.size());
  20. }
  21. }
  22. 1
  23. 2
  24. 3
  25. 4
  26. 5
  27. 6
  28. 7
  29. 8
  30. 9
  31. 10
  32. 11
  33. 12
  34. 13
  35. 14
  36. 15
  37. 16
  38. 17
  39. 18
  40. 19
  41. 20
  42. 21
  43. 22
  44. 23
  45. 24
  46. 25
  47. 26
  48. 27
  49. 28
  50. 29
  51. 30
  52. 31
  53. 32
  54. 33
  55. 34

那么怎么来运行这段代码呢?如果您使用的是eclipse编写了您第一个RMI Server和RMI Client,并且您使用的是“本地RMI 注册表”。那么您不需要做任何的配置、脚本指定等工作(包括不需要专门设置JRE权限、不需要专门指定classpath、不需要专门生成Stub和Skeleton),就可以看到RMI的运行和调用效果了:

下图为RemoteUnicastMain的效果RMI 服务注册和执行效果:

这里写图片描述

可以看到,RemoteUnicastMain中的代码执行完成后整个应用程序没有退出。如下图:

这里写图片描述

这是因为这个应用程序要承担“真实的RMI Server实现”的服务调用。如果它退出,RMI 注册表就无法请求真实的服务实现了。

我们再来看下图,RemoteClient调用RMI 服务的效果:

这里写图片描述

很明显控制台将返回

0 [main] INFO testRMI.RemoteClient - users.size() = 2

1-4、代码示例二

好吧,文章写到这里我不得不承认我在误导大家。因为上面的代码既没有涉及到Stub的问题,也没有涉及到RMI注册表的讲解。那么在示例代码一的时候,我们讲到了RMI注册表和RMI Server 实现是可以分成两个JVM运行的;我们还讲到Stub是需要手动生成的。那么这个该怎么做呢?

  • 首先我们需要改写RemoteUnicastMain类,将RemoteUnicastMain中使用LocateRegistry类创建“本地RMI注册表”的代码去掉:

    package testRMI;

  1. import java.rmi.registry.LocateRegistry;
  2. import java.rmi.registry.Registry;
  3. public class RemoteRegistryUnicastMain {
  4. public static void main(String[] args) throws Exception {
  5. /* * 我们通过LocateRegistry的get方法,寻找一个存在于远程JVM上的RMI注册表 * */
  6. Registry registry = LocateRegistry.getRegistry("192.168.61.1", 1099);
  7. // 以下是向远程RMI注册表(绑定/重绑定)RMI Server的Stub。
  8. // 同样的远程RMI注册表的JVM-classpath下,一定要有这个RMI Server的Stub
  9. RemoteUnicastServiceImpl remoteService = new RemoteUnicastServiceImpl();
  10. /* * 在不写LocateRegistry.createRegistry(1099);的情况下。 * 下面这句代码就是注册 远程RMI注册表 (运行在另外一个JVM上的RMI注册表, * 可能是同一台物理机也可能不是同一台物理机) * * 注册的RMI注册表存在于192.168.61.1这个IP上 * * 使用注册表registry进行绑定或者重绑定时,不需要写完整的RMI URL * */
  11. registry.rebind("queryAllUserinfo" , remoteService);
  12. }
  13. }
  14. 1
  15. 2
  16. 3
  17. 4
  18. 5
  19. 6
  20. 7
  21. 8
  22. 9
  23. 10
  24. 11
  25. 12
  26. 13
  27. 14
  28. 15
  29. 16
  30. 17
  31. 18
  32. 19
  33. 20
  34. 21
  35. 22
  36. 23
  37. 24
  38. 25
  39. 26
  40. 27
  41. 28
  42. 29
  • 然后我们要为RMI Remote Server的实现RemoteUnicastServiceImpl手动生成Stub(RMI中称之为桩);为什么要生成呢?因为RMI Remote Server的实现和RMI注册表将工作在两个独立的JVM上,RMI注册表需要知道Server实现的基本信息(包括类方法信息、类的引用情况等),这些信息就是定义在Stub类中的。下面我们在windows环境下生成Stub(linux环境下过程基本相同):

rmic -classpath E:\testworkspace\testBSocket\target\classes testRMI.RemoteServiceImpl

上面的Dos窗口代码讲解一下:

rmic命令:就是rmic命令了,这个命令专门用来生成Stub和Skeleton(JDK1.5+不会生成Skeleton了)

-classpath:classpath参数。指定class目录的位置。这个参数和您安装JDK时,在环境变量中设置的CLASSPATH参数含义是一样的。只是在我的环境中,工程编译的路径是E:\testworkspace\testBSocket\target\classes,这个路径没有设置设置在环境变量中,所以在生成Stub需要专门指定(否则rmic没法识别到哪个根路径识别class)

-testRMI.RemoteServiceImpl:要生成Stub的RMI Server服务实现类。这个类一定要实现java.rmi.Remote接口。

在执行完成后,对应的class目录下您将可以看到生成好的Stub class。RemoteUnicastServiceImpl_Stub.class就是刚才生成的Stub class。这个Stub class和RemoteServiceInterface需要放到“RMI 注册表”运行JVM的classpath下面。

  • 接下来我们启动远程“RMI 注册表”服务:

//设置classpath
set CLASSPATH=%CLASSPATH%;E:
\testworkspace\testBSocket\target\classes

//linux下的话,就这么命令
export CLASSPATH=$CLASSPATH:/usr/java/classpath

//启动注册表应用程序
rmiregistry -p 1099

如果不指定“-p”端口参数,那么默认的端口就是1099。

  • 再接下来使用修改后的RemoteUnicastMain,将RMI Remote Server注册到远程“RMI注册表”中:

这里写图片描述

现在这个RMI Remote Server就被注册到远程“RMI注册表”上了。但是RemoteRegistryUnicastMain的执行效果和之前RemoteUnicastMain的执行效果是一样的。执行到bind/rebind语句时,应用程序也没有退出。原因和示例代码一中的原因是一样的。

  • 最后指定Client的调用,调用RMI URL的IP地址需要更改一下:

    package testRMI;

    import java.rmi.Naming;
    import java.util.List;

    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.log4j.BasicConfigurator;

    import testRMI.entity.UserInfo;

    /* 客户端调用RMI测试 @author yinwenjie */
    public class RemoteClient {

    1. static {
    2. BasicConfigurator.configure();
    3. }
    4. /** * 日志 */
    5. private static final Log LOGGER = LogFactory.getLog(RemoteClient.class);
    6. public static void main(String[] args) throws Exception {
    7. RemoteServiceInterface remoteServiceInterface = (RemoteServiceInterface)Naming.lookup("rmi://192.168.61.1:1099/queryAllUserinfo");
    8. List<UserInfo> users = remoteServiceInterface.queryAllUserinfo();
    9. RemoteClient.LOGGER.info("users.size() = " +users.size());
    10. }

    }

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33

3、JAVA RMI 原理

下篇文章开始,我们继续讲解JAVA RMI中工作原理。并且详细分析RMI框架底层的IO通信模型。

版权声明:欢迎转载,但是看在我辛勤劳动的份上,请注明来源:http://blog.csdn.net/yinwenjie(未经允许严禁用于商业用途!)

发表评论

表情:
评论列表 (有 0 条评论,541人围观)

还没有评论,来说两句吧...

相关阅读

    相关 进程通信方式

    1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。 2. 命名管道FIFO:有名管道