Rmi工作原理的理解

Rmi工作原理的理解 

http://blog.sina.cn/dpool/blog/s/blog_51777ce90100metj.html
一.RMI的定义
RMI(Remote Method Invocation,远程方法调用)是Java的一组拥护开发分布式应用程序的API
简单的理解就是:A机器上JVM中运行的Java程序去远程调用B机器上JVM中的Java程序,计算完毕,返回给A机器。调用者不用去关心底层的网络操作,就像调用本地的对象一样,对象名.方法名
RMI大大增强了Java开发分布式应用的能力,例如可以将计算方法复杂的程序放在其他的服务器上,主服务器只需要去调用,而真正的运算是在其他服务器上进行,最后将运算结果返回给主服务器,这样就减轻了主服务器的负担,提高了效率(有其他的开销)
二.理解RMI的工作原理
下图是RMI的体系结构:
Rmi工作原理的理解 (部分内容是转载) X

通过RMI的体系结构,已经可以看出RMI的大致工作原理:
服务器端提供服务,服务中要暴露可以调用的远程方法,以接口的形式表现,这样在客户端可以通过服务接口来调用远程方法,实现复杂的业务逻辑。在服务器端,首先要对接口中提供的方法实现,以便客户端调用能够完成一定的业务逻辑; 接着需要生成Skeleton,在Skeleton中真正地实现了对商业方法的调用,完成了客户请求的调用的过程,将获取到的调用方法的结果通过序列化机制返回给客户端 ,进行应答。在客户端,通过Stub来接收服务器返回的数据(对象),即在这里进行了反序列化,也就是读取网络传输的字节流,进而进行重构。 在Skeleton和Stub中,都对网络通信进行了处理,例如建立套接字,建立网络连接,为实际的业务需要做好准备。
可见,客户端并没有真正地执行与服务器端组件进行直接交互。
这里,有个重要的概念,就是Java对象序列化机制。序列化,就是将对象写入流,以便能够在网络上传输对象,它是输出端执行的。反序列化,也就是在接收端,为了能够获取传输的对象,需要将对象传输而来的字节流进行重构,重新得到完整的该对象。Java对象序列化的过程,是对已有的类的实例进行序列化,首先要存在一个具体的实例。
下面通过在客户端调用服务器端的方法,实现一个例子, 可以很直观地模拟RMI工作 ,从而进一步深化对RMI工作机制的理解。
1. 在服务接口中,将客户端可以进行调用的方法暴露给客户端。
public interface CountServer {
public void add();
public void minus();
}
2.实现接口的类
public class CountServerImpl implements CountServer {
public String add() {
System.out.println('这是服务器端的加法');
return '加法的结果';
}
public String minus() {
System.out.println('这是服务器端的减法');
return '减法的结果';
}
}
3.服务器端skeleton的实现
CountServer_Skeleton类继承了Thread,因此它可以启动服务器端线程,而且是单线程。在 CountServer_Skeleton中,真正与提供服务实现的类进行了交互。在建立起连接以后,首先获取到客户端调用的方法,根据客户的选择来直接与服务的实现交互,执行方法获得执行结果数据,从而将结果数据进行序列化,通过网络传输给客户完成应答过程。
实例化CountServer_Skeleton类的一个实例后,启动线程,这很像是一个服务器端监听器,监听客户端动作,从而完成服务的请求。
import java.io.*;
import java.net.*;
public class CountServer_Skeleton extends Thread {
CountServerImpl count;
public CountServer_Skeleton(CountServerImpl count){
this.count= count;
}
public void run() {
try {
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
while (true) {
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String method = in.readLine();
String result= '';
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
if (method.equals('call add')) {
result = count.add();
}
if (method.equals('call minus')) {
result = count.minus();
}
out.write(result+'\n');
out.flush();
Thread.sleep(2000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
CountServerImpl count= new CountServerImpl();
CountServer_Skeleton count_skel= new CountServer_Skeleton(count);
count_skel.start();
}
}
4、客户端Stub的实现
ShirdrnStub类实现了ShirdrnService接口,它是客户端获取服务结果数据最直接的实现。 然而,它并没有与服务器端真正实现服务接口的实现类直接打交道 ,而是通过网络传输获取到调用方法的执行结果数据。
import java.io.*;
import java.net.Socket;
import aa.CountServer;
public class CountServer_Stub implements CountServer {
Socket socket= null;
BufferedWriter out= null;
BufferedReader in= null;
public CountServer_Stub(){
try {
socket= new Socket('172.16.7.116', 8888);
out= new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
in= new BufferedReader(new InputStreamReader(socket.getInputStream()));
} catch (Exception e) {
e.printStackTrace();
}
}
public String add() {
try {
out.write('call add\n');
out.flush();
String result= in.readLine();
return result;
} catch (Exception e) {r /> return null;
}
}
public String minus() {
try {
out.write('call minus\n');
out.flush();
String result= in.readLine();
return result;
} catch (Exception e) {
return null;
}
}
}
5.启动客户端
public class Client {
public static void main(String[] args) {
CountServer count= new CountServer_Stub();
System.out.println(count.add());
System.out.println(count.minus());
}
}
从代码中可以看出 是 客户端的Stub 在和服务器端的 Skeleton 交互
三.RMI 的简单举例 (JDK1.5版本)
在jdk1.5 及 以上 版本 就不用 rmic 生成stub和skeleton了,而是由jvm 内部生成(不可见)
结构:
Rmi工作原理的理解 (部分内容是转载)

服务器端:RmiServer(接口)、RmiServerImpl(实现类)、Test(启动类)
客户端:Client
1.接口:RmiServer.java
package server;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RmiServer extends Remote {
public String add() throws RemoteException;
}
2.实现类:RmiServerImpl.java
package server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RmiServerImpl extends UnicastRemoteObject implements RmiServer {
nbsp;public RmiServerImpl() throws RemoteException{
super();
}
public String add() throws RemoteException {
System.out.println('服务端开始执行加的方法');
return '这是服务器的加方法';
}

3.Test.java
package server;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.server.RemoteStub;
public class Test extends RemoteStub{
public static void main(String[] args) {
try {
System.out.println('开始注册 ...');
RmiServerImpl server= new RmiServerImpl();
LocateRegistry.createRegistry(1099);
Naming.bind('rmi://172.16.7.116:1099/server',server);
System.out.println('注册成功');
} catch (Exception e) {
System.out.println('向注册表中注册对象失败'+e.toString());
}
}
}
4.客户端类:Client.java
package client;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import server.RmiServer;
public class Client {
public static void main(String[] args) {
try {
RmiServer server= (RmiServer)Naming.lookup('rmi://172.16.7.116:1099/server');
System.out.println(server.add());
} catch (Exception e) {
System.out.println('获得远程服务器注册表失败'+e.toString());
}
}
}
5.编译后生成 class文件
6.将server包 下的 .class 文件放到 E:/ 目录, 将client包下的文件 放到 D:/ 目录, 同时需要将 server下的RmiServer.class 放到D:/server下面 (一定要加上server目录,因为RmiServer 有包server)
7.打开cmd , 输入rmiregistry,启动注册表服务 (打开后不要关闭)
8.打开cmd,进入 E:/ 下,输入 java -Djava.rmi.server.codebase=file:///E:/ server.Test ,启动服务端程序 (打开后不要关闭)
9.打开cmd,进入 D:/下,输入 java client.Client 就可以看到想要的结果了
个人认为:
1. 因为RmiServerImpl 继承了UnicastRemoteObject,并且RmiServerImpl 的构造方法使用了 super(),调用的是UnicastRemoteObject的构造方法 ,UnicastRemoteObject的构造方法就是 exportObject(obj,port), exportObject(obj,port)的作用就是生成RmiServerImpl 对象的代理对象 stub
所以 RmiServerImpl server= new RmiServerImpl(); 就是生成了 代理对象 stub
2.Naming.bind('rmi://172.16.7.116:1099/server',server)就是将代理对象stub 注册到 注册服务中, -Djava.rmi.server.codebase=file:///E:/ 就是告诉注册服务 stub在 哪里,并且客户端从哪里下载stub
3.当客户端 Naming.lookup('rmi://172.16.7.116:1099/server')时,如果客户端不存在 stub类,那么客户端就去访问 服务端 的注册服务,注册服务 通过查找注册列表,然后告诉它从  -Djava.rmi.server.codebase=file:///E:/ 中下载 stub类
其中 RmiServer server= (RmiServer)Naming.lookup('rmi://172.16.7.116:1099/server'); 得到的server其实就是stub,stub是RmiServerImpl对象的代理对象(但是内部代码和RmiServerImpl对象不同),stub也实现了 RmiServer 接口
4.当下载好stub类后,stub就开始和 服务端的 skeleton 开始通信,server.add()就是stub向skeleton发出 请求,skeleton 接到请求后,去访问真正的 服务端的 RmiServerImpl对象 执行add方法,然后返回值给 skeleton ,skeleton再返回给 stub,stub再返回给客户端
下面我们来看一下Stub在server端的生成过程,以及client发现它的过程。
RmiServerImpl的构造方法调用了super()方法,该方法实际调用到UnicastRemoteObject的构造方法,而UnicastRemoteObject的构造方法将调用exportObject()方法。exportObject试图用RmiServerImpl的ClassLoader加载 RmiServerImpl_Stub.class并生成它的实例,也就是:
String name = obj.getClass().getName();
Class c = Class.forName(name + “_Stub”, false, obj.getClass().getClassLoader());
RemoteStub stub = (RemoteStub)c.newInstance();
然后把它发布到指定的socket上,这时候该远程对象就可以使用了。为了client端查找方便,server用Naming Service把这个Stub实例公布出来,这就是Registry.bind()起的作用
注意:
1.stub是在服务端生成的,然后根据需要传到 客户端的
2. 看别人的文章时,发现 有的人 使用 Naming.bind('rmi://172.16.7.116:1099/server',server) 实现注册,有的人使用 Registry registry=LocateRegistry.getRegistry(); registry.bind('Hello',server); 来注册,搞不清楚两者的区别,后来查看源码 才发现,Naming.bind() 方法的源码就是 后者,不懂得可以查看bind()方法的源码
3. RmiServerImpl 一定要 继承 UnicastRemoteObject ,并且在RmiServerImpl 的构造方法中 调用 super(),如果 RmiServerImpl 继承了其他父类,导致不能extends UnicastRemoteObject ,有两种方法:
(1)可以在RmiServerImpl 的构造方法中 调用UnicastRemoteObject.exportObject(this,0)方法
(2) 在Test类中
RmiServerImpl server= new RmiServerImpl();
RmiServer stub= (RmiServer)UnicastRemoteObject.exportObject(server, 0);
Naming.bind('rmi://172.16.7.116:1099/server',stub);
4.在启动服务程序时,必须指定java.rmi.server.codebase属 性, 这样才能动态地将存根类下载到注册表及客户机 。运行服务程序,将codebase属性设置为实现存根的地点。 因为codebase属性只能引用一个目录,所以必须确定在被 java.rmi.server.codebase所引用的目录中已经安装了需要下载的所有类。
出现的错误:
1.忘记 接口实现类 extends UnicastRemoteObject
2.有的时候 java -Djava.rmi.server.codebase=file:///路径/ server.Test 时会出错 java.lang.ClassNotFoundException: server.RmiServer
可能是路径写的不对,路径一定要写到 RmiServerImpl 所在的根路径,例如 RmiServerImpl有包名 server,server放到了D:/test/下 ,那么 路径 就应该写  file:///D:/test/  不能写 file:///D:/test/server/ ,因为 test才是 RmiServerImpl的根路径
四. 在servlet中注册服务
1. 新建web 项目RmiWebServer
2. 将三 中的 Test类 改写为 一个servlet:setSever, 在setServer的 post方法中注册服务
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
System.out.println('开始注册 ...');
RmiServerImpl server= new RmiServerImpl();
Naming.bind('rmi://172.16.7.116:1099/server',server);
System.out.println('注册成功');
} catch (RemoteException e) {
// TODO Auto-generated catch block
System.out.println('获得本地RMI注册表对象是失败'+e.toString());
}catch (MalformedURLException e) {
System.out.println('向注册表中注册对象失败'+e.toString());
}catch (AlreadyBoundException e) {
System.out.println('向注册表中注册对象失败'+e.toString());
}
}
3.然后 编写一个index.html,让它的form 的action指向 setServer
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
<title>Insert title here</title>
</head>
<body>
<form action='setServer' method='post'>
<input type='submit' value='注册服务'></input>
</form>
</body>
</html>
4.打开cmd,启动注册服务 rmiregistry (不要关闭)
5.启动 项目RmiWebServer,进入到index.html页面,点击 '注册服务' 按钮,从控制台可以看出服务注册成功
6.执行 三 中的第9步 ,就可以看到结果了,或者 将三 中的Client也 改为jsp或者servlet 都可以
不明白的地方:将启动服务的程序 变为servlet形式,就不用设置 java.rmi.server.codebase了,但如果是应用程序就 需要设置 codebase,不知道为什么

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值