http2 的特性
多路复用
首部压缩:假定一个页面有 100 个资源需要加载, 而每一次请求都有 1kb 的消息头(这同样也并不少见,因为 Cookie 和引用等东西的存在), 则至少需要多消耗 100kb 来获取这些消息头。HTTP2.0 可以维护一个字典,差量更新 HTTP 头部,大大降低因头部传输产生的流量。(多路复用允许一次 TCP 链接处理多次 HTTP 请求,头部压缩又大大减少了多个 HTTP 请求可能产生的重复头部数据消耗。因此 HTTP/2 可以很好的支持并发请求)
二进制分帧
服务器推送:减少了请求数量。服务端推送能把客户端所需要的资源伴随着请求页面一起发送到客户端,省去了客户端重复请求的步骤。正因为没有发起请求,建立连接等操作,所以静态资源通过服务端推送的方式可以极大地提升速度
什么是多路复用?
就是一个连接上可以同时传输多个请求&响应(下一个请求不需要等前一个响应返回后再发出;对于响应服务器可以先处理完哪个就返回哪个而不需要按次请求的顺序返回)。
和长链接的区别?
首先讲一点:浏览器对于同一时间同一域名下的连接数有限制,一般是 4-8 个,Chrome 是 6 个。超过限制的数目会被阻塞。HTTP 允许客户端打开多条连接,并行地执行多个 HTTP 事务。
这就是为什么有些站点会有多个静态资源 CDN 域名的原因之一,变相的解决浏览器针对同一域名连接的限制阻塞问题。
http1.1 提出支持长链接和管道化。
长链接:即可以在一个 http 连接上传输多个请求&响应(http1.1 之前每个 http 链接只可以传输一对请求&响应然后就断开链接)。 在没有管道化的情况下,一个连接上的下一个请求需要在前一个响应返回后再发出。(并发请求只能通过同时建立多个连接实现)
问题:
串行传输
同域并发连接限制带来的阻塞(6~8)个
管道化:
可以在一个 http 链接上同时发送多个请求 (可以克服同域并行连接限制带来的阻塞)
服务器可以同时处理多个请求,但是必须按照请求的顺序返回结果(即使后面的请求先处理完成也还是需要缓存起来,等前面的请求处理完返回之后再返回)(对头阻塞:如果前一个请求处理的时间过长或者被无线挂起,那么后面的请求就会排队等待)
问题:
HTTP 请求无法很好地利用多路复用,不允许一个连接上的多个响应数据交错返回(多路复用)。因而一个响应必须完全返回后,下一个响应才会开始传输。
对头阻塞
Connection: keep-alive 声明一个链接是长链接
问:那么在 http1.1 中我们如何实现并发请求?
答:实现并发请求的唯一办法就是利用多个持久连接。可以发现在 HTTP1.1 中存在一些局限,它严格串行的返回响应,它不允许多个数据交错到达(多路复用),只能等待一个响应完全返回后,下一个响应才能发送,无论下一个响应是否早于前一个响应完成处理,这也叫做队首阻塞。这就带来一个非常糟糕的体验,如果第一个请求需要处理的时间非常长,那么后续的请求即使被服务器已经处理完成,响应也不能立即返回,而是存储在服务端的缓存区中,等待第一个响应的完成,才能按照 FIFO 顺序返回。
实际上,一些支持管道的浏览器,通常都将其作为一个高级配置选项,但大多数浏览器都会禁用它。换句话说,作为前端工程师,开发的应用是面向普通浏览器应用的话,还是不要过多的指望 HTTP 管道
结论:所以即使是 http1.1,我们也无法充分的利用其优势,如果需要同时并行发送请求还是要建立多个连接来实现。HTTP 持久连接虽然帮我们解决了 TCP 连接复用的问题,但是现阶段的 HTTP 管道却无法实现多个请求结果的交错返回,所以浏览器只能开启多个 TCP 连接,以达到并行地加载资源的目的。
多路复用:
通过单一的 HTTP/2 连接发起多重的请求-响应消息,即在一个连接里,客户端和浏览器都可以同时发送多个请求和响应,而不用按照顺序一一对应,这样避免了“队头堵塞”。(服务器可以处理完就可以直接返回)很好地解决了浏览器限制同一个域名下连接数量的问题。
区别
长链接在非管道化的情况下后一个请求只能等前一个请求的响应返回之后才可以被发送;在管道化的情况下,请求可以同时发送,服务器可以并行处理,但是响应却是依次返回。导致的问题就是对头阻塞(如果前一个响应的处理时间长或被无限挂起则后面的响应只能等待)& 无法重复实现多路复用,还可能会有服务器缓存响应过多而造成资源浪费的问题(大多数浏览器都是禁用 http1.1 的管道化功能的,要实现并行发送请求只能通过建立多个连接实现)
多路复用则很好的解决了长链接和管道化带来的对头阻塞问题。它允许在单一的 http2 连接上并行交错的传输请求和响应,而不需要顺序一一对应
多路复用怎么实现?
在 http2 中:
同域名下所有通信都在单个连接上完成,同个域名只需要占用一个 TCP 连接,使用一个连接并行发送多个请求和响应。
单个连接可以承载任意数量的双向数据流,单个连接上可以并行交错的请求和响应,之间互不干扰。
数据流以消息的形式发送,而消息又由一个或多个帧组成,帧之间可以乱序发送(不同流的帧乱序发送,同一个流的帧顺序发送),因为根据帧首部的流标识可以在接收端重新组装。每个请求都可以带一个 31bit 的优先值,0 表示最高优先级, 数值越大优先级越低。
多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。
在 HTTP/2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)
帧
帧代表着最小的数据单位,实现了对消息的封装。每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。
帧的字节中保存了不同的信息,前 9 个字节对于每个帧都是一致的,“服务器”解析 HTTP/2 的数据帧时只需要解析这些字节,就能准确的知道整个帧期望多少字节数来进行处理信息。
名称 | 长度 | 描述 |
---|---|---|
Length | 3 字节 | 表示帧负载的长度 |
Type | 1 字节 | 帧的类型 |
Flags | 1 字节 | 具体帧的标识 |
R | 1 字节 | 保留位,不需要设置,否则可能带来严重后果 |
Stream Identifier | 31 位(约 4 字节) | 每个流的唯一 ID |
Frame Payload | 不固定 | 是主体内容,由帧类型决定 |
为了能够发送不同的“数据信息”,通过帧数据传递不同的内容,HTTP/2 中定义了 10 种不同类型的帧,在上面表格的 Type 字段中可对“帧”类型进行设置。
- HEADERS(包含 HTTP 首部)
- DATA(传输流的核心内容)
- PRIORITY(设置流的优先级)
- RST_STREAM(允许一端停止流(通常是由于错误导致的))
- SETTINGS(设置此连接的参数)
- PUSH_PROMISE(服务器推送)
- PING(测量 RTT(往返时延))
- GOAWAY(终止连接,告诉另外一端,当前端已结束)
- WINDOW_UPDATE(协商一端要接收多少字节(用于流量控制))
- CONTINUATION(继续传输头部数据,用以拓展 HEADER 数据块)
流
HTTP/2 连接上独立的、双向的帧序列交换。流 ID(帧首部的 6-9 字节)用来标识帧所属的流。
流可以承载双向消息,每个流都有一个唯一的整数 ID。 HTTP/2 长连接中的数据包是不按请求-响应顺序发送的,一个完整的请求或响应(称一个数据流 stream,每个数据流都有一个独一无二的编号)可能会分成非连续多次发送。它具有如下几个特点:
- 流的 ID 都是奇数,说明是由客户端发起的,这是标准规定的,那么服务端发起的就是偶数了。
- 单个 HTTP/2 连接可以包含多个并发打开的 stream 流,任一一个端点都可能交叉收到来自多个 stream 流的帧。
- stream 流可以单方面建立和使用,也可以由客户端或服务器共享。
- 任何一个端都可以关闭 stream 流。
- 在 stream 流上发送帧的顺序非常重要。收件人按照收到的顺序处理帧。特别是,HEADERS 和 DATA 帧的顺序在语义上是重要的。
- stream 流由整数标识。stream 流标识符是由发起流的端点分配给 stream 流的。
- 并行性:流中的 二进制帧 都是被并行传输的,无需按顺序等待。(不同流的帧是可以穿插传递的,然后再根据帧上流的编号进行组装)
- 双向性:同一个流内,可同时发送和接受数据。
在一条 Connection 中,不同的流可以穿插传递,但是同一条流的达到顺序必须是有序的,比如 1 号流,流内的 帧必须有序。
这就是传输中无序,接收时组装。
拓展
为什么限制并发连接数?
每个连接都需要占用一些服务器的资源,服务器需要维持每一个连接请求的信息,因此过多的连接数意味着服务器的负担很大。
Dos 攻击:DOS(A Denial of Service) 攻击意味着让服务器忙于跟你打交道,以至于他无法跟其他客户端打交道。当其他客户端无法连接,这就是 DOS。
HTTP2 缺点
TCP 的队头阻塞并没有彻底解决: TCP 为了保证可靠传输,有一个“超时重传”机制,丢失的包必须等待重传确认
多路复用导致服务器压力上升:多路复用没有限制同时请求数。请求的平均数量与往常相同,但实际会有许多请求的短暂爆发,导致瞬时 QPS 暴增
多路复用容易 Timeout: 大批量的请求同时发送,由于 HTTP2 连接内存在多个并行的流,而网络带宽和服务器资源有限,每个流的资源会被稀释,虽然它们开始时间相差更短,但却都可能超时。
http 层面的对头阻塞
HTTP 层面的队头阻塞在于,HTTP/1.1 协议中同一个 TCP 连接中的多个 HTTP 请求只能按顺序处理,方式有两种标准,非管道化和管道化两种,非管道化方式:即串行执行,请求 1 发送并响应完成后才会发送请求 2,一但前面的请求卡住,后面的请求就被阻塞了;管道化方式:即请求可以并行发出,但是响应也必须串行返回(只提出过标准,没有真正应用过)。
TCP 层面的对头阻塞
TCP 层面的队头阻塞在于,TCP 本身不知道传输的是 HTTP 请求,TCP 只负责传递数据,传递数据的过程中会将数据分包,由于网络本身是不可靠的,TCP 传输过程中,当存在数据包丢失的情况时,顺序排在丢失的数据包之后的数据包即使先被接收也不会进行处理,只会将其保存在接收缓冲区中,为了保证分包数据最终能完整拼接成可用数据,所丢失的数据包会被重新发送,待重传副本被接收之后再按照正确的顺序处理它以及它后面的数据包。
But,由于 TCP 的握手协议存在,TCP 相对比较可靠,TCP 层面的丢包现象比较少见,需要明确的是,TCP 队头阻塞是真实存在的,但是对 Web 性能的影响比 HTTP 层面队头阻塞小得多,因此 HTTP/2 的性能提升还是很有作用的。
为什么 HTTP/1.1 不能实现“多路复用”?
简单回答就是:HTTP/2 是基于二进制“帧”的协议,HTTP/1.1 是基于“文本分割”解析的协议。
HTTP/1.1 发送请求消息的文本格式:以换行符分割每一条 key:value 的内容,解析这种数据用不着什么高科技,相反的,解析这种数据往往速度慢且容易出错。“服务端”需要不断的读入字节,直到遇到分隔符(这里指换行符,代码中可能使用/n 或者/r/n 表示)
一次只能处理一个请求或响应,因为这种以分隔符分割消息的数据,在完成之前不能停止解析。