HTTP协议


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 framedata frame。也就是头部帧和数据体帧。
帧的传输最终在流中进行,流中的帧,头部(header)帧data 帧可以分为多个片段帧,例如data帧即是可以 data = data_1 + data_2 + ... + data_n

此外地,如果基于二进制帧整体来划分,除了报文的帧分类。还有其它一些辅助帧类型,例如:SETTINGSPINGGOWAYWINDOW_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多路复用能力大打折扣
  • TCPTLS叠加了握手时延,建链时长还有1倍的下降空间
  • 基于TCP四元组确定一个连接,这种诞生于有线网络的设计,并不适合移动状态下的无线网络,这意味着IP地址的频繁变动会导致TCP连接TLS会话反复握手,成本高昂

HTTP3协议解决了这些问题:

  • HTTP3基于UDP协议重新定义了连接,在QUIC层实现了无序、并发字节流的传输,解决了队头阻塞问题(包括基于QPACK解决了动态表的队头阻塞)
  • HTTP3重新定义了TLS协议加密QUIC头部的方式,既提高了网络攻击成本,又降低了建立连接的速度(仅需1个RTT就可以同时完成建链与密钥协商)
  • HTTP3PacketQUIC FrameHTTP3 Frame分离,实现了连接迁移功能,降低了5G环境下高速移动设备的连接维护成本

HTTP3协议到底是什么?

就像HTTP2协议一样,HTTP3并没有改变HTTP1的语义。那什么是HTTP语义呢?在我看来,它包括以下3个点:

  • 请求只能由客户端发起,而服务器针对每个请求返回一个响应
  • 请求与响应都由HeaderBody(可选)组成,其中请求必须含有URL方法,而响应必须含有响应码
  • Header中各Name对应的含义保持不变

HTTP3在保持HTTP1语义不变的情况下,更改了编码格式,这由2个原因所致:

首先,是为了减少编码长度。HTTP1协议的编码使用了ASCII码,用空格冒号以及\r\n作为分隔符,编码效率很低

HTTP2HTTP3采用二进制、静态表、动态表与Huffman算法对HTTP Header编码,不只提供了高压缩率,还加快了发送端编码、接收端解码的速度

其次,由于HTTP1协议不支持多路复用,这样高并发只能通过多开一些TCP连接实现。然而,通过TCP实现高并发有3个弊端:

  • 实现成本高。TCP是由操作系统内核实现的,如果通过多线程实现并发,并发线程数不能太多,否则线程间切换成本会以指数级上升;如果通过异步、非阻塞socket实现并发,开发效率又太低
  • 每个TCP连接与TLS会话都叠加了2-3个RTT的建链成本
  • TCP连接有一个防止出现拥塞的慢启动流程,它会对每个TCP连接都产生减速效果

因此,HTTP2HTTP3都在应用层实现了多路复用功能

HTTP2协议基于TCP有序字节流实现,因此应用层的多路复用并不能做到无序地并发,在丢包场景下会出现队头阻塞问题

当网络繁忙时,丢包概率会很高,多路复用受到了很大限制。因此,HTTP3采用UDP作为传输层协议,重新实现了无序连接,并在此基础上通过有序的QUIC Stream提供了多路复用

连接迁移功能是怎样实现的?

对于当下的HTTP1HTTP2协议,传输请求前需要先完成耗时1个RTTTCP三次握手、耗时1个RTTTLS握手(TLS1.3),由于它们分属内核实现的传输层、openssl库实现的表示层,所以难以合并在一起,如下图所示:

IoT时代,移动设备接入的网络会频繁变动,从而导致设备IP地址改变。对于通过四元组(源IP、源端口、目的IP、目的端口)定位连接的TCP协议来说,这意味着连接需要断开重连,所以上述2个RTT的建链时延、TCP慢启动都需要重新来过。
HTTP3QUIC层实现了连接迁移功能,允许移动设备更换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 HeaderBody的格式,以及服务器推送、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 NumberTLS层加密保护了,这是为了防范各类网络攻击的一种设计

Packet Header中被加密保护的字段:

显示为E(Encrypt)的字段表示被TLS加密过

Stream多路复用时的队头阻塞是怎样解决的?

允许微观上有序发出的Packet报文,在接收端无序到达后也可以应用于并发请求中

Packet Header之上的QUIC Frame Header,定义了有序字节流Stream,而且Stream之间可以实现真正的并发。HTTP3Stream,借鉴了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

数值名称
0x00PADDING
0x01PING
0x02 – 0x03ACK
0x04RESET_STREAM
0x05STOP_SENDING
0x06CRYPTO
0x07NEW_TOKEN
0x08 – 0x0fSTREAM
0x10MAX_DATA
0x11MAX_STREAM_DATA
0x12-0x13MAX_STREAMS
0x14DATA_BLOCKED
0x15STREAM_DATA_BLOCKED
0x16-0x17STREAM_BLOCKED
0x18NEW_CONNECTION_ID
0x19RETRY_CONNECTION_ID
0x1aPATH_CHALLENGE
0x1bPATH_RESPONSE
0x1c-0x1dCONNECTION_CLOSE
0x1eHANDSHAKE_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位有特殊用途)包含了以下类型:

数值名称说明
0x00DATA用于传输HTTP Body包体;
0x01HEADERS通过QPACK 编码,传输HTTP Header头部;
0x03CANCEL_PUSH控制帧,用于取消1次服务器推送消息,通常客户端在收到PUSH_PROMISE帧后,通过它告知服务器不需要这次推送;
0x04SETTINGS控制帧,设置各类通讯参数;
0x05PUSH_PROMISE用于服务器推送HTTP Body前,先将HTTP Header头部发给客户端,流程与HTTP2相似;
0x07GOAWAY控制帧,用于关闭连接(注意,不是关闭Stream);
0x0dMAX_PUSH_ID客户端用来限制服务器推送消息数量的控制帧

QUIC Stream Frame定义了有序字节流,且多个Stream间的传输没有时序性要求,这样,HTTP消息基于QUIC Stream就实现了真正的多路复用,队头阻塞问题自然就被解决掉了

QPACK编码是如何解决队头阻塞问题的?

HTTP2中的HPACK编码方式相似,HTTP3中的QPACK也采用了静态表动态表Huffman编码

在上图中,GET方法映射为数字2,这是通过客户端、服务器协议实现层的硬编码完成的。在HTTP2中,共有61个静态表项:

QPACK中,则上升为98个静态表项,比如Nginx上的ngx_htt_v3_static_table数组所示:

对于Huffman以及整数的编码,QPACKHPACK并无多大不同,但动态表编解码方式差距很大

所谓动态表,就是将未包含在静态表中的Header项,在其首次出现时加入动态表,这样后续传输时仅用1个数字表示,大大提升了编码效率。
因此,动态表是天然具备时序性的,如果首次出现的请求出现了丢包,后续请求解码HPACK头部时,一定会被阻塞

QPACK动态表的编码、解码独立在单向Stream中传输,仅当单向Stream中的动态表编码成功后,接收端才能解码双向StreamHTTP消息里的动态表索引

单向指只有一端可以发送消息,双向则指两端都可以发送消息。其中的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动态表的更新结果

由于HTTP3STREAM之间是乱序传输的,因此,若先发送的编码Stream后到达,双向Stream中的QPACK头部就无法解码,此时传输HTTP消息的双向Stream就会进入Block阻塞状态(两端可以通过控制帧定义阻塞Stream的处理方式)

Reference

https://www.taohui.tech/2021/02/04/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/%E6%B7%B1%E5%85%A5%E5%89%96%E6%9E%90HTTP3%E5%8D%8F%E8%AE%AE/

https://juejin.cn/post/6844903796225785870


本文不允许转载。
  目录