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("