2021.10.25:RMI远程调用
Java的RMI远程调用是指,一个JVM中的代码可以通过网络实现远程调用另一个JVM的某个方法。RMI是Remote Method Invocation的缩写。
提供服务的一方我们称之为Server,实现远程调用的一方我们称之为Client。
我们先来实现一个最简单的RMI:Server会提供一个WorldClock服务,允许Client获取指定时区的时间,即允许Client调用下面的方法:
LocalDateTime getLocalDateTime(String zoneId);
要实现RMI,Client与Server必须共享同一个接口。我们定义一个WorldClock接口,代码如下:
public interface WorldClock extends Remote{ LocalDateTime getLocalDateTime(String zoneId) throws RemoteException; }
Java的RMI规定此接口必须派生于java.rmi.Remote,并在每个方法的声明中抛出RemoteException。
接下来是写Server的实现类,因为Client请求的调用方法getLocalDateTime()最终会通过这个实现类返回结果。实现类WorldClockService代码如下:
public class WorldClockService implements WorldClock{ @Override public LocalDateTime getLocalDateTime(String zoneId) throws RemoteException{ return LocalDateTime.now(zoneId).withNano(0); } }
现在,Server端的服务相关代码就编写完毕了。我们需要通过Java RMI提供的一系列底层支持接口,把上面编写的服务以RMI的形式暴露在网络上,Client才能调用:
public class Server { public static void main(String[]args) throws RemoteException{ System.out.println("create World clock remote service..."); //实例化一个WorldClock: WorldClock worldClock = new WorldClockService(); //将该Server转换为RMI WorldClock skeleton = (WorldClock) UnicastRemoteObject.exportObject(worldClock,0); //将RMI服务注册到1099端口 Registry registry = LocateRegistry.createRegistry(1099); //注册此服务,服务名为"WorldClock" registry.rebind("WorldClock",skeleton); } }
上述代码,通过RMI提供的相关类,将我们自己的WorldClock实例注册到RMI服务上。RMI的默认端口是1099,最后一步注册服务时通过rebind()指定服务名称为“WorldClock”。
下一步我们就可以编写Client代码。RMI要求Server与Client共享同一个端口,因此我们要把WorldClock.java这个接口文件复制到Client,然后在Client实现RMI调用:
public class Client { public static void main(String[]args) throws RemoteException,NotBoundException{ //连接到Sever:localhost,端口1099: Registry registry = LocateRegistry.getRegistry("localhost",1099); //查找名称为"WorldClock"的服务并强制转型为WorldClock接口: WorldClock worldClock = (WorldClock) registry.lookup("WorldClock"); //正常调用接口方法 LocalDateTime now = worldClock.getLocalDateTime("Asia/Shanghai"); //打印调用结果 System.out.println(now); } }
先运行Server,再运行Client。从运行结果可知,由于Client只有接口,没有实现类。因此,Client获得的接口方法返回值实际上是通过网络从服务器端获取的。整个过程实际上很简单,对于Client来说,客户端持有的WorldClock接口实际上对应了一个“实现类”,它是由Registry内部动态生成的,并负责把方法通过网络传递到Server。而Server接收网络调用的服务并不是我们自己编写的WorldClockService,而是Registry自动生成的代码。我们把Client端的“实现类”称为stub,Server端的网络服务类称为skeleton,它会真正调用Server端的WorldClockService,获取结果,然后把结果通过网络传递给客户端。整个过程由RMI底层负责实现序列化和反序列化。

Java的RMI严重依赖序列化和反序列化,而这种情况下可能会造成严重的安全漏洞,因为Java序列化和反序列化不但涉及到数据,还涉及到二进制的字节码,即使使用白名单机制也很难保证100%排除恶意构造的字节码。因此,使用RMI时,双方必须是内网互相信任的机器,不要把1099端口暴露在公网上作为对外服务。
此外,其他语言很难调用Java的RMI。如果要使用不同语言进行RPC调用,可以选择更通用的协议,例如gRPC。
小结
Java提供了RMI实现远程方法调用:
RMI通过自动生成stub和skeleton实现网络调用,Client只需要查找服务并获得接口实例,Server只需要编写实现类并注册为服务。

浙公网安备 33010602011771号