HTTP 又称超文本传输协议,自从1991年发布初始版本0.9,现如今已经到了HTTP 3.0。在这里做个笔记,来记录一下HTTP的前世今生
HTTP 0.9/1.0
HTTP 0.9不支持请求头,只支持GET方法。
HTTP 1.0 在0.9的基础了,增加了一些变化:
请求中加入的版本号,例如
HTTP/1.0
增加了请求头&响应头
增加了HTTP Status Code 当然,业务开发中,一般都返回200
增加了
Content-Type
,可以传输其他文件了新增< code>Expires 缓存机制
在HTTP 1.0版本中存在的问题
- 每发起一个请求都需要新建一个TCP链接,而每次建立TCP链接都需要进行三次握手,会产生巨大开销。
- 串行请求:多个请求时需要等到上一个请求返回后才能继续后续请求,导致页面/数据加载缓慢。
- 不支持断点续传。
HTTP 1.1
千呼万唤始出来,HTTP 1.1主要解决了 1.0 的网络性能问题,并增加了一些特性:
- HTTP 长链接: 增加
Keepalive
实现TCP链接重用,减少TCP三次握手的开销。 - 管道传输:可以同时发送多个请求,避免请求端对头阻塞,减少整体响应时间。(对于非幂等的POST请求或者有依赖的请求不适用)
- 支持
Chunked Responses
响应数据分块 - 缓存机制:增加
Cache Control
缓存机制 - 补充协议头内容,Language、Encoding、Type等等
- 增加
HOST
,可以用于服务端区分域名信息 - 增加
OPTIONS
方法, - 新增了TLS协议
在HTTP 1.1版本中存在的问题
- 请求并行数量限制,一般限制6个。(精灵图的优化来源于此,减少请求数,提高页面加载速度)
- 头部冗余,每次携带都需要携带重复的头部信息。
- 队头阻塞:由于管道机制,同一个TCP连接中的多个请求为串行,必须等到上一个请求返回响应时才可以进行下一个。如果上一个请求因为某种原因被阻塞时,在后面排队的所有请求也一并被阻塞,导致带宽无法被充分利用
- 明文传输,主要以文本格式传输数据,传输成本较大。
队头阻塞的解决方案:
- 域名分片:将同一个页面的资源分散到不同域名,提升并发连接上限,因为浏览器通常对同一域名的 HTTP 连接最大只能是 6 个
- 雪碧图:将多个小图合并,减少请求数
- 小图使用base64内联实现
HTTP 2.0
同样的,HTTP2.0也解决了1.1的一些问题:
并发请求:
我们都知道 HTTP/1.1 的实现是基于请求-响应模型的。同一个连接中,HTTP 完成一个事务(请求与响应),才能处理下一个事务,也就是说在发出请求等待响应的过程中,是没办法做其他事情的,如果响应迟迟不来,那么后续的请求是无法发送的,也造成了队头阻塞的问题。
而在HTTP/2中,通过流的概念,多个流复用同一个TCP连接,达到并发效果,提高了HTTP传输的吞吐量。其实也就是多路复用的概念
多路复用:
一个TCP连接中存在多个流,同时发送多个请求,对端可以通过帧中的表示知道该帧属于哪个请求。在客户端,这些帧乱序发送,到对端后再根据每个帧首部的流标识符重新组装
头部压缩
Http协议报文一般由「Header + Body」构成,对于body部分,HTTP/1.1使用了「content-Encoding」对Body进行了压缩,比如gzip压缩,这样可以节约带宽,但对于Header,却没有针对它的优化手段。
在HTTP/1.1中Header部分存在的问题:
- 含有很多固定字段,比如Cookie、User Agent、Accept等,
- 请求跟响应的报文中,大量字段是重复的,每次请求使得不必要的带宽被浪费
- 字段是ASCII编码的,虽然利于观察,但是效率低
于是HTTP/2.0对于Header部分做了一些优化:使用HPACK算法对Header 进行压缩,主要包括三部分:
- 静态字典
- 动态字典
- Huffman编码(压缩算法)
静态表
HTTP/2为高频出现在头部的字符串跟字段建立了一张静态表,静态表中共有61组,表头分别为Header Name
、Header Value
,其中Index
表示索引,Header Name
表示字段名称,Header Value
表示字段的值。
动态表
对于静态表之外的头部字符串,就需要建立动态表了,索引从62开始。
比如,第一次发送时头部中的「user-agent 」
字段数据有上百个字节,经过 Huffman
编码发送出去后,客户端和服务器双方都会更新自己的动态表,添加一个新的 Index
号 62。那么在下一次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 62 号就好了,因为双方都可以根据自己的动态表获取到字段的数据。
所以,使得动态表生效有一个前提:必须同一个连接上,重复传输完全相同的 HTTP 头部。如果消息字段在 1 个连接上只发送了 1 次,或者重复传输时,字段总是略有变化,动态表就无法被充分利用了。
二进制帧
HTTP/2 厉害的地方在于将 HTTP/1 的文本格式改成二进制格式传输数据,极大提高了 HTTP 传输效率,而且二进制数据使用位运算能高效解析
HTTP/2将响应报文划分成两个帧,头部帧跟数据帧,并分别采用了二进制来进行编码。
在头部帧中,主要包含了帧长度,帧类型,标志位及流id,其中通过标志位可以设置流的优先级,流id用于区分流,接收方可以根据这个信息从乱序的帧里找到相同 id 的帧,然后按照顺序组装数据。
服务端推送(server push)
服务端主动推送数据至客户端。 注:Chrome 106 和之后的版本已默认禁用 HTTP/2 Server Push特性,改用103 Early Hints
在HTTP 2.0中存在的问题
- 队头阻塞:HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。
- 建立链接时需要先进行TCP三次握手、再TLS四次握手(https)
HTTP 3.0
HTTP3.0使用了基于UDP的QUIC协议
- 没有了TCP的三次握手,并且TLS版本升级为1.3,允许客户端无需等待TLS握手完成就开始发送数据,实现快速建立连接,QUIC内包括TLS,可以减少握手时间,首次连接只需1个RTT,第二次连接的时候数据包跟QUIC握手信息一起传递,达到 0 RTT的效果
- 无队头阻塞,当某个流的发生了丢包,不会影响其他流
- 通过连接ID来确定身份,当网络环境发生变化时,即使ip发生了变化,只要上下文信息仍然有效,就可以无缝使用上次的连接,消除重连的成本。
- HTTP3不需要在二进制中定义流,因为QUIC协议自带了流,所以HTTP3的帧结构较为简单,头部帧的字段中包含了长度跟类型
- 压缩算法优化,使用了QPACK算法,不同于HTTP2的HPAKCK算法,静态表又HTTP2的61扩展到91个,使用两个单向流来同步双方的动态表
三次握手
关键词: SYN、syn、Seq、ACK、ack
假设我们有客户端A与服务端B
在进行HTTP请求时,TCP需要建立连接,分为三步:
- 第一次握手,A向服务端发端报文 SYN=1,Seq=16位随机数,由B接收到A要建立连接的消息。其中SYN=1表示向服务端确认。
- 第二次握手,B收到A的消息后,作出回应,向A发送ack number = seq + 1,syn=1,ack=1,以及生成的seq,表示确认联机。
- 第三次握手,A收到B的消息。通过
ack number
判断来源正确,通过ack=1
得知B的确认联机消息,继续向B发送ack number = seq + 1
,ack=1,B通过同样的对比得知A的确认消息,连接建立成功。
可以看出主要是通过关键字及值的判断来确定双方信息及接收能力。
为什么三次
- 确认双方收发消息的功能都正常
- 建立多次连接时,如果第一个连接的握手丢失/延迟,后续B再收到时,以为要建立新的连接,导致B一直在等待A的响应。
DDOS攻击
通过频繁的GET请求向服务端发起建立连接的信息,恶意消耗服务器性能
慢速攻击
利用HTTP Keepalive特性,要求WebServer保持TVCP连接不断开,通过阶段行的请求使的TCP被占满而无法接收新的请求
四次挥手
同样的,假设我们有客户端A与服务端B
在进行HTTP请求完成之后,TCP需要断开连接,分为四步:
- 第一次挥手,A向B发送请求,FIN=1,Seq,告知B要断开连接了
- 第二次挥手,B收到A的请求,回应ACK=1,ack,告知A已经收到断开请求
- 第三次挥手,B等待所以的请求处理完,发起第三次挥手,FIN-1,Seq随机数,告知A准备断开连接
- A收到B断开连接的请求,向B表示确认,ACK=1,Seq=随机数,告诉B收到请求,至此断开连接