Android|HTTP 2.0与OkHttp

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

  • 流,已建立的连接上的双向字节流。
  • 消息,与逻辑消息(RequestResponse)对应的完整的一系列数据帧。
  • 帧,HTTP 2.0通信的最小单位,如Header帧(存储的是Header)、DATA帧(存储的是发送的内容或者内容的一部分)。
1、HTTP 2.0简介 ?总所周知,HTTP 1.x拥有队首阻塞、不支持多路复用、Header无法压缩等诸多缺点。尽管针对这些缺点也提出了很多解决方案,如长连接、连接与合并请求、HTTP管道等,但都治标不治本,直到HTTP 2.0的出现,它新增的以下设计从根本上解决了HTTP 1.x所面临的诸多问题。
  • 二进制分帧层,是HTTP 2.0性能增强的核心,改变了客户端与服务器之间交互数据的方式,将传输的信息(HeaderBody等)分割为更小的消息和帧,并采用二进制格式的编码。
  • 并行请求与响应,客户端及服务器可以把HTTP消息分解为互不依赖的帧,然后乱序发送,最后再在另一端把这些消息组合起来。
  • 请求优先级(0表示最高优先级、 2 31 2^{31} 231-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使用换行符分割纯文本不同,二进制分帧层更加简介,通过代码处理起来更简单也更有效。
Android|HTTP 2.0与OkHttp
文章图片

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

共有的8字节帧首部
  • 16位的长度前缀意味着一帧大约可以携带64KB数据,不包括8字节首部
  • 8位的类型字段决定如何解释帧其余部分的内容
  • 8位的标志字段允许不同的帧类型定义特定于帧的消息标志
  • 1位的保留字段始终置为0
  • 31位的流标识符唯一标识HTTP 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”,没有优先值
Android|HTTP 2.0与OkHttp
文章图片

带优先值得HEADERS帧 2.2、DATA帧 ?应用数据可以分为多个DATA帧,最后一帧要翻转帧首部的END_STREAM字段。
Android|HTTP 2.0与OkHttp
文章图片

DATA帧 ?数据净荷不会被另行编码或压缩。DATA帧的编码方式取决于应用或者服务器,纯文本、gzip压缩、图片或者视频压缩格式都可以。整个帧由公用的8字节首部及HTTP净荷组成。
?从技术上说,DATA帧的长度字段决定了每帧的数据净荷最多可达 2 31 2^{31} 231-1(65535)字节。可是,为了减少队首阻塞,HTTP 2.0标准要求DATA帧不能超过2 14 ? 1 2^{14}-1 214?1(16383)字节。长度超过这个阀值的数据,就得分帧发送。
3、HTTP 2.0在OKHttp中的应用 ?HTTP 2.0是通过RealConnectionstartHttp2方法开启的,在该方法中会创建一个Http2Connection对象,然后调用Http2Connectionstart方法。
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 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 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.1、向服务器写入Headers ?向服务器写入Header是通过httpCodec.writeRequestHeaders(request)来实现的,httpCodecHTTP 2.0协议下的实现类是Http2CodecwriteRequestHeaders方法主要是创建一个新流Http2Stream,在这个流创建成功后就会向服务器发送Headers类型数据。
boolean hasRequestBody = request.body() != null; List requestHeaders = http2HeadersList(request); //创建新流 stream = connection.newStream(requestHeaders, hasRequestBody); //我们可能在创建新流并发送Headers时被要求取消,但仍然没有要关闭的流。 if (canceled) { stream.closeLater(ErrorCode.CANCEL); throw new IOException("Canceled"); } ... } //以下方法在Http2Connection类中 public Http2Stream newStream(List requestHeaders, boolean out) throws IOException { return newStream(0, requestHeaders, out); }private Http2Stream newStream( int associatedStreamId, List 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.2、读取服务器返回的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.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.4、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 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 headerBlock) throws IOException {...} }

【Android|HTTP 2.0与OkHttp】?下面再来看看从服务器读数据,基本上就是根据数据的类型来进行分发。
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. }//0123 //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
4、总结 ?到这里,相必对HTTP 2.0有了一个大概的了解,更多的就需要去实践了。当然如果要使用HTTP 2.0协议,就需要客户端及服务器一起才能搞定。注意:目前OKHttp仅支持在https请求下使用HTTP 2.0
【参考资料】
《Web性能权威指南》
科普:QUIC协议原理分析
初识HTTP2.0协议
HTTP 2.0 协议详解
HTTP协议探究(六):H2帧详解和HTTP优化
HTTP/2笔记之连接建立

    推荐阅读