前言:随着一个系统被用户认可,业务量、请求量不断上升,那么单机系统必然就无法满足了,于是系统就慢慢走向分布式了,随之而来的是系统之间“沟通”的障碍。一般来说,解决系统之间的通信可以有两种方式:即远程调用和消息。RMI(Remote Method Invocation)就是远程调用的一种方式,也是这篇文章主要介绍的。
一、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这种东西。假设我们需要自己实现上面例子中的效果,怎么办呢?可以想到的步骤是:
- 编写服务端服务,并将其通过某个服务机的端口暴露出去供客户端调用。
- 编写客户端程序,客户端通过指定服务所在的主机和端口号、将请求封装并序列化,最终通过网络协议发送到服务端。
- 服务端解析和反序列化请求,调用服务端上的服务,将结果序列化并返回给客户端。
- 客户端接收并反序列化服务端返回的结果,反馈给用户。
这是大致的流程,我们不难想到,RMI其实也是帮我们封装了一些细节而通用的部分,比如序列化和反序列化,连接的建立和释放等,下面是RMI的具体流程:
这里涉及到几个新概念:
Stub和Skeleton:这两个的身份是一致的,都是作为代理的存在。客户端的称作Stub,服务端的称作Skeleton。要做到对程序员屏蔽远程方法调用的细节,这两个代理是必不可少的,包括网络连接等细节。
Registry:顾名思义,可以认为Registry是一个“注册所”,提供了服务名到服务的映射。如果没有它,意味着客户端需要记住每个服务所在的端口号,这种设计显然是不优雅的。
三、走进RMI原理之前,先来看看用到的类及其层次结构和主要的方法。
哪里看不懂随时回来看看结构。。。开始了
四、一步步解剖RMI的底层原理
- 服务端启动Registry服务
Registry registry=LocateRegistry.createRegistry(1099);从上面这句代码入手,追溯下去,可以发现服务端创建了一个RegistryImpl对象,这里做了一个判断。如果服务端指定的端口号是1099并且系统开启了安全管理器,那么可以在限定的权限集内(listen和accept)绕过系统的安全校验。反之则必须进行安全校验。这里纯粹是为了效率起见。真正做的事情在setUp()方法中,继续看下去。
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