package com.koushikdutta.async.http.spdy; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.BufferedDataSink; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.future.SimpleFuture; import com.koushikdutta.async.http.Protocol; import java.io.IOException; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import static com.koushikdutta.async.http.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE; /** * Created by koush on 7/16/14. */ public class AsyncSpdyConnection implements FrameReader.Handler { AsyncSocket socket; BufferedDataSink bufferedSocket; FrameReader reader; FrameWriter writer; Variant variant; Hashtable<Integer, SpdySocket> sockets = new Hashtable<Integer, SpdySocket>(); Protocol protocol; boolean client = true; /** * Returns a new locally-initiated stream. * * @param out true to create an output stream that we can use to send data to the remote peer. * Corresponds to {@code FLAG_FIN}. * @param in true to create an input stream that the remote peer can use to send data to us. * Corresponds to {@code FLAG_UNIDIRECTIONAL}. */ public SpdySocket newStream(List<Header> requestHeaders, boolean out, boolean in) { return newStream(0, requestHeaders, out, in); } private SpdySocket newStream(int associatedStreamId, List<Header> requestHeaders, boolean out, boolean in) { boolean outFinished = !out; boolean inFinished = !in; SpdySocket socket; int streamId; if (shutdown) { return null; } streamId = nextStreamId; nextStreamId += 2; socket = new SpdySocket(streamId, outFinished, inFinished, requestHeaders); if (socket.isOpen()) { sockets.put(streamId, socket); // setIdle(false); } try { if (associatedStreamId == 0) { writer.synStream(outFinished, inFinished, streamId, associatedStreamId, requestHeaders); } else if (client) { throw new IllegalArgumentException("client streams shouldn't have associated stream IDs"); } else { // HTTP/2 has a PUSH_PROMISE frame. writer.pushPromise(associatedStreamId, streamId, requestHeaders); } return socket; } catch (IOException e) { throw new AssertionError(e); } } int totalWindowRead; void updateWindowRead(int length) { totalWindowRead += length; if (totalWindowRead >= okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) { try { writer.windowUpdate(0, totalWindowRead); } catch (IOException e) { throw new AssertionError(e); } totalWindowRead = 0; } } public class SpdySocket implements AsyncSocket { long bytesLeftInWriteWindow = AsyncSpdyConnection.this.peerSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE); WritableCallback writable; final int id; CompletedCallback closedCallback; CompletedCallback endCallback; DataCallback dataCallback; ByteBufferList pending = new ByteBufferList(); SimpleFuture<List<Header>> headers = new SimpleFuture<List<Header>>(); boolean isOpen = true; int totalWindowRead; public AsyncSpdyConnection getConnection() { return AsyncSpdyConnection.this; } public SimpleFuture<List<Header>> headers() { return headers; } void updateWindowRead(int length) { totalWindowRead += length; if (totalWindowRead >= okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) { try { writer.windowUpdate(id, totalWindowRead); } catch (IOException e) { throw new AssertionError(e); } totalWindowRead = 0; } AsyncSpdyConnection.this.updateWindowRead(length); } public SpdySocket(int id, boolean outFinished, boolean inFinished, List<Header> headerBlock) { this.id = id; } public boolean isLocallyInitiated() { boolean streamIsClient = ((id & 1) == 1); return client == streamIsClient; } public void addBytesToWriteWindow(long delta) { long prev = bytesLeftInWriteWindow; bytesLeftInWriteWindow += delta; if (bytesLeftInWriteWindow > 0 && prev <= 0) Util.writable(writable); } @Override public AsyncServer getServer() { return socket.getServer(); } @Override public void setDataCallback(DataCallback callback) { dataCallback = callback; } @Override public DataCallback getDataCallback() { return dataCallback; } @Override public boolean isChunked() { return false; } boolean paused; @Override public void pause() { paused = true; } @Override public void resume() { paused = false; } @Override public void close() { isOpen = false; } @Override public boolean isPaused() { return paused; } @Override public void setEndCallback(CompletedCallback callback) { endCallback = callback; } @Override public CompletedCallback getEndCallback() { return endCallback; } @Override public String charset() { return null; } ByteBufferList writing = new ByteBufferList(); @Override public void write(ByteBufferList bb) { int canWrite = (int)Math.min(bytesLeftInWriteWindow, AsyncSpdyConnection.this.bytesLeftInWriteWindow); canWrite = Math.min(bb.remaining(), canWrite); if (canWrite == 0) { return; } if (canWrite < bb.remaining()) { if (writing.hasRemaining()) throw new AssertionError("wtf"); bb.get(writing, canWrite); bb = writing; } try { writer.data(false, id, bb); bytesLeftInWriteWindow -= canWrite; } catch (IOException e) { throw new AssertionError(e); } } @Override public void setWriteableCallback(WritableCallback handler) { writable = handler; } @Override public WritableCallback getWriteableCallback() { return writable; } @Override public boolean isOpen() { return isOpen; } @Override public void end() { try { writer.data(true, id, writing); } catch (IOException e) { throw new AssertionError(e); } } @Override public void setClosedCallback(CompletedCallback handler) { closedCallback = handler; } @Override public CompletedCallback getClosedCallback() { return closedCallback; } public void receiveHeaders(List<Header> headers, HeadersMode headerMode) { this.headers.setComplete(headers); } } final Settings okHttpSettings = new Settings(); private int nextPingId; private static final int OKHTTP_CLIENT_WINDOW_SIZE = 16 * 1024 * 1024; public AsyncSpdyConnection(AsyncSocket socket, Protocol protocol) { this.protocol = protocol; this.socket = socket; this.bufferedSocket = new BufferedDataSink(socket); if (protocol == Protocol.SPDY_3) { variant = new Spdy3(); } else if (protocol == Protocol.HTTP_2) { variant = new Http20Draft13(); } reader = variant.newReader(socket, this, true); writer = variant.newWriter(bufferedSocket, true); boolean client = true; nextStreamId = client ? 1 : 2; if (client && protocol == Protocol.HTTP_2) { nextStreamId += 2; // In HTTP/2, 1 on client is reserved for Upgrade. } nextPingId = client ? 1 : 2; // Flow control was designed more for servers, or proxies than edge clients. // If we are a client, set the flow control window to 16MiB. This avoids // thrashing window updates every 64KiB, yet small enough to avoid blowing // up the heap. if (client) { okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, OKHTTP_CLIENT_WINDOW_SIZE); } } /** * Sends a connection header if the current variant requires it. This should * be called after {@link Builder#build} for all new connections. */ public void sendConnectionPreface() throws IOException { writer.connectionPreface(); writer.settings(okHttpSettings); int windowSize = okHttpSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE); if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) { writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE); } } /** Even, positive numbered streams are pushed streams in HTTP/2. */ private boolean pushedStream(int streamId) { return protocol == Protocol.HTTP_2 && streamId != 0 && (streamId & 1) == 0; } @Override public void data(boolean inFinished, int streamId, ByteBufferList source) { if (pushedStream(streamId)) { throw new AssertionError("push"); // pushDataLater(streamId, source, length, inFinished); // return; } SpdySocket socket = sockets.get(streamId); if (socket == null) { try { writer.rstStream(streamId, ErrorCode.INVALID_STREAM); } catch (IOException e) { throw new AssertionError(e); } source.recycle(); return; } int length = source.remaining(); source.get(socket.pending); socket.updateWindowRead(length); Util.emitAllData(socket, socket.pending); if (inFinished) { sockets.remove(streamId); socket.close(); Util.end(socket, null); } } private int lastGoodStreamId; private int nextStreamId; @Override public void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, List<Header> headerBlock, HeadersMode headersMode) { if (pushedStream(streamId)) { throw new AssertionError("push"); // pushHeadersLater(streamId, headerBlock, inFinished); // return; } // If we're shutdown, don't bother with this stream. if (shutdown) return; SpdySocket socket = sockets.get(streamId); if (socket == null) { // The headers claim to be for an existing stream, but we don't have one. if (headersMode.failIfStreamAbsent()) { try { writer.rstStream(streamId, ErrorCode.INVALID_STREAM); return; } catch (IOException e) { throw new AssertionError(e); } } // If the stream ID is less than the last created ID, assume it's already closed. if (streamId <= lastGoodStreamId) return; // If the stream ID is in the client's namespace, assume it's already closed. if (streamId % 2 == nextStreamId % 2) return; throw new AssertionError("unexpected receive stream"); // Create a stream. // socket = new SpdySocket(streamId, outFinished, inFinished, headerBlock); // lastGoodStreamId = streamId; // sockets.put(streamId, socket); // handler.receive(newStream); // return; } // The headers claim to be for a new stream, but we already have one. if (headersMode.failIfStreamPresent()) { try { writer.rstStream(streamId, ErrorCode.INVALID_STREAM); } catch (IOException e) { throw new AssertionError(e); } sockets.remove(streamId); return; } // Update an existing stream. socket.receiveHeaders(headerBlock, headersMode); if (inFinished) { sockets.remove(streamId); Util.end(socket, null); } } @Override public void rstStream(int streamId, ErrorCode errorCode) { if (pushedStream(streamId)) { throw new AssertionError("push"); // pushResetLater(streamId, errorCode); // return; } SpdySocket rstStream = sockets.remove(streamId); if (rstStream != null) { Util.end(rstStream, new IOException(errorCode.toString())); } } long bytesLeftInWriteWindow; Settings peerSettings = new Settings(); private boolean receivedInitialPeerSettings = false; @Override public void settings(boolean clearPrevious, Settings settings) { long delta = 0; int priorWriteWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE); if (clearPrevious) peerSettings.clear(); peerSettings.merge(settings); try { writer.ackSettings(); } catch (IOException e) { throw new AssertionError(e); } int peerInitialWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE); if (peerInitialWindowSize != -1 && peerInitialWindowSize != priorWriteWindowSize) { delta = peerInitialWindowSize - priorWriteWindowSize; if (!receivedInitialPeerSettings) { addBytesToWriteWindow(delta); receivedInitialPeerSettings = true; } } for (SpdySocket socket: sockets.values()) { socket.addBytesToWriteWindow(delta); } } void addBytesToWriteWindow(long delta) { bytesLeftInWriteWindow += delta; for (SpdySocket socket: sockets.values()) { Util.writable(socket); } } @Override public void ackSettings() { try { writer.ackSettings(); } catch (IOException e) { throw new AssertionError(e); } } private Map<Integer, Ping> pings; private void writePing(boolean reply, int payload1, int payload2, Ping ping) throws IOException { if (ping != null) ping.send(); writer.ping(reply, payload1, payload2); } private synchronized Ping removePing(int id) { return pings != null ? pings.remove(id) : null; } @Override public void ping(boolean ack, int payload1, int payload2) { if (ack) { Ping ping = removePing(payload1); if (ping != null) { ping.receive(); } } else { // Send a reply to a client ping if this is a server and vice versa. try { writePing(true, payload1, payload2, null); } catch (IOException e) { throw new AssertionError(e); } } } boolean shutdown; @Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { shutdown = true; // Fail all streams created after the last good stream ID. for (Iterator<Map.Entry<Integer, SpdySocket>> i = sockets.entrySet().iterator(); i.hasNext(); ) { Map.Entry<Integer, SpdySocket> entry = i.next(); int streamId = entry.getKey(); if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) { Util.end(entry.getValue(), new IOException(ErrorCode.REFUSED_STREAM.toString())); i.remove(); } } } @Override public void windowUpdate(int streamId, long windowSizeIncrement) { if (streamId == 0) { addBytesToWriteWindow(windowSizeIncrement); return; } SpdySocket socket = sockets.get(streamId); if (socket != null) socket.addBytesToWriteWindow(windowSizeIncrement); } @Override public void priority(int streamId, int streamDependency, int weight, boolean exclusive) { } @Override public void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders) { throw new AssertionError("pushPromise"); } @Override public void alternateService(int streamId, String origin, ByteString protocol, String host, int port, long maxAge) { } @Override public void error(Exception e) { socket.close(); for (Iterator<Map.Entry<Integer, SpdySocket>> i = sockets.entrySet().iterator(); i.hasNext();) { Map.Entry<Integer, SpdySocket> entry = i.next(); Util.end(entry.getValue(), e); i.remove(); } } }