标题: 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信息相对应,该名字也在汇报内容 里,某种语境下可以简记成三元组。周知端口把所有这种三元组保存 在一张哈希表里,或者说保存在一个数据库里。假设客户端现在想访问name对应的动 态服务,就先用name去向周知端口(1099/TCP)查询,周知端口根据name在哈希表里找 对应项,找到后周知端口向客户端返回。客户端现在知道了name对应的动 态服务侦听在哪个IP、哪个端口上,客户端访问以完成RPC。中间细节略去, 大致架构是这么个意思,跟所有的RPC架构并无本质区别。 现在来看前述现象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()就是在动态端口向周知端口汇报(注册)三元组。第 一形参是name,那么在哪里体现呢?第二形参类型是java.rmi.Remote, 隐藏在Remote实例中。 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:180 // this(0) // 0表示将来随机分配端口 UnicastRemoteObject.exportObject // UnicastRemoteObject:198 // exportObject(this, port) UnicastServerRef. // UnicastRemoteObject:320 UnicastRemoteObject.exportObject // UnicastRemoteObject:320 // 重载过的另一个版本 // exportObject(obj, new UnicastServerRef(port)) ((UnicastRemoteObject) obj).ref = sref // UnicastRemoteObject:381 // hello.ref被赋值"127.0.0.1:0" UnicastServerRef.exportObject // UnicastRemoteObject:383 // sref.exportObject(obj, null, false) // sref此时对应"127.0.0.1:0" UnicastServerRef.setSkeleton // UnicastServerRef:232 LiveRef.exportObject // UnicastServerRef:237 TCPEndpoint.exportObject // LiveRef:147 TCPTransport.exportObject // TCPEndpoint:411 TCPTransport.listen // TCPTransport:254 // 这个listen()的含义很复杂,不只是TCP层的listen // 缺省情况下侦听"0.0.0.0:port",无论前面的ep是什么 TCPEndpoint.newServerSocket // TCPTransport:335 RMIMasterSocketFactory.createServerSocket // TCPEndpoint:666 RMIDirectSocketFactory.createServerSocket // RMIMasterSocketFactory:345 ServerSocket. // RMIDirectSocketFactory:45 // new ServerSocket(port) ServerSocket. // ServerSocket:143 // this(port, 50, null) // 重载过的另一个版本,第三形参bindAddr等于null InetSocketAddress. // ServerSocket:252 // new InetSocketAddress(bindAddr, port) addr == null ? InetAddress.anyLocalAddress() : addr // InetSocketAddress:188 // addr为null时,替换成"0.0.0.0" ServerSocket.bind // ServerSocket:252 // 这个bind()实际包含了bind+listen AbstractPlainSocketImpl.bind // ServerSocket:390 AbstractPlainSocketImpl.listen // ServerSocket:391 if (listenPort == 0) // TCPEndpoint:670 // 若前面port为0,条件满足,转去"TCPEndpoint:671" TCPEndpoint.setDefaultPort // TCPEndpoint:671 // setDefaultPort(server.getLocalPort(), csf, ssf) ep.port = port // TCPEndpoint:335 // 在此修正ref中的端口信息 new NewThreadAction(new AcceptLoop()) // TCPTransport:341 t.start() // TCPTransport:344 // 单开一个线程去accept() -------------------------------------------------------------------------- "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.put // RegistryImpl:277 // this.bindings.put(name, obj) -------------------------------------------------------------------------- hello.ref包含信息,r.rebind()直接将放到 RegistryImpl.bindings中去了,相当于将存入数据库,这是绑定操 作的本质。将来客户端发起lookup(name),周知端口会在RegistryImpl.bindings中 根据name找hello,然后就有了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对应是什么。如果 查询涉及socket通信,就是远程查询,否则属于本地查询。远程查询最终会调用: sun.rmi.registry.RegistryImpl_Stub.lookup 如果客户端lookup()拿到的中的ip是127.0.0.1,客户端就会去访问 "127.0.0.1:port",此即"动态IP问题"。 8) RemoteObject.ref 周知端口维护的数据库(哈希表)中存有,其中来自 RemoteObject.ref,比如前面的hello.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. // UnicastRemoteObject:180 // this(0) // 0表示将来随机分配端口 UnicastRemoteObject.exportObject // UnicastRemoteObject:198 // exportObject(this, port) UnicastServerRef. // UnicastRemoteObject:320 // new UnicastServerRef(port) LiveRef. // UnicastServerRef:168 // new LiveRef(port) TCPEndpoint. // LiveRef:93 // this(objID, TCPEndpoint.getLocalEndpoint(port), true) // 先进入TCPEndpoint的static代码块 localHostKnown = true // TCPEndpoint:107 TCPEndpoint.getHostnameProperty // TCPEndpoint:108 // localHost = getHostnameProperty() // 设置ep.localHost GetPropertyAction("java.rmi.server.hostname") // TCPEndpoint:97 // 给JVM参数以机会 if (localHost == null) // TCPEndpoint:111 // 假设指定过"java.rmi.server.hostname",则localHost不为null // 此时后面的代码都不会去,localHostKnown保持为true InetAddress.getLocalHost // TCPEndpoint:113 Inet4AddressImpl.getLocalHostName // InetAddress:1475 // local = impl.getLocalHostName() // 与hostname命令的返回结果同步,一般是"localhost" if (local.equals("localhost")) // InetAddress:1481 Inet4AddressImpl.loopbackAddress() // InetAddress:1482 // 返回"localhost/127.0.0.1" localHostKnown = false // TCPEndpoint:119 Inet4Address.getHostAddress // TCPEndpoint:131 // localHost = localAddr.getHostAddress() // 设置ep.localHost // hello.ref中的"127.0.0.1"来自于此 TCPEndpoint.getLocalEndpoint // LiveRef:93 // this(objID, TCPEndpoint.getLocalEndpoint(port), true) TCPEndpoint.resampleLocalHost // TCPEndpoint:201 TCPEndpoint.getHostnameProperty // TCPEndpoint:256 // hostnameProperty = getHostnameProperty() // 再给"java.rmi.server.hostname"一次机会 GetPropertyAction("java.rmi.server.hostname") // TCPEndpoint:97 return localHost // TCPEndpoint:281 // 返回ep.localHost TCPEndpoint. // TCPEndpoint:207 this.host = host // TCPEndpoint:172 // ep.host=127.0.0.1 LiveRef. // LiveRef:93 // this(objID, TCPEndpoint.getLocalEndpoint(port), true) ep = endpoint // LiveRef:64 // ep等于[127.0.0.1:port] UnicastRemoteObject.exportObject // UnicastRemoteObject:320 // 重载过的另一个版本 // exportObject(obj, new UnicastServerRef(port)) ((UnicastRemoteObject) obj).ref = sref // UnicastRemoteObject:381 // hello.ref被赋值"127.0.0.1:port" -------------------------------------------------------------------------- 未指定"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。