HTTP1.1存在的问题
- TCP连接数限制
- 对于同一个域名,浏览器最多只能同时创建 6~8 个 TCP 连接 (不同浏览器不一样)。为了解决数量限制,出现了 域名分片 技术,其实就是资源分域,将资源放在不同域名下 (比如二级子域名下),这样就可以针对不同域名创建连接并请求,以一种讨巧的方式突破限制,但是滥用此技术也会造成很多问题,比如每个 TCP 连接本身需要经过 DNS 查询、三步握手、慢启动等,还占用额外的 CPU 和内存,对于服务器来说过多连接也容易造成网络拥挤、交通阻塞等,对于移动端来说问题更明显;每次新建连接 DNS 解析需要时间(几 ms 到几百 ms 不等)、TCP 慢启动也需要时间、TLS 握手又要时间,而且后续请求都要等待队列调度
- 线头阻塞(Head Of Line Blocking)问题
- 每个 TCP 连接同时只能处理一个请求 - 响应,浏览器按 FIFO 原则处理请求,如果上一个响应没返回,后续请求响应都会受阻。为了解决此问题,出现了 管线化 - pipelining 技术,但是管线化存在诸多问题,比如第一个响应慢还是会阻塞后续响应、服务器为了按序返回相应需要缓存多个响应占用更多资源、浏览器中途断连重试服务器可能得重新处理多个请求、还有必须客户端 - 代理 - 服务器都支持管线化
- Header内容多,而且每次请求 Header 不会变化太多,没有相应的压缩传输优化方案
- 为了尽可能减少请求数,需要做合并文件、雪碧图、资源内联等优化工作,但是这无疑造成了单个请求内容变大延迟变高的问题,且内嵌的资源不能有效地使用缓存机制
- 明文传输不安全
HTTP2优势
- 二进制分帧层 (Binary Framing Layer)
- 帧是数据传输的最小单位,以二进制传输代替原本的明文传输,原本的报文消息被划分为更小的数据帧
- 多路复用 (MultiPlexing)
- 在一个 TCP 连接上,我们可以向对方不断发送帧,每帧的 stream identifier 的标明这一帧属于哪个流,然后在对方接收时,根据 stream identifier 拼接每个流的所有帧组成一整块数据。把 HTTP/1.1 每个请求都当作一个流,那么多个请求变成多个流,请求响应数据分成多个帧,不同流中的帧交错地发送给对方,这就是 HTTP/2 中的多路复用。流的概念实现了单连接上多请求 - 响应并行,解决了线头阻塞的问题,减少了 TCP 连接数量和 TCP 连接慢启动造成的问题,所以 http2 对于同一域名只需要创建一个连接,而不是像 http/1.1 那样创建 6~8 个连接
- 服务端推送 (Server Push)
- 浏览器发送一个请求,服务器主动向浏览器推送与这个请求相关的资源,这样浏览器就不用发起后续请求。
- Server-Push 主要是针对资源内联做出的优化,相较于 http/1.1 资源内联的优势:
- 客户端可以缓存推送的资源
- 客户端可以拒收推送过来的资源
- 推送资源可以由不同页面共享
- 服务器可以按照优先级推送资源
- Header 压缩 (HPACK)
- 使用 HPACK 算法来压缩首部内容
- 应用层的重置连接
- 对于 HTTP/1 来说,是通过设置 tcp segment 里的 reset flag 来通知对端关闭连接的。这种方式会直接断开连接,下次再发请求就必须重新建立连接。HTTP/2 引入 RST_STREAM 类型的 frame,可以在不断开连接的前提下取消某个 request 的 stream,表现更好。
- 请求优先级设置
- HTTP/2 里的每个 stream 都可以设置依赖 (Dependency) 和权重,可以按依赖树分配优先级,解决了关键请求被阻塞的问题
- 流量控制
- 每个 http2 流都拥有自己的公示的流量窗口,它可以限制另一端发送数据。对于每个流来说,两端都必须告诉对方自己还有足够的空间来处理新的数据,而在该窗口被扩大前,另一端只被允许发送这么多数据。
- HTTP/1 的几种优化可以弃用
- 合并文件、内联资源、雪碧图、域名分片对于 HTTP/2 来说是不必要的,使用 h2 尽可能将资源细粒化,文件分解地尽可能散,不用担心请求数多
HTTP2帧结构
所有帧都是一个固定的 9 字节头部 (payload之前)跟一个指定长度的负载 (payload):
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
- Length 代表整个 frame 的长度,用一个 24 位无符号整数表示。除非接收者在 SETTINGS_MAX_FRAME_SIZE 设置了更大的值 (大小可以是 2^14(16384) 字节到 2^24-1(16777215) 字节之间的任意值),否则数据长度不应超过 2^14(16384) 字节。头部的 9 字节不算在这个长度里
- Type 定义 frame 的类型,用 8 bits 表示。帧类型决定了帧主体的格式和语义,如果 type 为 unknown 应该忽略或抛弃。
- Flags 是为帧类型相关而预留的布尔标识。标识对于不同的帧类型赋予了不同的语义。如果该标识对于某种帧类型没有定义语义,则它必须被忽略且发送的时候应该赋值为 (0x0)
- R 是一个保留的比特位。这个比特的语义没有定义,发送时它必须被设置为 (0x0), 接收时需要忽略。
- Stream Identifier 用作流控制,用 31 位无符号整数表示。客户端建立的 sid 必须为奇数,服务端建立的 sid 必须为偶数,值 (0x0) 保留给与整个连接相关联的帧 (连接控制消息),而不是单个流
- Frame Payload 是主体内容,由帧类型决定
帧类型
- HEADERS: 报头帧 (type=0x1),用来打开一个流或者携带一个首部块片段
- DATA: 数据帧 (type=0x0),装填主体信息,可以用一个或多个 DATA 帧来返回一个请求的响应主体
- PRIORITY: 优先级帧 (type=0x2),指定发送者建议的流优先级,可以在任何流状态下发送 PRIORITY 帧,包括空闲 (idle) 和关闭 (closed) 的流
- RST_STREAM: 流终止帧 (type=0x3),用来请求取消一个流,或者表示发生了一个错误,payload 带有一个 32 位无符号整数的错误码 (Error Codes),不能在处于空闲 (idle) 状态的流上发送 RST_STREAM 帧
- SETTINGS: 设置帧 (type=0x4),设置此 连接 的参数,作用于整个连接
- PUSH_PROMISE: 推送帧 (type=0x5),服务端推送,客户端可以返回一个 RST_STREAM 帧来选择拒绝推送的流
- PING: PING 帧 (type=0x6),判断一个空闲的连接是否仍然可用,也可以测量最小往返时间 (RTT)
- GOAWAY: GOWAY 帧 (type=0x7),用于发起关闭连接的请求,或者警示严重错误。GOAWAY 会停止接收新流,并且关闭连接前会处理完先前建立的流
- WINDOW_UPDATE: 窗口更新帧 (type=0x8),用于执行流量控制功能,可以作用在单独某个流上 (指定具体 Stream Identifier) 也可以作用整个连接 (Stream Identifier 为 0x0),只有 DATA 帧受流量控制影响。初始化流量窗口后,发送多少负载,流量窗口就减少多少,如果流量窗口不足就无法发送,WINDOW_UPDATE 帧可以增加流量窗口大小
- CONTINUATION: 延续帧 (type=0x9),用于继续传送首部块片段序列,见 首部的压缩与解压缩