https://jiajunhuang.com/articles/2018_10_13-http2.md.html
https://liudanking.com/arch/latency-and-user-experience/
首先得说明,HTTP/2不再像HTTP/1那样是明文协议,HTTP/2是二进制协议,也就意味着,我们没法再简单的通过
telnet jiajunhuang.com 80
这样去请求一些内容。为什么HTTP/2要做成二进制协议呢?主要原因在 https://http2.github.io/faq/#why-is-http2-binary:- 使用TLS之后,也没法直接telnet这样进行调试
- 相比明文协议(可以手工输入),二进制协议必须使用库或者工具(当然也可以自己写),相对来说更不容易出错
- 二进制协议解析起来更加高效(再也不用逐个字符去判断哪里是头部结束了,直接偏移多少就可以知道对应的字节表示什么意思)
frame 是HTTP/2中最小的传输单位。HTTP/2 相对HTTP/1来说,把原来的头部和body分开来了,统一都丢到frame里,头部对应的frame的类型 是HEADERS,而body对应的frame的类型是DATA。除此之外,frame的类型还有例如:GOAWAY, WINDOW_UPDATE等等。可以这么理解,HTTP/2 连接开始之后,所有的数据都是一个frame的内容,直到开始下一个frame。因此,Go语言gRPC实现中有这么一段代码:
上一节我们看到了每个frame的定义中都有31bit用来标识当前frame所在stream的id。那么stream是个什么鬼?stream是一个抽象概念, 因为HTTP/2把原本HTTP/1中一个请求中的头部和body打散了,分成了HEADERS和DATA两个frame。那当服务器收到一堆的frame之后,他如何 知道哪个frame和哪个frame是一起的,组合起来是一个完整的请求呢?所以我们需要stream这个抽象概念,而且由于一个HTTP/2连接可以 同时传输多个stream,所以我们可以通过下面的图片来理解stream:
那我们怎么判断服务器是不是支持HTTP/2呢?HTTP/1中有一个状态码,叫做 101 Switching Protocols。从HTTP/1升级到HTTP/2连接,就靠它了。
首先发起HTTP/1请求,并且给出如下报文:
GET / HTTP/1.1
Host: jiajunhuang.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
如果服务器不支持HTTP/2,就该怎么返回怎么返回,例如返回:
HTTP/1.1 200 OK
Content-Length: 243
Content-Type: text/html
...
但是如果服务器支持HTTP/2,那就返回101状态码,然后随即开始的内容就是HTTP/2的二进制内容了:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
[ HTTP/2 connection ...
HTTP/2如今(2018.02)已经逐渐普及,其设计的第一目标就是降低延迟。主要采用了两个手段来解决:
- TCP连接复用。连接复用减少了TCP每次握手带来的延迟,同时避免了每次新建TCP连接的窗口慢启动带来的数据吞吐开启延迟。
- 使用数据分帧解决队头阻塞问题。当然,这个问题HTTP/2解决得不彻底,具体可以参见《当我们在谈论HTTP队头阻塞时,我们在谈论什么?》了解细节。
有些是技术层面的,比如上面提到的HTTP/2的低延迟设计、DNS查询缓存、HTTP1.1并发多个TCP连接请求资源、雪碧图等;有些是用户体验层面的,比如异步加载/预加载js资源、优先加载影响首屏渲染的CSS资源、避免使用大表布局、图片渐变加载、过渡动画等。
在这个思想下,CDN应运而生。将内容分发到距离用户近的网络节点来降低用户访问延迟。这个idea非常简单,甚至简陋,但是非常有效,并且廉价。
顺着这个思路下去,如果把算力分发到距离用户近的节点,那是不是也可以让用户觉得计算任务也变快了呢?在一定程度上,这是可以做到的。比如,我们使用了数十年运行于浏览器的javascript,以及我们的多IDC、多主架构方案,都有这方面的考量。近来流行的serverless、边缘计算其实也可以是认为是将算力部署到离用户尽可能近的物理位置或业务流程中。
TCP连接复用技术通过将前端多个客户的HTTP请求复用到后端与服务器建立的一个TCP连接上。这种技术能够大大减小服务器的性能负载,减少与服务器之间新建TCP连接所带来的延时,并最大限度的降低客户端对后端服务器的并发连接数请求,减少服务器的资源占用。
一般情况下,客户端在发送HTTP请求之前需要先与服务器进行TCP三次握手,建立TCP连接,然后发送HTTP请求。服务器收到HTTP请求后进行处理,并将处理的结果发送回客户端,然后客户端和服务器互相发送FIN并在收到FIN的ACK确认后关闭连接。在这种方式下,一个简单的HTTP请求需要十几个TCP数据包才能处理完成。
采用TCP连接复用技术后,客户端(如:ClientA)与负载均衡设备之间进行三次握手并发送HTTP请求。负载均衡设备收到请求后,会检测服务器是否存在空闲的长连接,如果不存在,服务器将建立一个新连接。当HTTP请求响应完成后,客户端则与负载均衡设备协商关闭连接,而负载均衡则保持与服务器之间的这个连接。当有其它客户端(如:ClientB)需要发送HTTP请求时,负载均衡设备会直接向与服务器之间保持的这个空闲连接发送HTTP请求,避免了由于新建TCP连接造成的延时和服务器资源耗费。
在HTTP 1.0中,客户端的每一个HTTP请求都必须通过独立的TCP连接进行处理,而在HTTP 1.1中,对这种方式进行了改进。客户端可以在一个TCP连接中发送多个HTTP请求,这种技术叫做HTTP复用(HTTP Multiplexing)。它与TCP连接复用最根本的区别在于,TCP连接复用是将多个客户端的HTTP请求复用到一个服务器端TCP连接上,而HTTP复用则是一个客户端的多个HTTP请求通过一个TCP连接进行处理。前者是负载均衡设备的独特功能;而后者是HTTP 1.1协议所支持的新功能,目前被大多数浏览器所支持。