HTTP1
HTTP1.0
浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个
TCP
连接(TCP
连接的新建成本很高,因为需要客户端和服务器三次握手),服务器完成请求处理后立即断开TCP
连接,服务器不跟踪每个客户也不记录过去的请求
通过添加头信息——非标准的Connection
字段Connection: keep-alive
来暂时解决
HTTP1.1
- 持久连接
引入了持久连接,即TCP
连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive
(对于同一个域名,大多数浏览器允许同时建立6个持久连接) - 管道机制
在同一个TCP
连接里面,客户端可以同时发送多个请求 - 分块传输编码
服务端没产生一块数据,就发送一块,采用”流模式”而取代”缓存模式” - 新增请求方式
PUT DELETE OPTIONS TRACE CONNECT
虽然允许复用TCP
连接,但是同一个TCP
连接里面,所有的数据通信是按次序进行的。服务器只有处理完一个请求,才会接着处理下一个请求。如果前面的处理特别慢,后面就会有许多请求排队等着。这将导致“队头堵塞”
避免方式:一是减少请求数,二是同时多开持久连接
HTTP2
超文本传输协议 HTTP 的 2.0 版本的协议在 2015 年的时候已经发布了。相比于前面的 HTTP 1.1 版本。它多出了下面三个主要的新特性
- 在建立连接后,可以
多路复用
- 在建立连接后,一次的请求与被响应,视为
流
- 数据传输分为
二进制帧
片段
二进制帧
在 TCP
协议中,数据的传输单位是数据报
。数据分成两大部分。头部(header) 和 实际数据部分(body)
在 HTTP 2.0
中,它把数据报的两大部分分成了 header frame
和 data frame
。也就是头部帧和数据体帧。
帧的传输最终在流中进行,流中的帧,头部(header)帧
和 data 帧
可以分为多个片段帧,例如data
帧即是可以 data = data_1 + data_2 + ... + data_n
此外地,如果基于二进制帧整体来划分,除了报文的帧分类。还有其它一些辅助帧类型,例如:SETTINGS
、PING
、GOWAY
、WINDOW_UPDATE
等控制帧
流
流
代表了一个完整的请求-响应数据交互过程。它具有如下几个特点:
双向性
:同一个流内,可同时发送和接受数据。有序性
:流中被传输的数据就是二进制帧
。帧在流上的被发送与被接收都是按照顺序进行的。并行性
:流中的二进制帧
都是被并行传输的,无需按顺序等待。但却不会引起数据混乱,因为每个帧都有顺序标号。它们最终会被按照顺序标号来合并。流的创建
:流可以被客户端或服务器单方面建立, 使用或共享。流的关闭
:流也可以被任意一方关闭
帧
是流
中的数据单位。一个数据报的header 帧可以分成多个 header 帧,data 帧可以分成多个data 帧
多路复用
HTTP 2.0
的多路复用其实是 HTTP 1.1
中长链接的升级版本
在 HTTP 1.1
中,一次链接成功后,只要该链接还没断开,那么 client
端可以在这么一个链接中有序地发起多个请求,并以此获得每个请求对应的响应数据。它的缺点是,一次请求与响应的交互必须要等待前面的请求交互完成,否则后面的只能等待,这个就是线头阻塞
在 HTTP 2.0
中,一次链接成功后,只要链接还没断开,那么 client
端就可以在一个链接中并发地发起多个请求,每个请求及该请求的响应不需要等待其他的请求,某个请求任务耗时严重,不会影响到其它连接的正常执行
HTTP3
HTTP2
协议虽然大幅提升了HTTP/1.1
的性能,然而,基于TCP
实现的HTTP2
遗留下3个问题:
- 有序字节流引出的
队头阻塞
(Head-of-line blocking),使得HTTP2
的多路复用
能力大打折扣 TCP
与TLS
叠加了握手时延,建链时长还有1倍的下降空间- 基于
TCP
四元组确定一个连接,这种诞生于有线网络的设计,并不适合移动状态下的无线网络,这意味着IP地址的频繁变动会导致TCP连接
、TLS会话
反复握手,成本高昂
HTTP3
协议解决了这些问题:
HTTP3
基于UDP
协议重新定义了连接,在QUIC
层实现了无序、并发字节流的传输,解决了队头阻塞问题(包括基于QPACK
解决了动态表的队头阻塞)HTTP3
重新定义了TLS
协议加密QUIC
头部的方式,既提高了网络攻击成本,又降低了建立连接的速度(仅需1个RTT
就可以同时完成建链与密钥协商)HTTP3
将Packet
、QUIC Frame
、HTTP3 Frame
分离,实现了连接迁移功能,降低了5G环境下高速移动设备的连接维护成本
HTTP3协议到底是什么?
就像HTTP2
协议一样,HTTP3
并没有改变HTTP1
的语义。那什么是HTTP
语义呢?在我看来,它包括以下3个点:
- 请求只能由客户端发起,而服务器针对每个请求返回一个响应
- 请求与响应都由
Header
、Body
(可选)组成,其中请求必须含有URL
和方法
,而响应必须含有响应码
Header
中各Name对应的含义保持不变
HTTP3
在保持HTTP1
语义不变的情况下,更改了编码格式,这由2个原因所致:
首先,是为了减少编码长度。HTTP
1协议的编码使用了ASCII
码,用空格
、冒号
以及\r\n
作为分隔符,编码效率很低
HTTP2
与HTTP3
采用二进制、静态表、动态表与Huffman算法对HTTP Header编码,不只提供了高压缩率,还加快了发送端编码、接收端解码的速度
其次,由于HTTP1
协议不支持多路复用
,这样高并发只能通过多开一些TCP
连接实现。然而,通过TCP
实现高并发有3个弊端:
- 实现成本高。
TCP
是由操作系统内核实现的,如果通过多线程实现并发,并发线程数不能太多,否则线程间切换成本会以指数级上升;如果通过异步、非阻塞socket实现并发,开发效率又太低 - 每个
TCP
连接与TLS
会话都叠加了2-3个RTT
的建链成本 TCP
连接有一个防止出现拥塞的慢启动流程,它会对每个TCP
连接都产生减速效果
因此,HTTP2
与HTTP3
都在应用层实现了多路复用功能
HTTP2
协议基于TCP
有序字节流实现,因此应用层的多路复用并不能做到无序地并发,在丢包场景下会出现队头阻塞问题
当网络繁忙时,丢包概率会很高,多路复用
受到了很大限制。因此,HTTP3
采用UDP
作为传输层协议,重新实现了无序连接,并在此基础上通过有序的QUIC Stream
提供了多路复用
连接迁移功能是怎样实现的?
对于当下的HTTP1
和HTTP2
协议,传输请求前需要先完成耗时1个RTT
的TCP
三次握手、耗时1个RTT
的TLS
握手(TLS1.3),由于它们分属内核实现的传输层、openssl库实现的表示层,所以难以合并在一起,如下图所示:
在IoT
时代,移动设备接入的网络会频繁变动,从而导致设备IP地址改变。对于通过四元组(源IP、源端口、目的IP、目的端口)定位连接的TCP
协议来说,这意味着连接需要断开重连,所以上述2个RTT
的建链时延、TCP
慢启动都需要重新来过。
而HTTP3
的QUIC
层实现了连接迁移功能,允许移动设备更换IP地址后,只要仍保有上下文信息(比如连接ID、TLS密钥等),就可以复用原连接
在UDP
报文头部与HTTP
消息之间,共有3层头部,定义连接且实现了Connection Migration
主要是在Packet Header
中完成的
Packet Header
实现了可靠的连接。当UDP
报文丢失后,通过Packet Header
中的Packet Number
实现报文重传。连接也是通过其中的Connection ID
字段定义的;QUIC Frame Header
在无序的Packet
报文中,基于QUIC Stream
概念实现了有序的字节流,这允许HTTP
消息可以像在TCP
连接上一样传输;HTTP3 Frame Header
定义了HTTP Header
、Body
的格式,以及服务器推送、QPACK编解码流等功能
为了进一步提升网络传输效率,Packet Header
又可以细分为两种:
Long Packet Header
用于首次建立连接;Short Packet Header
用于建立连接后传输数据
Long Packet Header
的格式如下图所示
建立连接时,连接是由服务器通过Source Connection ID
字段分配的,这样,后续传输时,双方只需要固定住Destination Connection ID
,就可以在客户端IP地址、端口变化后,绕过UDP四元组(与TCP四元组相同),实现连接迁移功能
Short Packet Header
头部的格式,这里就不再需要传输Source Connection ID
字段了
Packet Number
是每个报文独一无二的序号,基于它可以实现丢失报文的精准重发。如果你通过抓包观察Packet Header
,会发现Packet Number
被TLS
层加密保护了,这是为了防范各类网络攻击的一种设计
Packet Header
中被加密保护的字段:
显示为E(Encrypt)的字段表示被TLS加密过
Stream多路复用时的队头阻塞是怎样解决的?
允许微观上有序发出的Packet
报文,在接收端无序到达后也可以应用于并发请求中
在Packet Header
之上的QUIC Frame Header
,定义了有序字节流Stream
,而且Stream
之间可以实现真正的并发。HTTP3
的Stream
,借鉴了HTTP2
中的部分概念
每个Stream
就像HTTP1
中的TCP
连接,它保证了承载的HEADERS frame
(存放HTTP Header)、DATA frame
(存放HTTP Body)是有序到达的,多个Stream
之间可以并行传输。
在HTTP3中,HTTP2 frame
会被拆解为两层,我们先来看底层的QUIC Frame
一个Packet
报文中可以存放多个QUIC Frame
,当然所有Frame
的长度之和不能大于PMTUD
(Path Maximum Transmission Unit Discovery,这是大于1200字节的值),你可以把它与IP路由中的MTU
概念对照理解
每一个Frame
都有明确的类型:
前4个字节的Frame Type
字段描述的类型不同,接下来的编码也不相同,下表是各类Frame
的16进制Type
值
数值 | 名称 |
---|---|
0x00 | PADDING |
0x01 | PING |
0x02 – 0x03 | ACK |
0x04 | RESET_STREAM |
0x05 | STOP_SENDING |
0x06 | CRYPTO |
0x07 | NEW_TOKEN |
0x08 – 0x0f | STREAM |
0x10 | MAX_DATA |
0x11 | MAX_STREAM_DATA |
0x12-0x13 | MAX_STREAMS |
0x14 | DATA_BLOCKED |
0x15 | STREAM_DATA_BLOCKED |
0x16-0x17 | STREAM_BLOCKED |
0x18 | NEW_CONNECTION_ID |
0x19 | RETRY_CONNECTION_ID |
0x1a | PATH_CHALLENGE |
0x1b | PATH_RESPONSE |
0x1c-0x1d | CONNECTION_CLOSE |
0x1e | HANDSHAKE_DONE |
分析0x08-0x0f
这8种STREAM
类型的Frame
,就能弄明白Stream
流的实现原理,自然也就清楚队头阻塞是怎样解决的了
Stream Frame
用于传递HTTP
消息,它的格式如下所示:
Stream ID
标识了一个有序字节流。当HTTP
Body非常大,需要跨越多个Packet
时,只要在每个Stream Frame
中含有同样的Stream ID
,就可以传输任意长度的消息。多个并发传输的HTTP消息,通过不同的Stream ID
加以区别- 消息序列化后的“有序”特性,是通过
Offset
字段完成的,它类似于TCP
协议中的Sequence
序号,用于实现Stream
内多个Frame
间的累计确认功能 Length
指明了Frame
数据的长度
为什么会有8种Stream Frame
呢?这是因为0x08-0x0f
这8种类型其实是由3个二进制位组成,它们实现了以下3 标志位的组合:
- 第1位表示是否含有
Offset
,当它为0时,表示这是Stream
中的起始Frame
,这也是上图中Offset是可选字段的原因; - 第2位表示是否含有
Length
字段; - 第3位
Fin
,表示这是Stream
中最后1个Frame
,与HTTP2
协议Frame
帧中的FIN
标志位相同
Stream
数据中并不会直接存放HTTP
消息,因为HTTP3
还需要实现服务器推送、权重优先级设定、流量控制等功能,所以Stream Data
中首先存放了HTTP3 Frame
Length
指明了HTTP
消息的长度,而Type
字段(请注意,低2位有特殊用途)包含了以下类型:
数值 | 名称 | 说明 |
---|---|---|
0x00 | DATA | 用于传输HTTP Body包体; |
0x01 | HEADERS | 通过QPACK 编码,传输HTTP Header头部; |
0x03 | CANCEL_PUSH | 控制帧,用于取消1次服务器推送消息,通常客户端在收到PUSH_PROMISE帧后,通过它告知服务器不需要这次推送; |
0x04 | SETTINGS | 控制帧,设置各类通讯参数; |
0x05 | PUSH_PROMISE | 用于服务器推送HTTP Body前,先将HTTP Header头部发给客户端,流程与HTTP2相似; |
0x07 | GOAWAY | 控制帧,用于关闭连接(注意,不是关闭Stream); |
0x0d | MAX_PUSH_ID | 客户端用来限制服务器推送消息数量的控制帧 |
QUIC Stream Frame
定义了有序字节流,且多个Stream
间的传输没有时序性要求,这样,HTTP
消息基于QUIC Stream
就实现了真正的多路复用,队头阻塞问题自然就被解决掉了
QPACK编码是如何解决队头阻塞问题的?
与HTTP2
中的HPACK
编码方式相似,HTTP3
中的QPACK
也采用了静态表
、动态表
及Huffman编码
:
在上图中,GET
方法映射为数字2,这是通过客户端、服务器协议实现层的硬编码完成的。在HTTP
2中,共有61个静态表项:
在QPACK
中,则上升为98个静态表项,比如Nginx
上的ngx_htt_v3_static_table
数组所示:
对于Huffman
以及整数的编码,QPACK
与HPACK
并无多大不同,但动态表
编解码方式差距很大
所谓动态表
,就是将未包含在静态表
中的Header
项,在其首次出现时加入动态表,这样后续传输时仅用1个数字表示,大大提升了编码效率。
因此,动态表
是天然具备时序性的,如果首次出现的请求出现了丢包,后续请求解码HPACK
头部时,一定会被阻塞
QPACK
将动态表
的编码、解码独立在单向Stream
中传输,仅当单向Stream
中的动态表编码成功后,接收端才能解码双向Stream
上HTTP
消息里的动态表索引
单向指只有一端可以发送消息,双向则指两端都可以发送消息。其中的Stream ID
别有玄机,除了标识Stream
外,它的低2位还可以表达以下组合:
当Stream ID
是0、4、8、12时,这就是客户端发起的双向Stream
(HTTP3不支持服务器发起双向Stream),它用于传输HTTP
请求与响应。单向Stream
有很多用途,所以它在数据前又多出一个Stream Type
字段:
Stream Type
有以下取值:
0x00
:控制Stream
,传递各类Stream
控制消息;0x01
:服务器推送消息;0x02
:用于编码QPACK
动态表,比如面对不属于静态表的HTTP
请求头部,客户端可以通过这个Stream
发送动态表编码;0x03
:用于通知编码端QPACK
动态表的更新结果
由于HTTP3
的STREAM
之间是乱序传输的,因此,若先发送的编码Stream
后到达,双向Stream
中的QPACK
头部就无法解码,此时传输HTTP
消息的双向Stream
就会进入Block
阻塞状态(两端可以通过控制帧定义阻塞Stream
的处理方式)