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表示最高优先级、 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
使用换行符分割纯文本不同,二进制分帧层更加简介,通过代码处理起来更简单也更有效。
建立了HTTP 2.0
连接后,客户端与服务器会通过交换帧来通信,帧也是基于这个新协议通信的最小单位。所有帧都共享一个8字节的首部,其中包括帧的长度、类型、标志,还有一个保留位和一个31位的流标识符。
- 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”,没有优先值
2.2、DATA帧
应用数据可以分为多个DATA帧,最后一帧要翻转帧首部的END_STREAM
字段。
数据净荷不会被另行编码或压缩。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
是通过RealConnection
的startHttp2
方法开启的,在该方法中会创建一个Http2Connection
对象,然后调用Http2Connection
的start
方法。
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.