HTTP 2.0與OkHttp

HTTP 2.0是對1.x的擴展而非替代,之因此是「2.0」,是由於它改變了客戶端與服務器之間交換數據的方式。HTTP 2.0增長了新的二進制分幀數據層,而這一層並不兼容以前的HTTP 1.x服務器及客戶端——是謂2.0。  在正式介紹HTTP 2.0以前,咱們須要先了解幾個概念。html

  • 流,已創建的鏈接上的雙向字節流。
  • 消息,與邏輯消息(RequestResponse)對應的完整的一系列數據幀。
  • 幀,HTTP 2.0通訊的最小單位,如Header幀(存儲的是Header)、DATA幀(存儲的是發送的內容或者內容的一部分)。
一、HTTP 2.0簡介

 總所周知,HTTP 1.x擁有隊首阻塞、不支持多路複用、Header沒法壓縮等諸多缺點。儘管針對這些缺點也提出了不少解決方案,如長鏈接、鏈接與合併請求、HTTP管道等,但都治標不治本,直到HTTP 2.0的出現,它新增的如下設計從根本上解決了HTTP 1.x所面臨的諸多問題。java

  • 二進制分幀層,是HTTP 2.0性能加強的核心,改變了客戶端與服務器之間交互數據的方式,將傳輸的信息(HeaderBody等)分割爲更小的消息和幀,並採用二進制格式的編碼。
  • 並行請求與響應,客戶端及服務器能夠把HTTP消息分解爲互不依賴的幀,而後亂序發送,最後再在另外一端把這些消息組合起來。
  • 請求優先級(0表示最高優先級、2^{31}-1表示最低優先級),每一個流能夠攜帶一個優先值,有了這個優先值,客戶端及服務器就能夠在處理不一樣的流時採起不一樣的策略,以最優的方式發送流、消息和幀。但優先級的處理須要慎重,不然有可能會引入隊首阻塞問題。
  • 單TCP鏈接HTTP 2.0可讓全部數據流共用一個鏈接,從而更有效的使用TCP鏈接
  • 流量控制,控制每一個流佔用的資源,與TCP的流量控制實現是如出一轍的。
  • 服務器推送HTTP 2.0能夠對一個客戶端請求發送多個響應,即除了最初請求響應外,服務器還能夠額外的向客戶端推送資源,而無需客戶端明確地請求。
  • 首部(Header)壓縮HTTP 2.0會在客戶端及服務器使用「首部表」來跟蹤和存儲以前發送的鍵-值對,對於相同的數據,不會再經過每次請求和響應發送。首部表在鏈接存續期間始終存在,由客戶端及服務器共同漸進的更新。每一個新的首部鍵-值對要麼追加到當前表的末尾,要麼替換表中的值。

 雖然HTTP 2.0解決了1.x中的諸多問題,但它也存在如下問題。linux

  • 雖然消除了HTTP隊首阻塞現象,但TCP層次上仍然存在隊首阻塞現象。要想完全解決這個問題,就須要完全拋棄TCP,本身來定義協議。能夠參考谷歌的QUIC
  • 若是TCP窗口縮放被禁用,那寬帶延遲積效應可能會限制鏈接的吞吐量。
  • 丟包時,TCP擁塞窗口會縮小。
二、二進制分幀簡介

HTTP 2.0的根本改進仍是新增的二進制分幀層。與HTTP 1.x使用換行符分割純文本不一樣,二進制分幀層更加簡介,經過代碼處理起來更簡單也更有效。 web

在這裏插入圖片描述

圖片來自HTTP/2 簡介

 創建了HTTP 2.0鏈接後,客戶端與服務器會經過交換幀來通訊,幀也是基於這個新協議通訊的最小單位。全部幀都共享一個8字節的首部,其中包括幀的長度、類型、標誌,還有一個保留位和一個31位的流標識符。 算法

在這裏插入圖片描述

共有的8字節幀首部
  • 16位的長度前綴意味着一幀大約能夠攜帶64KB數據,不包括8字節首部
  • 8位的類型字段決定如何解釋幀其他部分的內容
  • 8位的標誌字段容許不一樣的幀類型定義特定於幀的消息標誌
  • 1位的保留字段始終置爲0
  • 31位的流標識符惟一標識HTTP 2.0的流

HTTP 2.0規定了如下的幀類型。promise

  • DATA,用於傳輸HTTP消息體
  • HEADERS,用於傳輸關於流的額外的首部字段(Header
  • PRIORITY,用於指定或者從新指定流的優先級
  • RST_STREAM,用於通知流的非正常終止
  • SETTINGS,用於通知兩端通訊方式的配置數據
  • PUSH_PROMISE,用於發出建立流和服務器引用資源的要約
  • PING,用於計算往返時間,執行「活性」檢查
  • GOAWAY,用於通知客戶端/服務器中止在當前鏈接中建立流
  • WINDOW_UPDATE,用於針對個別流或者個別鏈接實現流量控制
  • CONTINUATION,用於繼續一系列首部塊片斷
2.一、HEADER幀

 在發送應用數據以前,必須建立一個新流並隨之發送相應的元數據,好比流的優先級、HTTP首部等。HTTP 2.0協議規定客戶端和服務器均可以發起新流,所以有如下兩種可能。服務器

  • 客戶端經過發送HEADERS幀來發起新流,這個幀裏包含帶有新流ID的公用首部、可選的31位優先值,以及一組HTTP鍵值對首部
  • 服務器經過發送PUSH_PROMISE幀來發起推送流,這個幀與HEADER幀等效,但它包含「要約流ID」,沒有優先值

在這裏插入圖片描述

帶優先值得HEADERS幀
2.二、DATA幀

 應用數據能夠分爲多個DATA幀,最後一幀要翻轉幀首部的END_STREAM字段。 網絡

在這裏插入圖片描述

DATA幀

 數據淨荷不會被另行編碼或壓縮。DATA幀的編碼方式取決於應用或者服務器,純文本、gzip壓縮、圖片或者視頻壓縮格式均可以。整個幀由公用的8字節首部及HTTP淨荷組成。  從技術上說,DATA幀的長度字段決定了每幀的數據淨荷最多可達2^{31}-1(65535)字節。但是,爲了減小隊首阻塞,HTTP 2.0標準要求DATA幀不能超過 2^{14}-1(16383)字節。長度超過這個閥值的數據,就得分幀發送。併發

三、HTTP 2.0在OKHttp中的應用

HTTP 2.0是經過RealConnectionstartHttp2方法開啓的,在該方法中會建立一個Http2Connection對象,而後調用Http2Connectionstart方法。app

private void startHttp2(int pingIntervalMillis) throws IOException {
    socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
    //建立Http2Connection對象
    http2Connection = new Http2Connection.Builder(true)
        .socket(socket, route.address().url().host(), source, sink)
        .listener(this)
        .pingIntervalMillis(pingIntervalMillis)
        .build();
    //開啓HTTP 2.0
    http2Connection.start();
  }
複製代碼

 在start方法中會首先給服務器發送一個字符串PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n來進行協議的最終肯定,並用於創建 HTTP/2 鏈接的初始設置。而後給服務器發送一個SETTINGS類型的Header幀,該幀主要是將客戶端每一幀的最大容量、Header表的大小、是否開啓推送等信息告訴給服務器。若是Window的大小發生改變,就還須要更新Window的大小(HTTP 2.0的默認窗口大小爲64KB,而客戶端則須要將該大小改成16M,從而避免頻繁的更新)。最後開啓一個子線程來讀取從服務器返回的數據。

public void start() throws IOException {
    start(true);
  }
  void start(boolean sendConnectionPreface) throws IOException {
    if (sendConnectionPreface) {
      //發送一個字符串PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n來進行協議的最終肯定,即序言幀
      writer.connectionPreface();
      //告訴服務器本地的配置信息
      writer.settings(okHttpSettings);
      //okHttpSetting中Window的大小是設置爲16M
      int windowSize = okHttpSettings.getInitialWindowSize();
      //默認是64kb,但若是在客戶端則須要從新設置爲16M
      if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) {
        //更新窗口大小
        writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE);
      }
    }
    //子線程監聽服務器返回的消息
    new Thread(readerRunnable).start(); // Not a daemon thread.
  }

複製代碼

 從ReaderRunnable的名稱就能夠看出它是用來讀取從服務器返回的各類類型數據。

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

    @Override protected void execute() {
      ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
      ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
      try {
        //讀取服務器返回的序言幀
        reader.readConnectionPreface(this);
        //不斷的讀取下一幀,全部消息從這裏開始分發
        while (reader.nextFrame(false, this)) {
        }
        connectionErrorCode = ErrorCode.NO_ERROR;
        streamErrorCode = ErrorCode.CANCEL;
      } catch (IOException e) {
        ...
      } finally {
        ...
      }
    }
    //讀取返回的DATA類型數據
    @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length) throws IOException {...}
    //讀取返回的HEADERS類型數據
    @Override public void headers(boolean inFinished, int streamId, int associatedStreamId, List<Header> headerBlock) {...}
    //讀取返回的RST_TREAM類型數據 
    @Override public void rstStream(int streamId, ErrorCode errorCode) {...}
    //讀取返回的SETTINGS類型數據
    @Override public void settings(boolean clearPrevious, Settings newSettings) {...}
    //回覆服務器返回的ackSettings
    private void applyAndAckSettings(final Settings peerSettings) ...} //恢復客戶端發送的SETTING數據,客戶端默認不實現 @Override public void ackSettings() {...}
    //讀取返回的PING類型數據
    @Override public void ping(boolean reply, int payload1, int payload2) {...}
    //讀取服務器返回的GOAWAY類型數據
    @Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {...}
    //讀取服務器返回的WINDOW_UPDATE類型數據
    @Override public void windowUpdate(int streamId, long windowSizeIncrement) {...}
    //讀取服務器返回的PRIORITY類型數據
    @Override public void priority(int streamId, int streamDependency, int weight, boolean exclusive) {...}
    //讀取返回的PUSH_PROMISE類型數據
    @Override
    public void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders) {... }
    //備用Service
    @Override public void alternateService(int streamId, String origin, ByteString protocol, String host, int port, long maxAge) {...}
  }
複製代碼

 上面簡述了在OkHttp中如何開啓HTTP 2.0協議。下面就來介紹客戶端與服務器經過HTTP 2.0協議來進行數據讀寫操做。

3.一、向服務器寫入Headers

 向服務器寫入Header是經過httpCodec.writeRequestHeaders(request)來實現的,httpCodecHTTP 2.0協議下的實現類是Http2CodecwriteRequestHeaders方法主要是建立一個新流Http2Stream,在這個流建立成功後就會向服務器發送Headers類型數據。

boolean hasRequestBody = request.body() != null;
    List<Header> requestHeaders = http2HeadersList(request);
    //建立新流
    stream = connection.newStream(requestHeaders, hasRequestBody);
    //咱們可能在建立新流併發送Headers時被要求取消,但仍然沒有要關閉的流。
    if (canceled) {
      stream.closeLater(ErrorCode.CANCEL);
      throw new IOException("Canceled");
    }
    ...
  }
  //如下方法在Http2Connection類中
  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) {
        //每一個TCP鏈接的流數量不能超過Integer.MAX_VALUE
        if (nextStreamId > Integer.MAX_VALUE / 2) {
          shutdown(REFUSED_STREAM);
        }
        if (shutdown) {
          throw new ConnectionShutdownException();
        }
        //每一個流的ID
        streamId = nextStreamId;
        //下一個流的ID是在當前流ID基礎上加2
        nextStreamId += 2;
        //建立新流
        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) {
        //向服務器寫入Headers
        writer.headers(outFinished, streamId, requestHeaders);
      } else if (client) {
        throw new IllegalArgumentException("client streams shouldn't have associated stream IDs");
      } else {//用於服務器
        writer.pushPromise(associatedStreamId, streamId, requestHeaders);
      }
    }
    //刷新
    if (flushHeaders) {
      writer.flush();
    }

    return stream;
  }
複製代碼

 在客戶端,流的ID是從3開始的全部奇數,在服務器,流的ID則是全部偶數。在Http2Connection的構造函數中定義了定義了流ID的初始值。

Http2Connection(Builder builder) {
    ....
    //若是是客戶端,流的ID則從1開始
    nextStreamId = builder.client ? 1 : 2;
    if (builder.client) {
      //在HTTP2中,1保留,用於升級
      nextStreamId += 2;
    }
    ...
  }
複製代碼
3.二、讀取服務器返回的Headers

readResponseHeaders是從服務器讀取Headers數據,該方法在Http2Codec中。

@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
    //從流中拿到Headers信息,
    Headers headers = stream.takeHeaders();
    Response.Builder responseBuilder = readHttp2HeadersList(headers, protocol);
    if (expectContinue && Internal.instance.code(responseBuilder) == HTTP_CONTINUE) {
      return null;
    }
    return responseBuilder;
  }
  //該方法在Http2Stream中
  public synchronized Headers takeHeaders() throws IOException {
    readTimeout.enter();
    try {
      //若是隊列中沒有數據就等待
      while (headersQueue.isEmpty() && errorCode == null) {
        waitForIo();
      }
    } finally {
      readTimeout.exitAndThrowIfTimedOut();
    }
    //從隊列中拿到Headers數據
    if (!headersQueue.isEmpty()) {
      return headersQueue.removeFirst();
    }
    throw new StreamResetException(errorCode);
  }
複製代碼

headersQueue是一個雙端隊列,它主要是存儲服務器返回的Headers。當服務器返回Headers時,就會更新該鏈表。

3.三、讀/寫Body

 在建立流的時候,都會建立一個FramingSinkFramingSource對象。FramingSink用來向服務器寫入數據,FramingSource則讀取服務器返回的數據。所以關於讀/寫Body其實就是對Okio的運用,不熟悉Okio的能夠先去了解一下Okio的知識。

//向服務器寫數據
  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 {
        //向服務器寫入DATA類型數據
        connection.writeData(id, outFinished && toWrite == sendBuffer.size(), sendBuffer, toWrite);
      } finally {
        writeTimeout.exitAndThrowIfTimedOut();
      }
    }
    ...
  }
  //從服務器讀取數據
  private final class FramingSource implements Source {
    //將從網絡讀取的數據寫入該Buffer,僅供讀線程訪問
    private final Buffer receiveBuffer = new Buffer();

    //可讀buffer
    private final Buffer readBuffer = new Buffer();

    //緩衝的最大字節數
    private final long maxByteCount;

    ...
    //從receiveBuffer中讀取數據
    @Override public long read(Buffer sink, long byteCount) throws IOException {...}
    ...
    //接收服務器傳遞的數據,僅在ReaderRunnable中調用
    void receive(BufferedSource in, long byteCount) throws IOException {...}
    ...
  }
複製代碼
3.四、Http2Reader與Http2Writer

 前面介紹了從服務器讀寫數據,但不管如何都離不開Http2ReaderHttp2Writer這兩個類,畢竟這兩個類纔是真正向服務器執行讀寫操做的。先來看向服務器寫數據。

final class Http2Writer implements Closeable {
  ...

  //寫入序言幀,來進行協議的最終肯定
  public synchronized void connectionPreface() throws IOException {...}

  //發送PUSH_PROMISE類型數據
  public synchronized void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders) throws IOException {...}
  ...
  //發送RST_TREAM類型數據 
  public synchronized void rstStream(int streamId, ErrorCode errorCode) throws IOException {...}

  

  //發送DATA類型數據
  public synchronized void data(boolean outFinished, int streamId, Buffer source, int byteCount) throws IOException {...}

  //發送SETTINGS類型數據
  public synchronized void settings(Settings settings) throws IOException {...}

  //發送PING類型數據
  public synchronized void ping(boolean ack, int payload1, int payload2) throws IOException {...}

  //發送GOAWAY類型數據
  public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData) throws IOException {...}

  //發送WINDOW_UPDATE類型數據,進行Window更新
  public synchronized void windowUpdate(int streamId, long windowSizeIncrement) throws IOException {...}
  //發送HEADERS類型數據
  public void frameHeader(int streamId, int length, byte type, byte flags) throws IOException {...}

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

  ...
  //寫入CONTINUATION類型數據
  private void writeContinuationFrames(int streamId, long byteCount) throws IOException {...}
  //寫入headers
  void headers(boolean outFinished, int streamId, List<Header> headerBlock) throws IOException {...}
}
複製代碼

 下面再來看看從服務器讀數據,基本上就是根據數據的類型來進行分發。

final class Http2Reader implements Closeable {
  ...
  //讀取數據
  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() & 0xff);
    if (requireSettings && type != TYPE_SETTINGS) {
      throw ioException("Expected a SETTINGS frame but was %s", type);
    }
    byte flags = (byte) (source.readByte() & 0xff);
    int streamId = (source.readInt() & 0x7fffffff); // Ignore reserved bit.
    if (logger.isLoggable(FINE)) logger.fine(frameLog(true, streamId, length, type, flags));
    //這裏的handler是ReaderRunnable對象
    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;
  }
  ...
}
複製代碼

 在Http2ReaderHttp2Writer中都是以幀的形式(二進制)來讀取或者寫入數據的,這樣相對字符串效率會更高,固然,咱們還能夠用哈夫曼算法(OkHttp支持哈夫曼算法)來對幀進行壓縮,從而得到更好的性能。  記得在HTTP 1.x協議下的網絡優化就有用Protocol Buffer(二進制)來替代字符串傳遞這一個選擇,而若是用HTTP 2.0則無需使用Protocol Buffer

四、總結

 到這裏,相必對HTTP 2.0有了一個大概的瞭解,更多的就須要去實踐了。固然若是要使用HTTP 2.0協議,就須要客戶端及服務器一塊兒才能搞定。注意:目前OKHttp僅支持在https請求下使用HTTP 2.0

【參考資料】 《Web性能權威指南》 科普:QUIC協議原理分析 初識HTTP2.0協議 HTTP 2.0 協議詳解 HTTP協議探究(六):H2幀詳解和HTTP優化 HTTP/2筆記之鏈接創建

相關文章
相關標籤/搜索