Understanding HTTP 2.0 and OkHttp

HTTP 2.0 is an extension of 1.x rather than a replacement. It is called “2.0” because it changes the way data is exchanged between clients and servers. HTTP 2.0 introduces a new binary framing layer that is not compatible with previous HTTP 1.x servers and clients — hence the name 2.0. Before formally introducing HTTP 2.0, we need to understand a few concepts.

  • Stream, a bidirectional byte stream on an established connection.

  • Message, a complete set of data frames corresponding to logical messages (Request, Response).

  • Frame, the smallest unit of communication in HTTP 2.0, such as Header frames (which store headers) and DATA frames (which store the content or a part of the content).

1. Introduction to HTTP 2.0

As we know, HTTP 1.x has many shortcomings, such as head-of-line blocking, lack of multiplexing, and inability to compress headers. Although many solutions have been proposed for these shortcomings, such as long connections, connection and request merging, HTTP pipelining, etc., they only treat the symptoms, not the root causes, until the emergence of HTTP 2.0, which fundamentally addresses many of the issues faced by HTTP 1.x with the following new designs.

  • The binary framing layer is the core of HTTP 2.0’s performance enhancement, changing the way data is exchanged between clients and servers by splitting the transmitted information (headers, body, etc.) into smaller messages and frames, encoded in binary format.

  • Parallel requests and responses allow clients and servers to decompose HTTP messages into independent frames, send them out of order, and then reassemble these messages on the other end.

  • Request prioritization (0 represents the highest priority, -1 represents the lowest priority), each stream can carry a priority value, which allows clients and servers to adopt different strategies when processing different streams to optimally send streams, messages, and frames. However, priority handling needs to be done carefully; otherwise, it may introduce head-of-line blocking issues.

  • A single TCP connection allows HTTP 2.0 to share one connection for all data streams, thus utilizing the TCP connection more effectively.

  • Flow control manages the resources occupied by each stream, which is identical to TCP’s flow control implementation.

  • Server push allows HTTP 2.0 to send multiple responses to a client request. In addition to the initial request response, the server can also push resources to the client without an explicit request from the client.

  • Header compression means that HTTP 2.0 uses a “header table” to track and store previously sent key-value pairs. For the same data, it won’t be sent with every request and response. The header table persists during the connection and is progressively updated by both the client and server. Each new header key-value pair is either appended to the end of the current table or replaces a value in the table. Although HTTP 2.0 solves many problems in 1.x, it also has the following issues.

While eliminating head-of-line blocking in HTTP, the head-of-line blocking phenomenon still exists at the TCP level. To completely solve this problem, it is necessary to completely discard TCP and define the protocol oneself. One can refer to Google’s QUIC. If TCP window scaling is disabled, the cumulative effect of bandwidth delay may limit the throughput of the connection. In case of packet loss, the TCP congestion window will shrink.

2. Introduction to Binary Framing

The fundamental improvement of HTTP 2.0 is the new binary framing layer. Unlike HTTP 1.x, which uses line breaks to separate plain text, the binary framing layer is more concise and simpler to handle programmatically.

Understanding HTTP 2.0 and OkHttp

Once the HTTP 2.0 connection is established, the client and server communicate by exchanging frames, which are also the smallest unit of communication based on this new protocol. All frames share an 8-byte header, which includes the frame’s length, type, flags, a reserved bit, and a 31-bit stream identifier.

Understanding HTTP 2.0 and OkHttpHTTP 2.0 defines the following frame types.

  • DATA, used to transmit HTTP message bodies.

  • HEADERS, used to transmit additional header fields about the stream.

  • PRIORITY, used to specify or re-specify the priority of the stream.

  • RST_STREAM, used to notify abnormal termination of the stream.

  • SETTINGS, used to notify configuration data for communication between the two ends.

  • PUSH_PROMISE, used to issue a promise to create streams and server-referenced resources.

  • PING, used to measure round-trip time and perform “keep-alive” checks.

  • GOAWAY, used to notify the client/server to stop creating streams on the current connection.

  • WINDOW_UPDATE, used for flow control on individual streams or connections.

  • CONTINUATION, used to continue a series of header block fragments.

2.1. HEADER Frame

Before sending application data, a new stream must be created, along with the corresponding metadata, such as stream priority, HTTP headers, etc. The HTTP 2.0 protocol stipulates that both clients and servers can initiate new streams, leading to the following two possibilities.

The client initiates a new stream by sending a HEADERS frame, which contains common headers with a new stream ID, an optional 31-bit priority value, and a set of HTTP key-value pairs. The server initiates a push stream by sending a PUSH_PROMISE frame, which is equivalent to a HEADER frame but contains a “promised stream ID” and no priority value.Understanding HTTP 2.0 and OkHttp

HEADERS frame with priority value

2.2. DATA Frame

Application data can be divided into multiple DATA frames, and the last frame should flip the END_STREAM field in the frame header.

Understanding HTTP 2.0 and OkHttp

DATA frame: The payload is not encoded or compressed separately. The encoding method of the DATA frame depends on the application or server; plain text, gzip compression, image, or video compression formats can be used. The entire frame consists of a common 8-byte header and the HTTP payload. Technically, the length field of the DATA frame determines that the payload of each frame can be up to -1 (65535) bytes. However, to reduce head-of-line blocking, the HTTP 2.0 standard requires that DATA frames must not exceed (16383) bytes. Data exceeding this threshold must be sent in multiple frames.

3. Application of HTTP 2.0 in OKHttp

HTTP 2.0 is enabled through the startHttp2 method of RealConnection, which creates an Http2Connection object and then calls the start method of Http2Connection.

private void startHttp2(int pingIntervalMillis) throws IOException {
    socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
    // Create Http2Connection object
    http2Connection = new Http2Connection.Builder(true)
        .socket(socket, route.address().url().host(), source, sink)
        .listener(this)
        .pingIntervalMillis(pingIntervalMillis)
        .build();
    // Start HTTP 2.0
    http2Connection.start();
  }

In the start method, a string PRI * HTTP/2.0/r/n/r/nSM/r/n/r/n is first sent to the server to finalize the protocol and establish the initial settings for the HTTP/2 connection. Then, a SETTINGS type Header frame is sent to the server, which mainly informs the server of the maximum capacity of each frame from the client, the size of the header table, whether to enable push, etc. If the window size changes, the window size must also be updated (the default window size for HTTP 2.0 is 64KB, while the client needs to change this size to 16M to avoid frequent updates). Finally, a separate thread is started to read the data returned from the server.

public void start() throws IOException {
    start(true);
  }
  void start(boolean sendConnectionPreface) throws IOException {
    if (sendConnectionPreface) {
      // Send a string PRI * HTTP/2.0/r/n/r/nSM/r/n/r/n to finalize the protocol, i.e., the connection preface frame
      writer.connectionPreface();
      // Inform the server of local configuration information
      writer.settings(okHttpSettings);
      // In okHttpSetting, the window size is set to 16M
      int windowSize = okHttpSettings.getInitialWindowSize();
      // Default is 64kb, but if on the client, it needs to be reset to 16M
      if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) {
        // Update window size
        writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE);
      }
    }
    // Start a thread to listen for messages returned from the server
    new Thread(readerRunnable).start(); // Not a daemon thread.
  }

From the name of ReaderRunnable, it is clear that it is used to read various types of data returned from the server.

class ReaderRunnable extends NamedRunnable implements Http2Reader.Handler {
    ...


    @Override protected void execute() {
      ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
      ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
      try {
        // Read the server's returned connection preface frame
        reader.readConnectionPreface(this);
        // Continuously read the next frame, all messages start distributing from here
        while (reader.nextFrame(false, this)) {
        }
        connectionErrorCode = ErrorCode.NO_ERROR;
        streamErrorCode = ErrorCode.CANCEL;
      } catch (IOException e) {
        ...
      } finally {
        ...
      }
    }
    // Read returned DATA type data
    @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length)
        throws IOException {...}
    // Read returned HEADERS type data
    @Override public void headers(boolean inFinished, int streamId, int associatedStreamId,
        List<Header> headerBlock) {...}
    // Read returned RST_STREAM type data   
    @Override public void rstStream(int streamId, ErrorCode errorCode) {...}
    // Read returned SETTINGS type data
    @Override public void settings(boolean clearPrevious, Settings newSettings) {...}
    // Respond to the server's returned ackSettings
    private void applyAndAckSettings(final Settings peerSettings) ...}
    // Restore the client's sent SETTING data, the client does not implement by default
    @Override public void ackSettings() {...}
    // Read returned PING type data
    @Override public void ping(boolean reply, int payload1, int payload2) {...}
    // Read the server's returned GOAWAY type data
    @Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {...}
    // Read returned WINDOW_UPDATE type data
    @Override public void windowUpdate(int streamId, long windowSizeIncrement) {...}
    // Read returned PRIORITY type data
    @Override public void priority(int streamId, int streamDependency, int weight,
        boolean exclusive) {...}
    // Read returned PUSH_PROMISE type data
    @Override
    public void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders) {... }
    // Alternate Service
    @Override public void alternateService(int streamId, String origin, ByteString protocol,
        String host, int port, long maxAge) {...}
  }

The above briefly describes how to enable HTTP 2.0 protocol in OkHttp. Next, let’s discuss how clients and servers perform data read and write operations through the HTTP 2.0 protocol.

3.1. Writing Headers to the Server

Writing headers to the server is achieved through httpCodec.writeRequestHeaders(request). The implementation class of httpCodec under HTTP 2.0 protocol is Http2Codec. The writeRequestHeaders method mainly creates a new stream Http2Stream, which will send HEADERS type data to the server once the stream is successfully created.

boolean hasRequestBody = request.body() != null;
    List<Header> requestHeaders = http2HeadersList(request);
    // Create new stream
    stream = connection.newStream(requestHeaders, hasRequestBody);
    // We may be asked to cancel while creating a new stream and sending headers, but there is still no stream to close.
    if (canceled) {
      stream.closeLater(ErrorCode.CANCEL);
      throw new IOException("Canceled");
    }
    ...
  }
  // The following method is in the Http2Connection class
  public Http2Stream newStream(List<Header> requestHeaders, boolean out) throws IOException {
    return newStream(0, requestHeaders, out);
  }


  private Http2Stream newStream(
      int associatedStreamId, List<Header> requestHeaders, boolean out) throws IOException {
    ...
    synchronized (writer) {
      synchronized (this) {
        // The number of streams per TCP connection cannot exceed Integer.MAX_VALUE
        if (nextStreamId > Integer.MAX_VALUE / 2) {
          shutdown(REFUSED_STREAM);
        }
        if (shutdown) {
          throw new ConnectionShutdownException();
        }
        // Stream ID for each stream
        streamId = nextStreamId;
        // The next stream ID is the current stream ID plus 2
        nextStreamId += 2;
        // Create new stream
        stream = new Http2Stream(streamId, this, outFinished, inFinished, null);
        flushHeaders = !out || bytesLeftInWriteWindow == 0L || stream.bytesLeftInWriteWindow == 0L;
        if (stream.isOpen()) {
          streams.put(streamId, stream);
        }
      }
      if (associatedStreamId == 0) {
        // Write headers to the server
        writer.headers(outFinished, streamId, requestHeaders);
      } else if (client) {
        throw new IllegalArgumentException("client streams shouldn't have associated stream IDs");
      } else {// For servers
        writer.pushPromise(associatedStreamId, streamId, requestHeaders);
      }
    }
    // Flush
    if (flushHeaders) {
      writer.flush();
    }


    return stream;
  }

On the client side, stream IDs start from 3 and are all odd numbers, while on the server side, stream IDs are all even numbers. The initial values of stream IDs are defined in the constructor of Http2Connection.

Http2Connection(Builder builder) {
    ....
    // If it is a client, stream ID starts from 1
    nextStreamId = builder.client ? 1 : 2;
    if (builder.client) {
      // In HTTP2, 1 is reserved for upgrades
      nextStreamId += 2;
    }
    ...
  }

3.2. Reading Headers Returned by the Server

readResponseHeaders is used to read header data from the server. This method is in Http2Codec.

@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
    // Get header information from the stream,
    Headers headers = stream.takeHeaders();
    Response.Builder responseBuilder = readHttp2HeadersList(headers, protocol);
    if (expectContinue &amp;&amp; Internal.instance.code(responseBuilder) == HTTP_CONTINUE) {
      return null;
    }
    return responseBuilder;
  }
  // This method is in Http2Stream
  public synchronized Headers takeHeaders() throws IOException {
    readTimeout.enter();
    try {
      // Wait if there is no data in the queue
      while (headersQueue.isEmpty() &amp;&amp; errorCode == null) {
        waitForIo();
      }
    } finally {
      readTimeout.exitAndThrowIfTimedOut();
    }
    // Get header data from the queue
    if (!headersQueue.isEmpty()) {
      return headersQueue.removeFirst();
    }
    throw new StreamResetException(errorCode);
  }

headersQueue is a deque that mainly stores the headers returned by the server. When the server returns headers, this list is updated.

3.3. Reading/Writing Body

When creating a stream, a FramingSink and FramingSource object are created. FramingSink is used to write data to the server, while FramingSource reads data returned from the server. Therefore, reading/writing body is essentially an application of Okio; those unfamiliar with Okio can first understand the knowledge of Okio.

// Write data to the server
  final class FramingSink implements Sink {
    private static final long EMIT_BUFFER_SIZE = 16384;


    ...
    @Override public void write(Buffer source, long byteCount) throws IOException {
      assert (!Thread.holdsLock(Http2Stream.this));
      sendBuffer.write(source, byteCount);
      while (sendBuffer.size() >= EMIT_BUFFER_SIZE) {
        emitFrame(false);
      }
    }


    //
    private void emitFrame(boolean outFinished) throws IOException {
      ...
      try {
        // Write DATA type data to the server
        connection.writeData(id, outFinished &amp;&amp; toWrite == sendBuffer.size(), sendBuffer, toWrite);
      } finally {
        writeTimeout.exitAndThrowIfTimedOut();
      }
    }
    ...
  }
  // Read data from the server
  private final class FramingSource implements Source {
    // Data read from the network is written into this Buffer, accessible only to the read thread
    private final Buffer receiveBuffer = new Buffer();


    // Readable buffer
    private final Buffer readBuffer = new Buffer();


    // Maximum bytes buffered
    private final long maxByteCount;


    ...
    // Read data from receiveBuffer
    @Override public long read(Buffer sink, long byteCount) throws IOException {...}
    ...
    // Receive data passed from the server, called only in ReaderRunnable
    void receive(BufferedSource in, long byteCount) throws IOException {...}
    ...
  }

3.4. Http2Reader and Http2Writer

The previous sections introduced reading and writing data from the server, but it is essential to understand that Http2Reader and Http2Writer are the two classes that perform the actual read and write operations to the server. First, let’s look at writing data to the server.

final class Http2Writer implements Closeable {
  ...

  // Write connection preface frame to finalize the protocol
  public synchronized void connectionPreface() throws IOException {...}

  // Send PUSH_PROMISE type data
  public synchronized void pushPromise(int streamId, int promisedStreamId,
      List<Header> requestHeaders) throws IOException {...}
  ...
  // Send RST_STREAM type data   
  public synchronized void rstStream(int streamId, ErrorCode errorCode)
      throws IOException {...}


  // Send DATA type data
  public synchronized void data(boolean outFinished, int streamId, Buffer source, int byteCount)
      throws IOException {...}

  // Send SETTINGS type data
  public synchronized void settings(Settings settings) throws IOException {...}

  // Send PING type data
  public synchronized void ping(boolean ack, int payload1, int payload2) throws IOException {...}

  // Send GOAWAY type data
  public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData)
      throws IOException {...}

  // Send WINDOW_UPDATE type data for window updates
  public synchronized void windowUpdate(int streamId, long windowSizeIncrement) throws IOException {...}
  // Send HEADERS type data
  public void frameHeader(int streamId, int length, byte type, byte flags) throws IOException {...}

  @Override public synchronized void close() throws IOException {
    closed = true;
    sink.close();
  }

  ...
  // Write CONTINUATION type data
  private void writeContinuationFrames(int streamId, long byteCount) throws IOException {...}
  // Write headers
  void headers(boolean outFinished, int streamId, List<Header> headerBlock) throws IOException {...}
}

Next, let’s look at reading data from the server, which is essentially about dispatching based on the type of data.

final class Http2Reader implements Closeable {
  ...
  // Read data
  public boolean nextFrame(boolean requireSettings, Handler handler) throws IOException {
    try {
      source.require(9); // Frame header size
    } catch (IOException e) {
      return false; // This might be a normal socket close.
    }

    //  0                   1                   2                   3
    //  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    // |                 Length (24)                   |
    // +---------------+---------------+---------------+
    // |   Type (8)    |   Flags (8)   |
    // +-+-+-----------+---------------+-------------------------------+
    // |R|                 Stream Identifier (31)                      |
    // +=+=============================================================+
    // |                   Frame Payload (0...)                      ...
    // +---------------------------------------------------------------+
    int length = readMedium(source);
    if (length < 0 || length > INITIAL_MAX_FRAME_SIZE) {
      throw ioException("FRAME_SIZE_ERROR: %s", length);
    }
    byte type = (byte) (source.readByte() &amp; 0xff);
    if (requireSettings &amp;&amp; type != TYPE_SETTINGS) {
      throw ioException("Expected a SETTINGS frame but was %s", type);
    }
    byte flags = (byte) (source.readByte() &amp; 0xff);
    int streamId = (source.readInt() &amp; 0x7fffffff); // Ignore reserved bit.
    if (logger.isLoggable(FINE)) logger.fine(frameLog(true, streamId, length, type, flags));
    // The handler here is the ReaderRunnable object
    switch (type) {
      case TYPE_DATA:
        readData(handler, length, flags, streamId);
        break;


      case TYPE_HEADERS:
        readHeaders(handler, length, flags, streamId);
        break;


      case TYPE_PRIORITY:
        readPriority(handler, length, flags, streamId);
        break;


      case TYPE_RST_STREAM:
        readRstStream(handler, length, flags, streamId);
        break;


      case TYPE_SETTINGS:
        readSettings(handler, length, flags, streamId);
        break;


      case TYPE_PUSH_PROMISE:
        readPushPromise(handler, length, flags, streamId);
        break;


      case TYPE_PING:
        readPing(handler, length, flags, streamId);
        break;


      case TYPE_GOAWAY:
        readGoAway(handler, length, flags, streamId);
        break;


      case TYPE_WINDOW_UPDATE:
        readWindowUpdate(handler, length, flags, streamId);
        break;


      default:
        // Implementations MUST discard frames that have unknown or unsupported types.
        source.skip(length);
    }
    return true;
  }
  ...
}

In Http2Reader and Http2Writer, data is read or written in the form of frames (binary), which is more efficient than strings. Of course, we can also use Huffman coding (OkHttp supports Huffman coding) to compress frames for better performance. Remember that in the network optimization under the HTTP 1.x protocol, using Protocol Buffer (binary) to replace string transmission was one option, and with HTTP 2.0, there is no need to use Protocol Buffer.

Leave a Comment