RPC(Remote Procedure Call,远程过程调用),一般用来实现部署在不同机器上的系统之间的方法调用,使得程序能够像访问本地系统资源一样,通过网络传输去访问远端系统资源;对于客户端来说,传输层使用什么协议,序列化、反序列化都是透明的。
RMI 全称是 remote method invocation – 远程方法调用,一种用于远程过程调用的应用程序编程接口,是纯 java 的网络分布式应用系统的核心解决方案之一。 RMI 目前使用 Java 远程消息交换协议 JRMP(Java Remote Messageing Protocol)进行通信,由于 JRMP 是专为 Java对象制定的,是分布式应用系统的百分之百纯 java 解决方案,用 Java RMI 开发的应用系统可以部署在任何支持 JRE的平台上,缺点是,由于 JRMP 是专门为 java 对象指定的,因此 RMI 对于非 JAVA 语言开发的应用系统的支持不足,不能与非 JAVA 语言书写的对象进行通信。
在单工程中调用IHelloService中的方法是很简单的:
但是在跨工程中就无法这样使用了,因为我们无法直接获取Service实例。如果想要进行远程调用,我们需要对Service进行一些改造:
IHelloService需要继承Remote:
HelloServiceImpl需要继承UnicastRemoteObject:
接下来需要发布服务:
在客户端:
启动服务:
客户端调用:
由于我在HelloServiceImpl中打印出了name,所以客户端调用后服务端的控制台会输出:
一般会把相关的公共依赖抽取出来让消费端去依赖。
首先想如果自己实现一个远程通信框架需要做些什么。
1.能够对外提供服务(Socket);
2.服务的调用问题;
在示例中,有一个HelloServiceImpl和IHelloService,总体类的关系图如下:
HelloServiceImpl需要继承UnicastRemoteObject,并在构造函数中抛出RemoteException异常:
看看父类的构造方法:
其中调用了exportObject()方法,看名称就可以大致猜出这个方法是将当前对象发布出去。而这个对象又实现了序列化,那么这个对象是可以被传输调用的。
如果obj是UnicastRemoteObject,那么就会赋值给sref,sref就是之前new出来的UnicastServerRef。然后又调用了UnicastServerRef的exportObject()方法:
public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
Class var4 = var1.getClass();
Remote var5;
try {
//使用Util创建了一个var4的代理,这个var4是var1的Class,而var1就是当前对象也就是HelloServiceImpl对象
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);
}
//包装一个暴露在TCP端口上的对象
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;
}
也就是说在new HelloServiceImpl的时候已经发布了一个远程对象。中间会生成一个HelloServiceImpl的代理对象。
接下来服务端启动Registry服务是基于这一行代码:
//注册,这个端口必须是1099
LocateRegistry.createRegistry(1099);
这里new了一个RegistryImpl,明显是Registry的一个实现。
而Registry也继承了Remote,这说明Registry也是可以对外发布的。结合这两张关系图,可以看出其实HelloServiceImpl和RegistryImp处于同一层级下。
public RegistryImpl(final int var1) throws RemoteException {
//其实这个if和else里面的内容都是差不多了,但是if里面多了安全机制的判断
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);
//setup是安装的意思,这里将LiveRef包装成一个UnicastServerRef
this.setup(new UnicastServerRef(var2));
}
}
再进一步看这个setup()方法:
这里跟上面是一样的。最终还是调用了这个exportObject()方法:
会生成RegistryImpl的代理对象。
也就是说这两段代码:
分别生成了HelloServiceImpl和RegistryImpl的代理对象。
后面的Naming.rebind()方法就是绑定的url和Service的一个对应关系。有点像注册中心的意思
再回过头来看sun.rmi.server.UnicastServerRef#exportObject(java.rmi.Remote, java.lang.Object, boolean)这个方法(这个包是rt包下的,并未开源):
随后会调用TCPEndpoint的exportObject()方法:
这个transport就是TCPTransport,这里调用的TCPTransport的exportObject()方法:
public void exportObject(Target var1) throws RemoteException {
synchronized(this) {
//开启监听
this.listen();
++this.exportCount;
}
boolean var2 = false;
boolean var12 = false;
try {
var12 = true;
super.exportObject(var1);
var2 = true;
var12 = false;
} finally {
if(var12) {
if(!var2) {
synchronized(this) {
this.decrementExportCount();
}
}
}
}
if(!var2) {
synchronized(this) {
this.decrementExportCount();
}
}
}
在开启监听的方法中:
通过工厂模式创建了ServerSocket:
这个方法有多个实现,具体是那个实现取决于var1,这里先看看LocalRMIServerSocketFactory的实现:
这个方法很长,其实本质就是创建的ServerSocket,细节方面没必要过多关注了:
再回过头接着看sun.rmi.transport.tcp.TCPTransport#listen:
在 TCP 协议层发起 socket 监听,并采用多线程循环接收请求:TCPTransport.AcceptLoop(this.server):
继续通过线程池来处理 socket 接收到的请求:
这个run0()方法非常复杂,使用了一个switch语句做了一些判断,猜想是对不同的协议进行处理,在这个示例中根据debug最终会到达这里:
一步一步找到了 Transport 的 serviceCall()方法,这个方法是关键。回顾一下主要的代码,到ObjectTable.getTarget()为止做的事情是从socket流中获取ObjId,并通过ObjId和Transport对象获取Target对象,这里的Target对象已经是服务端的对象。再借由 Target的派发器Dispatcher,传入参数服务实现和请求对象RemoteCall,将请求派发给服务端那个真正提供服务的RegistryImpl的lookUp()方法,这就是Skeleton移交给具体实现的过程了,Skeleton 负责底层的操作。
再回到sun.rmi.transport.tcp.TCPTransport#exportObject:
将当前的Target对象放到了一个ObjectTabele里面
其实本质就是放到了一个Map里面:
先会创建一个 RegistryImpl_Stub 的代理类,通过这个代理类进行 socket 网络请求,将 lookup 发送到服务端,服务端通过接收到请求以后,通过服务端的 RegistryImpl_Stub(Skeleton),执行RegistryImpl的lookUp。而服务端的RegistryImpl返回的就是服务端的HeloServiceImpl的实现类。 简单点说就是通过地址获取到了HelloServiceImpl的一个实例对象,但是是在两个不同的系统进程中,肯定是不能直接获取实例的,这里实际上获取到的是HelloServiceImpl_stub。
先看看Naming.lookup()方法:
public static Remote lookup(String name)
throws NotBoundException,
java.net.MalformedURLException,
RemoteException
{
ParsedNamingURL parsed = parseURL(name);
//解析后,获取对应的Registry的实例对象,明显这里也不可能是RegistryImpl的实例对象,而是RegistryImpl_stub
Registry registry = getRegistry(parsed);
if (parsed.name == null)
return registry;
return registry.lookup(parsed.name);
}
在看看这个getRegistry()方法:
public static Registry getRegistry(String host, int port,
RMIClientSocketFactory csf)
throws RemoteException
{
Registry registry = null;
if (port <= 0)
//默认端口1099
port = Registry.REGISTRY_PORT;
if (host == null || host.length() == 0) {
// If host is blank (as returned by "file:" URL in 1.0.2 used in
// java.rmi.Naming), try to convert to real local host name so
// that the RegistryImpl's checkAccess will not fail.
try {
host = java.net.InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
// If that failed, at least try "" (localhost) anyway...
host = "";
}
}
/*
* Create a proxy for the registry with the given host, port, and
* client socket factory. If the supplied client socket factory is
* null, then the ref type is a UnicastRef, otherwise the ref type
* is a UnicastRef2. If the property
* java.rmi.server.ignoreStubClasses is true, then the proxy
* returned is an instance of a dynamic proxy class that implements
* the Registry interface; otherwise the proxy returned is an
* instance of the pregenerated stub class for RegistryImpl.
**/
//这里又出现了LiveRef
LiveRef liveRef =
new LiveRef(new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint(host, port, csf, null),
false);
RemoteRef ref =
(csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);
//创建代理对象
return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
}
这里使用了JDK的动态代理创建代理对象。
客户端获取通过lookUp()查询获得的客户端HelloServiceImpl的Stub对象客户端通过Lookup查询获得的是客户端HelloServiceImpl 的Stub对象(这一块我们看不到,因为这块由 Skeleton 为我们屏蔽了),然后后续的处理仍然是通过HelloServiceImpl_Stub 代理对象通过socket网络请求到服务端,通过服务端的HelloServiceImpl_Stub(Skeleton) 进行代理,将请求通过Dispatcher转发到对应的服务端方法获得结果以后再次通过 socket 把结果返回到客户端; RMI做了什么,根据上面的源码阅读,实际上我们看到的应该是有两个代理类,一个是RegistryImpl的代理类和我们HelloServiceImpl的代理类。
要注意的是,在RMI Client实施正式的 RMI 调用前,它必须通过LocateRegistry或者Naming方式到RMI注册表寻找要调用的 RMI 注册信息。找到RMI事务注册信息后,Client会从RMI注册表获取这个RMI Remote Service的Stub信息。这个过程成功后,RMI Client 才能开始正式的调用过程。
另外要说明的是 RMI Client 正式调用过程,也不是由 RMI Client 直接访问 Remote Service,而是由客户端获取的Stub 作为 RMI Client 的代理访问Remote Service的代理Skeleton,如上图所示的顺序。也就是说真实的请求调用是在Stub-Skeleton之间进行的。Registry 并不参与具体的Stub-Skeleton的调用过程,只负责记录“哪个服务名”使用哪一个Stub,并在Remote Client询问它时将这个Stub拿给Client(如果没有就会报错)。