HTTP 2.0与OkHttp

Posted Java杂记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HTTP 2.0与OkHttp相关的知识,希望对你有一定的参考价值。

HTTP 2.0 是对1.x的扩展而非替代,之所以是“2.0”,是因为它改变了客户端与服务器之间交换数据的方式。 HTTP 2.0 增加了新的二进制分帧数据层,而这一层并不兼容之前的 HTTP 1.x 服务器及客户端——是谓2.0。 在正式介绍 HTTP 2.0 之前,我们需要先了解几个概念。

  • 流,已建立的连接上的双向字节流。

  • 消息,与逻辑消息( Request 、 Response )对应的完整的一系列数据帧。

  • 帧, HTTP 2.0 通信的最小单位,如 Header 帧(存储的是 Header )、 DATA 帧(存储的是发送的内容或者内容的一部分)。

1、HTTP 2.0简介

总所周知, HTTP 1.x 拥有队首阻塞、不支持多路复用、 Header 无法压缩等诸多缺点。尽管针对这些缺点也提出了很多解决方案,如长连接、连接与合并请求、HTTP管道等,但都治标不治本,直到 HTTP 2.0 的出现,它新增的以下设计从根本上解决了 HTTP 1.x 所面临的诸多问题。

  • 二进制分帧层 ,是 HTTP 2.0 性能增强的核心,改变了客户端与服务器之间交互数据的方式,将传输的信息( Header 、 Body 等)分割为更小的消息和帧,并采用二进制格式的编码。

  • 并行请求与响应 ,客户端及服务器可以把 HTTP 消息分解为互不依赖的帧,然后乱序发送,最后再在另一端把这些消息组合起来。

  • 请求优先级(0表示最高优先级、 -1表示最低优先级) ,每个流可以携带一个优先值,有了这个优先值,客户端及服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。但优先级的处理需要慎重,否则有可能会引入队首阻塞问题。

  • 单TCP连接 , HTTP 2.0 可以让所有数据流共用一个连接,从而更有效的使用 TCP 连接

  • 流量控制 ,控制每个流占用的资源,与 TCP 的流量控制实现是一模一样的。

  • 服务器推送 , HTTP 2.0 可以对一个客户端请求发送多个响应,即除了最初请求响应外,服务器还可以额外的向客户端推送资源,而无需客户端明确地请求。

  • 首部(Header)压缩 , HTTP 2.0 会在客户端及服务器使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不会再通过每次请求和响应发送。首部表在连接存续期间始终存在,由客户端及服务器共同渐进的更新。每个新的首部键-值对要么追加到当前表的末尾,要么替换表中的值。 虽然 HTTP 2.0 解决了1.x中的诸多问题,但它也存在以下问题。

虽然消除了 HTTP 队首阻塞现象,但 TCP 层次上仍然存在队首阻塞现象。要想彻底解决这个问题,就需要彻底抛弃 TCP ,自己来定义协议。可以参考谷歌的QUIC。 如果 TCP 窗口缩放被禁用,那宽带延迟积效应可能会限制连接的吞吐量。 丢包时, TCP 拥塞窗口会缩小。

2、二进制分帧简介

HTTP 2.0 的根本改进还是新增的二进制分帧层。与 HTTP 1.x 使用换行符分割纯文本不同,二进制分帧层更加简介,通过代码处理起来更简单也更有效。

HTTP 2.0与OkHttp

建立了 HTTP 2.0 连接后,客户端与服务器会通过交换帧来通信,帧也是基于这个新协议通信的最小单位。所有帧都共享一个8字节的首部,其中包括帧的长度、类型、标志,还有一个保留位和一个31位的流标识符。

HTTP 2.0与OkHttpHTTP 2.0 HTTP 2.0 规定了以下的帧类型。

  • DATA ,用于传输 HTTP 消息体

  • HEADERS ,用于传输关于流的额外的首部字段( Header )

  • PRIORITY ,用于指定或者重新指定流的优先级

  • RST_STREAM ,用于通知流的非正常终止

  • SETTINGS ,用于通知两端通信方式的配置数据

  • PUSH_PROMISE ,用于发出创建流和服务器引用资源的要约

  • PING ,用于计算往返时间,执行“活性”检查

  • GOAWAY ,用于通知客户端/服务器停止在当前连接中创建流

  • WINDOW_UPDATE ,用于针对个别流或者个别连接实现流量控制

  • CONTINUATION ,用于继续一系列首部块片段

2.1、HEADER帧

在发送应用数据之前,必须创建一个新流并随之发送相应的元数据,比如流的优先级、HTTP首部等。 HTTP 2.0 协议规定客户端和服务器都可以发起新流,因此有以下两种可能。

客户端通过发送 HEADERS 帧来发起新流,这个帧里包含带有新流ID的公用首部、可选的31位优先值,以及一组 HTTP 键值对首部 服务器通过发送 PUSH_PROMISE 帧来发起推送流,这个帧与 HEADER 帧等效,但它包含“要约流ID”,没有优先值HTTP 2.0与OkHttp

带优先值得HEADERS帧

2.2、DATA帧

应用数据可以分为多个DATA帧,最后一帧要翻转帧首部的 END_STREAM 字段。

HTTP 2.0与OkHttp

DATA帧 数据净荷不会被另行编码或压缩。DATA帧的编码方式取决于应用或者服务器,纯文本、gzip压缩、图片或者视频压缩格式都可以。整个帧由公用的8字节首部及HTTP净荷组成。 从技术上说,DATA帧的长度字段决定了每帧的数据净荷最多可达 -1(65535)字节。可是,为了减少队首阻塞, HTTP 2.0 标准要求DATA帧不能超过 (16383)字节。长度超过这个阀值的数据,就得分帧发送。

3、HTTP 2.0在OKHttp中的应用

HTTP 2.0 是通过 RealConnection 的 startHttp2 方法开启的,在该方法中会创建一个 Http2Connection 对象,然后调用 Http2Connection 的 start 方法。

 
   
   
 
  1. private void startHttp2(int pingIntervalMillis) throws IOException {

  2. socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.

  3. //创建Http2Connection对象

  4. http2Connection = new Http2Connection.Builder(true)

  5. .socket(socket, route.address().url().host(), source, sink)

  6. .listener(this)

  7. .pingIntervalMillis(pingIntervalMillis)

  8. .build();

  9. //开启HTTP 2.0

  10. http2Connection.start();

  11. }

在 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 ,从而避免频繁的更新)。最后开启一个子线程来读取从服务器返回的数据。

 
   
   
 
  1. public void start() throws IOException {

  2. start(true);

  3. }

  4. void start(boolean sendConnectionPreface) throws IOException {

  5. if (sendConnectionPreface) {

  6. //发送一个字符串PRI * HTTP/2.0/r/n/r/nSM/r/n/r/n来进行协议的最终确定,即序言帧

  7. writer.connectionPreface();

  8. //告诉服务器本地的配置信息

  9. writer.settings(okHttpSettings);

  10. //okHttpSetting中Window的大小是设置为16M

  11. int windowSize = okHttpSettings.getInitialWindowSize();

  12. //默认是64kb,但如果在客户端则需要重新设置为16M

  13. if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) {

  14. //更新窗口大小

  15. writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE);

  16. }

  17. }

  18. //子线程监听服务器返回的消息

  19. new Thread(readerRunnable).start(); // Not a daemon thread.

  20. }

从 ReaderRunnable 的名称就可以看出它是用来读取从服务器返回的各种类型数据。

 
   
   
 
  1. class ReaderRunnable extends NamedRunnable implements Http2Reader.Handler {

  2. ...


  3. @Override protected void execute() {

  4. ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;

  5. ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;

  6. try {

  7. //读取服务器返回的序言帧

  8. reader.readConnectionPreface(this);

  9. //不断的读取下一帧,所有消息从这里开始分发

  10. while (reader.nextFrame(false, this)) {

  11. }

  12. connectionErrorCode = ErrorCode.NO_ERROR;

  13. streamErrorCode = ErrorCode.CANCEL;

  14. } catch (IOException e) {

  15. ...

  16. } finally {

  17. ...

  18. }

  19. }

  20. //读取返回的DATA类型数据

  21. @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length)

  22. throws IOException {...}

  23. //读取返回的HEADERS类型数据

  24. @Override public void headers(boolean inFinished, int streamId, int associatedStreamId,

  25. List<Header> headerBlock) {...}

  26. //读取返回的RST_TREAM类型数据

  27. @Override public void rstStream(int streamId, ErrorCode errorCode) {...}

  28. //读取返回的SETTINGS类型数据

  29. @Override public void settings(boolean clearPrevious, Settings newSettings) {...}

  30. //回复服务器返回的ackSettings

  31. private void applyAndAckSettings(final Settings peerSettings) ...}

  32. //恢复客户端发送的SETTING数据,客户端默认不实现

  33. @Override public void ackSettings() {...}

  34. //读取返回的PING类型数据

  35. @Override public void ping(boolean reply, int payload1, int payload2) {...}

  36. //读取服务器返回的GOAWAY类型数据

  37. @Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {...}

  38. //读取服务器返回的WINDOW_UPDATE类型数据

  39. @Override public void windowUpdate(int streamId, long windowSizeIncrement) {...}

  40. //读取服务器返回的PRIORITY类型数据

  41. @Override public void priority(int streamId, int streamDependency, int weight,

  42. boolean exclusive) {...}

  43. //读取返回的PUSH_PROMISE类型数据

  44. @Override

  45. public void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders) {... }

  46. //备用Service

  47. @Override public void alternateService(int streamId, String origin, ByteString protocol,

  48. String host, int port, long maxAge) {...}

  49. }

上面简述了在 OkHttp 中如何开启 HTTP 2.0 协议。下面就来介绍客户端与服务器通过 HTTP 2.0 协议来进行数据读写操作。

3.1、向服务器写入Headers

向服务器写入 Header 是通过 httpCodec.writeRequestHeaders(request) 来实现的, httpCodec 在 HTTP 2.0 协议下的实现类是 Http2Codec 。 writeRequestHeaders 方法主要是创建一个新流 Http2Stream ,在这个流创建成功后就会向服务器发送 Headers 类型数据。

 
   
   
 
  1. boolean hasRequestBody = request.body() != null;

  2. List<Header> requestHeaders = http2HeadersList(request);

  3. //创建新流

  4. stream = connection.newStream(requestHeaders, hasRequestBody);

  5. //我们可能在创建新流并发送Headers时被要求取消,但仍然没有要关闭的流。

  6. if (canceled) {

  7. stream.closeLater(ErrorCode.CANCEL);

  8. throw new IOException("Canceled");

  9. }

  10. ...

  11. }

  12. //以下方法在Http2Connection类中

  13. public Http2Stream newStream(List<Header> requestHeaders, boolean out) throws IOException {

  14. return newStream(0, requestHeaders, out);

  15. }


  16. private Http2Stream newStream(

  17. int associatedStreamId, List<Header> requestHeaders, boolean out) throws IOException {

  18. ...

  19. synchronized (writer) {

  20. synchronized (this) {

  21. //每个TCP连接的流数量不能超过Integer.MAX_VALUE

  22. if (nextStreamId > Integer.MAX_VALUE / 2) {

  23. shutdown(REFUSED_STREAM);

  24. }

  25. if (shutdown) {

  26. throw new ConnectionShutdownException();

  27. }

  28. //每个流的ID

  29. streamId = nextStreamId;

  30. //下一个流的ID是在当前流ID基础上加2

  31. nextStreamId += 2;

  32. //创建新流

  33. stream = new Http2Stream(streamId, this, outFinished, inFinished, null);

  34. flushHeaders = !out || bytesLeftInWriteWindow == 0L || stream.bytesLeftInWriteWindow == 0L;

  35. if (stream.isOpen()) {

  36. streams.put(streamId, stream);

  37. }

  38. }

  39. if (associatedStreamId == 0) {

  40. //向服务器写入Headers

  41. writer.headers(outFinished, streamId, requestHeaders);

  42. } else if (client) {

  43. throw new IllegalArgumentException("client streams shouldn't have associated stream IDs");

  44. } else {//用于服务器

  45. writer.pushPromise(associatedStreamId, streamId, requestHeaders);

  46. }

  47. }

  48. //刷新

  49. if (flushHeaders) {

  50. writer.flush();

  51. }


  52. return stream;

  53. }

在客户端,流的ID是从3开始的所有奇数,在服务器,流的ID则是所有偶数。在 Http2Connection 的构造函数中定义了定义了流ID的初始值。

 
   
   
 
  1. Http2Connection(Builder builder) {

  2. ....

  3. //如果是客户端,流的ID则从1开始

  4. nextStreamId = builder.client ? 1 : 2;

  5. if (builder.client) {

  6. //在HTTP2中,1保留,用于升级

  7. nextStreamId += 2;

  8. }

  9. ...

  10. }

3.2、读取服务器返回的Headers

readResponseHeaders 是从服务器读取 Headers 数据,该方法在 Http2Codec 中。

 
   
   
 
  1. @Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {

  2. //从流中拿到Headers信息,

  3. Headers headers = stream.takeHeaders();

  4. Response.Builder responseBuilder = readHttp2HeadersList(headers, protocol);

  5. if (expectContinue && Internal.instance.code(responseBuilder) == HTTP_CONTINUE) {

  6. return null;

  7. }

  8. return responseBuilder;

  9. }

  10. //该方法在Http2Stream中

  11. public synchronized Headers takeHeaders() throws IOException {

  12. readTimeout.enter();

  13. try {

  14. //如果队列中没有数据就等待

  15. while (headersQueue.isEmpty() && errorCode == null) {

  16. waitForIo();

  17. }

  18. } finally {

  19. readTimeout.exitAndThrowIfTimedOut();

  20. }

  21. //从队列中拿到Headers数据

  22. if (!headersQueue.isEmpty()) {

  23. return headersQueue.removeFirst();

  24. }

  25. throw new StreamResetException(errorCode);

  26. }

headersQueue 是一个双端队列,它主要是存储服务器返回的 Headers 。当服务器返回 Headers 时,就会更新该链表。

3.3、读/写Body

在创建流的时候,都会创建一个 FramingSink 及 FramingSource 对象。 FramingSink 用来向服务器写入数据, FramingSource 则读取服务器返回的数据。因此关于读/写 Body 其实就是对 Okio 的运用,不熟悉 Okio 的可以先去了解一下Okio的知识。

 
   
   
 
  1. //向服务器写数据

  2. final class FramingSink implements Sink {

  3. private static final long EMIT_BUFFER_SIZE = 16384;


  4. ...

  5. @Override public void write(Buffer source, long byteCount) throws IOException {

  6. assert (!Thread.holdsLock(Http2Stream.this));

  7. sendBuffer.write(source, byteCount);

  8. while (sendBuffer.size() >= EMIT_BUFFER_SIZE) {

  9. emitFrame(false);

  10. }

  11. }


  12. //

  13. private void emitFrame(boolean outFinished) throws IOException {

  14. ...

  15. try {

  16. //向服务器写入DATA类型数据

  17. connection.writeData(id, outFinished && toWrite == sendBuffer.size(), sendBuffer, toWrite);

  18. } finally {

  19. writeTimeout.exitAndThrowIfTimedOut();

  20. }

  21. }

  22. ...

  23. }

  24. //从服务器读取数据

  25. private final class FramingSource implements Source {

  26. //将从网络读取的数据写入该Buffer,仅供读线程访问

  27. private final Buffer receiveBuffer = new Buffer();


  28. //可读buffer

  29. private final Buffer readBuffer = new Buffer();


  30. //缓冲的最大字节数

  31. private final long maxByteCount;


  32. ...

  33. //从receiveBuffer中读取数据

  34. @Override public long read(Buffer sink, long byteCount) throws IOException {...}

  35. ...

  36. //接收服务器传递的数据,仅在ReaderRunnable中调用

  37. void receive(BufferedSource in, long byteCount) throws IOException {...}

  38. ...

  39. }

3.4、Http2Reader与Http2Writer

前面介绍了从服务器读写数据,但无论如何都离不开 Http2Reader 与 Http2Writer 这两个类,毕竟这两个类才是真正向服务器执行读写操作的。先来看向服务器写数据。

 
   
   
 
  1. final class Http2Writer implements Closeable {

  2. ...


  3. //写入序言帧,来进行协议的最终确定

  4. public synchronized void connectionPreface() throws IOException {...}


  5. //发送PUSH_PROMISE类型数据

  6. public synchronized void pushPromise(int streamId, int promisedStreamId,

  7. List<Header> requestHeaders) throws IOException {...}

  8. ...

  9. //发送RST_TREAM类型数据

  10. public synchronized void rstStream(int streamId, ErrorCode errorCode)

  11. throws IOException {...}




  12. //发送DATA类型数据

  13. public synchronized void data(boolean outFinished, int streamId, Buffer source, int byteCount)

  14. throws IOException {...}


  15. //发送SETTINGS类型数据

  16. public synchronized void settings(Settings settings) throws IOException {...}


  17. //发送PING类型数据

  18. public synchronized void ping(boolean ack, int payload1, int payload2) throws IOException {...}


  19. //发送GOAWAY类型数据

  20. public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData)

  21. throws IOException {...}


  22. //发送WINDOW_UPDATE类型数据,进行Window更新

  23. public synchronized void windowUpdate(int streamId, long windowSizeIncrement) throws IOException {...}

  24. //发送HEADERS类型数据

  25. public void frameHeader(int streamId, int length, byte type, byte flags) throws IOException {...}


  26. @Override public synchronized void close() throws IOException {

  27. closed = true;

  28. sink.close();

  29. }


  30. ...

  31. //写入CONTINUATION类型数据

  32. private void writeContinuationFrames(int streamId, long byteCount) throws IOException {...}

  33. //写入headers

  34. void headers(boolean outFinished, int streamId, List<Header> headerBlock) throws IOException {...}

  35. }

下面再来看看从服务器读数据,基本上就是根据数据的类型来进行分发。

 
   
   
 
  1. final class Http2Reader implements Closeable {

  2. ...

  3. //读取数据

  4. public boolean nextFrame(boolean requireSettings, Handler handler) throws IOException {

  5. try {

  6. source.require(9); // Frame header size

  7. } catch (IOException e) {

  8. return false; // This might be a normal socket close.

  9. }


  10. // 0 1 2 3

  11. // 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

  12. // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

  13. // | Length (24) |

  14. // +---------------+---------------+---------------+

  15. // | Type (8) | Flags (8) |

  16. // +-+-+-----------+---------------+-------------------------------+

  17. // |R| Stream Identifier (31) |

  18. // +=+=============================================================+

  19. // | Frame Payload (0...) ...

  20. // +---------------------------------------------------------------+

  21. int length = readMedium(source);

  22. if (length < 0 || length > INITIAL_MAX_FRAME_SIZE) {

  23. throw ioException("FRAME_SIZE_ERROR: %s", length);

  24. }

  25. byte type = (byte) (source.readByte() & 0xff);

  26. if (requireSettings && type != TYPE_SETTINGS) {

  27. throw ioException("Expected a SETTINGS frame but was %s", type);

  28. }

  29. byte flags = (byte) (source.readByte() & 0xff);

  30. int streamId = (source.readInt() & 0x7fffffff); // Ignore reserved bit.

  31. if (logger.isLoggable(FINE)) logger.fine(frameLog(true, streamId, length, type, flags));

  32. //这里的handler是ReaderRunnable对象

  33. switch (type) {

  34. case TYPE_DATA:

  35. readData(handler, length, flags, streamId);

  36. break;


  37. case TYPE_HEADERS:

  38. readHeaders(handler, length, flags, streamId);

  39. break;


  40. case TYPE_PRIORITY:

  41. readPriority(handler, length, flags, streamId);

  42. break;


  43. case TYPE_RST_STREAM:

  44. readRstStream(handler, length, flags, streamId);

  45. break;


  46. case TYPE_SETTINGS:

  47. readSettings(handler, length, flags, streamId);

  48. break;


  49. case TYPE_PUSH_PROMISE:

  50. readPushPromise(handler, length, flags, streamId);

  51. break;


  52. case TYPE_PING:

  53. readPing(handler, length, flags, streamId);

  54. break;


  55. case TYPE_GOAWAY:

  56. readGoAway(handler, length, flags, streamId);

  57. break;


  58. case TYPE_WINDOW_UPDATE:

  59. readWindowUpdate(handler, length, flags, streamId);

  60. break;


  61. default:

  62. // Implementations MUST discard frames that have unknown or unsupported types.

  63. source.skip(length);

  64. }

  65. return true;

  66. }

  67. ...

  68. }

在 Http2Reader 与 Http2Writer 中都是以帧的形式(二进制)来读取或者写入数据的,这样相对字符串效率会更高,当然,我们还可以用哈夫曼算法( OkHttp 支持哈夫曼算法)来对帧进行压缩,从而获得更好的性能。 记得在 HTTP 1.x 协议下的网络优化就有用 Protocol Buffer (二进制)来替代字符串传递这一个选择,而如果用 HTTP 2.0 则无需使用 Protocol Buffer 。

HTTP 2.0与OkHttp

HTTP 2.0与OkHttp HTTP 2.0与OkHttp HTTP 2.0与OkHttp HTTP 2.0与OkHttp HTTP 2.0与OkHttp HTTP 2.0与OkHttp HTTP 2.0与OkHttp HTTP 2.0与OkHttp

HTTP 2.0与OkHttp HTTP 2.0与OkHttp HTTP 2.0与OkHttp HTTP 2.0与OkHttp HTTP 2.0与OkHttp HTTP 2.0与OkHttp  


以上是关于HTTP 2.0与OkHttp的主要内容,如果未能解决你的问题,请参考以下文章

Retrofit 2.0 超能实践,okHttp完美支持Https传输

奇怪的知识: okhttp 是如何支持 Http2 的?

OkHttp 2.0 响应(POST 请求)正文字符串为空字符串

Retrofit 2.0:迄今为止最大的更新最好的Android HTTP客户端库

HTTP/2 与 OkHttp

OkHttp与HTTP协议