okhttp3.internal.http2.Http2Writer Maven / Gradle / Ivy
/*
* Copyright (C) 2011 The Android Open Source Project
*
* 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 okhttp3.internal.http2;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.logging.Logger;
import okio.Buffer;
import okio.BufferedSink;
import static java.util.logging.Level.FINE;
import static okhttp3.internal.Util.format;
import static okhttp3.internal.http2.Http2.CONNECTION_PREFACE;
import static okhttp3.internal.http2.Http2.FLAG_ACK;
import static okhttp3.internal.http2.Http2.FLAG_END_HEADERS;
import static okhttp3.internal.http2.Http2.FLAG_END_STREAM;
import static okhttp3.internal.http2.Http2.FLAG_NONE;
import static okhttp3.internal.http2.Http2.INITIAL_MAX_FRAME_SIZE;
import static okhttp3.internal.http2.Http2.TYPE_CONTINUATION;
import static okhttp3.internal.http2.Http2.TYPE_DATA;
import static okhttp3.internal.http2.Http2.TYPE_GOAWAY;
import static okhttp3.internal.http2.Http2.TYPE_HEADERS;
import static okhttp3.internal.http2.Http2.TYPE_PING;
import static okhttp3.internal.http2.Http2.TYPE_PUSH_PROMISE;
import static okhttp3.internal.http2.Http2.TYPE_RST_STREAM;
import static okhttp3.internal.http2.Http2.TYPE_SETTINGS;
import static okhttp3.internal.http2.Http2.TYPE_WINDOW_UPDATE;
import static okhttp3.internal.http2.Http2.frameLog;
import static okhttp3.internal.http2.Http2.illegalArgument;
/** Writes HTTP/2 transport frames. */
final class Http2Writer implements Closeable {
private static final Logger logger = Logger.getLogger(Http2.class.getName());
private final BufferedSink sink;
private final boolean client;
private final Buffer hpackBuffer;
private int maxFrameSize;
private boolean closed;
final Hpack.Writer hpackWriter;
Http2Writer(BufferedSink sink, boolean client) {
this.sink = sink;
this.client = client;
this.hpackBuffer = new Buffer();
this.hpackWriter = new Hpack.Writer(hpackBuffer);
this.maxFrameSize = INITIAL_MAX_FRAME_SIZE;
}
public synchronized void connectionPreface() throws IOException {
if (closed) throw new IOException("closed");
if (!client) return; // Nothing to write; servers don't send connection headers!
if (logger.isLoggable(FINE)) {
logger.fine(format(">> CONNECTION %s", CONNECTION_PREFACE.hex()));
}
sink.write(CONNECTION_PREFACE.toByteArray());
sink.flush();
}
/** Applies {@code peerSettings} and then sends a settings ACK. */
public synchronized void applyAndAckSettings(Settings peerSettings) throws IOException {
if (closed) throw new IOException("closed");
this.maxFrameSize = peerSettings.getMaxFrameSize(maxFrameSize);
if (peerSettings.getHeaderTableSize() != -1) {
hpackWriter.setHeaderTableSizeSetting(peerSettings.getHeaderTableSize());
}
int length = 0;
byte type = TYPE_SETTINGS;
byte flags = FLAG_ACK;
int streamId = 0;
frameHeader(streamId, length, type, flags);
sink.flush();
}
/**
* HTTP/2 only. Send a push promise header block.
*
* A push promise contains all the headers that pertain to a server-initiated request, and a
* {@code promisedStreamId} to which response frames will be delivered. Push promise frames are
* sent as a part of the response to {@code streamId}. The {@code promisedStreamId} has a priority
* of one greater than {@code streamId}.
*
* @param streamId client-initiated stream ID. Must be an odd number.
* @param promisedStreamId server-initiated stream ID. Must be an even number.
* @param requestHeaders minimally includes {@code :method}, {@code :scheme}, {@code :authority},
* and {@code :path}.
*/
public synchronized void pushPromise(int streamId, int promisedStreamId,
List requestHeaders) throws IOException {
if (closed) throw new IOException("closed");
hpackWriter.writeHeaders(requestHeaders);
long byteCount = hpackBuffer.size();
int length = (int) Math.min(maxFrameSize - 4, byteCount);
byte type = TYPE_PUSH_PROMISE;
byte flags = byteCount == length ? FLAG_END_HEADERS : 0;
frameHeader(streamId, length + 4, type, flags);
sink.writeInt(promisedStreamId & 0x7fffffff);
sink.write(hpackBuffer, length);
if (byteCount > length) writeContinuationFrames(streamId, byteCount - length);
}
public synchronized void flush() throws IOException {
if (closed) throw new IOException("closed");
sink.flush();
}
public synchronized void rstStream(int streamId, ErrorCode errorCode)
throws IOException {
if (closed) throw new IOException("closed");
if (errorCode.httpCode == -1) throw new IllegalArgumentException();
int length = 4;
byte type = TYPE_RST_STREAM;
byte flags = FLAG_NONE;
frameHeader(streamId, length, type, flags);
sink.writeInt(errorCode.httpCode);
sink.flush();
}
/** The maximum size of bytes that may be sent in a single call to {@link #data}. */
public int maxDataLength() {
return maxFrameSize;
}
/**
* {@code source.length} may be longer than the max length of the variant's data frame.
* Implementations must send multiple frames as necessary.
*
* @param source the buffer to draw bytes from. May be null if byteCount is 0.
* @param byteCount must be between 0 and the minimum of {@code source.length} and {@link
* #maxDataLength}.
*/
public synchronized void data(boolean outFinished, int streamId, Buffer source, int byteCount)
throws IOException {
if (closed) throw new IOException("closed");
byte flags = FLAG_NONE;
if (outFinished) flags |= FLAG_END_STREAM;
dataFrame(streamId, flags, source, byteCount);
}
void dataFrame(int streamId, byte flags, Buffer buffer, int byteCount) throws IOException {
byte type = TYPE_DATA;
frameHeader(streamId, byteCount, type, flags);
if (byteCount > 0) {
sink.write(buffer, byteCount);
}
}
/** Write okhttp's settings to the peer. */
public synchronized void settings(Settings settings) throws IOException {
if (closed) throw new IOException("closed");
int length = settings.size() * 6;
byte type = TYPE_SETTINGS;
byte flags = FLAG_NONE;
int streamId = 0;
frameHeader(streamId, length, type, flags);
for (int i = 0; i < Settings.COUNT; i++) {
if (!settings.isSet(i)) continue;
int id = i;
if (id == 4) {
id = 3; // SETTINGS_MAX_CONCURRENT_STREAMS renumbered.
} else if (id == 7) {
id = 4; // SETTINGS_INITIAL_WINDOW_SIZE renumbered.
}
sink.writeShort(id);
sink.writeInt(settings.get(i));
}
sink.flush();
}
/**
* Send a connection-level ping to the peer. {@code ack} indicates this is a reply. The data in
* {@code payload1} and {@code payload2} opaque binary, and there are no rules on the content.
*/
public synchronized void ping(boolean ack, int payload1, int payload2) throws IOException {
if (closed) throw new IOException("closed");
int length = 8;
byte type = TYPE_PING;
byte flags = ack ? FLAG_ACK : FLAG_NONE;
int streamId = 0;
frameHeader(streamId, length, type, flags);
sink.writeInt(payload1);
sink.writeInt(payload2);
sink.flush();
}
/**
* Tell the peer to stop creating streams and that we last processed {@code lastGoodStreamId}, or
* zero if no streams were processed.
*
* @param lastGoodStreamId the last stream ID processed, or zero if no streams were processed.
* @param errorCode reason for closing the connection.
* @param debugData only valid for HTTP/2; opaque debug data to send.
*/
public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData)
throws IOException {
if (closed) throw new IOException("closed");
if (errorCode.httpCode == -1) throw illegalArgument("errorCode.httpCode == -1");
int length = 8 + debugData.length;
byte type = TYPE_GOAWAY;
byte flags = FLAG_NONE;
int streamId = 0;
frameHeader(streamId, length, type, flags);
sink.writeInt(lastGoodStreamId);
sink.writeInt(errorCode.httpCode);
if (debugData.length > 0) {
sink.write(debugData);
}
sink.flush();
}
/**
* Inform peer that an additional {@code windowSizeIncrement} bytes can be sent on {@code
* streamId}, or the connection if {@code streamId} is zero.
*/
public synchronized void windowUpdate(int streamId, long windowSizeIncrement) throws IOException {
if (closed) throw new IOException("closed");
if (windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL) {
throw illegalArgument("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: %s",
windowSizeIncrement);
}
int length = 4;
byte type = TYPE_WINDOW_UPDATE;
byte flags = FLAG_NONE;
frameHeader(streamId, length, type, flags);
sink.writeInt((int) windowSizeIncrement);
sink.flush();
}
public void frameHeader(int streamId, int length, byte type, byte flags) throws IOException {
if (logger.isLoggable(FINE)) logger.fine(frameLog(false, streamId, length, type, flags));
if (length > maxFrameSize) {
throw illegalArgument("FRAME_SIZE_ERROR length > %d: %d", maxFrameSize, length);
}
if ((streamId & 0x80000000) != 0) throw illegalArgument("reserved bit set: %s", streamId);
writeMedium(sink, length);
sink.writeByte(type & 0xff);
sink.writeByte(flags & 0xff);
sink.writeInt(streamId & 0x7fffffff);
}
@Override public synchronized void close() throws IOException {
closed = true;
sink.close();
}
private static void writeMedium(BufferedSink sink, int i) throws IOException {
sink.writeByte((i >>> 16) & 0xff);
sink.writeByte((i >>> 8) & 0xff);
sink.writeByte(i & 0xff);
}
private void writeContinuationFrames(int streamId, long byteCount) throws IOException {
while (byteCount > 0) {
int length = (int) Math.min(maxFrameSize, byteCount);
byteCount -= length;
frameHeader(streamId, length, TYPE_CONTINUATION, byteCount == 0 ? FLAG_END_HEADERS : 0);
sink.write(hpackBuffer, length);
}
}
public synchronized void headers(
boolean outFinished, int streamId, List headerBlock) throws IOException {
if (closed) throw new IOException("closed");
hpackWriter.writeHeaders(headerBlock);
long byteCount = hpackBuffer.size();
int length = (int) Math.min(maxFrameSize, byteCount);
byte type = TYPE_HEADERS;
byte flags = byteCount == length ? FLAG_END_HEADERS : 0;
if (outFinished) flags |= FLAG_END_STREAM;
frameHeader(streamId, length, type, flags);
sink.write(hpackBuffer, length);
if (byteCount > length) writeContinuationFrames(streamId, byteCount - length);
}
}