Java RMI + SSL +压缩=不可能!

我已经设置了RMI + SSL。 这很好用。 但似乎不可能在RMI和SSL之间减少压缩。 这样RMI请求在通过SSL发送之前就会被压缩。

我在网上看到一些post建议使用SSLSocketFactory.createSocket() ,它使用Socket将SSL包装在压缩套接字上。 但这似乎会尝试压缩SSL协议本身,这可能不是非常可压缩的。

我想我应该创建一个Socket代理( Socket子类,它推迟到另一个Socket ,就像FilterOutputStream那样)。 让代理用压缩包装输入/输出流。 让我的SocketFactoryServerSocketFactory返回代理,包装SSLSocket

但后来我们遇到了缓冲问题。 压缩缓冲数据,直到它足够值得压缩,或被告知要刷新。 当您没有通过套接字进行来回通信时,这很好。 但是使用RMI中的缓存套接字,你就可以了。 无法识别RMI请求的结束,因此您可以刷新压缩数据。

Sun有一个RMISocketFactory示例做这样的事情,但他们根本没有解决这个问题。

笔记:
1. SSL支持压缩,但我在JSSE中找不到任何关于启用它的信息
2.我知道对许多小的无关块进行压缩(因为RMI通常由其组成)并不是非常有益。
3.我知道如果我发送大量请求,RMI不是最好的选择。
4. Java 6中有一个SSLRMISocketFactory ,但它不会在我的自定义实现中添加任何内容。

我们这里有几个问题:

  • 我们不能简单地将SocketFactories包装在一起,就像我们可以为InputStreams和OutputStreams做的那样。
  • Java的基于zlib的DeflatorOutputStream不实现刷新。

我想我找到了一种机制,看起来似乎有效。

这将是一个部分系列,因为它需要一些时间来编写。 (您可以在我的github存储库中找到已完成内容的源代码)。

自定义SocketImpl

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

 /** * 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; } 

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, as we don't have any options. } 

所有剩下的方法都没有必要,我们实现它们抛出exception(所以我们会注意到这个假设是错误的)。

  // 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(); } } 

这是构造函数使用的StreamPair:

 /** * 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; } } 

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


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

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

通常,这些工厂的想法是它们以某种方式连接到另一个服务器而不是原始服务器( 代理 ),然后进行一些协商,并且要么简单就可以在同一连接中继续,或者必须通过其他协议来隧道真实连接(使用包裹流)。 我们反而想让其他套接字工厂进行原始连接,然后只做自己包装的流。

RMI具有用于客户端和服务器套接字工厂的单独接口。 客户端套接字工厂将被序列化并与远程存根一起从服务器传递到客户端,允许客户端到达服务器。

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

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

 /** * 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 { 

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

我们想要参考其他工厂,这里是领域。

 /** * 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; 

这些将由简单的构造函数初始化(我在此不再重复 – 查看完整代码的github存储库)。

抽象wrap方法

为了让这个“套接字工厂包装”成为通用的,我们在这里只做一般的机制,并在子类中实际包装流。 然后我们可以有一个压缩/解压缩子类,一个加密子类,一个日志记录子类等。

这里我们只声明wrap方法:

 /** * 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); 

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

客户端套接字工厂

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

 /** * 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); 

我们从我们的基地工厂检索一个套接字,然后……

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

…用新流包裹它的流。 (这个wrap必须由子类实现,见下文)。

  SocketImpl wrappingImpl = new WrappingSocketImpl(streams, baseSocket); 

然后我们使用这些流来创建我们的WrappingSocketImpl(见上文),然后传递它……

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

…到一个新的Socket。 我们必须inheritanceSocket因为这个构造函数是受保护的,但这很合适,因为我们还必须覆盖isConnected方法以返回true而不是false 。 (请记住,我们的SocketImpl已经连接,并且不支持连接。)

 } 

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

包装ServerSockets

似乎没有办法用给定的SocketImpl对象创建ServerSocket – 它总是使用静态SocketImplFactory。 因此,我们现在将ServerSocket子类化,只是忽略其SocketImpl,而是委托给另一个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; } 

事实certificate,我们必须实现此getLocalPort ,因为此数字随远程存根一起发送到客户端。

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

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

  /** * 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 { 

我们让基本的ServerSocket接受一个连接,然后包装它的流:

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

然后我们创建我们的WrappingSocketImpl,…

  SocketImpl wrappingImpl = new WrappingSocketImpl(streams, baseSocket); 

…并创建另一个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(); } }; 

这个需要一些更多的重写方法,因为它们似乎是由RMI引擎调用的。

我试图将它们放在一个单独的(非本地)类中,但这不起作用(在连接时在客户端提供例外)。 我不知道为什么。 如果有人有想法,我很感兴趣。

  return result; } } 

拥有这个ServerSocket子类,我们可以完成我们的……

包装RMI服务器套接字工厂

 /** * 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; } 

不多说,这一切都已经在评论中了。 是的,我知道我可以一行完成这一切。 (最初在线之间有一些调试输出。)

让我们完成课程:

 } 

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


跟踪套接字工厂。

要测试我们的包装并查看是否有足够的刷新,这里是第一个子类的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] flush() [example] write(287) [example] flush() [example] flush() [example] flush() [example] write(1) [example] flush() [example] write(425) [example] flush() [example] flush() 

我们看到有足够的冲洗,甚至绰绰有余。 (数字是输出块的长度。)(在客户端,这实际上抛出了一个java.rmi.NoSuchObjectException。它之前有效……不知道为什么它现在不起作用。因为压缩示例确实有效并且我累了,我现在不会去搜索它。)

下一步:压缩。


冲洗压缩流

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

(还有DeflaterInputStream和InflaterOutputStream,它们以相反的方式工作。)

基于此, GZipOutputStreamGZipInputStream实现了GZip文件格式。 (这主要添加了一些页眉和页脚,以及校验和。)

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

自1999年1月以来,有关于此问题的错误#4206909开放,看起来它最终为Java 7修复了,欢呼! 如果你有Java 7,你可以在这里使用DeflaterOutputStream。

由于我还没有Java 7,我将使用rsaddey在23-JUN-2002上的bug评论中发布的解决方法。

 /** * 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 

(这些都在我的github存储库中的de.fencing_game.tools包中 。)它有一些德语注释,因为我最初一年前将其复制到我的另一个项目中。)

在Stackoverflow上搜索了一下我发现BalusC给出了一个相关问题的答案 ,它提供了另一个压缩输出流,并优化了刷新。 我没有测试这个,但它可能是这个的替代品。 (它使用gzip格式,而我们在这里使用纯deflate格式。确保写入和读取流都适合。)

另一种选择是使用JZlib作为最佳建议,使用它的ZOutputStream和ZInputStream。 它没有太多文档 ,但我正在努力。

下次:压缩RMI socket工厂


压缩RMI套接字工厂

现在我们可以把它们全部拉到一起。

 /** * 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)); } } 

而已。 我们现在将此工厂对象提供给UnicastRemoteObject.export(...)作为参数(包括客户端和服务器工厂),并且将压缩所有通信。 ( 我的github存储库中的版本有一个带有示例的main方法。)

当然,压缩优势不会像RMI那样巨大,至少当您不传输大字符串或类似的东西作为参数或返回值时。

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


结合SSL套接字工厂

如果我们使用默认类,那么Java部分很容易:

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

这些类(在javax.rmi.ssl中)使用默认的SSLSocketFactory和SSLServerSocketFactory(在javax.net.ssl中),它使用系统的默认密钥库和信任库。

因此,有必要使用keytool -genkeypair -v创建密钥库(例如通过keytool -genkeypair -v ),并使用系统属性javax.net.ssl.keyStore (密钥库的文件名)和javax.net.ssl.keyStorePassword将其提供给VM。 javax.net.ssl.keyStorePassword (密钥库的密码)。

在客户端,我们需要一个信任存储 – 即包含公钥的密钥存储区,或者一些签署了服务器公钥的证书。 出于测试目的,我们可以使用与服务器相同的密钥库,对于生产,您当然不希望客户端上的服务器私钥。 我们为此提供了javax.net.ssl.trustStore javax.net.ssl.trustStorePassword属性。

然后它归结为此(在服务器端):

  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); 

客户端是股票客户端,与前面的示例一样:

  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")); 

现在,与EchoServer的所有通信都经过压缩和加密。 当然,为了完全的安全性,我们还希望通过SSL保护与注册表的通信,以避免任何中间人攻击(这也允许通过向客户端提供假的RMIClientSocketFactory来拦截与EchoServer的通信,或者假冒服务器地址)。