对RMI的学习已经过去了3天时间,概念很简单,但是在使用过程中总是遇到这样那样的问题,也再次从一个侧面证明了,技术的学习一定要通过真实环境的测试,否则知其然不知其所以然也是没有办法在生产环境中来使用的,下面对rmi的原理与问题进行分析。
下面的描述部分内容收集来自互联网.
黑体部分是我加注并需要注意的
RMI(即Remote Method Invoke 远程方法调用)。在Java中,只要一个类extends了java.rmi.Remote接口,即可成为存在于服务器端的远程对象,供客户端访问并提供一定的服务。JavaDoc描述:Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口”(扩展 java.rmi.Remote 的接口)中指定的这些方法才可远程使用。
注意:extends了Remote接口的类或者其他接口中的方法若是声明抛出了RemoteException异常,则表明该方法可被客户端远程访问调用。
同时,远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信,而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。
RMI 框架的基本原理大概如下图,应用了代理模式来封装了本地存根与真实的远程对象进行通信的细节。
下面给出一个简单的RMI 应用,其中类图如下:其中IService接口用于声明服务器端必须提供的服务(即service()方法),ServiceImpl类是具体的服务实现类,而Server类是最终负责注册服务器远程对象,以便在服务器端存在骨架代理对象来对客户端的请求提供处理和响应。
======================================================这里是分隔线================================================
/** * @author Alan * * 服务接口 */ public interface WareHouse extends Remote { double getPrice(String decription) throws RemoteException; } |
/** * @author Alan * * 服务实现类型 */ public class WareHouseImpl extends UnicastRemoteObject implements WareHouse {
private Map<String, Double> prices;
protected WareHouseImpl() throws RemoteException { prices = new HashMap<String, Double>(); prices.put("Blackwell Toaster", 24.95); prices.put("Zap", 49.95); }
@Override public double getPrice(String decription) throws RemoteException { Double price = prices.get(decription);
return price == null ? 0 : price; }
} |
public class WareHouseServer {
/** * @param args * @throws RemoteException * @throws NamingException * @throws AlreadyBoundException * @throws MalformedURLException */ public static void main(String[] args) throws RemoteException, NamingException, MalformedURLException, AlreadyBoundException {
//这里是RMI远程调用的一个最容易不被主要到的点,如果没有下面的设置,可能你在本地测试都是没有问题 //但是如果开启防火墙的主机上面以测试就会遇到问题 try { RMISocketFactory.setSocketFactory(new SMRMISocket()); } catch (Exception ex) { }
WareHouseImpl centralWareHouse = new WareHouseImpl();
LocateRegistry.createRegistry(8888); Naming.rebind("rmi://host:8888/central_warehouse", centralWareHouse); System.out.println("远程服务启动完成...");
} }
/** * * @author Alan * *指定RMI socket创建工厂 */ public class SMRMISocket extends RMISocketFactory {
public Socket createSocket(String host, int port) throws IOException { return new Socket(host, port); }
public ServerSocket createServerSocket(int port) throws IOException { //在这里可以指定 RMI 服务监听端口 if (port == 0) { port = 21100; // 由于测试这里就写硬代码了 } return new ServerSocket(port);
} }
|
public class WareHouseClient {
/** * @param args * @throws NamingException * @throws RemoteException * @throws NotBoundException * @throws MalformedURLException */ public static void main(String[] args) throws NamingException, RemoteException, MalformedURLException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("host", 8888);
for (String s : registry.list()) { System.out.println(s); }
WareHouse wareHouse = (WareHouse) Naming.lookup("rmi://host:8888/central_warehouse");
String des = "Blackwell Toaster"; double price = wareHouse.getPrice(des);
System.out.println(des + " -> " + price); }
} |
上面黑体部分是我遇到最大的问题,开始没有加,我在本机测试始终是没有问题的,后面部署到linux上面就始终访问不到,查了很久资料发现网上很多资料都只是在本地测试,并且基本上都是demo阶段,大家都是不求甚解的(呵呵…)。后面总算在一个大神的blog中发现了蛛丝马迹,原因如下:
RMI 远程调用分为三部分 注册表服务 、我们的服务 、客户端
上面代码中8888端口只是注册表服务,在我们的server端代码中我们开启了注册表服务,然后我们向它注册了一个WareHouse服务,但是WareHouse服务并不是通过8888来访问的,而是被随机分配了一个(在我们没有指定RMISocketFactory的情况下),所以在linux上面因为这个随机端口问题,我们始终被防火墙拦在了外面。