您的位置 首页 杂谈

HTTP 2.0与OkHttp

国务院总理是谁,qmsw,天河北房屋出租

1、HTTP 2.0简介 总所周知, HTTP 1.x 拥有队首阻塞、不支持多路复用、 Header 无法压缩等诸多缺点。尽管针对这些缺点也提出了很多解决方案,如长连接、连接与合并…

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

HTTP 2.0HTTP 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”,没有优先值

带优先值得HEADERS帧

2.2、DATA帧

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

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. ListHeader 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, ListHeader 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. ListHeader 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(ListHeader requestHeaders, boolean out) throws IOException {

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

  15. }

  16. private Http2Stream newStream(

  17. int associatedStreamId, ListHeader 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. ListHeader 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, ListHeader 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://www.bafangmiaomu.com/shehui/98641/

作者: 头条新闻

为您推荐