花10分钟理解 HTTP/1,2,3 这5条特性,基础就牢了

113 阅读10分钟

大家好呀,我是码财同行。

HTTP 协议大家也是再熟悉不过了,尤其是 web 开发的前后端同学。HTTP 协议的发展也经历了很长时间。今天,我们就来聊聊它。


HTTP

大家可能知道,20世纪90年代,互联网在美国诞生,随之得到了迅猛发展,美股90年代的大牛市正是互联网的发展带来的。

HTTP 协议随之诞生。它是一种非常简单的协议,它基于 TCP,在应用协议层做了以下规定:

  • HTTP 是一个文本协议,使用和调试起来很方便
  • 约定了具体的格式,如包头(方法、URI等)、空行、包体等
  • 采用 request-response 的交互模式

1996年,HTTP/1.0 正式定型,具体到它的交互模式,是这样的:

建立连接(tcp三次握手)  --> 发送 request 1,等待 response 1 --> 关闭连接(tcp四次挥手)
建立连接(tcp三次握手)  --> 发送 request 2,等待 response 2 --> 关闭连接(tcp四次挥手)

这就是 HTTP/1.0 的短连接模式。大家可能发现了,这样的协议规定好像有点点怪:干嘛要关闭连接之后又继续建立连接?直接继续发送请求、接收响应呗。

于是很快,官方组织很快接受了大家的意见,1997年 HTTP/1.1 发布,解决了这个问题。

下载.jpg


HTTP/1.1协议,连接建立后,不会关闭,可以一直发送数据和接收数据,这就是长连接模式。长连接的交互方式变成:

  • 建立连接(tcp三次握手)
  • 发送 request 1,等待 response 1
  • 发送 request 2,等待 response 2
  • 发送 request 3,等待 response 3
  • ...
  • 关闭连接(tcp四次挥手)

这样提高了效率,去掉了多次建立连接关闭连接的开销。

http请求头里 Connection:keep-alive 可以开启长连接模式,不过一般默认就是开启 keepalive 长连接模式。


二、HTTP/2

串行问题

HTTP/1.1 虽然解决了连接频繁建立的问题,但是还存在一个问题,就是串行(专业术语叫队头阻塞)。因为 HTTP 协议规定的就是 request-response 模式:request 2 必须要等到 response 1 收到之后才能发送出去。

从 HTTP 客户端的角度,简单用一张图来表示:

screenshot-20240307-171330.png

这种串行处理的方式为什么问题很大?

首先是慢,http 的客户端需要一个个请求的等待,如果遇到下载资源等比较耗时的请求,体验会非常差。

其次,服务器在处理一个请求的时候,很有可能有富余的资源来处理其他的请求,因为服务器往往有强大的高并发能力,能并行处理多个请求。如果是串行通信,必须等前一个请求处理完再发起另外一个请求,是对服务器资源的很大浪费。

那么怎么解决所有请求串行传输的问题呢?

答案就是并发传输


并发传输

并发这种思想在 CPU 支持多任务操作系统的切换时候就有应用,就是所谓的时分复用:CPU 的时间被分成一个个时间片,每个程序的执行流也被切分成一小份一小份,CPU 一会执行这个程序的执行流,一会执行那个程序的执行流;而从整体上看,所有程序就是并行执行(交替执行)的。

screenshot-20240311-211022.png

有了 HTTP/2,可以一次性并发发起多个 request,这些 request 被拆分成无差别的 stream,通过 HTTP 的连接通道(TCP连接)传输。

screenshot-20240311-211523.png

多个 request 以 stream 为单位被并发的发给服务器,服务器收到 stream 后,再还原出原先的多个 request,就可以并行处理了。类似的,response 的传输也是并发进行的。

整个过程如下图所示:

screenshot-20240307-164811.png

一条 HTTP 连接(底层 tcp, 发送和接收两个方向)的通道被切分成多个 stream 来传输数据,每个 HTTP request 占用几个,从整体上看,就是多个 request 被同时发送。

这种在一条连接上并发传输的技术更学院派的叫法是 多路复用(multiplexing)

很显然,HTTP/2 为什么引入 stream 的技术也有了答案,类似于 CPU 的时间被切分成多个时间片,长的协议包数据也被切分成多个 stream,方便交替的传输。

而正是由于包被并发的传输到服务器,服务器也能并发处理客户端的多个 request。

screenshot-20240328-162846.png


推送

HTTP/2 除了引入并发传输/多路复用的技术,还支持服务器主动推送。

由于 HTTP/2 没有了 request - response 的限制,包就不需要一一对应,服务器可以主动推送协议包给客户端,这种单一的包传输方便了很多资源的下发,如下图中 css 文件的下发:

Untitled.png

在 Golang 中,服务器实现这种推送的代码也很简单:

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    // 正常处理 request 并发送 response
    //...
    
    // 服务主动单向推送新的协议包
    if pusher, ok := w.(http.Pusher); ok {
        if err := pusher.Push("/static/example.css", nil); err != nil {
            log.Printf("Failed to push: %v", err) 
        } 
    }    
    // ... 
 })


三、HTTP/3

2018年11月,HTTP/3 得到了官方批准。

基于 TCP 的问题

HTTP/2 2015年才发布,为何 HTTP/3 这么快又来了?!

v2-0a73979ff0153cf3a2417814c44b2c03_720w.png


我们知道了,HTTP/1.1、HTTP/2.0 在传输层上都是基于 TCP 的,而 TCP 有自己的问题

这时候,小伙伴们就要问了:TCP 有哪些问题呢?

我们列举一下:

  • 强调公平,效率不高。只要网络一有风吹草动,如网络状况变差,立马谦虚礼让,之前1秒传输1k,现在给你缩减到100 ~
  • 需要三次握手建立连接,效率不高。即使连接建立好之后,TCP 和咱们打工人一样,也不是假期归来一秒进入工作状态,还是有不应期的,而是要一点点慢慢来,传输的数据量根据一定规则提升,直到跑满。这就是所谓的 慢启动
  • 把传输视为 字节流,有严格的先后顺序,而实际上很多web应用需要并行传输很多数据,充分利用带宽。比如,可能访问一个网站,会同时下载很多资源。此时就有严重的串行/队头阻塞问题。

这里,小伙伴肯定有疑惑了,前面不是说 HTTP/2 已经可以并发传输协议了么?

其实,HTTP/2 并没有完全解决串行/对头阻塞这个问题。客户端在并发的收到服务器回的 stream 时,仍然还需要组装成多个 response,这些 response 还是串行的返回给调用层,严格按照顺序。

HTTP/2 最大的改进,其实是充分利用了服务器的并发处理能力。虽然协议包能并发传输,但是并发传输并不代表一定是并行传输,因为底层还是唯一的一条 TCP 连接。

熟悉并发/并行术语的同学就能理解了,HTTP/2 做到了并发,但底层执行的时候没有做到并行,还是在 TCP 的串行通道上传输。这就无法充分利用带宽,满负荷工作。

还因为前面提到的 TCP 本身的问题,单独一条 TCP 连接本身的传输效率也并不高。

为此,HTTP/3 决定放弃底层的 TCP,改用 QUIC 协议


QUIC 对 TCP 的替换

QUIC 是基于 UDP 的。

在 UDP 协议中,并没有连接的概念(其实 TCP 也没有实体的连接概念,只是规则上类似一条逻辑上的连接),没有 TCP 中字节流顺序传输的约定。

传输的时候,以报文为单位,可以随便发,传输的时候可以并行传输在网络上。完全没有规则。

QUIC 协议为什么要基于 UDP 呢?为何不直接新增一个传输层协议,和 TCP、UDP并列?

原因还是这种操作系统协议栈级别的更新会涉及成千上万种设备,替换是几乎不可能的。

无论是改进 TCP 为 TCP2.0 或者新发明一种协议,都需要旷日持久的时间。

而 UDP 协议已经广泛采用,且简单,没有特殊的功能。基于 UDP 是最优选择。

但是 QUIC 也不能等于 UDP,因为 UDP 毫无规则,丢包了也不知道,网络阻塞了也不知道。因此,QUIC 还是需要一种类似 TCP 一样保证可靠性,有流量控制及拥塞控制这些基本的规则,只不过,这些规则实现起来比 TCP 更高效。

从这个意义上说,QUIC 是一种可靠的UDP协议。

screenshot-20240328-180524.png

具体来说,基于 QUIC 的 HTTP/3 模型示意图如下:

quic.png

QUIC 设计了不同于 HTTP/2 的 stream 机制),可以以 stream 为单位发起重传,互相不影响。而在 HTTP/2 中,如果有 stream 丢包,从根本上解决了队头阻塞问题。

举个例子,现在要传输3个文件,假如每个文件都可以被切分成4个stream,HTTP/1.1 和 HTTP/2 中分别是这样传输:

multiplexing-basic.png

HTTP/1.1 中 B 必须等 A全部传输完毕,C也类似;HTTP/2 中可以交替传输。

此时,如果在 HTTP/2 中第三个 stream,也就是文件 B 发生了丢包:

screenshot-20240328-193647.png

此时,虽然后面的 stream 都正常传输过去,但是 tcp 并不知道,TCP 是基于字节流的,它的概念中没有什么 A、B、C 这种 stream,因此它只能重新发送整段未被确认的字节流,包括从丢失的数据段开始到当前已发送的数据段之间的所有数据。这是因为,虽然有可能只有一个段丢失,但由于接收方期望的是按序列号顺序到达的数据,如果后续的数据先到达了,它们也无法被正确处理,直到丢失的段被成功接收。

因此,即使后面的字节流已经收到了,也只能等前面一段字节流传输完毕,才能继续。

从这个角度来说,基于 TCP 的 HTTP/2 虽然在上层实现了 stream,但底层还是字节流,尤其是重传的时候,不能按照小块的 stream 来重传,效率还是较低。

而 HTTP/3 就没有这个问题,它的传输和重传都是基于 stream 的,前后不用等待,多个协议包之间完全互不影响。

screenshot-20240328-194658.png


四、HTTP 协议的普及情况

根据 w3techs.com 的统计,HTTP/2、HTTP/3 的普及基本达到了 30% 左右:

image.png

因此,HTTP/2 和 HTTP/3 的普及速度还是不错的。如果情况允许,还是尽量升级到新的版本吧。


小结

到这里,HTTP 几个版本的特性我们就了解完了,总结一下:

  • HTTP/1 实现了基本的网络协议,有串行传输的问题;
  • HTTP/2 采用了并发/多路复用的技术,最大程度上利用了服务器的处理能力;
  • HTTP/3 摒弃了前两个版本底层的TCP,实现了可靠的基于UDP的机制,从根本上解决了传输效率以及串行/队头阻塞问题

好了,看了这么多,一定很费脑力吧。来个笑话放松一下 :)

【笑话一则】一条鱼往深海里游,游着游着它就哭了,因为压力好大。

感谢您花时间阅读这篇文章!如果觉得有趣或有收获,请来个关注、评论、点赞吧,您的鼓励是我持续创作的动力,蟹蟹!

点赞1.png

| 往期推荐

# 协程没有秘密(二):几张图文快速入门协程,推荐收藏

基于本地知识库,定制一个私有GPT助手,不能再简单了

【建议收藏】服务注册与发现原理+踩坑,一文包教会

【技术·真相】谈一谈游戏AI - 真的搞懂寻路(一)

【技术·真相】谈一谈K8S的存储(一)