专栏文章 JDK RMI 探索与使用

opentest-oper@360.cn · March 26, 2020 · 46 hits

探索JDK RMI

RMI(Remote Method Invoke)是JDK自带的一个远程调用框架,可以基于RMI构建自己的分布式应用。现在很多分布式服务框架都会选择支持RMI协议,比如dubbo,spring rmi,Hessian等RPC框架。为了对RMI有更多的了解,下面我们一起来读代码。

一、代码示例

为了更直观的了解RMI的使用,先来看一段代码。
新建三个maven工程,分别是rmiclient,rmidependency,rmiserver,其中rmiclient是客服端,rmiserver是服务端,rmidependency是一个只包含接口的服务工程。rmiclient和rmiserver引入rmidependency依赖。rmidependency中HelloWorldService 接口如下:

public interface HelloWorldService extends Remote {
public String sayHello(String from, String to) throws RemoteException;
}

rmiserver中HelloWordServiceImpl实现了HelloWorldService接口的类:

public class HelloWorldServiceImpl extends UnicastRemoteObject implements HelloWorldService {
public HelloWorldServiceImpl() throws RemoteException {
super();
}
@Override
public String sayHello(String from, String to) throws RemoteException {
return from + " say hello to " + to;
}
}

在rmiServer中发布服务

public class RMIServer
{
public static void main( String[] args )
{
try {
//1.服务创建及发布:HelloWorldService 需要继承自 UnicastRemoteObject,当初始化时会自动将 HelloWorldServiceImpl任务一个服务发布。
HelloWorldService helloWorldService = new HelloWorldServiceImpl();
// 2. 创建注册中心:创建本机 1099 端口上的 RMI 注册表
Registry registry = LocateRegistry.createRegistry(1099);
// 3. 服务注册:将服务绑定到注册表中
registry.bind("rmi://localhost:1099/helloWorldService", helloWorldService);
} catch (RemoteException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}

在rmiclent中调用远程服务

public class RMIClient
{
public static void main( String[] args )
{
try {
HelloWorldService helloWorldService = (HelloWorldService) Naming.lookup("rmi://localhost:1099/helloWorldService");
if(helloWorldService != null) {
String result = helloWorldService.sayHello("java", "菜鸟");
System.out.println(result);
} else {
System.out.println("rmi 查找服务错误");
}
//Naming.list("rmi://localhost:1099");
} catch (NotBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}

以上,可以看出RMI开发中有三个重要角色,分别是客户端,服务端,注册中心,其中
(1)注册中心:负责服务注册
(2)服务端:是提供服务的地方,是真正实现约定接口的地方
(3)客户端:是服务的调用方,是需要使用服务的一方。

通过上面的代码发现,无论是HelloWorldServiceImpl还是Registry都是RemoteObjet的子类。具体类图如下

RemoteObject 最重要的属性是 RemoteRef ref, RemoteRef 的实现类 UnicastRef,UnicastRef 包含属性 LiveRef ref。LiveRef 类中的 Endpoint、Channel 封装了与网络通信相关的方法。

二、启动服务端

服务端通过LocateRegistry.createRegistry(1099)来创建注册中心,如下,在创建注册中心的时候,会初始化一个HashTable的bindings属性,在使用bind()方法注册服务的时候,会保存到bindings结构中。之后如果注册中心的端口是1099并且系统开启了安全管理器,那么可以在限定的权限集内(listen和accept)绕过系统的安全校验,反之需要进行安全校验。之后通过setup()中的exportObject方法来暴露服务。Ps:继承UnicastRemoteObject的类,最终也会调用调用 exportObject 方法。

public RegistryImpl(final int var1) throws RemoteException {
this.bindings = new Hashtable(101);
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, (var0) -> {
return RegistryImpl.registryFilter(var0);
}));
return null;
}
}, (AccessControlContext)null, 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::registryFilter));
}

}
private void setup(UnicastServerRef var1) throws RemoteException {
this.ref = var1;
var1.exportObject(this, (Object)null, true);
}

setup()里面调用的是UnicastServerRef的exportObject()方法,进入该方法,这里面为RestryImpl创建了一个代理,代理的作用是创建了服务于客户端的RegistryImpl的Stub对象,最后用skeleton、stub、UnicastServerRef对象、id和一个boolean值构造了一个Target对象,也就是这个Target对象基本上包含了全部的信息。

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

上面提到了Stub(存根)和Skeleton(骨架)对象,分别是客户端的代理和服务端的代理,Stub和Skeleton通过socket进行通信。上面的代码封装了Target方法之后,调用了LiveRef的exportObject()方法,追到了TCPTransport的exportObject()方法,如下,这个方法里面做了两件事。一是listen()方法创建一个ServerSocket。并启动一条线程等待客户端的请求,接着调用父类Transport的exportObject()将Target对象存放进ObjectTable中。

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

三、客户端获取获取服务

通过客户端调用远程服务分为两个步骤:一是获取注册中心 registry;二是根据注册中心获取服务的代理类 service。通过示例代码,可以追溯到LocateRegistry的getRegistry()方法。

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

这个方法做的是通过传入的host和port构造RemoteRef对象,并创建了一个本地代理。代理对象其实是RegistryImpl_Stub对象。但注意此时这个代理其实还没有和服务端的RegistryImpl对象关联,毕竟是两个JVM上面的对象,这里我们也可以猜测,代理和远程的Registry对象之间是通过socket消息来完成的。

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = this.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var17) {
throw new MarshalException("error marshalling arguments", var17);
}

this.ref.invoke(var2);

Remote var22;
try {
ObjectInput var4 = var2.getInputStream();
var22 = (Remote)var4.readObject();
} catch (IOException var14) {
throw new UnmarshalException("error unmarshalling return", var14);
} catch (ClassNotFoundException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} finally {
this.ref.done(var2);
}

return var22;
} catch (RuntimeException var18) {
throw var18;
} catch (RemoteException var19) {
throw var19;
} catch (NotBoundException var20) {
throw var20;
} catch (Exception var21) {
throw new UnexpectedException("undeclared checked exception", var21);
}
}

获取注册中心后,在客户端直接生成代理对象 RegistryImpl_Stub,RegistryImpl_Stub 通过newCall()方法和服务端建立连接,并向服务端传输远程接口名称,使用的注册中心方法集合以及集合的索引,来调用远程方法。实际调用 RemoteRef 的 invoke 方法进行网络通信,来获得远程对象实例。

四、总结

通过上面的分析,RMI的调用流程可以简化成下面的结构。

但是里面还有很多细节值得仔细研究,例如rmi在调用的方法中存在引用型数据,是如何进行序列化和反序列化?bindings数据结构在整个调用过程中到底处于什么样的角色?骨干和存根是如何封装通信?后续笔者也会持续分享。
另外RMI优点是避免重复造轮子,降低开发工作量,优化部署结构等,缺点是调用过程是两个jvm通信,增加了调用时间,而且如果网络不稳定会影响方法调用,降低了可用性,所以技术选型的时候需要按照业务需要平衡一下,或者可以选择dubbo这种支持rmi协议的RPC框架来进行管理。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
No Reply at the moment.
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up