SPDY 没有完全改写 HTTP 协议,而是在 TCP/IP 的应用层与运输层之 间通过新加会话层的形式运作。同时考虑到安全性问题,SPDY 规定通信中使用 SSL。SPDY 以会话层的形式加入,控制对数据的流动,但还是采用 HTTP 建立通信连接,改层工作在SSL层之上、HTTP层之下。
HTTP Working-Group 以 SPDY/2 为基础,开发 HTTP/2。但是,HTTP/2跟 SPDY 仍有不同的地方,主要是以下两点:
HTTP2.0性能增强的核心:二进制分帧。
HTTP 2.0最大的特点: 不会改动HTTP 的语义,HTTP 方法、状态码、URI 及首部字段,等等这些核心概念上一如往常,却能致力于突破上一代标准的性能限制,改进传输性能,实现低延迟和高吞吐量。而之所以叫2.0,是在于新增的二进制分帧层。
在二进制分帧层上,HTTP 2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码 ,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。帧是数据传输的最小单位,以二进制传输代替原本的明文传输。
HTTP 2.0 所有的通信都在一个连接(TCP连接)上完成,这个连接可以承载任意数量的双向数据流。相应地,每个数据流以消息的形式发送,而消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装。
HTTP性能的关键在于低延迟而不是高带宽!大多数HTTP 连接的时间都很短,而且是突发性的,但TCP 只在长时间连接传输大块数据时效率才最高。HTTP 2.0 通过让所有数据流共用同一个连接,可以更有效地使用TCP 连接,让高带宽也能真正的服务于HTTP的性能提升。
单连接多资源方式的好处:
HTTP/2 的四个概念:
Frame 由 Frame Header 和 Frame Payload 两部分组成。所有帧都以固定的 9字节大小的头作为帧开始,后跟可变长度的有效载荷 payload。
帧头的字段定义如下:
DATA: 用于传输HTTP消息体
HEADERS:用户传输关于流的额外的首部字段
PRIORITY:用户指定或者重新指定引用资源的优先级
RST_STRING:用于通知流的非正常终止
SETTINGS:用于通知两端通信方式的数据配置
PUSH_PROMISE:用于发出创建流和服务器引用资源的要约
PING:用于计算往返时间,执行“活性”检查
GOAWAY:用于通知对端停止在当前连接的创建流
WINDOW_UPDATE:用于针对个别流或个别连接实现流量控制
CONTINUATION:用于继续一系列首部块片段
Flags:标志位,常用的标志位有 END_HEADERS 表示头数据结束,相当于 HTTP/1里头后的空行(“\r\n”)。
R:保留的 1 位。该位的语义未定义,发送时必须保持未设置 (0x0),接收时必须忽略。
Stream Identifier:流标识符,表示为无符号 31 位整数。由客户端发起的流必须使用奇数编号的流标识符;那些由服务器发起的必须使用偶数编号的流标识符。DATA 帧必须与某一个流相互关联。
stream ID 的作用:
实现多路复用的关键。接收端的实现可以根据这个 ID 并发、组装消息。同一个 stream 内 frame 必须是有序的。
推送依赖性请求的关键。客户端发起的流是奇数编号,服务端发起的流是偶数编号。
在 HTTP/1 中,HTTP 请求和响应都是由「状态行、请求 / 响应头部、消息主体」三部分组成。一般而言,消息主体都会经过 gzip 压缩,或者本身传输的就是压缩过后的二进制文件(例如图片、音频),但状态行和头部却没有经过任何压缩,直接以纯文本传输。
根据 HTTP Archive 的统计,当前平均每个页面都会产生上百个请求。越来越多的请求导致消耗在头部的流量越来越多,尤其是每次都要传输 UserAgent、Cookie 这类不会频繁变动的内容,完全是一种浪费。
以下是我随手打开的一个页面的抓包结果。可以看到,传输头部的网络开销超过 100kb,比 HTML 还多:
HTTP/2协议中定义了 HPACK,这是一种新的压缩方法,它消除了多余的header 字段,将漏洞限制到已知的安全攻击,并且在受限的环境中具有有限的内存需求。HPACK 格式特意被设计成简单且不灵活的形式:两种特性都降低了由于实现错误而引起的互操作性或安全性问题的风险;没有定义扩展机制,只能通过定义完整的替换来更改格式。
需要注意的是,http 2.0关注的是首部压缩,而我们常用的gzip等是报文内容(body)的压缩,二者不仅不冲突,且能够一起达到更好的压缩效果。
简单说,HPACK头部压缩需要在支持 HTTP/2 的浏览器和服务端之间:
一个预定义且不可更改的 header 字段列表。先定义好的内容,只有固定的几十个值,如果要发送的值符合静态表时,用对应的 Index 替换即可,这样就大大压缩了头部的大小,如果遇到不在静态表中的值,就会用到动态表。
一共有61个,这里只写了部分
http1.1中,浏览器客户端在同一时间,针对同一域名下的请求有一定数量的限制,超过限制数目的请求会被阻塞。这也是为何一些站点会有多个静态资源CDN 域名的原因之一。
http 2.0 连接都是持久化的,而且客户端与服务器之间也只需要一个连接(每个域名一个连接)即可。http2连接可以承载数十或数百个流的复用,多路复用意味着来自很多流的数据包能够混合在一起通过同样连接传输。当到达终点时,再根据不同帧首部的流标识符重新连接将不同的数据流进行组装。
websocket 原生协议由于没有这个 stream ID 类似的字段,所以它原生不支持多路复用。在同一个 stream 内部的 frame 由于没有其他的 ID 编号了,所以无法乱序,必须有序,无法并发。
服务器可以对一个客户端请求发送多个响应,服务器向客户端推送资源无需客户端明确地请求。并且,服务端推送能把客户端所需要的资源伴随着index.html一起发送到客户端,省去了客户端重复请求的步骤。
正因为没有发起请求,建立连接等操作,所以静态资源通过服务端推送的方式可以极大地提升速度。Server Push 让 http1.x 时代使用内嵌资源的优化手段变得没有意义;如果一个请求是由你的主页发起的,服务器很可能会响应主页内容、logo 以及样式表,因为它知道客户端会用到这些东西,这相当于在一个HTML 文档内集合了所有的资源。
不过与之相比,服务器推送还有一个很大的优势:可以缓存!也让在遵循同源的情况下,不同页面之间可以共享缓存资源成为可能。
注意两点:
当服务端需要主动推送某个资源时,便会发送一个 Frame Type 为PUSH_PROMISE 的 Frame,里面带了 PUSH 需要新建的 Stream ID。意思是告诉客户端:接下来我要用这个 ID 向你发送东西,客户端准备好接着。客户端解析 Frame 时,发现它是一个 PUSH_PROMISE 类型,便会准备接收服务端要推送的流。
启用http2.0后会给性能带来很大的提升,但同时也会带来新的性能瓶颈。因为现在所有的压力集中在底层一个TCP连接之上,TCP很可能就是下一个性能瓶颈,比如单个TCP packet丢失导致整个连接阻塞,无法逃避,此时所有消息都会受到影响。