以下翻译来自:o'reilly的RMI书籍:<<Java RMI>>
chapter18.使用定制的Socket
page 367
本书的主要主题是RMI,对于所有RMI的强大,便利功能,事实上只是在JDK的标准Socket对象上的一层实现.在本章中,将向你展示怎样使用其他的Socket来代替RMI使用的标准socket.
事实上RMI也允许你这样做,针对不同类型的服务器使用不同的socket,这是一种非常强大的特性.在本章的结束部分,你将理解怎样改变RMI使用的sockets,并知道当这样做的时候是一种恰当的设计策略.
正如我多次提到的,RMI是构建在sockets上的.这意味着:
1.在客户端与服务器端(双向)间传输数据是通过流的形式进行的.
2.客户端与服务器之间的连接的创建和维护,使用的是定义在java.net包中的socket类.
3.关于底层网络的公共设施和通信协议,RMI是不做任何假设的.(译者注:即底层的网络通信是由socket来完成的,对于RMI来说是透明的).
理解了这些重点,并知道它们背后隐含的含义,对于理解RMI的底层如何工作是至关重要的.然而,由于严格的分离,RMI作为一方面,操作系统和网络传输协议作为另一方面,自然会有下面的想法:
如果我能定义一个新的socket类,并且我的socket级的通信协议使用它来工作,那么构建在sockets上的且对底层网络不作任何假设的RMI应用程序也能够运行在我的新socket上.
的确,结果就是这样的.通过定制socket工厂实现的RMI,包含一种显著灵活的方式:让你使用你喜欢的socket来处理在网络上传送的未加工的二进制数据.
为什么要使用定制的Socket类?
默认情况下,RMI使用java.net包中的标准socket类来实现在网络上发送信息.服务端使用ServerSocket的实例来监听连接,而客户端使用Socket的实例来发起连接.
这些socket是按纯文本方式来发送数据的,数据没有经过任何加密或压缩.从积极的方面看,它们至少包含了CPU的使用(信息不以任何方式传送),并且大部分是直接使用的.
后者通常是这样的,因为压缩也是需要一些技巧的(get a little trick).flush()语法,特别是与GZIPOutputStream相关的语法,并不与发送远程方法调用的想法相一致.
从消极的方面看,它们可能需要使用更多的带宽,当信息在网络进行传送的时候,就显得越发脆弱.
这就导致了使用定制socket的两个理由:
1.如果你处在不安全的网络,并且在网络上发送的数据比较敏感,你可能想使用经过加密的socket,如SSL socket来发送数据.
2.如果你处在一个极度堵塞的网络,带宽是一个问题,你可能想在数据经过网络发送前,先对数据进行压缩.
除了这些理由外,还有一些其他的理由使用定制socket.一个最普遍的理由就是监控socket的使用并追踪通过网络发送的信息量.
这是性能分析的一部分,有可能实际导致压缩socket的使用.
18.1 定制socket工厂
让我们以假设开始,假设一个恰当的定制socket和服务端socket类已经被写好了并且准备好使用.我们以这样的假设开始有两个理由.首先,
在本书的第二章节中已经探讨过怎样创建一个定制socket.第二,com.ora.rmibook.chapter18.sockets包中包含了一个简单的定制socket类,其名称为MonitoringSocket.
MonitoringSocket有一个相关的ServerSocket的子类,其名称为MonitoringServerSocket.它们组合在一起,使得你可以监控当前已经分配了多少个sockets, RMI使用它们做了些什么.
MonitoringSocket类也可以让你追踪在任何时候,存在多少个打开的连接,并为你提供一个简单的方式,让你对应用程序的负载和资源消耗有一个感官认识.
同样地,对于调试它也是非常有用的.我们已经有了一系列的定制socket,我们还需要做两件事情:创建定制socket工厂和修改我们的服务器.
18.1.1 创建定制socket工厂
java.rmi.server包定义了两个socket工厂接口:RMIClientSocketFactory和RMIServerSocketFactory.为了使用定制的socket,必须实现这两个接口.幸运的是,它们的接口非常简单;每个接口
均只包含一个单一的方法.
在RMIClientSocketFactory中,其方法是:
public Socket createSocket(String host,int port)
在RMIServerSocketFactory中,其方法是:
public ServerSocket createServerSocket(int port)
这些方法只是Socket和ServerSocket标准构造函数的简单翻译.也就是说,你可以将其想像成RMIClientSocketFactory的实现就是在调用Socket类的构造函数,就像下面的代码片段一样:
public Socket createSocket(String host,int port){
return new Socket(host,port);
}
其结果是完全实现socket工厂是只是比这稍稍精细一点.以下使用我们的MonitoringSocket实现客户端socket工厂的全部代码:
public class MonitoringSocket_RMIClientSocketFactory implements RMIClientSocketFactory,Serializable{
private int _hashCode = "MonitoringSocket_RMIClientSocketFactory".hashCode();
public Socket createSocket(String host,int port){
try{
return new MonitoringSocket(host,port);
}catch(IOException e){
return null;
}
}
public boolean equals(Object object){
if(object instanceof MonitoringSocket_RMIClientSocketFactory){
return true;
}
return false;
}
public int hashCode(){
return hashCode();
}
}
这个类完成三件事情:它创建一个MoitoringSocket的实例,实现了Serializable接口,以及正确地覆盖了equals()和hashCode()方法.
对于为什么我们的socket工厂需要创建MonitoringSocket实例,这一点是很明确的,但其他两个任务也同样重要.
18.1.1.1 实现序列化
RMIClientSocketFactory的实现类必须实现序列化接口,因为服务器使用的socket类型完全是服务端属性.为了使这个客户端能连接上服务器,它必须使用正确的socket类型.
为了使得在客户端的RMI运行时环境能够辨认出使用的socket类型,它必须能反序列化一个恰当的socket工厂实例.当客户端在反序列化stub(stub拥有一个实现了RMIClientSocketFactory的实例引用)的时候,
这种动作是自动发生的.当stub绑定到一个命名服务的时候,序列化创建了一个正确socket工厂的拷贝.当客户端从命名服务中获取stub时,同时也会获得socket工厂的拷贝,因此它能连接到服务器上.
18.1.1.2 实现equals()和hashCode()方法
RMI运行时环境通过两种方式来使用socket工厂实例:创建sockets和索引已存在的sockets.换句话说,当stub想要发送消息时,RMI运行时将会按照下面的动作进行:
1.运行时从stub中获取RMIClientSocketFactory的stub实例
2.运行时将RMIClientSocketFactory的stub实例作为hashtable的键来查找那些已经打开但当前没有被使用的sockets.
3.如果获取sockets失败,RMI运行时将RMIClientSocketFactory的stub实例来创建一个socket
4.当远程方法调用完成了的时候,客户端运行时将此socket放入步骤中的hashtable中.
你可能认为有一种更好的对象来作为步骤2中hashtable的键。比如,stub自身就是一个很好的选择。然而,如果这样做的话,stub将无法工作。
为什么?不同服务器的两个stub,甚至使用同一个socket工厂的stub,它们会返回不同的hashcode,且当equals()方法被调用的时候,也会返回false.
使用stub来索引sockets意味着在一个特定的服务器中调用的时候,sockets能被重用,但对于不同的服务器则不能。
你不能使用stub对象也不能使用RMIClientSocketFactory的stub实例。使用它们中的任何一个作为hashtable的key都忽略这种可能性:即客户端工厂是有状态的,在不同的时期,将返回不同的sockets.
考虑一下以下完全合法的RMIClientSocketFactory的实现代码:
package com.ora.rmibook.chapter18.sockets;
import java.rmi.server.*;
import java.io.*;
import java.net.*;
public class PropertyBasedMonitoringSocket_RMIClientSocketFactory
implements RMIClientSocketFactory, Serializable {
private static final String USE_MONITORING_SOCKETS_PROPERTY = "com.ora.rmibook.useMonitoringSockets";
private static final String TRUE = "true";
private int _hashCode = "PropertyBasedMonitoringSocket_RMIClientSocketFactory".hashCode();
private boolean _isMonitoringOn;
public PropertyBasedMonitoringSocket_RMIClientSocketFactory() {
String monitoringProperty = System.getProperty(USE_MONITORING_SOCKETS_PROPERTY);
if ((null != monitoringProperty) && (monitoringProperty.equalsIgnoreCase(TRUE))) {
_isMonitoringOn = true;
_hashCode++;
} else {
_isMonitoringOn = false;
}
return;
}
public Socket createSocket(String host, int port) {
try {
if (_isMonitoringOn) {
return new MonitoringSocket(host, port);
} else {
return new Socket(host, port);
}
} catch (IOException e) {
}
return null;
}
public boolean equals(Object object) {
if (object instanceof PropertyBasedMonitoringSocket_RMIClientSocketFactory) {
return true;
}
return false;
}
public int hashCode() {
return _hashCode;
}
}
这个类做了一些不同寻常的事情。在程序的初始化部分,它检查系统属性以了解是否要使用监控socket.不同的服务器可能运行在不同的JVM中,并对这个值有不同的设置(可通过-D argument来改变其值)。
比如,下面的两行调用代码就会产不同类型的socket:
java -Dcom.ora.rmibook.useMonitoringSocket=false...
java -Dcom.ora.rmibook.useMonitoringSocket=true...
然而,如果我们使用stub的类对象或socket factory的类对象作为hashtable中可用sockets的键,服务器之间的区别将会消失,并且错误的socket类型有可能被使用。
几乎所有的这些讨论都可以用在RMIServerSocketFactory的实现上。生产服务端socket的工厂由于不会向客户端传递数据,所以它可以不需要序列化。
另一方面,服务器RMI运行时内部使用equals()和hashCode()方法。因为大部分服务器使用同样类型的sockets,使得看起来多个服务器在共享sockets.
为了解决这个问题,RMi维护已打开的sockets的映射。当一个服务器需要一个socket的时候,RMI使用socket factory作为key来确定是否有可用的sockets.为使用RMI能够有效地查找,我们必须覆盖equals()和hashCode()方法。
package com.ora.rmibook.chapter18.sockets;
import java.rmi.server.*;
import java.net.*;
import java.io.*;
public class MonitoringSocket_RMIServerSocketFactory
implements RMIServerSocketFactory {
private int _hashCode = "MonitoringSocket_RMIServerSocketFactory".hashCode();
public ServerSocket createServerSocket(int port) {
try {
return new MonitoringServerSocket(port);
} catch (IOException e) {
}
return null;
}
public boolean equals(Object object) {
if (object instanceof MonitoringSocket_RMIServerSocketFactory) {
return true;
}
return false;
}
public int hashCode() {
return _hashCode;
}
}
page 260
C:\Users\qbna>rmic -keep -classpath D:\myproject\JavaRMI\bin com.pa.rmi.test.upload.FileUploadHandlerImpl -d D:\myproject\JavaRMI\bin
147
废话就不多说了,现在开始进入正题.何为分布式,简单地说,就是一个JVM中的方法可以调用另一个JVM中的方法.在Java平台中,实现分布式应用程序通常有三种选择:
1.RMI,Java远程方法调用,支持Java的分布式对象之间的方法调用.
2.CORBA,通常对象请求代理架构,支持任何编程语言编写的对象之间的方法调用.使用
Internet Inter-ORB协议(IIOP)
3.webservice,独立于编程语言,使用基于xml的通信格式.用于传输对象的格式则是简单对象访问协议,即SOAP.
18.2将定制的socket集成到应用程序中
现在我们已经有了一个定制的socket factory,下一步是将其集成到我们的应用程序中。通常有两中方法可以实现集成:
1.可以修改服务器类
2.可以设置RMI的默认socket factory.
第一种选择,通过修改个别服务器,可以使我们能够细粒度地控制每个服务器能够使用不同的定制socket类。设置RMI的默认socket factory是以一种粗粒度的方式来控制的。改变RMI的默认socket factory则意味着任何服务器将使用新的默认的socket factory.
18.2.1修改原始服务器
UnicastRemoteObject有如下三个构造函数:
protected UnicastRemoteObject()
protected UnicastRemoteObject(int port)
protected UnicastRemoteObject(int port,RMIClientSocketFactory csf,RMIServerSocketFactory ssf)
直到现在为止,我们只在服务器程序中使用了前两种构造函数。事实上,我们主要在使用第一个构造函数,就像下面的代码片段:
public class Account_Impl extends UnicastRemoteObject implements Account {
private Money _balance;
public Account_Impl(Money startingBalance)
throws RemoteException {
_balance = startingBalance;
}
//...
}
有一个完美的理由这样做,无参的构造函数赋予了RMI运行时重用已经存在的server socket能力,带有一个参数的构造函数强迫
RMI运行时创建监听指定端口的server socket.然而,为了使用定制socket factory,我们必须使用第三个构造函数。因此,主要的代码改变就是使用第三个构造函数:
public class Account_Impl extends UnicastRemoteObject implements Account {
private Money _balance;
public Account_Impl(Money startingBalance)
throws RemoteException {
super(0,new PropertyBaseMonitoringSocket_RMIClientSocketFactory(),new PropertyBasedMonitoringSocket_RMIServerSocketFactory());
_balance = startingBalance;
}
//...
}
第一个参数为0,表示RMI可以自由选择一个端口作为server socket的端口,这样RMI可以重用已经存在的server socket.
UnicastRemoteObject的三个静态导出方法:
static RemoteStub exportObject(Remote obj)
static Remote exportObject(Remote obj,int port)
static Remote exportObject(Remote obj,int port,RMIClientSocketFactory csf,RMIServerSocketFactory ssf)
原始导出远程对象的代码为:
private static void launchServer(NameBalancePair serverDescription) {
try {
Account_Impl2 newAccount = new Account_Impl2(serverDescription.balance);
RemoteStub stub = UnicastRemoteObject.exportObject(newAccount);
Naming.rebind(serverDescription.name, stub);
System.out.println("Account " + serverDescription.name + " successfully launched.");
} catch (Exception e) {
}
}
修改后的导出代码为:
private static void launchServer(NameBalancePair serverDescription) {
try {
Account_Impl2 newAccount = new Account_Impl2(serverDescription.balance);
RemoteStub stub = UnicastRemoteObject.exportObject(newAccount,0,new PropertyBasedMonitoringSocket_RMIClientSocketFactory(),new PropertyBasedMonitoringSocket_RMIServerSocketFactory());
Naming.rebind(serverDescription.name, stub);
System.out.println("Account " + serverDescription.name + " successfully launched.");
} catch (Exception e) {
}
}
18.2.2修改活动的服务器
激活一个服务器的最大改变是从UnicastRemoteObject切换到Activatable.继承至UnicastRemoteObject服务器现在要继承Activatable,原来使用UnicastRemoteObject的exportObject()方法来导出远程对象,现在要使用Activatable的exportObject()方法来导出远程对象。
public class Account_Impl extends Activatable implements Account {
private Money _balance;
public Account_Impl(ActivationID id, MarshalledObject data)
throws RemoteException {
super (id, 0, new PropertyBasedMonitoringSocket_RMIClientSocketFactory(),
new PropertyBasedMonitoringSocket_RMIServerSocketFactory());
try {
_balance = (Money) data.get();
} catch (Exception e) {
/* Both ClassNotFoundException and IOException can
be thrown.*/
}
}
//....
}
18.2.3 修改默认的服务器
位于java.rmi.server包中的RMISocketFactory是RMI中的一个抽象类,它同时实现了RMIClientSocketFactory和RMIServerSocketFactory接口,RMI使用它作为默认的socket factory.
也就是说,除非服务器指定了使用一个不同的socket factory,否则RMI将使用RMISocketFactory的实例来作为它的客户端工厂和服务端工厂。
RMISocketFactory有5个静态方法来定制RMI运行时行为,它们是:
public static RMISocketFactory getDefaultSocketFactory()
public static RMISocketFactory getSocketFactory()
public static void setSocketFactory(RMISocketFactory fac)
public static RMIFailureHandler getFailureHandler()
public static void setFailureHandler(RMIFailureHandler fh)
以下一个RMISocketFactory的具体实现类:
package com.ora.rmibook.chapter18.sockets;
import java.rmi.server.*;
import java.io.*;
import java.net.*;
public class PropertyBasedMonitoringSocket_RMISocketFactory extends RMISocketFactory {
private static final String USE_MONITORING_SOCKETS_PROPERTY = "com.ora.rmibook.useMonitoringSockets";
private static final String TRUE = "true";
private int _hashCode = "PropertyBasedMonitoringSocket_RMISocketFactory".hashCode();
private boolean _isMonitoringOn;
public PropertyBasedMonitoringSocket_RMISocketFactory() {
String monitoringProperty = System.getProperty(USE_MONITORING_SOCKETS_PROPERTY);
if ((null != monitoringProperty) && (monitoringProperty.equalsIgnoreCase(TRUE))) {
_isMonitoringOn = true;
_hashCode++;
} else {
_isMonitoringOn = false;
}
return;
}
public Socket createSocket(String host, int port) {
try {
if (_isMonitoringOn) {
return new MonitoringSocket(host, port);
} else {
return new Socket(host, port);
}
} catch (IOException e) {
}
return null;
}
public ServerSocket createServerSocket(int port) {
try {
if (_isMonitoringOn) {
return new MonitoringServerSocket(port);
} else {
return new ServerSocket(port);
}
} catch (IOException e) {
}
return null;
}
public boolean equals(Object object) {
if (object instanceof PropertyBasedMonitoringSocket_RMIClientSocketFactory) {
return true;
}
return false;
}
public int hashCode() {
return _hashCode;
}
}
18.2.3.1失败处理
在RMISocketFactory中定义了两个静态方法:getFailureHandler()和setFailureHandler().RMIFailureHandler是当RMI需要ServerSocket时,但由于某种原因无法创建时调用的一个接口。
RMIFailureHandler定义了一个方法:
public boolean failure(Exception ex)
此方法的返回值如果为true,则告诉RMI再次创建ServerSocket的实例(这也是默认行为),反之,告诉RMI试图创建server socket的操作已经完全失败。
18.2.4交互式参数(Interaction with Parameters)
在16章中,我们讨论许多的传输层参数。这些全局参数用于配置sockets的RMI行为。这些传输层参数是:
sun.rmi.transport.connectionTimeout
sun.rmi.transport.tcp.readTimeout
sun.rmi.transport.proxy.connectionTimeout
RMISocketFactory的默认连接策略
1.使用JRMP(RMI默认协议)来直接socket连接
2.如果上一步失败,将远程方法调用包装在HTTP POST命令里,并将其发送到给远程服务器端口。
3.如果上一步失败,则将远程方法调用包装在HTTP POST命令里,将其通过代理发送给远程服务器端口。
4.如果上一步失败,则将远程方法调用包装在HTTP POST命令里,将其通过代理发送给远程服务器80端口。
这种策略既有用但也有安全风险。
答案是RMI使用定制的socket factories来创建sockets, 然后调用sockets上的方法配置它们。RMI使用下面的步骤获取server socket:
1.使用与server相关的RMIServerSocketFactory来检查server socket是否已经分配过,如果是,则使用已存在的server socket.
2.如果没有,则调用与服务器相关的RMIServerSocketFactory的createServerSocket()方法,在socket被创建后,再配置socket.