Java RMI + SSL + Compression = IMPOSSIBLE! [英] Java RMI + SSL + Compression = IMPOSSIBLE!

查看:270
本文介绍了Java RMI + SSL + Compression = IMPOSSIBLE!的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我设置了RMI + SSL。这工作伟大。但是似乎不可能在RMI和SSL之间滑动压缩。因此,RMI请求在通过SSL发送之前已压缩。



我已经看到一些在线的帖子建议使用 SSLSocketFactory.createSocket 它使用Socket在压缩套接字上包装SSL。但是,似乎它会尝试压缩SSL协议本身,这可能是不可压缩。



我应该创建一个 Socket 代理(的子类插入另一个 Socket > FilterOutputStream )。让代理用压缩包装输入/输出流。并让我的 SocketFactory ServerSocketFactory 返回代理,包装 SSLSocket



但是我们有缓冲问题。压缩缓冲数据,直到它获得足够的压缩,或被告知刷新。这是很好的,当你没有来回通信的插座。但是在RMI中缓存的套接字,你就有了。无法识别RMI请求的结束,因此您可以清除压缩的数据。



Sun有一个 RMISocketFactory



注意:

1. SSL支持压缩,但我不能找到任何关于在JSSE中启用的信息

2.我知道对许多小的无关块(因为RMI通常由其构成)的压缩不是非常有益。

3. I知道如果我发送大量请求,RMI不是最好的选择。

4.在Java 6中有一个 SSLRMISocketFactory 不添加任何东西在我的自定义实现。

解决方案

我们在这里有几个问题:





  • Java的基于zlib的DeflatorOutputStream不实现刷新。
  • $ b 我们不能简单地将SocketFactory包围到对方,就像InputStreams和OutputStreams。 $ b


我想我发现了一个机制,看似如何工作。



- 系列,因为它需要一些时间来写。
(您可以找到已完成内容的源代码

自定义SocketImpl



Socket 总是基于一个实现 SocketImpl 的对象。因此,拥有自定义套接字实际上意味着使用自定义SocketImpl类。这里是一个基于一对流(和一个基本套接字,用于关闭目的)的实现:

  / ** 
*一个SocketImpl实现,它在一对流
*上工作。
*
*此类的实例表示已经是
*连接的套接字,因此所有与
*连接,接受和这样的方法都没有实现。
*
*实现的方法是{@link #getInputStream},
* {@link #getOutputStream},{@link #available}和
* shutdown方法{@ link #close},{@link #shutdownInput},
* {@link #shutdownOutput}。
* /
private static类WrappingSocketImpl extends SocketImpl {
private InputStream inStream;
private OutputStream outStream;

private Socket base;

WrappingSocketImpl(StreamPair对,Socket base){
this.inStream = pair.input;
this.outStream = pair.output;
this.base = base;
}

A StreamPair



这些是重要的方法:

  protected InputStream getInputStream(){
return inStream;
}

protected OutputStream getOutputStream(){
return outStream;
}

protected int available()throws IOException {
return inStream.available();
}

然后一些方法允许关闭。这些没有真正测试(也许我们应该关闭或至少冲洗流?),但它似乎工作,我们的RMI使用。

  protected void close()throws IOException {
base.close();
}

protected void shutdownInput()throws IOException {
base.shutdownInput();
// TODO:inStream.close()?
}

protected void shutdownOutput()throws IOException {
base.shutdownOutput();
// TODO:outStream.close()?
}

接下来的一些方法将被Socket构造函数调用在RMI引擎中),但实际上不需要做任何事情。

  protected void create(boolean stream){
if(!stream){
throw new IllegalArgumentException(datagram socket not supported。
}
}

public Object getOption(int optID){
System.err.println(getOption(+ optID +));
return null;
}

public void setOption(int optID,Object value){
// noop,因为我们没有任何选项。
}

所有剩余的方法不是必需的,我们实现它们抛出异常将会注意到这个假设是否错误。)

  //不支持的操作

protected void connect host,int port){
System.err.println(connect(+ host +,+ port +));
throw new UnsupportedOperationException();
}


protected void connect(InetAddress address,int port){
System.err.println(connect(+ address +,+ port +));
throw new UnsupportedOperationException();
}

protected void connect(SocketAddress addr,int timeout){
System.err.println(connect(+ addr +,+ timeout +) );
throw new UnsupportedOperationException();
}

protected void bind(InetAddress host,int port){
System.err.println(bind(+ host +,+ port + );
throw new UnsupportedOperationException();
}

protected void listen(int backlog){
System.err.println(listen(+ backlog +));
throw new UnsupportedOperationException();
}

protected void accept(SocketImpl otherSide){
System.err.println(accept(+ otherSide +));
throw new UnsupportedOperationException();
}

protected void sendUrgentData(int data){
System.err.println(sendUrgentData());
throw new UnsupportedOperationException();
}
}

这是构造函数使用的StreamPair: p>

  / ** 
*一对流的简单持有者类。
* /
public static class StreamPair {
public InputStream input;
public OutputStream output;
public StreamPair(InputStream in,OutputStream out){
this.input = in; this.output = out;
}
}

下一部分:使用它来实现Socket工厂。






一个Socket工厂,包装另一个。



在这里处理RMI套接字工厂(即 RMIClientSocketFactory RMIServerSocketFactory RMISocketFactory 在java.rmi.server中),但同样的想法也适用于使用套接字工厂接口的其他库。示例包括 javax.net.SocketFactory < a>(和 ServerSocketFactory ),Apache Axis的 SocketFactory ,JSch的 SocketFactory



通常,这些工厂的想法是它们以某种方式连接到另一台服务器而不是原来的服务器( proxy 一些协商和简单可以继续现在在同一连接或必须通过某些其他协议(使用包装流)隧道实际连接。我们想让一些其他套接字工厂做原来的连接,然后只做流包装自己。



RMI为客户端和服务器套接字工厂提供了单独的接口。客户端套接字工厂将被串行化,并与远程存根一起从服务器传递到客户端,从而允许客户端到达服务器。



还有一个 RMISocketFactory 抽象类实现两个接口,并提供一个VM全局默认套接字工厂,将用于所有没有自己的远程对象。



现在我们将实现这个类的子类(从而也实现两个接口),允许用户提供一个基本客户端和服务器套接字工厂,然后我们将使用。我们的类必须是可序列化的,以允许将其传递给客户端。

  / ** 
* RMI的基类socket工厂通过从另一个
* Socket工厂封装Sockets流来完成他们的
*工作。
*
*子类必须覆盖{@link #wrap}方法。
*
*此类的实例可以同时用作客户端和
*服务器套接字工厂,或者只作为其中的一个。
* /
public abstract class WrappingSocketFactory
extends RMISocketFactory
实现可序列化
{

(想象所有其他相对于这个类的缩进。)



我们要引用其他工厂,这里的字段。

  / ** 
*基本客户端套接字工厂。这将被序列化。
* /
private RMIClientSocketFactory baseCFactory;

/ **
*基本服务器套接字工厂。这不会被序列化,
*,因为服务器套接字工厂只在服务器端使用。
* /
私有临时RMIServerSocketFactory baseSFactory;

这些将通过简单的构造函数进行初始化(这里不再重复) - 查看github库



b b b b b b 为了让这个套接字工厂的包装是一般的,我们只做这里的一般机制,并在子类中做流的实际包装。



这里我们只声明 wrap 方法:

  / ** 
*封装一对流。
*子类必须实现这个方法来做实际的
*工作。
* @param从基本套接字输入输入流。
* @param将输出流输出到基本套接字。
* @param server如果为true,我们在
* {@link ServerSocket#accept}中构造一个套接字。如果为false,这是一个纯
*客户端套接字。
* /
protected abstract StreamPair wrap(InputStream input,
OutputStream output,
boolean server);

此方法(以及Java不允许多个返回值的事实) StreamPair类。或者,我们可以有两个单独的方法,但在某些情况下(对于SSL),有必要知道哪两个流是配对的。



客户端套接字工厂



现在,让我们来看看客户端套接字工厂实现:

  / * * 
*创建一个客户端套接字并将其连接到给定的主机/端口对。
*
*这将从基本客户端
*套接字工厂检索一个套接字到主机/端口,然后在其周围封装一个新套接字(带有自定义SocketImpl)
*。
* @param托管我们想要连接的主机。
* @param port我们想要连接的端口。
* @返回连接到主机/端口对的新套接字。
* @throws IOException如果出现错误。
* /
public Socket createSocket(String host,int port)
throws IOException
{
Socket baseSocket = baseCFactory.createSocket(host,port);

我们从基本工厂检索套接字,然后...

  StreamPair streams = this.wrap(baseSocket.getInputStream(),
baseSocket.getOutputStream(),
false);

...用新流包装其流。 ( wrap 必须由子类实现,见下文)。

  SocketImpl wrappingImpl = new WrappingSocketImpl(streams,baseSocket); 

然后我们使用这些流创建WrappingSocketImpl(见上文) / p>

  return new Socket(wrappingImpl){
public boolean isConnected(){return true; }
};

...到一个新的Socket。我们必须子类化 Socket ,因为这个构造函数是受保护的,但这是适当的,因为我们还必须覆盖 isConnected 方法返回 true 而不是 false 。 (记住,我们的SocketImpl已经连接,不支持连接。)

 } 

对于客户端套接字工厂,这已经足够了。对于服务器套接字工厂,它变得更复杂。



包装ServerSockets



似乎没有办法创建一个ServerSocket与给定的SocketImpl对象 - 它总是使用静态SocketImplFactory。因此,我们现在子类化ServerSocket,只是忽略它的SocketImpl,而是委托给另一个ServerSocket。

  / ** 
* A服务器套接字子类,其将我们的定制套接字包围在
*套接字周围,由基本服务器套接字检索。
*
*我们只覆盖足够的方法工作。基本上,这是
*一个未绑定的服务器套接字,它特别处理{@link #accept}。
* /
private class WrappingServerSocket extends ServerSocket {
private ServerSocket base;

public WrappingServerSocket(ServerSocket b)
throws IOException
{
this.base = b;
}



原来我们必须实现这个 getLocalPort ,因为此号码与远程存根一起发送到客户端。

  / ** 
*返回此ServerSocket绑定到的本地端口。
* /
public int getLocalPort(){
return base.getLocalPort();
}

下一个方法是重要的。它的工作方式类似于上面的 createSocket()方法。

  / * * 
*接受来自某个远程主机的连接。
*这将从基本套接字接受一个套接字,然后
*包围一个新的自定义套接字。
* /
public Socket accept()throws IOException {

base ServerSocket接受一个连接,然后包装它的流:

  final Socket baseSocket = base.accept 
StreamPair streams =
WrappingSocketFactory.this.wrap(baseSocket.getInputStream(),
baseSocket.getOutputStream(),
true);

然后我们创建WrappingSocketImpl,...

  SocketImpl wrappingImpl = 
new WrappingSocketImpl(streams,baseSocket);

...并创建Socket的另一个匿名子类:

  //出于某种原因,这似乎只是作为一个
//匿名直接子类的Socket,而不是一个
//外部子类。奇怪。
Socket result = new Socket(wrappingImpl){
public boolean isConnected(){return true; }
public boolean isBound(){return true; }
public int getLocalPort(){
return baseSocket.getLocalPort();
}
public InetAddress getLocalAddress(){
return baseSocket.getLocalAddress();
}
};

这需要一些更多的重写方法,因为这些是由RMI引擎调用的。 / p>

我试图把这些放在一个单独的(非本地)类,但这没有工作(在客户端连接时给出异常)。我不知道为什么。如果有人有一个想法,我有兴趣。

 返回结果; 
}
}

有了这个ServerSocket子类,我们可以完成我们的。



包装RMI服务器套接字工厂



  / ** 
*创建在给定端口上侦听的服务器套接字。
*
*这将从基本服务器套接字工厂检索给定端口
*上侦听的ServerSocket,然后创建一个
*定制服务器套接字,在{@link ServerSocket #accept accept}
*从基本服务器套接字绕过套接字
*的新套接字(使用自定义SocketImpl)。
* @param托管我们想要连接的主机。
* @param port我们想要连接的端口。
* @返回连接到主机/端口对的新套接字。
* @throws IOException如果出现错误。
* /
public ServerSocket createServerSocket(int port)
throws IOException
{
final ServerSocket baseSocket = getSSFac()createServerSocket(port);
ServerSocket ss = new WrappingServerSocket(baseSocket);
return ss;
}

不多说,这一切都已经在评论中了。是的,我知道我可以在一行中做到这一切。



让我们完成类:

 } 

下一次:跟踪套接字工厂。






跟踪套接字工厂。



要测试我们的包装,看看是否有足够的刷新,这里是第一个子类的 wrap 方法:

  protected StreamPair wrap(InputStream in,OutputStream out,boolean server)
{
InputStream wrappedIn = in;
OutputStream wrappedOut = new FilterOutputStream(out){
public void write(int b)throws IOException {
System.err.println(write(。));
super.write(b);
}
public void write(byte [] b,int off,int len)
throws IOException {
System.err.println(write(+ len + );
super.out.write(b,off,len);
}
public void flush()throws IOException {
System.err.println(flush());
super.flush();
}
};
return new StreamPair(wrappedIn,wrappedOut);
}

输入流按原样使用,输出流只是添加一些日志记录。



在服务器端,它看起来像这样( [example] 来自ant):

  [example] write(14)
[example] b $ b [example] flush()
[example] flush()
[example]
[example] write(425)
[example] flush()
[example] flush()

我们看到有足够的刷新,甚至更多。 (数字是输出块的长度)
(在客户端,这实际上抛出一个java.rmi.NoSuchObjectException。它工作之前...不知道为什么它现在不工作作为压缩示例工作,我累了,我现在不会搜索它。)



下一步:压缩。






冲洗压缩流



对于压缩,Java在 java.util .zip 包。有一对 DeflaterOutputStream / InflaterInputStream ,通过包装另一个流实现 deflate ,分别通过 Deflater Inflater 过滤数据。 Deflater和Inflater基于调用常用 zlib 库的本机方法。 (实际上,如果有人提供子类具有 Deflater Inflater 的替代实现,流也可以支持其他算法。



(还有DeflaterInputStream和InflaterOutputStream,其工作方式相反。)



GZipOutputStream GZipInputStream 实现GZip文件格式。 (这主要添加一些页眉和页脚和校验和。)



两个输出流有问题(对于我们的用例),他们不真正支持 flush()。这是由Deflater的API定义的缺陷引起的,它允许缓冲尽可能多的数据,直到最后的 finish()。 Zlib允许刷新它的状态,只是Java包装器太蠢了。



有一个 bug#4206909 从1999年1月开始,它看起来像是最终固定的Java 7,hurray!如果你有Java 7,你可以在这里简单地使用DeflaterOutputStream。



因为我没有Java 7,但是,我会使用在错误评论2002年8月23日

  / ** 
* kaputten GZipOutputStream,von
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4206909
*(23-JUN-2002,rsaddey)
* @see DecompressingInputStream
* /
public class CompressingOutputStream
extends DeflaterOutputStream {


public CompressingOutputStream(final OutputStream out)
{
super out,
//使用deflater with nowrap == true将ommit头
//和trailers
new Deflater(Deflater.DEFAULT_COMPRESSION,true));
}

private static final byte [] EMPTYBYTEARRAY = new byte [0];
/ **
*确保输出所有剩余数据。
* /
public void flush()throws IOException {
/ **
*现在这是棘手的:我们强迫Deflater通过切换刷新
*压缩级别。
*现在,一个令人费解的简单解决方法为
* http://developer.java.sun.com/developer/bugParade/bugs/4255743.html
* /
def .setInput(EMPTYBYTEARRAY,0,0);

def.setLevel(Deflater.NO_COMPRESSION);
deflate();

def.setLevel(Deflater.DEFAULT_COMPRESSION);
deflate();

out.flush();
}

/ **
* Wirschließenauch den(selbst erstellten)Deflater,wenn
* wir fertig sind。
* /
public void close()
throws IOException
{
super.close();
def.end();
}

} // class

/ **
*解决方法GZipOutputStream,von
* http:// bugs。 sun.com/bugdatabase/view_bug.do?bug_id=4206909
*(23-JUN-2002,rsaddey)
* @see CompressingOutputStream
* /
public class DecompressingInputStream extends InflaterInputStream {

public DecompressingInputStream(final InputStream in){
//使用inflater with nowrap == true将ommit头和尾
super(in,new Inflater(true));
}

/ **
* available()应该返回可以读取的字节数,而不用
*运行到阻塞等待。完成这个宴会最终
*需要预先膨胀一个巨大的数据块,所以我们宁愿选择一个
*更放松的合同(java.util.zip.InflaterInputStream不
*适合账单)。
*此代码已经过测试,可与BufferedReader.readLine()一起使用;
* /
public int available()throws IOException {
if(!inf.finished()&&!inf.needsInput()){
return 1;
} else {
return in.available();
}
}

/ **
* Wirschließenauch den(selbst erstellten)Inflater,wenn
* wir fertig sind。
* /
public void close()
throws IOException
{
super.close();
inf.end();
}

} // class

de.fencing_game.tools 中的a href =https://github.com/ePaul/stackoverflow-examples/tree/master/src/de/fencing_game/tools =nofollow / code> package在我的github存储库)。它有一些德国评论,因为我最初一年前复制这个为我的另一个项目。)



在Stackoverflow上搜索一下我发现 BalusC的这个答案相关问题,其提供另一压缩输出流,具有优化的冲洗。我没有测试这个,但它可能是一个替代这一个。 (它使用 gzip 格式,而我们在此处使用纯的 deflate 格式。请确保写入和读取流匹配在一起。)



另一个替代方案是使用 JZlib ,作为最佳建议,它的ZOutputStream和ZInputStream 。 没有太多文档





压缩RMI套接字工厂



现在我们可以把它们放在一起了。

  / ** 
* RMISocketFactory,支持压缩传输。
*为此,我们使用{@link #CompressingInputStream}和{@link #CompressingOutputStream}
*。
*
*当我们扩展WrappingSocketFactory时,可以使用另一个
* {@link RMISocketFactory}。
* /
public class CompressedRMISocketFactory
extends WrappingSocketFactory
{

private static final long serialVersionUID = 1;

// ------------构造函数-----------------

/ * *
*基于一对
*套接字工厂创建CompressedRMISocketFactory。
*
* @param cFac用于创建客户端
*套接字的基本套接字工厂。这可能是{@code null},那么我们将使用客户端系统的
* {@linkplain RMISocketFactory#getDefault()默认套接字工厂}
*,其中此对象最终用于
*创建套接字。
*如果不是null,它应该是可序列化的。
* @param sFac用于创建服务器的基本套接字工厂
*套接字。这可能是{@code null},那么我们将使用
* {@linkplain RMISocketFactory#getDefault()默认RMI套接字工厂}。
*这不会被序列化到客户端。
* /
public CompressedRMISocketFactory(RMIClientSocketFactory cFac,
RMIServerSocketFactory sFac){
super(cFac,sFac);
}

// [删除更多构造函数]

// --------------实现----- --------

/ **
*将一对流封装到压缩/解压缩流中。
* /
protected StreamPair wrap(InputStream in,OutputStream out,
boolean server)
{
return new StreamPair(new DecompressingInputStream(in),
new CompressingOutputStream(out));
}
}

就是这样。我们现在为 UnicastRemoteObject.export(...)提供此工厂对象作为参数(对于客户端和服务器工厂),并且所有通信都将被压缩。 (我的github存储库中的版本< a>有一个main方法与一个例子。)



当然,压缩好处不会像RMI一样巨大,至少当你不传输大字符串或类似的东西作为参数或返回值。



下一次(在我睡觉后):与SSL套接字工厂结合。






与SSL套接字工厂结合



Java部分很容易,如果我们使用default classes:

CompressedRMISocketFactory fac = 
new CompressedRMISocketFactory(new SslRMIClientSocketFactory(),
new SslRMIServerSocketFactory() );

These classes (in javax.rmi.ssl) use the default SSLSocketFactory and SSLServerSocketFactory (in javax.net.ssl), which use the system’s default keystore and trust store.



Thus it is necessary to create a key store with keypair (for example by keytool -genkeypair -v), and provide this to the VM with the system properties javax.net.ssl.keyStore (the file name for the key store) and javax.net.ssl.keyStorePassword (the password for the key store).



On the client side, we need a trust store - i.e. a key store containing the public keys, or some certificate which signed the public keys of the server. For testing purposes, we simply can use the same keystore as the server, for production you certainly would not want the server’s private key on the client side. We provide this with the properties javax.net.ssl.trustStore javax.net.ssl.trustStorePassword.



Then it gets down to this (on the server side):

    Remote server = 
UnicastRemoteObject.exportObject(new EchoServerImpl(),
0, fac, fac);
System.err.println(\"server: \" + server);

Registry registry =
LocateRegistry.createRegistry(Registry.REGISTRY_PORT);

registry.bind(\"echo\", server);

The client is a stock client as for the previous examples:

    Registry registry = 
LocateRegistry.getRegistry(\"localhost\",
Registry.REGISTRY_PORT);

EchoServer es = (EchoServer)registry.lookup(\"echo\");
System.err.println(\"es: \" + es);
System.out.println(es.echo(\"hallo\"));

Now all communication to the EchoServer runs compressed and encrypted.
Of course, for complete security we also would want the communication to the registry SSL-protected, to avoid any man-in-the-middle attacks (which would allow also intercepting communication to the EchoServer by giving the client a fake RMIClientSocketFactory, or fake server address).





I've setup RMI + SSL. This works great. But it doesn't seem possible to slip compression in between RMI and SSL. So that the RMI requests are compressed before they're sent over SSL.

I've seen some posts online suggest using SSLSocketFactory.createSocket() which takes a Socket to wrap SSL over a compressing socket. But that seems like it would try to compress the SSL protocol itself, which probably isn't very compressable.

I supposed I should create a Socket proxy (subclass of Socket that defers to another Socket, like FilterOutputStream does). Have the proxy wrap the Input/Ouput streams with compression. And have my SocketFactory and ServerSocketFactory return the proxies, wrapping the SSLSocket.

But then we have the buffering issue. Compression buffers the data until it gets enough worth compressing, or is told to flush. This is fine when you don't have back-and-forth communication over the socket. But with cached sockets in RMI, you have that. With no way to identify the end of an RMI request so you can flush your compressed data.

Sun has an RMISocketFactory example doing something like this but they don't address this at all.

notes:
1. SSL supports compression but I can't find anything about enabling that in JSSE
2. I know that compression on lots of small unrelated blocks (as RMI is usually composed of) isn't very beneficial.
3. I know that if I'm sending large requests, RMI isn't the best choice.
4. There is an SSLRMISocketFactory in Java 6. but it doesn't add anything over my custom implementation.

解决方案

We have several problems here:

  • We can't simply wrap SocketFactories around each other, like we can do for InputStreams and OutputStreams.
  • Java's zlib-based DeflatorOutputStream does not implement flushing.

I think I found a mechanism how this would seems to work.

This will be a some-part series, as it needs some time to write. (You can find the source code of the completed stuff in my github repository).

A custom SocketImpl

A Socket always is based by an object implementing SocketImpl. Thus, having a custom socket in fact means using a custom SocketImpl class. Here is an implementation based on a pair of streams (and a base socket, for closing purposes):

/**
 * A SocketImpl implementation which works on a pair
 * of streams.
 *
 * A instance of this class represents an already
 * connected socket, thus all the methods relating to
 * connecting, accepting and such are not implemented.
 *
 * The implemented methods are {@link #getInputStream},
 * {@link #getOutputStream}, {@link #available} and the
 * shutdown methods {@link #close}, {@link #shutdownInput},
 * {@link #shutdownOutput}.
 */
private static class WrappingSocketImpl extends SocketImpl {
    private InputStream inStream;
    private OutputStream outStream;

    private Socket base;

    WrappingSocketImpl(StreamPair pair, Socket base) {
        this.inStream = pair.input;
        this.outStream = pair.output;
        this.base = base;
    }

A StreamPair is a simple data holder class, see below.

These are the important methods:

    protected InputStream getInputStream() {
        return inStream;
    }

    protected OutputStream getOutputStream() {
        return outStream;
    }

    protected int available() throws IOException {
        return inStream.available();
    }

Then some methods to allow closing. These are not really tested (maybe we should also close or at least flush the streams?), but it seems to work for our RMI usage.

    protected void close() throws IOException {
        base.close();
    }

    protected void shutdownInput() throws IOException {
        base.shutdownInput();
        // TODO: inStream.close() ?
    }

    protected void shutdownOutput() throws IOException {
        base.shutdownOutput();
        // TODO: outStream.close()?
    }

The next some methods will be called by the Socket constructor (or indirectly by something in the RMI engine), but don't really have to do anything.

    protected void create(boolean stream) {
        if(!stream) {
            throw new IllegalArgumentException("datagram socket not supported.");
        }
    }

    public Object getOption(int optID) {
        System.err.println("getOption(" + optID + ")");
        return null;
    }

    public void setOption(int optID, Object value) {
        // noop, as we don't have any options.
    }

All the remaining methods are not necessary, we implement them throwing Exceptions (so we will notice if this assumption was wrong).

    // unsupported operations

    protected void connect(String host, int port) {
        System.err.println("connect(" + host + ", " + port + ")");
        throw new UnsupportedOperationException();
    }


    protected void connect(InetAddress address, int port) {
        System.err.println("connect(" + address + ", " + port + ")");
        throw new UnsupportedOperationException();
    }

    protected void connect(SocketAddress addr, int timeout) {
        System.err.println("connect(" + addr + ", " + timeout + ")");
        throw new UnsupportedOperationException();
    }

    protected void bind(InetAddress host, int port) {
        System.err.println("bind(" + host + ", " + port + ")");
        throw new UnsupportedOperationException();
    }

    protected void listen(int backlog) {
        System.err.println("listen(" + backlog + ")");
        throw new UnsupportedOperationException();
    }

    protected void accept(SocketImpl otherSide) {
        System.err.println("accept(" + otherSide + ")");
        throw new UnsupportedOperationException();
    }

    protected void sendUrgentData(int data) {
        System.err.println("sendUrgentData()");
        throw new UnsupportedOperationException();
    }
}

Here is the StreamPair used by the constructor:

/**
 * A simple holder class for a pair of streams.
 */
public static class StreamPair {
    public InputStream input;
    public OutputStream output;
    public StreamPair(InputStream in, OutputStream out) {
        this.input = in; this.output = out;
    }
}

Next part: use this to implement a Socket factory.


A Socket factory, wrapping another one.

We are dealing here with RMI socket factories (i.e. RMIClientSocketFactory, RMIServerSocketFactory, RMISocketFactory in java.rmi.server), but the same idea applies to other libraries using a socket factory interface as well. Examples are javax.net.SocketFactory (and ServerSocketFactory), Apache Axis' SocketFactory, JSch's SocketFactory.

Often, the idea of these factories is that they somehow connect to another server than the original one (a proxy), then do some negotiating and either simple can continue now in the same connection or have to tunnel the real connection through some other protocol (using wrapping streams). We instead want to let some other socket factory do the original connecting, and then do only the stream wrapping ourselves.

RMI has separate interfaces for the client and server socket factories. The client socket factories will be serialized and passed from the server to the client together with the remote stubs, allowing the client to reach the server.

There is also a RMISocketFactory abstract class implementing both interfaces, and providing a VM-global default socket factory which will be used for all remote objects which don't have their own ones.

We will now implement a subclass of this class (and thereby also implementing both interfaces), allowing the user to give a base client and server socket factory, which we then will use. Our class must be serializable to allow passing it to the clients.

/**
 * A base class for RMI socket factories which do their
 * work by wrapping the streams of Sockets from another
 * Socket factory.
 *
 * Subclasses have to overwrite the {@link #wrap} method.
 *
 * Instances of this class can be used as both client and
 * server socket factories, or as only one of them.
 */
public abstract class WrappingSocketFactory 
    extends RMISocketFactory
    implements Serializable
{

(Imagine all the rest indented relative to this class.)

As we want to refer to other factories, here the fields.

/**
 * The base client socket factory. This will be serialized.
 */
private RMIClientSocketFactory baseCFactory;

/**
 * The base server socket factory. This will not be serialized,
 * since the server socket factory is used only on the server side.
 */
private transient RMIServerSocketFactory baseSFactory;

These will be initialized by straightforward constructors (which I don't repeat here - look at the github repository for the full code).

Abstract wrap method

To let this "wrapping of socket factories" be general, we do only the general mechanism here, and do the actual wrapping of the streams in subclasses. Then we can have a compressing/decompressing subclass, a encrypting one, a logging one, etc.

Here we only declare the wrap method:

/**
 * Wraps a pair of streams.
 * Subclasses must implement this method to do the actual
 * work.
 * @param input the input stream from the base socket.
 * @param output the output stream to the base socket.
 * @param server if true, we are constructing a socket in
 *    {@link ServerSocket#accept}. If false, this is a pure
 *   client socket.
 */
protected abstract StreamPair wrap(InputStream input,
                                   OutputStream output,
                                   boolean server);

This method (and the fact that Java doesn't allow multiple return values) is the reason for the StreamPair class. Alternatively we could have two separate methods, but in some cases (as for SSL) it is necessary to know which two streams are paired.

Client Socket Factory

Now, lets have a look at the client socket factory implementation:

/**
 * Creates a client socket and connects it to the given host/port pair.
 *
 * This retrieves a socket to the host/port from the base client
 * socket factory and then wraps a new socket (with a custom SocketImpl)
 * around it.
 * @param host the host we want to be connected with.
 * @param port the port we want to be connected with.
 * @return a new Socket connected to the host/port pair.
 * @throws IOException if something goes wrong.
 */
public Socket createSocket(String host, int port)
    throws IOException
{
    Socket baseSocket = baseCFactory.createSocket(host, port);

We retrieve a socket from our base factory, and then ...

    StreamPair streams = this.wrap(baseSocket.getInputStream(),
                                   baseSocket.getOutputStream(),
                                   false);

... wrap its streams by new streams. (This wrap has to be implemented by subclasses, see below).

    SocketImpl wrappingImpl = new WrappingSocketImpl(streams, baseSocket);

Then we use these streams to create our WrappingSocketImpl (see above), and pass it ...

    return new Socket(wrappingImpl) {
        public boolean isConnected() { return true; }
    };

... to a new Socket. We have to subclass Socket because this constructor is protected, but this is opportune since we also have to override the isConnected method to return true instead of false. (Remember, our SocketImpl is already connected, and does not support connecting.)

}

For client socket factories, this is already enough. For server socket factories, it gets a bit more complicated.

Wrapping ServerSockets

There seems to be no way to create a ServerSocket with a given SocketImpl object - it always uses the static SocketImplFactory. Thus we now subclass ServerSocket, simply ignoring its SocketImpl, instead delegating to another ServerSocket.

/**
 * A server socket subclass which wraps our custom sockets around the
 * sockets retrieves by a base server socket.
 *
 * We only override enough methods to work. Basically, this is
 * a unbound server socket, which handles {@link #accept} specially.
 */
private class WrappingServerSocket extends ServerSocket {
    private ServerSocket base;

    public WrappingServerSocket(ServerSocket b)
        throws IOException
    {
        this.base = b;
    }

It turns out we have to implement this getLocalPort, since this number is sent with the remote stub to the clients.

    /**
     * returns the local port this ServerSocket is bound to.
     */
    public int getLocalPort() {
        return base.getLocalPort();
    }

The next method is the important one. It works similar to our createSocket() method above.

    /**
     * accepts a connection from some remote host.
     * This will accept a socket from the base socket, and then
     * wrap a new custom socket around it.
     */
    public Socket accept() throws IOException {

We let the base ServerSocket accept a connection, then wrap its streams:

        final Socket baseSocket = base.accept();
        StreamPair streams =
            WrappingSocketFactory.this.wrap(baseSocket.getInputStream(),
                                            baseSocket.getOutputStream(),
                                            true);

Then we create our WrappingSocketImpl, ...

        SocketImpl wrappingImpl =
            new WrappingSocketImpl(streams, baseSocket);

... and create another anonymous subclass of Socket:

        // For some reason, this seems to work only as a
        // anonymous direct subclass of Socket, not as a
        // external subclass.      Strange.
        Socket result = new Socket(wrappingImpl) {
                public boolean isConnected() { return true; }
                public boolean isBound() { return true; }
                public int getLocalPort() {
                    return baseSocket.getLocalPort();
                }
                public InetAddress getLocalAddress() {
                    return baseSocket.getLocalAddress();
                }
            };

This one needs some more overridden methods, as these are called by the RMI engine, it seems.

I tried to put these in a separate (non-local) class, but this did not work (gave exceptions at the client side on connecting). I have no idea why. If someone has an idea, I'm interested.

        return result;
    }
}

Having this ServerSocket subclass, we can complete our ...

wrapping RMI server socket factory

/**
 * Creates a server socket listening on the given port.
 *
 * This retrieves a ServerSocket listening on the given port
 * from the base server socket factory, and then creates a 
 * custom server socket, which on {@link ServerSocket#accept accept}
 * wraps new Sockets (with a custom SocketImpl) around the sockets
 * from the base server socket.
 * @param host the host we want to be connected with.
 * @param port the port we want to be connected with.
 * @return a new Socket connected to the host/port pair.
 * @throws IOException if something goes wrong.
 */
public ServerSocket createServerSocket(int port)
    throws IOException
{
    final ServerSocket baseSocket = getSSFac().createServerSocket(port);
    ServerSocket ss = new WrappingServerSocket(baseSocket);
    return ss;
}

Not much to say, it all is already in the comment. Yes, I know I could do this all in one line. (There originally were some debugging outputs between the lines.)

Let's finish the class:

}

Next time: a tracing socket factory.


A tracing socket factory.

To test our wrapping and see if there are enough flushes, here the wrap method of a first subclass:

protected StreamPair wrap(InputStream in, OutputStream out, boolean server)
{
    InputStream wrappedIn = in;
    OutputStream wrappedOut = new FilterOutputStream(out) {
            public void write(int b) throws IOException {
                System.err.println("write(.)");
                super.write(b);
            }
            public void write(byte[] b, int off, int len)
                throws IOException {
                System.err.println("write(" + len + ")");
                super.out.write(b, off, len);
            }
            public void flush() throws IOException {
                System.err.println("flush()");
                super.flush();
            }
        };
    return new StreamPair(wrappedIn, wrappedOut);
}

The input stream is used as is, the output stream simply adds some logging.

On the server side, it looks like this (the [example] comes from ant):

  [example] write(14)
  [example] flush()
  [example] write(287)
  [example] flush()
  [example] flush()
  [example] flush()
  [example] write(1)
  [example] flush()
  [example] write(425)
  [example] flush()
  [example] flush()

We see that there are enough flushes, even more than enough. (The numbers are the lengths of the output chunks.) (On client side, this actually throws a java.rmi.NoSuchObjectException. It worked before ... no idea why it doesn't work now. As the compressing example does work and I'm tired, I'll not search for it now.)

Next: compressing.


Flushing compressed streams

For compression, Java has some classes in the java.util.zip package. There is the pair DeflaterOutputStream / InflaterInputStream which implement the deflate compression algorithm by wrapping another stream, filtering the data through a Deflater or Inflater, respectively. Deflater and Inflater are based on native methods calling the common zlib library. (Actually, the streams could also support other algorithms, if someone provided subclasses with alternate implementations of Deflater and Inflater.)

(There are also DeflaterInputStream and InflaterOutputStream, which work the other way around.)

Based on this, GZipOutputStream and GZipInputStream implement the GZip file format. (This adds mainly some header and footer, and a checksum.)

Both output streams have the problem (for our use case) that they don't truly support flush(). This is caused by a deficiency in the API definition of Deflater, which is allowed to buffer as much data as its want until the final finish(). Zlib allows flushing its state, just the Java wrapper is too stupid.

There is bug #4206909 open about this since January 1999, and it looks like it is finally fixed for Java 7, hurray! If you have Java 7, you can simply use DeflaterOutputStream here.

Since I don't have Java 7, yet, I'll use the workaround posted in the bug comments on 23-JUN-2002 by rsaddey.

/**
 * Workaround für kaputten GZipOutputStream, von
 * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4206909
 * (23-JUN-2002, rsaddey)
 * @see DecompressingInputStream
 */
public class CompressingOutputStream
    extends DeflaterOutputStream {


    public CompressingOutputStream (final OutputStream out)
    {
        super(out,
              // Using Deflater with nowrap == true will ommit headers
              //  and trailers
              new Deflater(Deflater.DEFAULT_COMPRESSION, true));
    }

    private static final byte [] EMPTYBYTEARRAY = new byte[0];
    /**
     * Insure all remaining data will be output.
     */
    public void flush() throws IOException {
        /**
         * Now this is tricky: We force the Deflater to flush
         * its data by switching compression level.
         * As yet, a perplexingly simple workaround for 
         *  http://developer.java.sun.com/developer/bugParade/bugs/4255743.html 
        */
        def.setInput(EMPTYBYTEARRAY, 0, 0);

        def.setLevel(Deflater.NO_COMPRESSION);
        deflate();

        def.setLevel(Deflater.DEFAULT_COMPRESSION);
        deflate();

        out.flush();
    }

    /**
     * Wir schließen auch den (selbst erstellten) Deflater, wenn
     * wir fertig sind.
     */
    public void close()
        throws IOException
    {
        super.close();
        def.end();
    }

} // class

/**
 * Workaround für kaputten GZipOutputStream, von
 * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4206909
 * (23-JUN-2002, rsaddey)
 * @see CompressingOutputStream
 */
public class DecompressingInputStream extends InflaterInputStream {

    public DecompressingInputStream (final InputStream in) {
        // Using Inflater with nowrap == true will ommit headers and trailers
        super(in, new Inflater(true));
    }

    /**
     * available() should return the number of bytes that can be read without
     * running into blocking wait. Accomplishing this feast would eventually
     * require to pre-inflate a huge chunk of data, so we rather opt for a
     * more relaxed contract (java.util.zip.InflaterInputStream does not 
     * fit the bill). 
     * This code has been tested to work with BufferedReader.readLine();
     */
    public int available() throws IOException {
        if (!inf.finished() && !inf.needsInput()) {
            return 1;
        } else {
            return in.available();
        }
    }

    /**
     * Wir schließen auch den (selbst erstellten) Inflater, wenn
     * wir fertig sind.
     */
    public void close()
        throws IOException
    {
        super.close();
        inf.end();
    }

} //class

(These are in the de.fencing_game.tools package in my github repository.) It has some German comments since I originally one year ago copied this for another project of mine.)

Searching a bit on Stackoverflow I found this answer by BalusC to a related question, which offers another compressing Outputstream, with optimized flushing. I did not test this, but it might be an alternative to this one. (It uses gzip format, while we are using the pure deflate format here. Make sure both writing and reading stream fit together.)

Another alternative would be using JZlib, as bestsss proposed, with it's ZOutputStream and ZInputStream. It has not much documentation, but I'm working on it.

Next time: compressed RMI socket factory


Compressing RMI socket factory

Now we can pull it all together.

/**
 * An RMISocketFactory which enables compressed transmission.
 * We use {@link #CompressingInputStream} and {@link #CompressingOutputStream}
 * for this.
 *
 * As we extend WrappingSocketFactory, this can be used on top of another
 * {@link RMISocketFactory}.
 */
public class CompressedRMISocketFactory
    extends WrappingSocketFactory
{

    private static final long serialVersionUID = 1;

    //------------ Constructors -----------------

    /**
     * Creates a CompressedRMISocketFactory based on a pair of
     * socket factories.
     *
     * @param cFac the base socket factory used for creating client
     *   sockets. This may be {@code null}, then we will use the
     *  {@linkplain RMISocketFactory#getDefault() default socket factory}
     *  of client system where this object is finally used for
     *   creating sockets.
     *   If not null, it should be serializable.
     * @param sFac the base socket factory used for creating server
     *   sockets. This may be {@code null}, then we will use the
     *  {@linkplain RMISocketFactory#getDefault() default RMI Socket factory}.
     *  This will not be serialized to the client.
     */
    public CompressedRMISocketFactory(RMIClientSocketFactory cFac,
                                      RMIServerSocketFactory sFac) {
        super(cFac, sFac);
    }

    // [snipped more constructors]

    //-------------- Implementation -------------

    /**
     * wraps a pair of streams into compressing/decompressing streams.
     */
    protected StreamPair wrap(InputStream in, OutputStream out,
                              boolean server)
    {
        return new StreamPair(new DecompressingInputStream(in),
                              new CompressingOutputStream(out));
    }
}

That's it. We now provide this factory object to UnicastRemoteObject.export(...) as arguments (both for client and server factory), and all the communication will be compressed. (The version in my github repository has a main method with an example.)

Of course, the compression benefits will not be huge fore things like RMI, at least when you don't transfer large strings or similar stuff as arguments or return values.

Next time (after I have slept): combining with an SSL socket factory.


Combining with an SSL socket factory

The Java part of this is easy, if we use the default classes:

CompressedRMISocketFactory fac =
    new CompressedRMISocketFactory(new SslRMIClientSocketFactory(),
                   new SslRMIServerSocketFactory());

These classes (in javax.rmi.ssl) use the default SSLSocketFactory and SSLServerSocketFactory (in javax.net.ssl), which use the system's default keystore and trust store.

Thus it is necessary to create a key store with keypair (for example by keytool -genkeypair -v), and provide this to the VM with the system properties javax.net.ssl.keyStore (the file name for the key store) and javax.net.ssl.keyStorePassword (the password for the key store).

On the client side, we need a trust store - i.e. a key store containing the public keys, or some certificate which signed the public keys of the server. For testing purposes, we simply can use the same keystore as the server, for production you certainly would not want the server's private key on the client side. We provide this with the properties javax.net.ssl.trustStore javax.net.ssl.trustStorePassword.

Then it gets down to this (on the server side):

    Remote server =
        UnicastRemoteObject.exportObject(new EchoServerImpl(),
                                         0, fac, fac);
    System.err.println("server: " + server);

    Registry registry =
        LocateRegistry.createRegistry(Registry.REGISTRY_PORT);

    registry.bind("echo", server);

The client is a stock client as for the previous examples:

    Registry registry =
        LocateRegistry.getRegistry("localhost",
                                   Registry.REGISTRY_PORT);

    EchoServer es = (EchoServer)registry.lookup("echo");
    System.err.println("es: " + es);
    System.out.println(es.echo("hallo"));

Now all communication to the EchoServer runs compressed and encrypted. Of course, for complete security we also would want the communication to the registry SSL-protected, to avoid any man-in-the-middle attacks (which would allow also intercepting communication to the EchoServer by giving the client a fake RMIClientSocketFactory, or fake server address).


这篇关于Java RMI + SSL + Compression = IMPOSSIBLE!的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆