标题: Java RMI入门(9)
创建: 2020-04-16 18:23 更新: 2020-04-17 22:40 链接: https://scz.617.cn/network/202004161823.txt
目录:
☆ 前言
☆ Java RMI中的绑定与查询
1) 展示几个现象
1.1) 现象1
1.2) 现象2
1.3) 现象3
2) RMI架构简述
3) Remote实例
4) 动态端口是如何开始侦听的
5) 本地绑定
6) 远程绑定
7) 动态IP问题
8) RemoteObject.ref
9) 远程绑定大概率不会遭遇"动态IP问题"
10) 调试思路回顾
☆ 前言
参看
《Java RMI入门》 https://scz.617.cn/network/202002221000.txt
《Java RMI入门(2)》 https://scz.617.cn/network/202003081810.txt
《Java RMI入门(3)》 https://scz.617.cn/network/202003121717.txt
《Java RMI入门(4)》 https://scz.617.cn/network/202003191728.txt
《Java RMI入门(5)》 https://scz.617.cn/network/202003241127.txt
《Java RMI入门(6)》 https://scz.617.cn/network/202004011650.txt
《Java RMI入门(7)》 https://scz.617.cn/network/202004101018.txt
《Java RMI入门(8)》 https://scz.617.cn/network/202004141657.txt
☆ Java RMI中的绑定与查询
在RMI搞事过程中有人碰上过客户端访问远程接口时莫名其妙去访问127.0.0.1的事, 为行文方便,本篇简称此问题为"动态IP问题"。如果没碰上过,就不要看下去,忘了 这篇。
这个问题跟安全没啥直接关系,亦有成熟解决方案,比如java.rmi.server.hostname。
本以为自己搞清楚过,后来才发现一直没搞清楚过。虽然与安全不直接相关,但个人 觉得值得一写。
1) 展示几个现象
1.1) 现象1
import java.rmi.registry.*;
public class HelloRMIServer { public static void main ( String[] argv ) throws Exception { int port = Integer.parseInt( argv[0] ); String name = argv[1]; Registry r = LocateRegistry.createRegistry( port ); HelloRMIInterface hello = new HelloRMIInterfaceImpl(); r.rebind( name, hello ); } }
import java.rmi.registry.*;
public class HelloRMIClient { public static void main ( String[] argv ) throws Exception { String addr = argv[0]; int port = Integer.parseInt( argv[1] ); String name = argv[2]; String sth = argv[3]; Registry r = LocateRegistry.getRegistry( addr, port ); HelloRMIInterface hello = ( HelloRMIInterface )r.lookup( name ); String resp = hello.Echo( sth ); System.out.println( resp ); } }
假设HelloRMIClient在A机,HelloRMIServer在B机,前者访问后者时会莫名其妙去访 问127.0.0.1,远程调用失败,这就是"动态IP问题"的一种外在表现。
可以通过指定JVM属性java.rmi.server.hostname解决,也可以修改hostname、配置 /etc/hosts解决。
1.2) 现象2
import java.net.InetAddress; import java.rmi.registry.*;
public class HelloRMIDynamicServer { public static void main ( String[] argv ) throws Exception { String addr_0 = argv[0]; int port_0 = Integer.parseInt( argv[1] ); String addr_1 = argv[2]; int port_1 = Integer.parseInt( argv[3] ); String name = argv[4]; InetAddress bindAddr_1 = InetAddress.getByName( addr_1 ); Registry r = LocateRegistry.getRegistry( addr_0, port_0 ); HelloRMIInterface hello = new HelloRMIInterfaceImpl3( port_1, bindAddr_1 ); r.rebind( name, hello ); } }
上述代码没有调用LocateRegistry.createRegistry(),必须与其他进程配合使用, 比如JDK自带rmiregistry。
假设HelloRMIClient在A机,rmiregistry、HelloRMIDynamicServer在B机,前者访问 后者时不会去访问127.0.0.1,远程调用成功,没有遭遇"动态IP问题"。
1.3) 现象3
import java.net.InetAddress; import java.rmi.server.UnicastRemoteObject; import java.rmi.registry.LocateRegistry; import javax.naming.*;
public class SomeDynamicServerEx5 { public static void main ( String[] argv ) throws Exception { int wellknown = Integer.parseInt( argv[0] ); String addr = argv[1]; int port = Integer.parseInt( argv[2] ); String name = argv[3]; byte pattern = ( byte )( Integer.parseUnsignedInt( argv[4], 16 ) & 0xff ); InetAddress bindAddr = InetAddress.getByName( addr ); LocateRegistry.createRegistry( wellknown ); XorServerSocketFactory ssf = new XorServerSocketFactory( bindAddr, pattern ); XorClientSocketFactory csf = new XorClientSocketFactory( pattern ); Context ctx = new InitialContext(); SomeInterface5 obj = new SomeInterface5Impl(); SomeInterface5 some = ( SomeInterface5 )UnicastRemoteObject.exportObject ( obj, port, csf, ssf ); ctx.rebind( name, some ); } }
现象3一般是这样操作的:
java \ -Djava.rmi.server.codebase=http://192.168.65.23:8080/ \ -Djava.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory \ -Djava.naming.provider.url=rmi://192.168.65.23:1099 \ SomeDynamicServerEx5 1099 192.168.65.23 1314 any 2e
保持一般性,未在代码中设置"java.naming.factory.initial"、 "java.naming.provider.url",而是通过JVM参数传入。
上述代码调用了LocateRegistry.createRegistry(),不依赖rmiregistry等其他进程。
假设SomeNormalClient5在A机,SomeDynamicServerEx5在B机,前者访问后者时不会 去访问127.0.0.1,远程调用成功,没有遭遇"动态IP问题"。
2) RMI架构简述
RMI是C/S架构,有服务端、客户端。但它提供的服务有很多个,分别侦听不同的动态
端口,客户端怎么找到这些动态端口呢?为了解决这个问题,在服务端有一个特殊的
服务出现,其侦听在周知端口上,一般是1099/TCP。所有侦听动态端口的服务均向周
知端口汇报(注册)自身的存在,汇报内容包括这些动态服务在哪个IP、哪个端口上侦
听,还有一个动态服务自定义名字与前述IP、PORT信息相对应,该名字也在汇报内容
里,某种语境下可以简记成
现在来看前述现象1、2、3:
周知端口
rmiregistry
LocateRegistry.createRegistry()
动态端口
new HelloRMIInterfaceImpl()
extends UnicastRemoteObject
UnicastRemoteObject.exportObject()
动态端口向周知端口汇报(注册)
r.rebind( name, hello )
ctx.rebind( name, some )
客户端向周知端口查询
r.lookup( name )
客户端访问动态端口
hello.Echo( sth )
代码不全不要紧,当成伪代码看就好,领会精神。
3) Remote实例
bind()、rebind()就是在动态端口向周知端口汇报(注册)
hello的实际类型有如下继承关系或接口实现关系:
HelloRMIInterfaceImpl java.rmi.server.UnicastRemoteObject java.rmi.server.RemoteServer java.rmi.server.RemoteObject // 有个ref成员 java.rmi.Remote
RemoteObject有个ref成员,类型是java.rmi.server.RemoteRef,UnicastServerRef 是RemoteRef的一种子孙类。
"new HelloRMIInterfaceImpl()"生成hello时会向hello.ref填入UnicastServerRef
实例,该类型包含
sun.rmi.server.UnicastServerRef sun.rmi.transport.LiveRef sun.rmi.transport.tcp.TCPEndpoint String host // 动态IP int port // 动态端口
4) 动态端口是如何开始侦听的
参看:
http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/java/rmi/server/UnicastRemoteObject.java
hello = new HelloRMIInterfaceImpl() // 8u232
UnicastRemoteObject.
"UnicastRemoteObject:320"处会生成一个UnicastServerRef(port)实例,缺省情况 下该实例只会对应"127.0.0.1:port"。
"UnicastRemoteObject:320"继而调用UnicastRemoteObject.exportObject(),前面 生成的UnicastServerRef实例是其第二形参。不需要纠缠 UnicastRemoteObject.exportObject()内部干了些啥,只要知道,它的第二形参带有 port信息,尽管第二形参带有ip信息,但未被使用,只用了ref中的port信息。后面 用port信息去listen()、去accept()。如果ref中的port为0,底层会随机选用一个端 口,然后在"TCPEndpoint:335"处修正ref中的port。修正ref中port的调用栈回溯:
stop at sun.rmi.transport.tcp.TCPEndpoint:335
[1] sun.rmi.transport.tcp.TCPEndpoint.setDefaultPort (TCPEndpoint.java:335), pc = 85 [2] sun.rmi.transport.tcp.TCPEndpoint.newServerSocket (TCPEndpoint.java:671), pc = 83 [3] sun.rmi.transport.tcp.TCPTransport.listen (TCPTransport.java:335), pc = 85 [4] sun.rmi.transport.tcp.TCPTransport.exportObject (TCPTransport.java:254), pc = 5 [5] sun.rmi.transport.tcp.TCPEndpoint.exportObject (TCPEndpoint.java:411), pc = 5 [6] sun.rmi.transport.LiveRef.exportObject (LiveRef.java:147), pc = 5 [7] sun.rmi.server.UnicastServerRef.exportObject (UnicastServerRef.java:237), pc = 78 [8] java.rmi.server.UnicastRemoteObject.exportObject (UnicastRemoteObject.java:383), pc = 19 [9] java.rmi.server.UnicastRemoteObject.exportObject (UnicastRemoteObject.java:346), pc = 11
简而言之,UnicastRemoteObject.exportObject()要点有二:
a) 给hello.ref赋值,对应"127.0.0.1:port" b) 侦听"0.0.0.0:port"
有没有觉得a、b之间的诡异?127.0.0.1和0.0.0.0是那么不搭,但这就是事实。
顺便说一句,生成hello对像时TCP层的accept()就已经就绪,与后面的r.rebind()无 关。
5) 本地绑定
bind()、rebind()就是绑定,绑定就是动态端口向周知端口汇报(注册)信息。如果绑 定涉及socket通信,就是远程绑定,否则属于本地绑定。此间远程、本地的区分是以 socket通信为准,即使socket通信只涉及127.0.0.1,也是远程绑定。
现象1属于本地绑定:
HelloRMIServer.main
sun.rmi.registry.RegistryImpl.rebind
java.util.Hashtable
hello.ref包含
6) 远程绑定
这里说的远程绑定不是指周知端口、动态端口位于不同主机,而是指动态端口的汇报 (注册)过程涉及socket通信。
现象2、3均属于远程绑定,它们最终会调用:
sun.rmi.registry.RegistryImpl_Stub.rebind
现象3远程绑定的调用栈回溯:
stop in sun.rmi.registry.RegistryImpl_Stub.rebind
[1] sun.rmi.registry.RegistryImpl_Stub.rebind (RegistryImpl_Stub.java:150), pc = 0 [2] com.sun.jndi.rmi.registry.RegistryContext.rebind (RegistryContext.java:175), pc = 42 [3] com.sun.jndi.rmi.registry.RegistryContext.rebind (RegistryContext.java:182), pc = 10 [4] javax.naming.InitialContext.rebind (InitialContext.java:433), pc = 7 [5] SomeDynamicServerEx5.main (SomeDynamicServerEx5.java:33), pc = 112
本篇不讲RegistryImpl_Stub是什么,总之,远程绑定与之强相关,必有socket通信。
7) 动态IP问题
lookup()即查询,查询是指客户端向周知端口询问name对应
sun.rmi.registry.RegistryImpl_Stub.lookup
如果客户端lookup()拿到的
8) RemoteObject.ref
周知端口维护的数据库(哈希表)中存有
RemoteObject.ref来自"UnicastRemoteObject:320"处的:
new UnicastServerRef(port)
这就有意思了,这行代码只有port形参,没有ip形参。现象1中这行代码显然是返回 "127.0.0.1:port",那127.0.0.1从何而来?从"TCPEndpoint:131"处而来。
参看:
http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/sun/rmi/server/UnicastServerRef.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/sun/rmi/transport/LiveRef.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/sun/rmi/transport/tcp/TCPEndpoint.java
hello = new HelloRMIInterfaceImpl() // 8u232
UnicastRemoteObject.
未指定"java.rmi.server.hostname"时,"new UnicastServerRef(port)"返回的ip源 自:
java.net.Inet4AddressImpl.loopbackAddress
而RemoteObject.ref来自"new UnicastServerRef(port)"。简言之,缺省情况下生成 的hello.ref只可能对应"127.0.0.1:port"。
如果提前指定"java.rmi.server.hostname",hello.ref中的ip随之改变。如果修改 过hostname、配置过/etc/hosts,也会影响hello.ref中的ip。这正是现象1解决方案 背后的原理。
9) 远程绑定大概率不会遭遇"动态IP问题"
现在很好理解现象1,遭遇"动态IP问题"是铁板钉钉的事。那么问题来了,怎么解释 现象2、3未遭遇"动态IP问题"?已知现象2、3属于远程绑定,难道跟这有关?
首次注意到现象2时,以为rmiregistry有什么附加动作修正过数据库中的ip。当时没 有调试,只是抓包观察到lookup()的响应报文中动态IP不是127.0.0.1,还在文档中 胡说了一把。后来又注意到现象3,才意识到这事跟rmiregistry无关,应该有别的机 理。
先说调试结论吧,远程绑定时有机会在动态端口一侧修正RemoteObject.ref中的ip。
RegistryImpl_Stub.rebind // 8u232 UnicastRef.newCall // RegistryImpl_Stub:150 // 发送请求 TCPChannel.newConnection // UnicastRef:338 TCPChannel.createConnection // TCPChannel:202 suggestedHost = in.readUTF() // TCPChannel:254 // 来自周知端口的回送,一般是当前TCP连接的源IP suggestedPort = in.readInt() // TCPChannel:255 // 来自周知端口的回送,一般是当前TCP连接的源端口 TCPEndpoint.setLocalHost(suggestedHost) // TCPChannel:263 // set local host name, if unknown if (!localHostKnown) // TCPEndpoint:296 // 参"5.3) 深入LocateRegistry.createRegistry()" // 如果没有指定过"java.rmi.server.hostname" // localHostKnown将为false ep.host = host // TCPEndpoint:308 // ep.host本来等于"127.0.0.1" // host来自周知端口的回送,一般是当前TCP连接的源IP
修正RemoteObject.ref中ip的调用栈回溯:
stop at sun.rmi.transport.tcp.TCPEndpoint:308
[1] sun.rmi.transport.tcp.TCPEndpoint.setLocalHost (TCPEndpoint.java:308), pc = 126 [2] sun.rmi.transport.tcp.TCPChannel.createConnection (TCPChannel.java:263), pc = 222 [3] sun.rmi.transport.tcp.TCPChannel.newConnection (TCPChannel.java:202), pc = 101 [4] sun.rmi.server.UnicastRef.newCall (UnicastRef.java:338), pc = 18 [5] sun.rmi.registry.RegistryImpl_Stub.rebind (RegistryImpl_Stub.java:150), pc = 12
如果没有指定"java.rmi.server.hostname",现象2的hello.ref将对应"127.0.0.1"。 然后现像2开始远程绑定,流程进入sun.rmi.registry.RegistryImpl_Stub.rebind。 在r.rebind()中,现象2的hello.ref将被修正,发生在"TCPEndpoint:308"处,被修 正成rebind()所用TCP连接的源IP。这可真是个隐蔽的骚操作。
如果指定过"java.rmi.server.hostname","TCPEndpoint:296"处的检查不满足,远 程绑定时hello.ref不被修正,此时"java.rmi.server.hostname"指定值保持有效。
假设动态端口用"127.0.0.1:1099"去访问周知端口,此时rebind()所用TCP连接的源 IP也是127.0.0.1,修正没有意义。但是,如果动态端口用"192.168.65.23:1099"去 访问周知端口,rebind()所用TCP连接的源IP将是192.168.65.23,修正有意义。不考 虑周知端口、动态端口位于不同主机的情况,出于安全原因,Java RMI底层会设法阻 止这样干。
这就解释了现象2、3,尤其是令人困挠的现象3。尽管现象3的周知端口、动态端口位 于同一进程,但ctx.rebind()触发的是远程绑定,JVM参数中指定的是 "rmi://192.168.65.23:1099",rebind()所用TCP连接的源IP将是192.168.65.23, some.ref中的ip将被修正成192.168.65.23。
为什么小节标题写的是大概率不会遭遇"动态IP问题"?如果动态端口作死,非要用 "127.0.0.1:1099"访问周知端口,客户端仍将遭遇"动态IP问题"。并不是说远程绑定 绝对不会遭遇"动态IP问题",这跟远程绑定所用TCP连接强相关。
10) 调试思路回顾
动态IP问题算是彻底搞清楚,回顾一下调试分析思路。
首先在lookup()响应报文中看到动态IP、动态端口,就去调试周知端口,重点调试:
sun.rmi.registry.RegistryImpl_Skel.dispatch java.io.ObjectOutputStream.writeObject java.io.ObjectOutputStream.readObject java.rmi.server.UnicastRemoteObject.readObject
发现rebind()提交到周知端口的RemoteObject.ref已经被修正过,说明修正发生在动 态端口侧,即r.rebind()的调用者。接下来调试:
new HelloRMIInterfaceImpl() sun.rmi.registry.RegistryImpl_Stub.rebind
怎么找到"sun.rmi.transport.tcp.TCPEndpoint:308"这个断点的呢?首先获得hello, 发现hello.ref中是127.0.0.1。选中ep.host,点击Eclipse右键菜单的 "Toggle Watchpoint",在断点列表里多出个Watchpoint。它这个Watchpoint很弱啊, 居然是对所有TCPEndpoint实例的host成员进行访问都会命中,不像x86调试中可以对 具体地址设置硬件访问断点。不管怎么说,聊胜于无,人工过滤无效命中,直至某一 次发现是将hello.ref中的127.0.0.1修正成192.168.65.23,然后在调用栈中回溯, 发现192.168.65.23来自"TCPChannel:254"处,来自周知端口的回送,一般是当前TCP 连接的源IP。