简介
HTTP1.0和HTTP1.1存在的问题
HTTP/1.0 只允许在给定的 TCP [TCP]连接上,一次发出一个未完成的请求。HTTP/1.1 [HTTP/1.1]添加了请求流水线,但这只能部分解决请求并发问题,并且仍然受到应用程序层队首阻塞(application-layer head-of-line blocking)的影响。因此,HTTP/1.0 和 HTTP/1.1 客户端与服务器使用多个连接来处理并发请求。
此外,HTTP 字段通常重复且冗长,不仅会造成不必要的网络流量,还会导致初始 TCP 拥塞窗口快速填满。当在新的 TCP 连接上发出多个请求时,这可能会导致过度延迟。
HTTP2.0优化改进
HTTP/2 通过定义 HTTP 语义到底层连接的优化映射来解决这些问题。具体来说,它允许在同一连接上交错消息,并使用高效的 HTTP 字段编码。它还允许对请求进行优先级排序,让更重要的请求更快地完成,从而进一步提高性能。
由此产生的协议对网络更加友好,因为与 HTTP/1.x 相比,可以使用更少的 TCP 连接。这意味着与其他流的竞争更少,连接寿命更长,从而可以更好地利用可用的网络容量。但请注意,此协议不解决 TCP 队头阻塞(TCP head-of-line blocking)问题。
最后,HTTP/2 还通过使用二进制消息帧实现更高效的消息处理。
HTTP2.0 二进制帧结构
HTTP Frame {
Length (24),
Type (8),
Flags (8),
Reserved (1),
Stream Identifier (31),
Frame Payload (..),
}
帧头(Frame Header)
- 长度(Length):帧有效负载的长度,以八位字节为单位,表示为无符号 24 位整数。除非接收方为SETTINGS_MAX_FRAME_SIZE设置了更大的值,否则不得发送大于 2 14 (16,384)的值。帧头的 9 个八位字节不包含在此值中。
- 类型(Type):帧的 8 位类型。DATA、HEADERS、PRIORITY、PING帧等。
- 标准(Flags):为特定于帧类型的布尔标志保留的 8 位字段。
- 保留(Reserved):保留位,目前固定位0
- 流标识符(Stream Identifier):用于标识帧所属的流,每个流有一个唯一的标识符。值 0x00 保留用于与整个连接(而不是单个流)关联的帧。
负载(Payload)
帧负载是帧中的实际数据,其长度由帧头中的“长度”字段指定。不同类型的帧有不同的负载格式。
帧类型
- Data
用于传输应用数据。DATA帧使用了END_STREAM标志位来表示流的结束。当设置了END_STREAM标志时,这意味着当前的 DATA 帧是该流中的最后一个数据帧,不会再有后续的 DATA 帧,也不会再有头部或其他类型的帧(例如 HEADERS帧)在该流中被发送。 - HEADERS
用于打开一个新的流,并携带HTTP头部信息。HEADERS帧可以单独使用,也可以与CONTINUATION帧结合使用,以传输较大的头部块。 - PRIORITY (已弃用)
用于设置流的优先级,影响资源分配。 - RST_STREAM
用于立即终止一个流,通常因为发生了错误。 - SETTINGS
用于配置连接参数,如初始窗口大小。 - PUSH_PROMISE
用于提前通知对等端点发送方打算启动的流。 - PING
用于测量来自发送方的最小往返时间以及确定空闲连接是否仍然有效的机制。
接收不包含 ACK 标志的 PING 帧的接收方必须发送一个设置了 ACK 标志的 PING 帧作为响应,并且帧负载相同。PING 响应应比任何其他帧具有更高的优先级。PING 帧不与任何单个流相关联。如果收到的 PING 帧的流标识符字段值不是 0x00(PING帧的标识符固定是0x00),则接收方必须以类型为PROTOCOL_ERROR的连接错误(第 5.4.1 节)进行响应。 - GOAWAY
用于优雅地关闭连接,可以携带关闭的原因。 - WINDOW_UPDATE
用于实现流量控制,增加接收方的窗口大小。 - CONTINUATION
用于继续传输被分割的HEADERS或PUSH_PROMISE帧的剩余部分,或未设置 END_HEADERS 标志的 CONTINUATION帧
头部压缩(HPACK)
在 HTTP/1.1中,头部字段未压缩。随着网页增长到需要数十到数百个请求,这些请求中的冗余标头字段不必要地消耗带宽,从而显著增加延迟。
HPACK是HTTP/2.0用于压缩和管理头部字段的一种专门算法。HPACK定义了静态表和动态表,在后续的请求和响应传输过程中,只需要简单的用表的索引来代替完整的头部字段,从而减少数据量。
-
静态表:由预定义的静态标头字段列表组成,包含常见的:authority,:method,:path,:scheme,:status等
-
动态表:动态表由按先进先出顺序维护的标头字段列表组成。动态表最初是空的。每个头块解压缩后都会添加条目。
动态表的大小受 SETTINGS_HEADER_TABLE_SIZE 控制
HTTP2.0使用哈夫曼编码算法对头部字段的名称和值进行进一步的压缩。编码器在编码每个头部字段时,可以选择是否使用哈夫曼编码来压缩字段的名称和值。
流和多路复用(Streams and Multiplexing)
stream 是 HTTP/2 连接客户端与服务器之间交换的独立双向帧序列。HTTP/2就是基于流实现多路用的 ,多路复用使得多个流可以在同一个TCP连接上同时进行,而不会相互阻塞。
流具有几个重要特征:
- 单个 HTTP/2 连接可以包含多个同时打开的流,其中任一端点都可以交错来自多个流的帧。
- 流可以单方面建立和使用,也可以由任一端点共享。
- 任一端点都可以关闭流。
- 帧发送的顺序很重要。接收方按照接收的顺序处理帧。特别是,HEADERS和DATA帧的顺序在语义上很重要。
- 流由整数标识。流标识符由发起流的端点分配给流。
流的状态
声明周期
- idle (空闲)
流初始状态,尚未启动。 - reserved (local) (本地保留)
流已被本地端点通过发送 PUSH_PROMISE 帧保留,用于将来发送数据。 - reserved (remote) (远程保留)
流已被远程端点通过发送 PUSH_PROMISE 帧保留,用于将来发送数据。 - open (打开)
流已经启动,可以发送和接收帧。 - half-closed (local) (本地半关闭)
流的本地端点已经结束发送帧,但仍然可以接收来自远程端点的帧。 - half-closed (remote) (远程半关闭)
流的远程端点已经结束发送帧,但仍然可以接收来自本地端点的帧。 - closed (关闭)
流的两端都已经结束发送帧,流已经完全关闭。
流标识符
流由无符号31位整数标识。客户端发起的流必须使用奇数流标识符;服务器发起的流必须使用偶数流标识符。零流标识符 (0x00) 用于连接控制消息;零流标识符不能用于建立新流。新建立的流标识符在数值上必须大于发起端点已打开或保留的所有流。 流标识符不能重复使用
流并发
流并发数受 SETTINGS帧中的SETTINGS_MAX_CONCURRENT_STREAMS 限制
流量控制(Flow Control)
- 在HTTP2协议中,流量控制只在客户端和服务器之间单个TCP上连接进行管理。
- 流量控制基于WINDOW_UPDATE帧,接收方会发送WINDOW_UPDATE帧来告诉发送方允许接收的数据大小
- 流量控制是由接受方总体控制的,接收方可以选择为每个流或整个连接设置所需窗口大小
- 初始流控窗口大小为65535字节
- 只有DATA帧受流量控制,其他帧类型不受影响。这可确保重要的控制帧不受流量限制
- 一个端点可以禁用自己的流控,但是不能忽略对端的流控信号
流优先级
RFC 9113 废弃了原先在 RFC 7540中定义的流优先级机制,简化了HTTP/2的实现,并解决了优先级机制带来的复杂性和兼容性问题。原先的PRIORITY帧已废弃
HTTP2请求/响应
HTTP连接
-
h2:这是通过安全连接(即TLS)使用的HTTP/2版本。为了协商HTTP/2协议,客户端和服务器使用了TLS协议的ALPN(Application-Layer Protocol Negotiation)扩展。ALPN允许在TLS握手期间协商使用哪种应用层协议(如HTTP/1.1或HTTP/2)。当客户端和服务器都支持HTTP/2时,它们会通过ALPN来确认使用HTTP/2协议。
-
h2c:(HTTP/2 over cleartext TCP) 明文(非加密)连接进行通信。h2c不能通过ALPN进行协商,因为ALPN是TLS的一部分。相反,h2c通常通过HTTP/1.1的“Upgrade”机制进行协商。客户端首先发送一个普通的HTTP/1.1请求,并在请求头中包含 Upgrade: h2c 和 HTTP2-Settings 字段,表示希望升级到HTTP/2。服务器如果同意升级,则会在响应中确认,并切换到HTTP/2协议。
在 HTTP/2 中,不管是h2还是h2c 每个端点都需要发送连接前言作为对所用协议的最终确认,并建立 HTTP/2 连接的初始设置。客户端和服务器各自发送不同的连接前言。
客户端连接前言以 24 个八位字节的序列开始,以十六进制表示为:
0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
服务器连接前言由一个可能为空的SETTINGS帧(第 6.5 节)组成,该帧必须是服务器在 HTTP/2 连接中发送的第一个帧。
伪头部字段
HTTP/2 使用以‘:’字符(ASCII 0x3a)开头的特殊伪头字段来传达消息控制数据,伪报头字段不是 HTTP 报头字段。
:method
:scheme
:authority
:path
:status(响应)
所有 HTTP/2 请求必须为“ :method ”、“ :scheme ”和“ :path ”伪标头字段包含一个有效值,除非它们是 CONNECT 请求。
单个 HTTP/2 请求不带有明确的协议版本指示。所有 HTTP/2 请求都隐式地具有“2.0”协议版本
HTTP1.1和HTTP2 格式对比
Request
POST /resource HTTP/1.1 HEADERS
Host: example.org ==> - END_STREAM
Content-Type: image/jpeg - END_HEADERS
Content-Length: 123 :method = POST
:authority = example.org
:path = /resource
{binary data} :scheme = https
CONTINUATION
+ END_HEADERS
content-type = image/jpeg
host = example.org
content-length = 123
DATA
+ END_STREAM
{binary data}
Response
HTTP/1.1 200 OK HEADERS
Content-Type: image/jpeg ==> - END_STREAM
Content-Length: 123 + END_HEADERS
:status = 200
{binary data} content-type = image/jpeg
content-length = 123
DATA
+ END_STREAM
{binary data}
在http1.1中,通过Connection表示的持久连接,而http2本身已经是持久连接了,已经废弃这个字段。不仅这个字段,任何表示连接语义的字段都不能用(即Proxy-Connection、Keep-Alive、Transfer-Encoding和Upgrade)
服务器推送
HTTP/2 允许服务器与先前客户端发起的请求一起预先发送(或“推送”)响应(以及相应的“承诺”请求)给客户端。
服务器推送旨在让服务器通过预测收到的请求之后会发出哪些请求来提高客户端感知的性能,从而减少请求的往返次数。例如,对 HTML 的请求通常会跟在对该页面引用的样式表和脚本的请求之后。推送这些请求后,客户端无需等待在 HTML 中接收对它们的引用并发出单独的请求。
实际上,服务器推送很难有效使用,因为它要求服务器正确预测客户端将发出的额外请求,同时考虑缓存、内容协商和用户行为等因素。预测错误可能会导致性能下降,因为网络上的额外数据代表着机会成本。特别是,推送任何大量数据都可能导致与更重要的响应发生争用问题。
可以根据实际情况禁用服务器推送。
协议升级
HTTP2不支持101状态码,如果需要协议升级可以通过TLS ALPN(Application-Layer Protocol Negotiation)扩展实现
HTTP2.0实现
Java实现HTTP/2的框架有 Apache HttpComponents 5.0-beta1、Apache Tomcat 8.5+、Jetty、Netty、OkHttp等。详情参考: https://github.com/httpwg/http2-spec/wiki/Implementations
使用curl访问h2地址
curl -v https://nghttp2.org
* Connected to nghttp2.org (139.162.123.134) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* SSL connection using TLSv1.2 / ECDHE-ECDSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x19838f0)
> GET / HTTP/2
> Host: nghttp2.org
> user-agent: curl/7.79.1
> accept: */*
可以看到使用ALPN协商的过程,最终使用h2协议。
使用curl访问h2c地址。
curl -v http://nghttp2.org --http2
* Connected to nghttp2.org (139.162.123.134) port 80 (#0)
> GET / HTTP/1.1
> Host: nghttp2.org
> User-Agent: curl/7.79.1
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAAQCAAAAAAIAAAAA
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 101 Switching Protocols
< Connection: Upgrade
< Upgrade: h2c
* Received 101
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
可以看到使用了Upgrade: h2c 进行协议升级。
使用Go语言访问HTTP2接口。go从1.6开始 net/http包默认已经支持HTTP2,使用ALPN协商升级。
func main() {
//transport := &http.Transport{
// TLSClientConfig: &tls.Config{
// //NextProtos: []string{}, // 禁用 ALPN
// NextProtos: []string{"h2", "http/1.1"},
// },
//}
// 创建 HTTP 客户端
client := &http.Client{
//Transport: transport,
}
// 创建请求
req, err := http.NewRequest("GET", "https://http2.pro/api/v1", nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// 发送请求
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return
}
// 打印响应体
fmt.Println(string(body))
}