分布式系列五: RMI通信
RPC(Remote Procedure Call)协议
RPC协议是一种通过网络从远程计算机上请求服务, 而不需要了解底层网络技术的协议, 在OSI模型中处在应用层和网络层.
作为一个规范, 使用RPC协议的框架有很多, Dubbo,Hessian等均使用这个协议, RMI也使用该协议实现.
RMI(Remote Method Invocation) 远程方法调用
RMI使用Java远程消息交换协议JRMP(Java Remote Messaging Protocol)进行通信,JRMP是纯java的.
- 定义接口, 使其extends
Remote
接口, 方法需要抛出异常RemoteException
, Remote是一个标记接口
public interface IRmiTest extends Remote {
String hello() throws RemoteException;
}
- 实现接口, 使其extends
UnicastRemoteObject
, 需要有构造方法, 并抛出异常RemoteException
public class RmiTest extends UnicastRemoteObject implements IRmiTest {
public RmiTest() throws RemoteException {
}
@Override
public String hello() {
return "Hello ....";
}
}
- 定义服务端, 注册和绑定
public class TestServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
IRmiTest rmiTest = new RmiTest();
LocateRegistry.createRegistry(8888);
Naming.bind("rmi://localhost:8888/hello",rmiTest);
System.out.println("server started");
}
}
- 定义客户端, lookup方法的参数url与服务端bind的必须一致. 接口需要定义为与服务端一致.
public class TestClient {
public static void main(String[] args) throws RemoteException, MalformedURLException, NotBoundException {
IRmiTest rmiTest = (IRmiTest) Naming.lookup("rmi://localhost:8888/hello");
System.out.println(rmiTest.hello());
}
}
RMI实现机制
RMI屏蔽了底层复杂的网络调用, 使得远程对象的方法调用变得透明, 就像调用本地方法一样方便.
下面深入探究下jdk中rmi的实现原理, 看看底层是如何实现远程调用的.
首先, 需要了解下比较重要的两个角色stub和skeleton, 这两个角色封装了与网络相关的代码. 原始的交互式这样的,客户端--网络--服务器--具体服务. 有了这两个角色之后的模型变为: 客户端--stub--网络--skeleton--服务器--服务.可以参考的图维基百科
下面来看源码...
一.实例化RegistryImpl,初始化
LocateRegistry.createRegistry(8888);
这句代码启动了一个注册器(其中有个Map对象来存储名称和服务的映射,这个后面再细看)
public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}
这个方法实例化了一个RegistryImpl
的实例,RegistryImpl
实现了Registry
.
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, "listen,accept")});
} catch (PrivilegedActionException var3) {
throw (RemoteException)var3.getException();
}
} else {
LiveRef var2 = new LiveRef(id, var1);
this.setup(new UnicastServerRef(var2));
}
}
两个分支最终都调用了setup()
方法, 主要关注该方法.if分支中var1=1099是指默认端口并且存在安全管理器的时候不做校验, 这是为了性能考虑.
private void setup(UnicastServerRef var1) throws RemoteException {
this.ref = var1; // UnicastServerRef继承了RemoteRef,this.ref的类型就是RemoteRef
var1.exportObject(this, (Object)null, true);
}
setup方法的参数是包装后的UnicastServerRef
对象, UnicastServerRef
继承了RemoteRef
因此可以赋值给ref变量. 该方法将调用委托给UnicastServerRef
的方法exportObject()
如果是拿文章开头的代码进行调试, 会发现这个方法会走两次, 除了RegistryImpl
, 还有一次是RmiTest
也会走这个方法.不同的是RegistryImpl
会走下面代码中的if(var5 instanceof RemoteStub)
分支语句, 这个语句最终将生成一个Skeleton实例并设置给当前实例的域变量skel, 不过自jdk1.2之后skeleton就没什么用了.
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) {
// 生成Skeleton实例并设置给当前实例的域变量skel
this.setSkeleton(var1);
}
Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
this.ref.exportObject(var6); //ref是实例化UnicastServerRef的时候传入的
this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
return var5;
}
上面方法首先根据Remote
的参数var1创建了一个代理对象var5, var1是RegistryImpl
类的实例. 然后实例化一个Target
的实例, 从参数可以看到,Target对象包含了几乎之前代码的所有对象.然后将这个对象作为参数,调用LiveRef
实例ref的exportObject()
方法.
二. 网络连接和对象传输
public void exportObject(Target var1) throws RemoteException {
this.ep.exportObject(var1);
}
接上一步, RemoteRef
的方法最终委托给TCPEndpoint
的同名方法(委托模式), 到此代码将控制权传递给传输层.
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();
}
}
}
这个方法实现了网络通信, 首先linsten()
启动了一个ServerSocket
的线程,并开始监听端口. 然后调用父类的方法将Target
对象暴露出去, 此时服务端的初始化就完成了.
三. 注册服务
Naming.bind("rmi://localhost:8888/hello",rmiTest);
完成名称和服务对象的绑定.
public static void bind(String name, Remote obj)
throws AlreadyBoundException,
java.net.MalformedURLException,
RemoteException
{
ParsedNamingURL parsed = parseURL(name);
Registry registry = getRegistry(parsed);
if (obj == null)
throw new NullPointerException("cannot bind to null");
registry.bind(parsed.name, obj);
}
上面代码Naming
类, 调用的是注册器Registry
的bind()
方法
public void bind(String var1, Remote var2) throws RemoteException, AlreadyBoundException, AccessException {
Hashtable var3 = this.bindings;
synchronized(this.bindings) {
Remote var4 = (Remote)this.bindings.get(var1);
if (var4 != null) {
throw new AlreadyBoundException(var1);
} else {
this.bindings.put(var1, var2);
}
}
}
注册使用的容器是一个HashTable
, 最终服务的名称和服务会被注册到这个map容器中.
到此为止, 服务端的初始化完成. 首先实例化了一个实现Register
注册器的实例, 通过层层组装, 最终生成一个Target
对象, 其中包含了组装过程中生成的全部状态, 最后调用RemoteRef
的方法将对象转交给传输层对象TCPEndpoint
的实例, 最终由这个对象启动Socket开启通信连接. 注册服务是通过Naming
的方法委托调用Register
注册器的方法实现, 并将结果最终注册到Register
域的map对象中.
四. 客户端远程调用
IRmiTest rmiTest = (IRmiTest) Naming.lookup("rmi://localhost:8888/hello");
客户端通过Naming
的方法获取服务的实例
public static Remote lookup(String name)
throws NotBoundException,
java.net.MalformedURLException,
RemoteException{
ParsedNamingURL parsed = parseURL(name);
Registry registry = getRegistry(parsed);
if (parsed.name == null)
return registry;
return registry.lookup(parsed.name);
}
与服务端注册时候使用Naming.bind()
方法一样, 这里lookup()
最终也会委托给Registry
的实例. 这个实例的实现不是用的服务端的Register_Impl
, 而是使用RegistryImpl_Stub
, 下面代码是lookup()
的实现, 可以看出这里封装了网络io的一些逻辑.
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);
}
}
至此, 服务端和客户端的连接完成, 可以开始通信了.
RMI自JDK1.1就已经提供了, 它提供了Java语言自己的RPC调用方式, 虽然有些老旧, 但依然经典. 目前有很多跨语言的技术或框架, 如后来的WebService, 再到目前的netty,shrift等基本已经取代了这种原始的调用方式, 他们是非阻塞的,且还能跨语言调用. 但熟悉RMI的实现方式对了解分布式系统的通信的实现原理有很大帮助.
分布式系列五: RMI通信的更多相关文章
- 分布式系列六: WebService简介
WebSerice盛行的时代已经过去, 这里只是简单介绍下其基本概念, 并用JDK自带的API实现一个简单的服务. WebSerice的概念 WebService是一种跨平台和跨语言的远程调用(RPC ...
- Netty4.x中文教程系列(五)编解码器Codec
Netty4.x中文教程系列(五)编解码器Codec 上一篇文章详细解释了ChannelHandler的相关构架设计,版本和设计逻辑变更等等. 这篇文章主要在于讲述Handler里面的Codec,也就 ...
- 分布式系列四: HTTP及HTTPS协议
分布式系列四: HTTP及HTTPS协议 非常全面的一篇HTTP的文章: 关于HTTP协议,一篇就够了 还有一个帮助理解HTTPS的文章: 也许,这样理解HTTPS更容易 本文的一些描述摘自这篇文章 ...
- [时序图笔记] 步步为营UML建模系列五、时序图(Squence diagram)【转】
概述 顺序图是一种详细表示对象之间以及对象与参与者实例之间交互的图,它由一组协作的对象(或参与者实例)以及它们之间可发送的消息组成,它强调消息之间的顺序. 顺序图是一种详细表示对象之间以及对象与系统外 ...
- WCF开发实战系列五:创建WCF客户端程序
WCF开发实战系列五:创建WCF客户端程序 (原创:灰灰虫的家http://hi.baidu.com/grayworm) 在前面的三篇文章中我们分别介绍了WCF服务的三种载体:IIS.Self-Hos ...
- CSS 魔法系列:纯 CSS 绘制各种图形《系列五》
我们的网页因为 CSS 而呈现千变万化的风格.这一看似简单的样式语言在使用中非常灵活,只要你发挥创意就能实现很多比人想象不到的效果.特别是随着 CSS3 的广泛使用,更多新奇的 CSS 作品涌现出来. ...
- WCF编程系列(五)元数据
WCF编程系列(五)元数据 示例一中我们使用了scvutil命令自动生成了服务的客户端代理类: svcutil http://localhost:8000/?wsdl /o:FirstServic ...
- JVM系列五:JVM监测&工具
JVM系列五:JVM监测&工具[整理中] http://www.cnblogs.com/redcreen/archive/2011/05/09/2040977.html 前几篇篇文章介绍了介 ...
- SQL Server 2008空间数据应用系列五:数据表中使用空间数据类型
原文:SQL Server 2008空间数据应用系列五:数据表中使用空间数据类型 友情提示,您阅读本篇博文的先决条件如下: 1.本文示例基于Microsoft SQL Server 2008 R2调测 ...
随机推荐
- Redis入门---字符串类型
阅读目录 1.keys * 命令 2.判断一个键是否存在(exists key) 3.删除键 4.获取键值的数据类型 5 递增数字(incr) 6.增加指定的整数 (INCRBY) 7.减少指定的整数 ...
- L2-2 小字辈 (25 分)
本题给定一个庞大家族的家谱,要请你给出最小一辈的名单. 输入格式: 输入在第一行给出家族人口总数 N(不超过 100 000 的正整数) —— 简单起见,我们把家族成员从 1 到 N 编号.随后第二行 ...
- Unable to start web server; nested exception is org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.
SpringBoot启动时的异常信息如下: "C:\Program Files\Java\jdk1.8.0_161\bin\java" ......... com.fangxing ...
- flask刷新token
我们在做前后端分离的项目中,最常用的都是使用token认证. 登录后将用户信息,过期时间以及私钥一起加密生成token,但是比较头疼的就是token过期刷新的问题,因为用户在登录后,如果在使用过程中, ...
- jenkins集成python时出现"Non-ASCII character '\xe6' in file"错误解决方法
我的问题: 使用python3.5,在Linux环境下手动执行python文件时不报错,但是用jenkins自动执行时就报"Non-ASCII character '\xe6' in fil ...
- Python——Window启动服务
一.新建启动服务程序 # ZPF # encoding=utf-8 import win32timezone from logging.handlers import TimedRotatingFil ...
- Python——接口类、抽象类
建立一个接口类.抽象类的规范 from abc import abstractmethod,ABCMeta class Payment(metaclass=ABCMeta): # 元类 默认的元类 t ...
- libstdc++.so.6: cannot open shared object file: No such file or directory
sudo apt-get install lib32stdc++6 sudo apt-get install lib32z1
- 小数点保留n位有效数字
char *psf = "183.0000000000000001"; ]; sprintf(chBuff, "%.2lf", atof(psf)); doub ...
- kubernetes 将pod运行在某些特定的节点上,给节点打标签
给节点打上标签: kubectl label node <node_name> GPU=true #打上标签 GPU=true 在创建pod的yaml文件时: 添加 nodeSel ...