From 15defec50f109349a161d38d7bf332e6d9ba025f Mon Sep 17 00:00:00 2001 From: Gaston Dombiak Date: Mon, 16 Jan 2006 17:34:56 +0000 Subject: [PATCH] Added stream compression support. SMACK-112 git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@3306 b35dd754-fafc-0310-a699-88a17e54d16e --- .../org/jivesoftware/smack/PacketReader.java | 43 ++++- .../jivesoftware/smack/XMPPConnection.java | 167 +++++++++++++++++- .../smack/test/SmackTestCase.java | 57 ++---- 3 files changed, 221 insertions(+), 46 deletions(-) diff --git a/source/org/jivesoftware/smack/PacketReader.java b/source/org/jivesoftware/smack/PacketReader.java index 892cb81c9..7da252805 100644 --- a/source/org/jivesoftware/smack/PacketReader.java +++ b/source/org/jivesoftware/smack/PacketReader.java @@ -320,10 +320,17 @@ class PacketReader { resetParser(); } else if (parser.getName().equals("failure")) { - if ("urn:ietf:params:xml:ns:xmpp-tls".equals(parser.getNamespace(null))) { + String namespace = parser.getNamespace(null); + if ("urn:ietf:params:xml:ns:xmpp-tls".equals(namespace)) { // TLS negotiation has failed. The server will close the connection throw new Exception("TLS negotiation has failed"); } + else if ("http://jabber.org/protocol/compress".equals(namespace)) { + // Stream compression has been denied. This is a recoverable + // situation. It is still possible to authenticate and + // use the connection but using an uncompressed connection + connection.streamCompressionDenied(); + } else { // SASL authentication has failed. The server may close the connection // depending on the number of retries @@ -347,6 +354,14 @@ class PacketReader { // will be to bind the resource connection.getSASLAuthentication().authenticated(); } + else if (parser.getName().equals("compressed")) { + // Server confirmed that it's possible to use stream compression. Start + // stream compression + connection.startStreamCompression(); + // Reset the state of the parser since a new stream element is going + // to be sent by the server + resetParser(); + } } else if (eventType == XmlPullParser.END_TAG) { if (parser.getName().equals("stream")) { @@ -464,6 +479,10 @@ class PacketReader { // The server supports sessions connection.getSASLAuthentication().sessionsSupported(); } + else if (parser.getName().equals("compression")) { + // The server supports stream compression + connection.setAvailableCompressionMethods(parseCompressionMethods(parser)); + } } else if (eventType == XmlPullParser.END_TAG) { if (parser.getName().equals("features")) { @@ -504,6 +523,28 @@ class PacketReader { return mechanisms; } + private Collection parseCompressionMethods(XmlPullParser parser) + throws IOException, XmlPullParserException { + List methods = new ArrayList(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + if (elementName.equals("method")) { + methods.add(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("compression")) { + done = true; + } + } + } + return methods; + } + /** * Parses an IQ packet. * diff --git a/source/org/jivesoftware/smack/XMPPConnection.java b/source/org/jivesoftware/smack/XMPPConnection.java index b1c9c92a2..f98daeacd 100644 --- a/source/org/jivesoftware/smack/XMPPConnection.java +++ b/source/org/jivesoftware/smack/XMPPConnection.java @@ -36,6 +36,7 @@ import javax.net.ssl.SSLSocket; import java.io.*; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.net.Socket; import java.net.UnknownHostException; import java.util.*; @@ -129,6 +130,15 @@ public class XMPPConnection { */ Map chats = Collections.synchronizedMap(new HashMap()); + /** + * Collection of available stream compression methods offered by the server. + */ + private Collection compressionMethods; + /** + * Flag that indicates if stream compression is actually in use. + */ + private boolean usingCompression; + /** * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be * performed to try to determine the IP address and port corresponding to the @@ -909,8 +919,37 @@ public class XMPPConnection { private void initReaderAndWriter() throws XMPPException { try { - reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); - writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); + if (!usingCompression) { + reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); + writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); + } + else { + try { + Class zoClass = Class.forName("com.jcraft.jzlib.ZOutputStream"); + //ZOutputStream out = new ZOutputStream(socket.getOutputStream(), JZlib.Z_BEST_COMPRESSION); + Constructor constructor = + zoClass.getConstructor(new Class[]{OutputStream.class, Integer.TYPE}); + Object out = constructor.newInstance(new Object[] {socket.getOutputStream(), new Integer(9)}); + //out.setFlushMode(JZlib.Z_PARTIAL_FLUSH); + Method method = zoClass.getMethod("setFlushMode", new Class[] {Integer.TYPE}); + method.invoke(out, new Object[] {new Integer(1)}); + writer = new BufferedWriter(new OutputStreamWriter((OutputStream) out, "UTF-8")); + + Class ziClass = Class.forName("com.jcraft.jzlib.ZInputStream"); + //ZInputStream in = new ZInputStream(socket.getInputStream()); + constructor = ziClass.getConstructor(new Class[]{InputStream.class}); + Object in = constructor.newInstance(new Object[] {socket.getInputStream()}); + //in.setFlushMode(JZlib.Z_PARTIAL_FLUSH); + method = ziClass.getMethod("setFlushMode", new Class[] {Integer.TYPE}); + method.invoke(in, new Object[] {new Integer(1)}); + reader = new BufferedReader(new InputStreamReader((InputStream) in, "UTF-8")); + } + catch (Exception e) { + e.printStackTrace(); + reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); + writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); + } + } } catch (IOException ioe) { throw new XMPPException( @@ -1061,4 +1100,128 @@ public class XMPPConnection { // Send a new opening stream to the server packetWriter.openStream(); } + + /** + * Sets the available stream compression methods offered by the server. + * + * @param methods compression methods offered by the server. + */ + void setAvailableCompressionMethods(Collection methods) { + compressionMethods = methods; + } + + /** + * Returns true if the specified compression method was offered by the server. + * + * @param method the method to check. + * @return true if the specified compression method was offered by the server. + */ + private boolean hasAvailableCompressionMethod(String method) { + return compressionMethods != null && compressionMethods.contains(method); + } + + /** + * Returns true if the server offered stream compression to the client. + * + * @return true if the server offered stream compression to the client. + */ + public boolean getServerSupportsCompression() { + return compressionMethods != null && !compressionMethods.isEmpty(); + } + + /** + * Returns true if network traffic is being compressed. When using stream compression network + * traffic can be reduced up to 90%. Therefore, stream compression is ideal when using a slow + * speed network connection. However, the server will need to use more CPU time in order to + * un/compress network data so under high load the server performance might be affected.

+ * + * Note: To use stream compression the smackx.jar file has to be present in the classpath. + * + * @return true if network traffic is being compressed. + */ + public boolean isUsingCompression() { + return usingCompression; + } + + /** + * Starts using stream compression that will compress network traffic. Traffic can be + * reduced up to 90%. Therefore, stream compression is ideal when using a slow speed network + * connection. However, the server and the client will need to use more CPU time in order to + * un/compress network data so under high load the server performance might be affected.

+ * + * Stream compression has to have been previously offered by the server. Currently only the + * zlib method is supported by the client. Stream compression negotiation has to be done + * before authentication took place.

+ * + * Note: To use stream compression the smackx.jar file has to be present in the classpath. + * + * @return true if stream compression negotiation was successful. + */ + public boolean useCompression() { + // If stream compression was offered by the server and we want to use + // compression then send compression request to the server + if (authenticated) { + throw new IllegalStateException("Compression should be negotiated before authentication."); + } + try { + Class.forName("com.jcraft.jzlib.ZOutputStream"); + } + catch (ClassNotFoundException e) { + throw new IllegalStateException("Cannot use compression. Add smackx.jar to the classpath"); + } + if (hasAvailableCompressionMethod("zlib")) { + requestStreamCompression(); + // Wait until compression is being used or a timeout happened + synchronized (this) { + try { + this.wait(SmackConfiguration.getPacketReplyTimeout() * 5); + } + catch (InterruptedException e) {} + } + return usingCompression; + } + return false; + } + + /** + * Request the server that we want to start using stream compression. When using TLS + * then negotiation of stream compression can only happen after TLS was negotiated. If TLS + * compression is being used the stream compression should not be used. + */ + private void requestStreamCompression() { + try { + writer.write(""); + writer.write("zlib"); + writer.flush(); + } + catch (IOException e) { + packetReader.notifyConnectionError(e); + } + } + + /** + * Start using stream compression since the server has acknowledged stream compression. + */ + void startStreamCompression() throws Exception { + // Secure the plain connection + usingCompression = true; + // Initialize the reader and writer with the new secured version + initReaderAndWriter(); + + // Set the new writer to use + packetWriter.setWriter(writer); + // Send a new opening stream to the server + packetWriter.openStream(); + // Notify that compression is being used + synchronized (this) { + this.notify(); + } + } + + void streamCompressionDenied() { + // Notify that compression has been denied + synchronized (this) { + this.notify(); + } + } } \ No newline at end of file diff --git a/test/org/jivesoftware/smack/test/SmackTestCase.java b/test/org/jivesoftware/smack/test/SmackTestCase.java index 6da87ad52..e7796aa03 100644 --- a/test/org/jivesoftware/smack/test/SmackTestCase.java +++ b/test/org/jivesoftware/smack/test/SmackTestCase.java @@ -1,53 +1,21 @@ /** * $RCSfile$ * $Revision$ - * $Date$ + * $Date: $ * - * Copyright (C) 2002-2003 Jive Software. All rights reserved. - * ==================================================================== - * The Jive Software License (based on Apache Software License, Version 1.1) + * Copyright 2003-2005 Jive Software. * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. + * http://www.apache.org/licenses/LICENSE-2.0 * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * 3. The end-user documentation included with the redistribution, - * if any, must include the following acknowledgment: - * "This product includes software developed by - * Jive Software (http://www.jivesoftware.com)." - * Alternately, this acknowledgment may appear in the software itself, - * if and wherever such third-party acknowledgments normally appear. - * - * 4. The names "Smack" and "Jive Software" must not be used to - * endorse or promote products derived from this software without - * prior written permission. For written permission, please - * contact webmaster@jivesoftware.com. - * - * 5. Products derived from this software may not be called "Smack", - * nor may "Smack" appear in their name, without prior written - * permission of Jive Software. - * - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR - * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF - * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT - * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - * ==================================================================== + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.jivesoftware.smack.test; @@ -242,6 +210,9 @@ public abstract class SmackTestCase extends TestCase { throw e; } } + if (Boolean.getBoolean("test.compressionEnabled")) { + getConnection(i).useCompression(); + } // Login with the new test account getConnection(i).login("user" + i, "user" + i); }