下面的这张图对比了 HTTP/1、HTTPS 和 HTTP/2 的协议栈,你可以清晰地看到,HTTP/2 是建立在“HPack”“Stream”“TLS1.2”基础之上的,比 HTTP/1、HTTPS 复杂了一些。
连接前言
由于 HTTP/2“事实上”是基于 TLS,所以在正式收发数据之前,会有 TCP 握手和 TLS 握手,这两个步骤相信你一定已经很熟悉了,所以这里就略过去不再细说。
TLS 握手成功之后,客户端必须要发送一个“连接前言”(connection preface),用来确认建立 HTTP/2 连接。
这个“连接前言”是标准的 HTTP/1 请求报文,使用纯文本的 ASCII 码格式,请求方法是特别注册的一个关键字“PRI”,全文只有 24 个字节:
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
只要服务器收到这个“有魔力的字符串”,就知道客户端在 TLS 上想要的是 HTTP/2 协议,而不是其他别的协议,后面就会都使用 HTTP/2 的数据格式。
nginx 开启 http2 也比较简单
server {
listen 443 http2 ssl; # 443 端口 http2 ssl
server_name alvin.run;
#... 这是是上面的配置
}
头部压缩 (略)
上文讲过
二进制帧
HTTP/2 中传输的帧结构如下图所示
每个帧分为帧头和帧体。先是三个字节的帧长度,这个长度表示的是帧体的长度。
然后是帧类型,大概可以分为数据帧和控制帧两种。数据帧用来存放 HTTP 报文,控制帧用来管理流的传输。
接下来的一个字节是帧标志,里面一共有 8 个标志位,常用的有 END_HEADERS
表示头数据结束,END_STREAM
表示单方向数据发送结束。
后 4 个字节是 Stream ID
, 也就是流标识符,有了它,接收方就能从乱序的二进制帧中选择出 ID 相同的帧,按顺序组装成请求/响应报文。
流的状态变化
从前面可以知道,在 HTTP/2 中,所谓的流,其实就是二进制帧的双向传输的序列。那么在 HTTP/2 请求和响应的过程中,流的状态是如何变化的呢?
HTTP/2 其实也是借鉴了 TCP 状态变化的思想,根据帧的标志位来实现具体的状态改变。这里我们以一个普通的请求-响应过程为例来说明:
最开始两者都是空闲状态,当客户端发送 Headers 帧后,开始分配 Stream ID
, 此时客户端的流打开, 服务端接收之后服务端的流也打开,两端的流都打开之后,就可以互相传递数据帧和控制帧了。
当客户端要关闭时,向服务端发送 END_STREAM
帧,进入半关闭状态, 这个时候客户端只能接收数据,而不能发送数据。
服务端收到这个 END_STREAM 帧后也进入半关闭状态,不过此时服务端的情况是只能发送数据,而不能接收数据。随后服务端也向客户端发送 END_STREAM 帧,表示数据发送完毕,双方进入关闭状态。
如果下次要开启新的流,流 ID 需要自增,直到上限为止,到达上限后开一个新的 TCP 连接重头开始计数。由于流 ID 字段长度为 4 个字节,最高位又被保留,因此范围是 0 ~ 2 的 31 次方,大约 21 亿个。