From 33fdb32bff30f9d73a7843396d11320086129e73 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Fri, 24 Feb 2012 19:14:58 +0100 Subject: [PATCH] Implemented usage of SPDY v3 compression dictionary. --- .../jetty/spdy/CompressionDictionary.java | 85 +++++++++++++++++++ .../jetty/spdy/frames/HeadersFrame.java | 17 ---- .../spdy/generator/HeadersBlockGenerator.java | 14 ++- .../jetty/spdy/parser/HeadersBlockParser.java | 24 ++++-- .../org/eclipse/jetty/spdy/SynReplyTest.java | 46 ++++++++++ 5 files changed, 160 insertions(+), 26 deletions(-) create mode 100644 spdy-core/src/main/java/org/eclipse/jetty/spdy/CompressionDictionary.java diff --git a/spdy-core/src/main/java/org/eclipse/jetty/spdy/CompressionDictionary.java b/spdy-core/src/main/java/org/eclipse/jetty/spdy/CompressionDictionary.java new file mode 100644 index 00000000000..0aa024d30f7 --- /dev/null +++ b/spdy-core/src/main/java/org/eclipse/jetty/spdy/CompressionDictionary.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2012 the original author or authors. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.eclipse.jetty.spdy; + +import org.eclipse.jetty.spdy.api.SPDY; + +public class CompressionDictionary +{ + private static final byte[] DICTIONARY_V2 = ("" + + "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-" + + "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi" + + "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser" + + "-agent10010120020120220320420520630030130230330430530630740040140240340440" + + "5406407408409410411412413414415416417500501502503504505accept-rangesageeta" + + "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic" + + "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran" + + "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati" + + "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo" + + "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe" + + "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic" + + "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1" + + ".1statusversionurl" + + // Must be NUL terminated + "\u0000").getBytes(); + + private static final byte[] DICTIONARY_V3 = ("" + + "\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004head\u0000\u0000\u0000\u0004post" + + "\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006delete\u0000\u0000\u0000\u0005trace" + + "\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000\u000Eaccept-charset" + + "\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Faccept-language" + + "\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000\u0000\u0000\u0005allow" + + "\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-control" + + "\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000\u0000\u0010content-encoding" + + "\u0000\u0000\u0000\u0010content-language\u0000\u0000\u0000\u000Econtent-length" + + "\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000\u000Bcontent-md5" + + "\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type\u0000\u0000\u0000\u0004date" + + "\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expect\u0000\u0000\u0000\u0007expires" + + "\u0000\u0000\u0000\u0004from\u0000\u0000\u0000\u0004host\u0000\u0000\u0000\bif-match" + + "\u0000\u0000\u0000\u0011if-modified-since\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range" + + "\u0000\u0000\u0000\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified" + + "\u0000\u0000\u0000\blocation\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma" + + "\u0000\u0000\u0000\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization" + + "\u0000\u0000\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after" + + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trailer" + + "\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000\u0000\u0000\nuser-agent" + + "\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via\u0000\u0000\u0000\u0007warning" + + "\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000\u0000\u0006method\u0000\u0000\u0000\u0003get" + + "\u0000\u0000\u0000\u0006status\u0000\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version" + + "\u0000\u0000\u0000\bHTTP/1.1\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public" + + "\u0000\u0000\u0000\nset-cookie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin" + + "100101201202205206300302303304305306307402405406407408409410411412413414415416417502504505" + + "203 Non-Authoritative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized" + + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Unavailable" + + "Jan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Thu, Fri, Sat, Sun, GMT" + + "chunked,text/html,image/png,image/jpg,image/gif,application/xml,application/xhtml+xml,text/plain," + + "text/javascript,publicprivatemax-age=gzip,deflate,sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.") + .getBytes(); + + public static byte[] get(short version) + { + switch (version) + { + case SPDY.V2: + return DICTIONARY_V2; + case SPDY.V3: + return DICTIONARY_V3; + default: + throw new IllegalStateException(); + } + } +} diff --git a/spdy-core/src/main/java/org/eclipse/jetty/spdy/frames/HeadersFrame.java b/spdy-core/src/main/java/org/eclipse/jetty/spdy/frames/HeadersFrame.java index bf4526a30cb..e0545b68703 100644 --- a/spdy-core/src/main/java/org/eclipse/jetty/spdy/frames/HeadersFrame.java +++ b/spdy-core/src/main/java/org/eclipse/jetty/spdy/frames/HeadersFrame.java @@ -21,23 +21,6 @@ import org.eclipse.jetty.spdy.api.HeadersInfo; public class HeadersFrame extends ControlFrame { - public static final byte[] DICTIONARY = ("" + - "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-" + - "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi" + - "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser" + - "-agent10010120020120220320420520630030130230330430530630740040140240340440" + - "5406407408409410411412413414415416417500501502503504505accept-rangesageeta" + - "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic" + - "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran" + - "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati" + - "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo" + - "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe" + - "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic" + - "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1" + - ".1statusversionurl" + - // Must be NUL terminated - "\u0000").getBytes(); - private final int streamId; private final Headers headers; diff --git a/spdy-core/src/main/java/org/eclipse/jetty/spdy/generator/HeadersBlockGenerator.java b/spdy-core/src/main/java/org/eclipse/jetty/spdy/generator/HeadersBlockGenerator.java index c4aaeb41f7e..99bbe04c879 100644 --- a/spdy-core/src/main/java/org/eclipse/jetty/spdy/generator/HeadersBlockGenerator.java +++ b/spdy-core/src/main/java/org/eclipse/jetty/spdy/generator/HeadersBlockGenerator.java @@ -20,20 +20,20 @@ import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import org.eclipse.jetty.spdy.CompressionDictionary; import org.eclipse.jetty.spdy.CompressionFactory; import org.eclipse.jetty.spdy.StreamException; import org.eclipse.jetty.spdy.api.Headers; import org.eclipse.jetty.spdy.api.SPDY; -import org.eclipse.jetty.spdy.frames.HeadersFrame; public class HeadersBlockGenerator { private final CompressionFactory.Compressor compressor; + private boolean needsDictionary = true; public HeadersBlockGenerator(CompressionFactory.Compressor compressor) { this.compressor = compressor; - this.compressor.setDictionary(HeadersFrame.DICTIONARY); } public ByteBuffer generate(short version, Headers headers) throws StreamException @@ -70,16 +70,22 @@ public class HeadersBlockGenerator buffer.write(valueBytes, 0, valueBytes.length); } - return compress(buffer.toByteArray()); + return compress(version, buffer.toByteArray()); } - private ByteBuffer compress(byte[] bytes) + private ByteBuffer compress(short version, byte[] bytes) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(bytes.length); // The headers compression context is per-session, so we need to synchronize synchronized (compressor) { + if (needsDictionary) + { + compressor.setDictionary(CompressionDictionary.get(version)); + needsDictionary = false; + } + compressor.setInput(bytes); // Compressed bytes may be bigger than input bytes, so we need to loop and accumulate them diff --git a/spdy-core/src/main/java/org/eclipse/jetty/spdy/parser/HeadersBlockParser.java b/spdy-core/src/main/java/org/eclipse/jetty/spdy/parser/HeadersBlockParser.java index d838d906f38..cdb1092b960 100644 --- a/spdy-core/src/main/java/org/eclipse/jetty/spdy/parser/HeadersBlockParser.java +++ b/spdy-core/src/main/java/org/eclipse/jetty/spdy/parser/HeadersBlockParser.java @@ -20,23 +20,24 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.zip.ZipException; +import org.eclipse.jetty.spdy.CompressionDictionary; import org.eclipse.jetty.spdy.CompressionFactory; import org.eclipse.jetty.spdy.StreamException; import org.eclipse.jetty.spdy.api.SPDY; import org.eclipse.jetty.spdy.api.StreamStatus; -import org.eclipse.jetty.spdy.frames.HeadersFrame; public abstract class HeadersBlockParser { private final CompressionFactory.Decompressor decompressor; private byte[] data; + private boolean needsDictionary = true; protected HeadersBlockParser(CompressionFactory.Decompressor decompressor) { this.decompressor = decompressor; } - public boolean parse(int version, int length, ByteBuffer buffer) throws StreamException + public boolean parse(short version, int length, ByteBuffer buffer) throws StreamException { // Need to be sure that all the compressed data has arrived // Because SPDY uses SYNC_FLUSH mode, and the Java API @@ -50,7 +51,7 @@ public abstract class HeadersBlockParser byte[] compressedHeaders = data; data = null; - ByteBuffer decompressedHeaders = decompress(compressedHeaders); + ByteBuffer decompressedHeaders = decompress(version, compressedHeaders); Charset iso1 = Charset.forName("ISO-8859-1"); @@ -151,8 +152,12 @@ public abstract class HeadersBlockParser protected abstract void onHeader(String name, String[] values); - private ByteBuffer decompress(byte[] compressed) throws StreamException + private ByteBuffer decompress(short version, byte[] compressed) throws StreamException { + // Differently from compression, decompression always happens + // non-concurrently because we read and parse with a single + // thread, and therefore there is no need for synchronization. + try { byte[] decompressed = null; @@ -165,9 +170,18 @@ public abstract class HeadersBlockParser if (count == 0) { if (decompressed != null) + { return ByteBuffer.wrap(decompressed); + } + else if (needsDictionary) + { + decompressor.setDictionary(CompressionDictionary.get(version)); + needsDictionary = false; + } else - decompressor.setDictionary(HeadersFrame.DICTIONARY); + { + throw new IllegalStateException(); + } } else { diff --git a/spdy-jetty/src/test/java/org/eclipse/jetty/spdy/SynReplyTest.java b/spdy-jetty/src/test/java/org/eclipse/jetty/spdy/SynReplyTest.java index a1f199b38a6..9a6a908fc8e 100644 --- a/spdy-jetty/src/test/java/org/eclipse/jetty/spdy/SynReplyTest.java +++ b/spdy-jetty/src/test/java/org/eclipse/jetty/spdy/SynReplyTest.java @@ -360,4 +360,50 @@ public class SynReplyTest extends AbstractTest Assert.assertEquals(stream.getId(), rstInfo.getStreamId()); Assert.assertSame(StreamStatus.PROTOCOL_ERROR, rstInfo.getStreamStatus()); } + + @Test + public void testSynReplyDataSynReplyData() throws Exception + { + final String data = "foo"; + ServerSessionFrameListener serverSessionFrameListener = new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Assert.assertTrue(stream.isHalfClosed()); + + stream.reply(new ReplyInfo(false)); + stream.data(new StringDataInfo(data, true)); + + return null; + } + }; + + Session session = startClient(startServer(serverSessionFrameListener), null); + + final CountDownLatch replyLatch = new CountDownLatch(2); + final CountDownLatch dataLatch = new CountDownLatch(2); + StreamFrameListener clientStreamFrameListener = new StreamFrameListener.Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + Assert.assertFalse(replyInfo.isClose()); + replyLatch.countDown(); + } + + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + String chunk = dataInfo.asString("UTF-8"); + Assert.assertEquals(data, chunk); + dataLatch.countDown(); + } + }; + session.syn(new SynInfo(true), clientStreamFrameListener); + session.syn(new SynInfo(true), clientStreamFrameListener); + + Assert.assertTrue(replyLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(dataLatch.await(5, TimeUnit.SECONDS)); + } }