RPC

RPC(Remote  Procedure  Call,远程过程调用),一般用来实现部署在不同机器上的系统之间的方法调用,使得程序能够像访问本地系统资源一样,通过网络传输去访问远端系统资源;对于客户端来说,传输层使用什么协议,序列化、反序列化都是透明的。 

Java RMI

RMI 全称是 remote method invocation –  远程方法调用,一种用于远程过程调用的应用程序编程接口,是纯 java 的网络分布式应用系统的核心解决方案之一。 RMI 目前使用 Java 远程消息交换协议 JRMP(Java Remote Messageing Protocol)进行通信,由于 JRMP 是专为 Java对象制定的,是分布式应用系统的百分之百纯 java 解决方案,用 Java RMI 开发的应用系统可以部署在任何支持 JRE的平台上,缺点是,由于 JRMP 是专门为 java 对象指定的,因此 RMI 对于非 JAVA 语言开发的应用系统的支持不足,不能与非 JAVA 语言书写的对象进行通信。 

简单基于RMI实现远程通信

在单工程中调用IHelloService中的方法是很简单的:

分布式通信之RMI_客户端

但是在跨工程中就无法这样使用了,因为我们无法直接获取Service实例。如果想要进行远程调用,我们需要对Service进行一些改造:

IHelloService需要继承Remote:

分布式通信之RMI_java_02

HelloServiceImpl需要继承UnicastRemoteObject:

分布式通信之RMI_服务端_03

接下来需要发布服务:

分布式通信之RMI_客户端_04

在客户端:

分布式通信之RMI_java_05

启动服务:

分布式通信之RMI_java_06

客户端调用:

分布式通信之RMI_服务端_07

由于我在HelloServiceImpl中打印出了name,所以客户端调用后服务端的控制台会输出:

分布式通信之RMI_客户端_08

一般会把相关的公共依赖抽取出来让消费端去依赖。

RMI实现远程发布的原理分析

首先想如果自己实现一个远程通信框架需要做些什么。

1.能够对外提供服务(Socket);

2.服务的调用问题;

在示例中,有一个HelloServiceImpl和IHelloService,总体类的关系图如下:

分布式通信之RMI_java_09

HelloServiceImpl需要继承UnicastRemoteObject,并在构造函数中抛出RemoteException异常:

分布式通信之RMI_java_10

看看父类的构造方法:

分布式通信之RMI_java_11

分布式通信之RMI_java_12

其中调用了exportObject()方法,看名称就可以大致猜出这个方法是将当前对象发布出去。而这个对象又实现了序列化,那么这个对象是可以被传输调用的。

分布式通信之RMI_服务端_13

分布式通信之RMI_服务端_14

如果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;
}

再来回过头看看Server端发布远程对象的过程:

分布式通信之RMI_客户端_15

分布式通信之RMI_java_16

也就是说在new HelloServiceImpl的时候已经发布了一个远程对象。中间会生成一个HelloServiceImpl的代理对象。

接下来服务端启动Registry服务是基于这一行代码:

//注册,这个端口必须是1099
LocateRegistry.createRegistry(1099);

分布式通信之RMI_服务端_17

这里new了一个RegistryImpl,明显是Registry的一个实现。

分布式通信之RMI_java_18

而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<Void>() {
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()方法:

分布式通信之RMI_客户端_19

这里跟上面是一样的。最终还是调用了这个exportObject()方法:

分布式通信之RMI_服务端_20

会生成RegistryImpl的代理对象。

也就是说这两段代码:

分布式通信之RMI_服务端_21

分别生成了HelloServiceImpl和RegistryImpl的代理对象。

后面的Naming.rebind()方法就是绑定的url和Service的一个对应关系。有点像注册中心的意思

再回过头来看sun.rmi.server.UnicastServerRef#exportObject(java.rmi.Remote, java.lang.Object, boolean)这个方法(这个包是rt包下的,并未开源):

分布式通信之RMI_java_22

分布式通信之RMI_客户端_23

随后会调用TCPEndpoint的exportObject()方法:

分布式通信之RMI_客户端_24

这个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();
}
}

}

在开启监听的方法中:

分布式通信之RMI_客户端_25

通过工厂模式创建了ServerSocket:

分布式通信之RMI_服务端_26

这个方法有多个实现,具体是那个实现取决于var1,这里先看看LocalRMIServerSocketFactory的实现:

分布式通信之RMI_java_27

这个方法很长,其实本质就是创建的ServerSocket,细节方面没必要过多关注了:

分布式通信之RMI_客户端_28

再回过头接着看sun.rmi.transport.tcp.TCPTransport#listen:

分布式通信之RMI_java_29

在 TCP 协议层发起 socket 监听,并采用多线程循环接收请求:TCPTransport.AcceptLoop(this.server):

分布式通信之RMI_java_30

继续通过线程池来处理 socket 接收到的请求:

分布式通信之RMI_服务端_31

分布式通信之RMI_客户端_32

这个run0()方法非常复杂,使用了一个switch语句做了一些判断,猜想是对不同的协议进行处理,在这个示例中根据debug最终会到达这里:

分布式通信之RMI_客户端_33

最终会再进入case 80中:

分布式通信之RMI_java_34

一步一步找到了 Transport 的 serviceCall()方法,这个方法是关键。回顾一下主要的代码,到ObjectTable.getTarget()为止做的事情是从socket流中获取ObjId,并通过ObjId和Transport对象获取Target对象,这里的Target对象已经是服务端的对象。再借由 Target的派发器Dispatcher,传入参数服务实现和请求对象RemoteCall,将请求派发给服务端那个真正提供服务的RegistryImpl的lookUp()方法,这就是Skeleton移交给具体实现的过程了,Skeleton 负责底层的操作。

分布式通信之RMI_java_35

分布式通信之RMI_java_36

再回到sun.rmi.transport.tcp.TCPTransport#exportObject:

分布式通信之RMI_java_37

将当前的Target对象放到了一个ObjectTabele里面

分布式通信之RMI_java_38

分布式通信之RMI_服务端_39

其实本质就是放到了一个Map里面:

分布式通信之RMI_客户端_40

服务端发布服务后,客户端调用服务端的代码为:

分布式通信之RMI_客户端_41

先会创建一个 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()方法:

分布式通信之RMI_服务端_42

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(如果没有就会报错)。 

分布式通信之RMI_java_43