1
0
mirror of https://github.com/square/okhttp.git synced 2026-01-27 04:22:07 +03:00

Merge pull request #80 from square/jwilson/spdy3

Upgrade from spdy/2 to spdy/3.
This commit is contained in:
Jesse Wilson
2013-01-06 15:33:32 -08:00
14 changed files with 285 additions and 133 deletions

View File

@@ -65,11 +65,11 @@ import javax.net.ssl.SSLSocket;
*/
public final class Connection implements Closeable {
private static final byte[] NPN_PROTOCOLS = new byte[] {
6, 's', 'p', 'd', 'y', '/', '2',
6, 's', 'p', 'd', 'y', '/', '3',
8, 'h', 't', 't', 'p', '/', '1', '.', '1',
};
private static final byte[] SPDY2 = new byte[] {
's', 'p', 'd', 'y', '/', '2',
private static final byte[] SPDY3 = new byte[] {
's', 'p', 'd', 'y', '/', '3',
};
private static final byte[] HTTP_11 = new byte[] {
'h', 't', 't', 'p', '/', '1', '.', '1',
@@ -159,7 +159,7 @@ public final class Connection implements Closeable {
byte[] selectedProtocol;
if (modernTls
&& (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
if (Arrays.equals(selectedProtocol, SPDY2)) {
if (Arrays.equals(selectedProtocol, SPDY3)) {
sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
spdyConnection = new SpdyConnection.Builder(true, in, out).build();
} else if (!Arrays.equals(selectedProtocol, HTTP_11)) {

View File

@@ -80,26 +80,30 @@ public final class ConnectionPool {
List<Connection> connections = connectionPool.get(address);
while (connections != null) {
Connection connection = connections.get(connections.size() - 1);
if (!connection.isSpdy()) {
boolean usable = connection.isEligibleForRecycling();
if (usable && !connection.isSpdy()) {
try {
Platform.get().tagSocket(connection.getSocket());
} catch (SocketException e) {
// When unable to tag, skip recycling and close
Platform.get().logW("Unable to tagSocket(): " + e);
usable = false;
}
}
if (!connection.isSpdy() || !usable) {
connections.remove(connections.size() - 1);
if (connections.isEmpty()) {
connectionPool.remove(address);
connections = null;
}
}
if (connections.isEmpty()) {
connectionPool.remove(address);
connections = null;
}
if (!connection.isEligibleForRecycling()) {
if (usable) {
return connection;
} else {
Util.closeQuietly(connection);
continue;
}
try {
Platform.get().tagSocket(connection.getSocket());
} catch (SocketException e) {
// When unable to tag, skip recycling and close
Platform.get().logW("Unable to tagSocket(): " + e);
Util.closeQuietly(connection);
continue;
}
return connection;
}
}
return null;

View File

@@ -125,30 +125,33 @@ public final class RawHeaders {
String version = null;
for (int i = 0; i < namesAndValues.size(); i += 2) {
String name = namesAndValues.get(i);
if ("status".equals(name)) {
if (":status".equals(name)) {
status = namesAndValues.get(i + 1);
} else if ("version".equals(name)) {
} else if (":version".equals(name)) {
version = namesAndValues.get(i + 1);
}
}
if (status == null || version == null) {
throw new ProtocolException("Expected 'status' and 'version' headers not present");
throw new ProtocolException("Expected ':status' and ':version' headers not present");
}
setStatusLine(version + " " + status);
}
/**
* @param method like "GET", "POST", "HEAD", etc.
* @param scheme like "https"
* @param url like "/foo/bar.html"
* @param path like "/foo/bar.html"
* @param version like "HTTP/1.1"
* @param host like "www.android.com:1234"
* @param scheme like "https"
*/
public void addSpdyRequestHeaders(String method, String scheme, String url, String version) {
public void addSpdyRequestHeaders(
String method, String path, String version, String host, String scheme) {
// TODO: populate the statusLine for the client's benefit?
add("method", method);
add("scheme", scheme);
add("url", url);
add("version", version);
add(":method", method);
add(":scheme", scheme);
add(":path", path);
add(":version", version);
add(":host", host);
}
public String getStatusLine() {
@@ -393,8 +396,9 @@ public final class RawHeaders {
throw new IllegalArgumentException("Unexpected header: " + name + ": " + value);
}
// Drop headers that are ignored when layering HTTP over SPDY.
if (name.equals("connection") || name.equals("accept-encoding")) {
// Drop headers that are forbidden when layering HTTP over SPDY.
if (name.equals("connection") || name.equals("host") || name.equals("keep-alive")
|| name.equals("proxy-connection") || name.equals("transfer-encoding")) {
continue;
}

View File

@@ -22,6 +22,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.CacheRequest;
import java.net.URL;
import java.util.List;
public final class SpdyTransport implements Transport {
@@ -49,8 +50,9 @@ public final class SpdyTransport implements Transport {
}
RawHeaders requestHeaders = httpEngine.requestHeaders.getHeaders();
String version = httpEngine.connection.getHttpMinorVersion() == 1 ? "HTTP/1.1" : "HTTP/1.0";
requestHeaders.addSpdyRequestHeaders(httpEngine.method, httpEngine.uri.getScheme(),
HttpEngine.requestPath(httpEngine.policy.getURL()), version);
URL url = httpEngine.policy.getURL();
requestHeaders.addSpdyRequestHeaders(httpEngine.method, HttpEngine.requestPath(url),
version, HttpEngine.getOriginAddress(url), httpEngine.uri.getScheme());
boolean hasRequestBody = httpEngine.hasRequestBody();
boolean hasResponseBody = true;
stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(),
@@ -67,7 +69,6 @@ public final class SpdyTransport implements Transport {
}
@Override public ResponseHeaders readResponseHeaders() throws IOException {
// TODO: fix the SPDY implementation so this throws a (buffered) IOException
List<String> nameValueBlock = stream.getResponseHeaders();
RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
rawHeaders.computeResponseStatusLineFromSpdyHeaders();

View File

@@ -38,8 +38,10 @@ final class Settings {
static final int DOWNLOAD_RETRANS_RATE = 0x6;
/** Window size in bytes. */
static final int INITIAL_WINDOW_SIZE = 0x7;
/** Window size in bytes. */
static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 0x8;
/** Total number of settings. */
static final int COUNT = 0x8;
static final int COUNT = 0x9;
/** Bitfield of which flags that values. */
private int set;
@@ -141,6 +143,11 @@ final class Settings {
return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue;
}
int getClientCertificateVectorSize(int defaultValue) {
int bit = 1 << CLIENT_CERTIFICATE_VECTOR_SIZE;
return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue;
}
/**
* Returns true if this user agent should use this setting in future SPDY
* connections to the same host.

View File

@@ -70,7 +70,13 @@ public final class SpdyConnection implements Closeable {
static final int TYPE_PING = 0x6;
static final int TYPE_GOAWAY = 0x7;
static final int TYPE_HEADERS = 0x8;
static final int VERSION = 2;
static final int TYPE_WINDOW_UPDATE = 0x9;
static final int TYPE_CREDENTIAL = 0x10;
static final int VERSION = 3;
static final int GOAWAY_OK = 0;
static final int GOAWAY_PROTOCOL_ERROR = 1;
static final int GOAWAY_INTERNAL_ERROR = 2;
/**
* True if this peer initiated the connection.
@@ -147,8 +153,9 @@ public final class SpdyConnection implements Closeable {
public SpdyStream newStream(List<String> requestHeaders, boolean out, boolean in)
throws IOException {
int flags = (out ? 0 : FLAG_FIN) | (in ? 0 : FLAG_UNIDIRECTIONAL);
int associatedStreamId = 0; // TODO: permit the caller to specify an associated stream.
int priority = 0; // TODO: permit the caller to specify a priority.
int associatedStreamId = 0; // TODO: permit the caller to specify an associated stream?
int priority = 0; // TODO: permit the caller to specify a priority?
int slot = 0; // TODO: permit the caller to specify a slot?
SpdyStream stream;
int streamId;
@@ -159,13 +166,14 @@ public final class SpdyConnection implements Closeable {
}
streamId = nextStreamId;
nextStreamId += 2;
stream = new SpdyStream(streamId, this, requestHeaders, flags);
stream = new SpdyStream(streamId, this, flags, priority, slot, requestHeaders);
if (stream.isOpen()) {
streams.put(streamId, stream);
}
}
spdyWriter.synStream(flags, streamId, associatedStreamId, priority, requestHeaders);
spdyWriter.synStream(flags, streamId, associatedStreamId, priority, slot,
requestHeaders);
}
return stream;
@@ -194,7 +202,7 @@ public final class SpdyConnection implements Closeable {
}
void writeSynReset(int streamId, int statusCode) throws IOException {
spdyWriter.synReset(streamId, statusCode);
spdyWriter.rstStream(streamId, statusCode);
}
/**
@@ -253,13 +261,27 @@ public final class SpdyConnection implements Closeable {
}
}
private void shutdownLater(final int statusCode) {
writeExecutor.execute(new Runnable() {
@Override public void run() {
try {
shutdown(statusCode);
} catch (IOException ignored) {
}
}
});
}
/**
* Degrades this connection such that new streams can neither be created
* locally, nor accepted from the remote peer. Existing streams are not
* impacted. This is intended to permit an endpoint to gracefully stop
* accepting new requests without harming previously established streams.
*
* @param statusCode one of {@link #GOAWAY_OK}, {@link
* #GOAWAY_INTERNAL_ERROR} or {@link #GOAWAY_PROTOCOL_ERROR}.
*/
public void shutdown() throws IOException {
public void shutdown(int statusCode) throws IOException {
synchronized (spdyWriter) {
int lastGoodStreamId;
synchronized (this) {
@@ -269,7 +291,7 @@ public final class SpdyConnection implements Closeable {
shutdown = true;
lastGoodStreamId = this.lastGoodStreamId;
}
spdyWriter.goAway(0, lastGoodStreamId);
spdyWriter.goAway(0, lastGoodStreamId, statusCode);
}
}
@@ -279,7 +301,7 @@ public final class SpdyConnection implements Closeable {
* internal executor services.
*/
@Override public void close() throws IOException {
shutdown();
shutdown(GOAWAY_OK);
SpdyStream[] streamsToClose = null;
Ping[] pingsToCancel = null;
@@ -354,6 +376,8 @@ public final class SpdyConnection implements Closeable {
try {
while (spdyReader.nextFrame(this)) {
}
} catch (ProtocolException e) {
shutdownLater(GOAWAY_PROTOCOL_ERROR);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
@@ -381,9 +405,9 @@ public final class SpdyConnection implements Closeable {
}
@Override public void synStream(int flags, int streamId, int associatedStreamId,
int priority, List<String> nameValueBlock) {
int priority, int slot, List<String> nameValueBlock) {
final SpdyStream synStream = new SpdyStream(streamId, SpdyConnection.this,
nameValueBlock, flags);
flags, priority, slot, nameValueBlock);
final SpdyStream previous;
synchronized (SpdyConnection.this) {
if (shutdown) {
@@ -421,7 +445,7 @@ public final class SpdyConnection implements Closeable {
replyStream.receiveFin();
}
} catch (ProtocolException e) {
replyStream.closeLater(SpdyStream.RST_PROTOCOL_ERROR);
replyStream.closeLater(SpdyStream.RST_STREAM_IN_USE);
}
}
@@ -470,7 +494,7 @@ public final class SpdyConnection implements Closeable {
}
}
@Override public void goAway(int flags, int lastGoodStreamId) {
@Override public void goAway(int flags, int lastGoodStreamId, int statusCode) {
synchronized (SpdyConnection.this) {
shutdown = true;
@@ -486,5 +510,9 @@ public final class SpdyConnection implements Closeable {
}
}
}
@Override public void windowUpdate(int flags, int streamId, int deltaWindowSize) {
// TODO
}
}
}

View File

@@ -21,7 +21,7 @@ import java.io.Closeable;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.ProtocolException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
@@ -33,28 +33,39 @@ import java.util.zip.InflaterInputStream;
* Read version 2 SPDY frames.
*/
final class SpdyReader implements Closeable {
private static final String DICTIONARY_STRING = ""
+ "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\0";
public static final byte[] DICTIONARY;
static {
try {
DICTIONARY = DICTIONARY_STRING.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
static final byte[] DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
+ "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
+ "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
+ "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
+ "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
+ "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
+ "ntrol\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\u0006expe"
+ "ct\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\blocati"
+ "on\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\u0007trai"
+ "ler\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-coo"
+ "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
+ "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
+ "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
+ "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
+ "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th"
+ "u, Fri, Sat, Sun, GMTchunked,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(Util.UTF_8);
private final DataInputStream in;
private final DataInputStream nameValueBlockIn;
@@ -86,6 +97,10 @@ final class SpdyReader implements Closeable {
int version = (w1 & 0x7fff0000) >>> 16;
int type = (w1 & 0xffff);
if (version != 3) {
throw new ProtocolException("version != 3: " + version);
}
switch (type) {
case SpdyConnection.TYPE_SYN_STREAM:
readSynStream(handler, flags, length);
@@ -120,6 +135,14 @@ final class SpdyReader implements Closeable {
readHeaders(handler, flags, length);
return true;
case SpdyConnection.TYPE_WINDOW_UPDATE:
readWindowUpdate(handler, flags, length);
return true;
case SpdyConnection.TYPE_CREDENTIAL:
Util.skipByReading(in, length);
throw new UnsupportedOperationException("TODO"); // TODO: implement
default:
throw new IOException("Unexpected frame");
}
@@ -136,17 +159,16 @@ final class SpdyReader implements Closeable {
int s3 = in.readShort();
int streamId = w1 & 0x7fffffff;
int associatedStreamId = w2 & 0x7fffffff;
int priority = s3 & 0xc000 >>> 14;
// int unused = s3 & 0x3fff;
int priority = (s3 & 0xe000) >>> 13;
int slot = s3 & 0xff;
List<String> nameValueBlock = readNameValueBlock(length - 10);
handler.synStream(flags, streamId, associatedStreamId, priority, nameValueBlock);
handler.synStream(flags, streamId, associatedStreamId, priority, slot, nameValueBlock);
}
private void readSynReply(Handler handler, int flags, int length) throws IOException {
int w1 = in.readInt();
in.readShort(); // unused
int streamId = w1 & 0x7fffffff;
List<String> nameValueBlock = readNameValueBlock(length - 6);
List<String> nameValueBlock = readNameValueBlock(length - 4);
handler.synReply(flags, streamId, nameValueBlock);
}
@@ -159,12 +181,19 @@ final class SpdyReader implements Closeable {
private void readHeaders(Handler handler, int flags, int length) throws IOException {
int w1 = in.readInt();
in.readShort(); // unused
int streamId = w1 & 0x7fffffff;
List<String> nameValueBlock = readNameValueBlock(length - 6);
List<String> nameValueBlock = readNameValueBlock(length - 4);
handler.headers(flags, streamId, nameValueBlock);
}
private void readWindowUpdate(Handler handler, int flags, int length) throws IOException {
int w1 = in.readInt();
int w2 = in.readInt();
int streamId = w1 & 0x7fffffff;
int deltaWindowSize = w2 & 0x7fffffff;
handler.windowUpdate(flags, streamId, deltaWindowSize);
}
private DataInputStream newNameValueBlockStream() {
// Limit the inflater input stream to only those bytes in the Name/Value block.
final InputStream throttleStream = new InputStream() {
@@ -203,7 +232,7 @@ final class SpdyReader implements Closeable {
private List<String> readNameValueBlock(int length) throws IOException {
this.compressedLimit += length;
try {
int numberOfPairs = nameValueBlockIn.readShort();
int numberOfPairs = nameValueBlockIn.readInt();
List<String> entries = new ArrayList<String>(numberOfPairs * 2);
for (int i = 0; i < numberOfPairs; i++) {
String name = readString();
@@ -226,7 +255,7 @@ final class SpdyReader implements Closeable {
}
private String readString() throws DataFormatException, IOException {
int length = nameValueBlockIn.readShort();
int length = nameValueBlockIn.readInt();
byte[] bytes = new byte[length];
Util.readFully(nameValueBlockIn, bytes);
return new String(bytes, 0, length, "UTF-8");
@@ -239,9 +268,10 @@ final class SpdyReader implements Closeable {
}
private void readGoAway(Handler handler, int flags, int length) throws IOException {
if (length != 4) throw ioException("TYPE_GOAWAY length: %d != 4", length);
if (length != 8) throw ioException("TYPE_GOAWAY length: %d != 8", length);
int lastGoodStreamId = in.readInt() & 0x7fffffff;
handler.goAway(flags, lastGoodStreamId);
int statusCode = in.readInt();
handler.goAway(flags, lastGoodStreamId, statusCode);
}
private void readSettings(Handler handler, int flags, int length) throws IOException {
@@ -274,13 +304,14 @@ final class SpdyReader implements Closeable {
public interface Handler {
void data(int flags, int streamId, InputStream in, int length) throws IOException;
void synStream(int flags, int streamId, int associatedStreamId, int priority,
List<String> nameValueBlock);
int slot, List<String> nameValueBlock);
void synReply(int flags, int streamId, List<String> nameValueBlock) throws IOException;
void headers(int flags, int streamId, List<String> nameValueBlock) throws IOException;
void rstStream(int flags, int streamId, int statusCode);
void settings(int flags, Settings settings);
void noop();
void ping(int flags, int streamId);
void goAway(int flags, int lastGoodStreamId);
void goAway(int flags, int lastGoodStreamId, int statusCode);
void windowUpdate(int flags, int streamId, int deltaWindowSize);
}
}

View File

@@ -50,6 +50,10 @@ public final class SpdyStream {
"CANCEL",
"INTERNAL_ERROR",
"FLOW_CONTROL_ERROR",
"STREAM_IN_USE",
"STREAM_ALREADY_CLOSED",
"INVALID_CREDENTIALS",
"FRAME_TOO_LARGE",
};
public static final int RST_PROTOCOL_ERROR = 1;
@@ -59,9 +63,15 @@ public final class SpdyStream {
public static final int RST_CANCEL = 5;
public static final int RST_INTERNAL_ERROR = 6;
public static final int RST_FLOW_CONTROL_ERROR = 7;
public static final int RST_STREAM_IN_USE = 8;
public static final int RST_STREAM_ALREADY_CLOSED = 9;
public static final int RST_INVALID_CREDENTIALS = 10;
public static final int RST_FRAME_TOO_LARGE = 11;
private final int id;
private final SpdyConnection connection;
private final int priority;
private final int slot;
private long readTimeoutMillis = 0;
/** Headers sent by the stream initiator. Immutable and non null. */
@@ -80,9 +90,12 @@ public final class SpdyStream {
*/
private int rstStatusCode = -1;
SpdyStream(int id, SpdyConnection connection, List<String> requestHeaders, int flags) {
SpdyStream(int id, SpdyConnection connection, int flags, int priority, int slot,
List<String> requestHeaders) {
this.id = id;
this.connection = connection;
this.priority = priority;
this.slot = slot;
this.requestHeaders = requestHeaders;
if (isLocallyInitiated()) {
@@ -316,6 +329,14 @@ public final class SpdyStream {
: Integer.toString(rstStatusCode);
}
int getPriority() {
return priority;
}
int getSlot() {
return slot;
}
/**
* An input stream that reads the incoming data frames of a stream. Although
* this class uses synchronization to safely receive incoming data frames,

View File

@@ -45,7 +45,7 @@ final class SpdyWriter implements Closeable {
}
public synchronized void synStream(int flags, int streamId, int associatedStreamId,
int priority, List<String> nameValueBlock) throws IOException {
int priority, int slot, List<String> nameValueBlock) throws IOException {
writeNameValueBlockToBuffer(nameValueBlock);
int length = 10 + nameValueBlockBuffer.size();
int type = SpdyConnection.TYPE_SYN_STREAM;
@@ -55,7 +55,7 @@ final class SpdyWriter implements Closeable {
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
out.writeInt(streamId & 0x7fffffff);
out.writeInt(associatedStreamId & 0x7fffffff);
out.writeShort((priority & 0x3) << 30 | (unused & 0x3FFF) << 16);
out.writeShort((priority & 0x7) << 13 | (unused & 0x1f) << 8 | (slot & 0xff));
nameValueBlockBuffer.writeTo(out);
out.flush();
}
@@ -64,13 +64,11 @@ final class SpdyWriter implements Closeable {
int flags, int streamId, List<String> nameValueBlock) throws IOException {
writeNameValueBlockToBuffer(nameValueBlock);
int type = SpdyConnection.TYPE_SYN_REPLY;
int length = nameValueBlockBuffer.size() + 6;
int unused = 0;
int length = nameValueBlockBuffer.size() + 4;
out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
out.writeInt(streamId & 0x7fffffff);
out.writeShort(unused);
nameValueBlockBuffer.writeTo(out);
out.flush();
}
@@ -79,18 +77,16 @@ final class SpdyWriter implements Closeable {
int flags, int streamId, List<String> nameValueBlock) throws IOException {
writeNameValueBlockToBuffer(nameValueBlock);
int type = SpdyConnection.TYPE_HEADERS;
int length = nameValueBlockBuffer.size() + 6;
int unused = 0;
int length = nameValueBlockBuffer.size() + 4;
out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
out.writeInt(streamId & 0x7fffffff);
out.writeShort(unused);
nameValueBlockBuffer.writeTo(out);
out.flush();
}
public synchronized void synReset(int streamId, int statusCode) throws IOException {
public synchronized void rstStream(int streamId, int statusCode) throws IOException {
int flags = 0;
int type = SpdyConnection.TYPE_RST_STREAM;
int length = 8;
@@ -112,9 +108,9 @@ final class SpdyWriter implements Closeable {
private void writeNameValueBlockToBuffer(List<String> nameValueBlock) throws IOException {
nameValueBlockBuffer.reset();
int numberOfPairs = nameValueBlock.size() / 2;
nameValueBlockOut.writeShort(numberOfPairs);
nameValueBlockOut.writeInt(numberOfPairs);
for (String s : nameValueBlock) {
nameValueBlockOut.writeShort(s.length());
nameValueBlockOut.writeInt(s.length());
nameValueBlockOut.write(s.getBytes("UTF-8"));
}
nameValueBlockOut.flush();
@@ -158,15 +154,22 @@ final class SpdyWriter implements Closeable {
out.flush();
}
public synchronized void goAway(int flags, int lastGoodStreamId) throws IOException {
public synchronized void goAway(int flags, int lastGoodStreamId, int statusCode)
throws IOException {
int type = SpdyConnection.TYPE_GOAWAY;
int length = 4;
int length = 8;
out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
out.writeInt(lastGoodStreamId);
out.writeInt(statusCode);
out.flush();
}
public synchronized void windowUpdate(int flags, int streamId, int deltaWindowSize)
throws IOException {
throw new UnsupportedOperationException("TODO"); // TODO
}
@Override public void close() throws IOException {
Util.closeAll(out, nameValueBlockOut);
}

View File

@@ -15,7 +15,6 @@
*/
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.internal.http.RawHeaders;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
@@ -28,19 +27,20 @@ public final class RawHeadersTest {
"no-cache, no-store",
"set-cookie",
"Cookie1\u0000Cookie2",
"status", "200 OK"
":status", "200 OK"
);
// TODO: fromNameValueBlock should synthesize a request status line
RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
assertEquals("no-cache, no-store", rawHeaders.get("cache-control"));
assertEquals("Cookie2", rawHeaders.get("set-cookie"));
assertEquals("200 OK", rawHeaders.get("status"));
assertEquals("200 OK", rawHeaders.get(":status"));
assertEquals("cache-control", rawHeaders.getFieldName(0));
assertEquals("no-cache, no-store", rawHeaders.getValue(0));
assertEquals("set-cookie", rawHeaders.getFieldName(1));
assertEquals("Cookie1", rawHeaders.getValue(1));
assertEquals("set-cookie", rawHeaders.getFieldName(2));
assertEquals("Cookie2", rawHeaders.getValue(2));
assertEquals("status", rawHeaders.getFieldName(3));
assertEquals(":status", rawHeaders.getFieldName(3));
assertEquals("200 OK", rawHeaders.getValue(3));
}
@@ -49,15 +49,23 @@ public final class RawHeadersTest {
rawHeaders.add("cache-control", "no-cache, no-store");
rawHeaders.add("set-cookie", "Cookie1");
rawHeaders.add("set-cookie", "Cookie2");
rawHeaders.add("status", "200 OK");
rawHeaders.add(":status", "200 OK");
// TODO: fromNameValueBlock should take the status line headers
List<String> nameValueBlock = rawHeaders.toNameValueBlock();
List<String> expected = Arrays.asList(
"cache-control",
"no-cache, no-store",
"set-cookie",
"Cookie1\u0000Cookie2",
"status", "200 OK"
":status", "200 OK"
);
assertEquals(expected, nameValueBlock);
}
@Test public void toNameValueBlockDropsForbiddenHeaders() {
RawHeaders rawHeaders = new RawHeaders();
rawHeaders.add("Connection", "close");
rawHeaders.add("Transfer-Encoding", "chunked");
assertEquals(Arrays.<String>asList(), rawHeaders.toNameValueBlock());
}
}

View File

@@ -18,6 +18,7 @@ package com.squareup.okhttp.internal.spdy;
import com.squareup.okhttp.internal.Util;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -34,7 +35,7 @@ import java.util.concurrent.LinkedBlockingQueue;
/**
* Replays prerecorded outgoing frames and records incoming frames.
*/
public final class MockSpdyPeer {
public final class MockSpdyPeer implements Closeable {
private int frameCount = 0;
private final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
private final SpdyWriter spdyWriter = new SpdyWriter(bytesOut);
@@ -43,6 +44,8 @@ public final class MockSpdyPeer {
private int port;
private final Executor executor = Executors.newCachedThreadPool(
Threads.newThreadFactory("MockSpdyPeer", true));
private ServerSocket serverSocket;
private Socket socket;
public void acceptFrame() {
frameCount++;
@@ -63,13 +66,14 @@ public final class MockSpdyPeer {
}
public void play() throws IOException {
final ServerSocket serverSocket = new ServerSocket(0);
if (serverSocket != null) throw new IllegalStateException();
serverSocket = new ServerSocket(0);
serverSocket.setReuseAddress(true);
this.port = serverSocket.getLocalPort();
executor.execute(new Runnable() {
@Override public void run() {
try {
readAndWriteFrames(serverSocket);
readAndWriteFrames();
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -77,8 +81,9 @@ public final class MockSpdyPeer {
});
}
private void readAndWriteFrames(ServerSocket serverSocket) throws IOException {
Socket socket = serverSocket.accept();
private void readAndWriteFrames() throws IOException {
if (socket != null) throw new IllegalStateException();
socket = serverSocket.accept();
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
SpdyReader reader = new SpdyReader(in);
@@ -118,6 +123,19 @@ public final class MockSpdyPeer {
return new Socket("localhost", port);
}
@Override public void close() throws IOException {
Socket socket = this.socket;
if (socket != null) {
socket.close();
this.socket = null;
}
ServerSocket serverSocket = this.serverSocket;
if (serverSocket != null) {
serverSocket.close();
this.serverSocket = null;
}
}
private static class OutFrame {
private final int sequence;
private final int start;
@@ -136,7 +154,9 @@ public final class MockSpdyPeer {
public int streamId;
public int associatedStreamId;
public int priority;
public int slot;
public int statusCode;
public int deltaWindowSize;
public List<String> nameValueBlock;
public byte[] data;
public Settings settings;
@@ -154,13 +174,14 @@ public final class MockSpdyPeer {
}
@Override public void synStream(int flags, int streamId, int associatedStreamId,
int priority, List<String> nameValueBlock) {
int priority, int slot, List<String> nameValueBlock) {
if (this.type != -1) throw new IllegalStateException();
this.type = SpdyConnection.TYPE_SYN_STREAM;
this.flags = flags;
this.streamId = streamId;
this.associatedStreamId = associatedStreamId;
this.priority = priority;
this.slot = slot;
this.nameValueBlock = nameValueBlock;
}
@@ -210,11 +231,20 @@ public final class MockSpdyPeer {
this.type = SpdyConnection.TYPE_NOOP;
}
@Override public void goAway(int flags, int lastGoodStreamId) {
@Override public void goAway(int flags, int lastGoodStreamId, int statusCode) {
if (this.type != -1) throw new IllegalStateException();
this.type = SpdyConnection.TYPE_GOAWAY;
this.flags = flags;
this.streamId = lastGoodStreamId;
this.statusCode = statusCode;
}
@Override public void windowUpdate(int flags, int streamId, int deltaWindowSize) {
if (this.type != -1) throw new IllegalStateException();
this.type = SpdyConnection.TYPE_WINDOW_UPDATE;
this.flags = flags;
this.streamId = streamId;
this.deltaWindowSize = deltaWindowSize;
}
}
}

View File

@@ -15,7 +15,6 @@
*/
package com.squareup.okhttp.internal.spdy;
import com.squareup.okhttp.internal.spdy.Settings;
import static com.squareup.okhttp.internal.spdy.Settings.DOWNLOAD_BANDWIDTH;
import static com.squareup.okhttp.internal.spdy.Settings.DOWNLOAD_RETRANS_RATE;
import static com.squareup.okhttp.internal.spdy.Settings.MAX_CONCURRENT_STREAMS;
@@ -63,6 +62,10 @@ public final class SettingsTest {
assertEquals(-3, settings.getInitialWindowSize(-3));
settings.set(Settings.INITIAL_WINDOW_SIZE, 0, 108);
assertEquals(108, settings.getInitialWindowSize(-3));
assertEquals(-3, settings.getClientCertificateVectorSize(-3));
settings.set(Settings.CLIENT_CERTIFICATE_VECTOR_SIZE, 0, 117);
assertEquals(117, settings.getClientCertificateVectorSize(-3));
}
@Test public void isPersisted() {

View File

@@ -20,6 +20,8 @@ import static com.squareup.okhttp.internal.Util.UTF_8;
import static com.squareup.okhttp.internal.spdy.Settings.PERSIST_VALUE;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.FLAG_FIN;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.FLAG_UNIDIRECTIONAL;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.GOAWAY_INTERNAL_ERROR;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.GOAWAY_PROTOCOL_ERROR;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_DATA;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_GOAWAY;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_NOOP;
@@ -31,6 +33,7 @@ import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_FLOW_CONTROL_ERRO
import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_INVALID_STREAM;
import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_PROTOCOL_ERROR;
import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_REFUSED_STREAM;
import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_STREAM_IN_USE;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -38,6 +41,7 @@ import java.io.OutputStream;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -52,6 +56,10 @@ public final class SpdyConnectionTest {
};
private final MockSpdyPeer peer = new MockSpdyPeer();
@After public void tearDown() throws Exception {
peer.close();
}
@Test public void clientCreatesStreamAndServerReplies() throws Exception {
// write the mocking script
peer.acceptFrame();
@@ -113,7 +121,7 @@ public final class SpdyConnectionTest {
@Test public void serverCreatesStreamAndClientReplies() throws Exception {
// write the mocking script
peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("a", "android"));
peer.sendFrame().synStream(0, 2, 0, 5, 129, Arrays.asList("a", "android"));
peer.acceptFrame();
peer.play();
@@ -124,6 +132,8 @@ public final class SpdyConnectionTest {
receiveCount.incrementAndGet();
assertEquals(Arrays.asList("a", "android"), stream.getRequestHeaders());
assertEquals(-1, stream.getRstStatusCode());
assertEquals(5, stream.getPriority());
assertEquals(129, stream.getSlot());
stream.reply(Arrays.asList("b", "banana"), true);
}
@@ -143,7 +153,7 @@ public final class SpdyConnectionTest {
@Test public void replyWithNoData() throws Exception {
// write the mocking script
peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("a", "android"));
peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("a", "android"));
peer.acceptFrame();
peer.play();
@@ -390,7 +400,7 @@ public final class SpdyConnectionTest {
@Test public void serverClosesClientOutputStream() throws Exception {
// write the mocking script
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReset(1, SpdyStream.RST_CANCEL);
peer.sendFrame().rstStream(1, SpdyStream.RST_CANCEL);
peer.acceptFrame(); // PING
peer.sendFrame().ping(0, 1);
peer.play();
@@ -547,7 +557,7 @@ public final class SpdyConnectionTest {
stream.getInputStream().read();
fail();
} catch (IOException e) {
assertEquals("stream was reset: PROTOCOL_ERROR", e.getMessage());
assertEquals("stream was reset: STREAM_IN_USE", e.getMessage());
}
// verify the peer received what was expected
@@ -559,14 +569,14 @@ public final class SpdyConnectionTest {
assertEquals(TYPE_RST_STREAM, rstStream.type);
assertEquals(1, rstStream.streamId);
assertEquals(0, rstStream.flags);
assertEquals(RST_PROTOCOL_ERROR, rstStream.statusCode);
assertEquals(RST_STREAM_IN_USE, rstStream.statusCode);
}
@Test public void remoteDoubleSynStream() throws Exception {
// write the mocking script
peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("a", "android"));
peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("a", "android"));
peer.acceptFrame();
peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("b", "banana"));
peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("b", "banana"));
peer.acceptFrame();
peer.play();
@@ -651,7 +661,7 @@ public final class SpdyConnectionTest {
@Test public void remoteSendsRefusedStreamBeforeReplyHeaders() throws Exception {
// write the mocking script
peer.acceptFrame();
peer.sendFrame().synReset(1, RST_REFUSED_STREAM);
peer.sendFrame().rstStream(1, RST_REFUSED_STREAM);
peer.sendFrame().ping(0, 2);
peer.acceptFrame();
peer.play();
@@ -680,7 +690,7 @@ public final class SpdyConnectionTest {
// write the mocking script
peer.acceptFrame(); // SYN STREAM 1
peer.acceptFrame(); // SYN STREAM 3
peer.sendFrame().goAway(0, 1);
peer.sendFrame().goAway(0, 1, GOAWAY_PROTOCOL_ERROR);
peer.acceptFrame(); // PING
peer.sendFrame().ping(0, 1);
peer.acceptFrame(); // DATA STREAM 1
@@ -726,7 +736,7 @@ public final class SpdyConnectionTest {
peer.acceptFrame(); // SYN STREAM 1
peer.acceptFrame(); // GOAWAY
peer.acceptFrame(); // PING
peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("b", "banana")); // Should be ignored!
peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("b", "b")); // Should be ignored!
peer.sendFrame().ping(0, 1);
peer.play();
@@ -734,7 +744,7 @@ public final class SpdyConnectionTest {
SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
connection.newStream(Arrays.asList("a", "android"), true, true);
Ping ping = connection.ping();
connection.shutdown();
connection.shutdown(GOAWAY_PROTOCOL_ERROR);
ping.roundTripTime(); // Ensure that the SYN STREAM has been received.
assertEquals(1, connection.openStreamCount());
@@ -746,6 +756,7 @@ public final class SpdyConnectionTest {
MockSpdyPeer.InFrame goaway = peer.takeFrame();
assertEquals(TYPE_GOAWAY, goaway.type);
assertEquals(0, goaway.streamId);
assertEquals(GOAWAY_PROTOCOL_ERROR, goaway.statusCode);
}
@Test public void noPingsAfterShutdown() throws Exception {
@@ -755,7 +766,7 @@ public final class SpdyConnectionTest {
// play it back
SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
connection.shutdown();
connection.shutdown(GOAWAY_INTERNAL_ERROR);
try {
connection.ping();
fail();
@@ -766,6 +777,7 @@ public final class SpdyConnectionTest {
// verify the peer received what was expected
MockSpdyPeer.InFrame goaway = peer.takeFrame();
assertEquals(TYPE_GOAWAY, goaway.type);
assertEquals(GOAWAY_INTERNAL_ERROR, goaway.statusCode);
}
@Test public void close() throws Exception {

View File

@@ -71,7 +71,7 @@ public final class SpdyServer implements IncomingStreamHandler {
System.out.println("UNSUPPORTED");
}
@Override public List<String> protocols() {
return Arrays.asList("spdy/2");
return Arrays.asList("spdy/3");
}
@Override public void protocolSelected(String protocol) {
System.out.println("PROTOCOL SELECTED: " + protocol);
@@ -85,7 +85,7 @@ public final class SpdyServer implements IncomingStreamHandler {
String path = null;
for (int i = 0; i < requestHeaders.size(); i += 2) {
String s = requestHeaders.get(i);
if ("url".equals(s)) {
if (":path".equals(s)) {
path = requestHeaders.get(i + 1);
break;
}
@@ -109,8 +109,8 @@ public final class SpdyServer implements IncomingStreamHandler {
private void send404(SpdyStream stream, String path) throws IOException {
List<String> responseHeaders = Arrays.asList(
"status", "404",
"version", "HTTP/1.1",
":status", "404",
":version", "HTTP/1.1",
"content-type", "text/plain"
);
stream.reply(responseHeaders, true);
@@ -122,8 +122,8 @@ public final class SpdyServer implements IncomingStreamHandler {
private void serveDirectory(SpdyStream stream, String[] files) throws IOException {
List<String> responseHeaders = Arrays.asList(
"status", "200",
"version", "HTTP/1.1",
":status", "200",
":version", "HTTP/1.1",
"content-type", "text/html; charset=UTF-8"
);
stream.reply(responseHeaders, true);
@@ -139,8 +139,8 @@ public final class SpdyServer implements IncomingStreamHandler {
InputStream in = new FileInputStream(file);
byte[] buffer = new byte[8192];
stream.reply(Arrays.asList(
"status", "200",
"version", "HTTP/1.1",
":status", "200",
":version", "HTTP/1.1",
"content-type", contentType(file)
), true);
OutputStream out = stream.getOutputStream();