前言:随着一个系统被用户认可,业务量、请求量不断上升,那么单机系统必然就无法满足了,于是系统就慢慢走向分布式了,随之而来的是系统之间“沟通”的障碍。一般来说,解决系统之间的通信可以有两种方式:即远程调用和消息。RMI(Remote Method Invocation)就是远程调用的一种方式,也是这篇文章主要介绍的。
一、RPC和RMI的关系
从目标上来看,它们都是为了实现远程调用的。不同点在于RMI具有语言的局限性,仅限于Java语言,而RPC则没有限制,更加通用。相对地,RMI有其较之RPC不可比拟的优点,那就是提供了面向对象的支持,这也是为了呼应Java作为面向对象语言的特性。
二、RMI通信的一个简单示例
这个示例拆分为服务端和客户端,放在两个idea项目中,并且通过了单机和双机两种环境的测试,是真正意义上的分布式应用了。
项目结构
服务端应用: Server
主程序: com.jnu.wwt.entry.Server
服务接口: com.jnu.wwt.service.IOperation
服务实现: com.jnu.wwt.service.impl.OperationImpl
客户端应用: Client
主程序: com.jnu.wwt.entry.Client
服务接口: com.jnu.wwt.service.IOperation
源码:
Server.java
/** * Created by wwt on 2016/9/14. */ public class Server { public static void main(String args[]) throws Exception{ //以1099作为LocateRegistry接收客户端请求的端口,并注册服务的映射关系 Registry registry=LocateRegistry.createRegistry(1099); IOperation iOperation=new OperationImpl(); Naming.rebind("rmi://127.0.0.1:1099/Operation",iOperation); System.out.println("service running..."); } }
IOperation.java(服务端和客户端各需要一份)
/** * 服务端接口必须实现java.rmi.Remote * Created by wwt on 2016/9/14. */ public interface IOperation extends Remote{ /** * 远程接口上的方法必须抛出RemoteException,因为网络通信是不稳定的,不能吃掉异常 * @param a * @param b * @return */ int add(int a, int b) throws RemoteException; }
OperationImpl.java
/** * Created by wwt on 2016/9/14. */ public class OperationImpl extends UnicastRemoteObject implements IOperation{ public OperationImpl() throws RemoteException { super(); } @Override public int add(int a, int b) throws RemoteException{ return a+b; } }
Client.java
/** * Created by wwt on 2016/9/15. */ public class Client { public static void main(String args[]) throws Exception{ IOperation iOperation= (IOperation) Naming.lookup("rmi://127.0.0.1:1099/Operation"); System.out.println(iOperation.add(1,1)); } }
运行结果
先运行Server应用,服务就起来了。然后切换到Client应用,点击运行,Client调用Server的服务,返回结果。
三、RMI做了些什么
现在我们先忘记Java中有RMI这种东西。假设我们需要自己实现上面例子中的效果,怎么办呢?可以想到的步骤是:
a.编写服务端服务,并将其通过某个服务机的端口暴露出去供客户端调用。
b.编写客户端程序,客户端通过指定服务所在的主机和端口号、将请求封装并序列化,最终通过网络协议发送到服务端。
c.服务端解析和反序列化请求,调用服务端上的服务,将结果序列化并返回给客户端。
d.客户端接收并反序列化服务端返回的结果,反馈给用户。
这是大致的流程,我们不难想到,RMI其实也是帮我们封装了一些细节而通用的部分,比如序列化和反序列化,连接的建立和释放等,下面是RMI的具体流程:
这里涉及到几个新概念:
Stub和Skeleton:这两个的身份是一致的,都是作为代理的存在。客户端的称作Stub,服务端的称作Skeleton。要做到对程序员屏蔽远程方法调用的细节,这两个代理是必不可少的,包括网络连接等细节。
Registry:顾名思义,可以认为Registry是一个“注册所”,提供了服务名到服务的映射。如果没有它,意味着客户端需要记住每个服务所在的端口号,这是极不优雅的。
四、按照上面的时序图,来看看RMI的底层细节
主要用到的类:
主要的类的层次结构:
1.启动Registry服务
//以1099作为LocateRegistry接收客户端请求的端口,并注册服务的映射关系 Registry registry=LocateRegistry.createRegistry(1099);
调用以上方法,将会以1099端口为参数实例化一个RegistryImpl对象。这里做了一个判断,假如指定的端口是1099并且打开了安全管理器,那么对于指定的权限(listen,accept)不需要做权限控制。反之则需要进行安全校验。提升了效率。
public RegistryImpl(final int var1) throws RemoteException { if(var1 == 1099 && System.getSecurityManager() != null) { try { AccessController.doPrivileged(new PrivilegedExceptionAction() { public Void run() throws RemoteException { LiveRef var1x = new LiveRef(RegistryImpl.id, var1); RegistryImpl.this.setup(new UnicastServerRef(var1x)); return null; } }, (AccessControlContext)null, new Permission[]{new SocketPermission("localhost:" + var1, "listen,accept")}); } catch (PrivilegedActionException var3) { throw (RemoteException)var3.getException(); } } else { LiveRef var2 = new LiveRef(id, var1); this.setup(new UnicastServerRef(var2)); } }
接下来会调用到RegistryImpl的setUp()方法,setUp方法又会调用到UnicastServerRef的exportObject()方法,如下:
public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException { Class var4 = var1.getClass(); Remote var5; try { var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse); } catch (IllegalArgumentException var7) { throw new ExportException("remote object implements illegal remote interface", var7); } if(var5 instanceof RemoteStub) { this.setSkeleton(var1); } Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3); this.ref.exportObject(var6); this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4); return var5; }
很明显,这里创建并设置了服务端的Skeleton对象。
2.创建服务对象,乍一看没啥好说的,其实不然。从上面的例子中,我们可以看到OperationImpl继承了UnicastRemoteObject对象,并且调用了其构造器,我们就来看看它做了什么。从Operation的super()开始,追溯到UnicastRemoteObject的exportObject()方法。
private static Remote exportObject(Remote obj, UnicastServerRef sref) throws RemoteException { // if obj extends UnicastRemoteObject, set its ref. if (obj instanceof UnicastRemoteObject) { ((UnicastRemoteObject) obj).ref = sref; } return sref.exportObject(obj, null, false); }
这里做了一个判断,如果我们需要暴露出去的服务实现类是UnicastRemoteObject的子类,那么我们只要设置这个服务实现类从RemoteObject继承的RemoteRef对象为传入的UnicastServerRef对象即可。否则(不继承UnicastRemoteObject也是可以通过其他方法实现)调用UnicastServerRef的exportObject()方法。这里我们是第一种情况,直接为ref赋值即可。可以看到第二种情况也是调用到UnicastServerRef的exportObject()方法。
3.注册服务对象。
Naming.rebind("rmi://127.0.0.1:1099/Operation",iOperation);
通过源码我们可以看到,其实Naming调用的都是Registry的方法。辗转调用到RegistryImpl类的rebind方法,这个方法做的事情是将服务名称和服务的实现映射存在一个map里面。
public void rebind(String var1, Remote var2) throws RemoteException, AccessException { checkAccess("Registry.rebind"); this.bindings.put(var1, var2); }
4.客户端访问服务器并通过服务名查找服务
IOperation iOperation= (IOperation) Naming.lookup("rmi://127.0.0.1:1099/Operation");
public static Remote lookup(String name) throws NotBoundException, java.net.MalformedURLException, RemoteException { ParsedNamingURL parsed = parseURL(name); Registry registry = getRegistry(parsed); if (parsed.name == null) return registry; return registry.lookup(parsed.name); }
上面主要做了两件事情,通过主机和端口(1099)获取服务端Registry对象的Stub(RegistryImpl_Stub)。通过Registry的Stub对象lookUp()找到服务实现的Stub对象。这里主要看看客户端如何获取服务实现的Stub对象,查看RegistryImpl_Stub的lookUp()方法。核心代码如下:
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L); try { ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(var1); } catch (IOException var18) { throw new MarshalException("error marshalling arguments", var18); } super.ref.invoke(var2); Remote var23; try { ObjectInput var6 = var2.getInputStream(); var23 = (Remote)var6.readObject(); } catch (IOException var15) { throw new UnmarshalException("error unmarshalling return", var15); } catch (ClassNotFoundException var16) { throw new UnmarshalException("error unmarshalling return", var16); } finally { super.ref.done(var2); } return var23;
这段代码做的事情是构造了RemoteCall对象,将服务名写入输出流中,调用方法。这样就调用了服务端的lookUp()方法。服务端将服务实现的Stub传递给客户端。于是客户端便有了Stub对象。
5.最后再来看看Stub,Skeleton,服务实现如何之间的交互在代码中的体现。这里博主试过用rmic编译产生这两个文件,但是失败了,所以暂且参考一下其他大神的博文里面的示例。java RMI原理详解
Stub类:
- public class Person_Stub implements Person {
- private Socket socket;
- public Person_Stub() throws Throwable {
- // connect to skeleton
- socket = new Socket("computer_name", 9000);
- }
- public int getAge() throws Throwable {
- // pass method name to skeleton
- ObjectOutputStream outStream =
- new ObjectOutputStream(socket.getOutputStream());
- outStream.writeObject("age");
- outStream.flush();
- ObjectInputStream inStream =
- new ObjectInputStream(socket.getInputStream());
- return inStream.readInt();
- }
- public String getName() throws Throwable {
- // pass method name to skeleton
- ObjectOutputStream outStream =
- new ObjectOutputStream(socket.getOutputStream());
- outStream.writeObject("name");
- outStream.flush();
- ObjectInputStream inStream =
- new ObjectInputStream(socket.getInputStream());
- return (String)inStream.readObject();
- }
- }
可以看到,Stub对象做的事情是建立到服务端Skeleton对象的Socket连接。将客户端的方法调用转换为字符串标识传递给Skeleton对象。并且同步阻塞等待服务端返回结果。
Skeleton类:
- public class Person_Skeleton extends Thread {
- private PersonServer myServer;
- public Person_Skeleton(PersonServer server) {
- // get reference of object server
- this.myServer = server;
- }
- public void run() {
- try {
- // new socket at port 9000
- ServerSocket serverSocket = new ServerSocket(9000);
- // accept stub‘s request
- Socket socket = serverSocket.accept();
- while (socket != null) {
- // get stub‘s request
- ObjectInputStream inStream =
- new ObjectInputStream(socket.getInputStream());
- String method = (String)inStream.readObject();
- // check method name
- if (method.equals("age")) {
- // execute object server‘s business method
- int age = myServer.getAge();
- ObjectOutputStream outStream =
- new ObjectOutputStream(socket.getOutputStream());
- // return result to stub
- outStream.writeInt(age);
- outStream.flush();
- }
- if(method.equals("name")) {
- // execute object server‘s business method
- String name = myServer.getName();
- ObjectOutputStream outStream =
- new ObjectOutputStream(socket.getOutputStream());
- // return result to stub
- outStream.writeObject(name);
- outStream.flush();
- }
- }
- } catch(Throwable t) {
- t.printStackTrace();
- System.exit(0);
- }
- }
- }
Skeleton对象做的事情是将服务实现传入构造参数,获取客户端通过socket传过来的方法调用字符串标识,将请求转发到具体的服务上面。获取结果之后返回给客户端。