浅谈http

1. HTTP介绍

1.1 HTTP概括

HTTP全称是超文本传输协议。它定义了客户端和服务器之间交换报文的格式和方式,默认使用 80 端口。它使用 TCP 作为传输层协议,保证了数据传输的可靠性。

HTTP 是一个无状态的协议,HTTP 服务器不会保存关于客户的任何信息。

HTTP 有两种连接模式,一种是持续连接,一种非持续连接。非持续连接指的是服务器必须为每一个请求的对象建立和维护 一个全新的连接。持续连接下,TCP 连接默认不关闭,可以被多个请求复用。采用持续连接的好处是可以避免每次建立 TCP 连接三次握手时所花费的时间。

1.2 HTTP报文

HTTP 请求报文的格式如下:

浅谈http 流程图

第一行叫做请求行,后面的行叫做首部行,首部行后还可以跟一个实体主体。请求首部之后有一个空行,这个空行不能省略,它用来划分首部与实体。

请求行包含三个字段:方法字段URL 字段和 HTTP 版本字段

方法字段可以取几种不同的值,一般有 GET、POST、HEAD、PUT 和 DELETE

一般GET 方法只被用于向服务器获取数据。 POST 方法用于将实体提交到指定的资源,通常会造成服务器资源的修改。HEAD 方法与 GET 方法类似,但是在返回的响应 中,不包含请求对象。PUT 方法用于上传文件到服务器,DELETE 方法用于删除服务器上的对象。OPTIONS方法用以以检测服务器支持哪些 HTTP 方法。

虽然请求的方法很多,但 更多表达的是一种语义上的区别,并不是说 POST 能做的事情,GET 就不能做了,主要看我们如何选择。

更多请求方法可查看:HTTP请求方法

响应报文的格式如下:

浅谈http 流程图

响应报文的第一行叫做状态行,后面的行是首部行,最后是实体主体。状态行包含了三个字段:协议版本字段、状态码和相应的状态信息。

常见的状态有:

一般 1XX 代表服务器接收到请求、2XX 代表成功、3XX 代表重定向、4XX 代表客户端错误、5XX 代表服务器端错误。

200-请求成功、202-服务器端已经收到请求消息,但是尚未进行处理 301-永久移动、302-临时移动、304-所请求的资源未修改、 400-客户端请求的语法错误、404-请求的资源不存在 500-服务器内部错误。

更多关于状态码的可以查看: HTTP状态码

实体部分是报文的主要部分,它包含了所请求的对象。如下:

浅谈http 流程图

1.3 HTTP首部行

常见的首部行分为四种:请求首部、响应首部、通用首部和实体首部。

常见的请求首部有:

1
2
3
Accept:text/html // 可接收的资源媒体类型
Accept-Charset: ISO-8859-1, utf-8 // 可接受的字符集
Host: baidu.com //请求的主机名

常用的响应首部有:

1
2
ETag: // 资源的匹配信息
Location: baidu.com // 客户端重定向的URI

常见的通用首部有:

1
2
Cache-control: // 缓存控制策略
Connection: keep-alive // 管理持久连接

常见的实体首部有:

1
2
3
Content-Length: // 实体主体的大小
Expires: // 实体主体的过期时间
Last-Modified: // 资源的最后修改时间

更多资料可查阅:HTTP首部字段详细介绍

2. HTTP的发展史

随着浏览器的发展,HTTP为了能适应新的形式也在持续进化,我认为学习 HTTP 的最佳途径就是了解其发展史。明确HTTP在进化过程中所遇到的各种瓶颈,以及对应的解决方法。

2.1 HTTP/0.9

HTTP/0.9是0991年提出的,主要用于学术交流。当时的需求仅仅是在网络之间传递体积很小的HTML超文本文件。

一个HTTP/0.9的大致流程如下:

  1. 客户端先根据IP地址、端口号和服务器建立TCP连接。
  2. 建立好连接之后,客户端会发送一个GET请求行的信息,如GET /index.html用来获取index.html
  3. 服务器接收到请求信息之后,读取对应的HTML文件,并将数据以ASCll字符流返回给客户端
  4. HTML文档传输完毕,断开连接

大致过程如下:

浅谈http 流程图

HTTP/0.9主要有以下三个特点:

  • 只有请求行,没有请求头和请求体
  • 服务器没有返回头信息,只返回数据
  • 返回的文件内容以ASCll字符流传输的

因为只支持GET一种请求方法、在通讯中没有指定版本号,而且不支持请求头,所以客户端无法向服务器传递太多的信息。该方法已经过时。

2.2 HTTP/1.0

1994年底出现了拨号上网服务,同年网景又新推出了一款浏览器,从此万维网就不再局限于学术交流了。而是进入了高速发展的阶段,随后万维网联盟(W3C)和HTTP工作组(HTTP-WG)成立,它们致力于HTML的发展和HTTP的改进。

万维网的高速发展带来了很多新的需求,HTTP/0.9 已经不能适用。详细分析HTTP/1.0 之前,我们先来分析下新兴网络都带来了哪些新需求。

(1) 支持多种类型文件的下载,文件格式不局限于ASCll编码,如JavaScript、CSS、图片、音视频等。

(2) 浏览器要有缓存机制

(3) 服务器需要统计客户端的基础信息,比如 Windows 和 macOS 的用户数量分别是多少

……

为了让客户端和服务器更深入地交流,HTTP/1.0基于HTTP0.9引入了请求头和响应头

为实现多类型文件的下载:

  • 浏览器要知道返回的数据是什么类型的,然后才能根据不同的类型做出不同的处理。
  • 为减轻传压力,提高性能。服务器会对传输的数据进行压缩再传输。浏览器要知道服务器使用的压缩方式
  • 为提供国际化的支持,服务器需要针对不同地区提供不同语言版本,浏览器要告诉服务器它想要什么语言类型的页面。
  • 因为每种文件类型采用的编码不同,为了准确读取文件,浏览器需要知道文件的编码类型

基于以上需求,一般的HTTP1.0的请求报文通常包含以下请求头:

1
2
3
4
accept:text/html
accept-encoding:gzip, delete, br
accept-language:zh-CN, zh
accept-Charset: ISO-8859-1, utf-8

服务器收到请求信息后,会根据请求头的信息来准备响应数据。(选取它支持的压缩/编码方法、语言等),最终响应头大致如下:

1
2
content-encoding:br
content-type:text/html; charset=UTF-8

有了响应头的信息,浏览器就会使用 br 方法来解压文件,再按照 UTF-8 的编码格式来处理原始文件,最后按照 HTML 的方式来解析该文件。这就是 HTTP/1.0 支持请求/响应头之后的一个基本的处理流程。

除了对多文件提供良好的支持外,还依据当时实际的需求引入了很多其他的特性,这些特性都是通过请求头和响应头来实现的。比如:

  • 引入了状态码,告诉浏览器服务器的处理情况
  • 提供了Cache机制。用来缓存已经下载过的数据
  • 引入了UserAgent,用于统计用户的机器信息

HTTP1.0至今都是一个仍在被广泛使用的协议。特别是在代理服务器中,它是第一个在通讯中指定版本号的HTTP协议版本。它采用的是非持续连接,但是可以是加上Connection:keep-alive来要求服务器不关闭TCP连接。

2.3 HTTP/1.1

HTTP1.1的主要变化有:

  • 持久连接
  • 管线化技术
  • 虚拟主机
  • 支持动态生成的数据
  • Cookie和安全机制

HTTP/1.0每进行一次HTTP通信,都需要建立和断开TCP连接。一开始通信的文件都比较小,而且每个页面的引用也不是非常的多。但随着浏览器的普及,单个页面中的资源越来越多,这样的通信方式会产生许多无谓的开销。

为了解决这个问题,HTTP/1.1默认采用持久连接的方式,目前对于同一个域,大多数浏览器支持同时建立6个TCP持久连接。如果不想使用持久连接,可以在请求头中加入Connection:close字段。

持久连接虽然能减少TCP建立和断开次数,但是它需要等待前面的请求返回之后,才能进行下一次请求。如果TCP通道中的某个请求因为某种特殊的原因没有及时返回,那么就会阻塞后面所有的请求,这就是著名的队头阻塞问题。队头阻塞会导致持久连接在达到最大数量时,剩余的资源需要等待其他资源请求完毕才能发起请求。

HTTP1.1中试图通过管线化技术来解决对头阻塞问题。它是讲多个HTTP请求成批提交给服务器,但服务器依然需要根据请求的顺序来回复浏览器的请求。FireFox、Chrome都做过管线化的实验,最终都放弃了。

HTTP1.1还对虚拟主机提供了支持。HTTP/1.0中,每个域名绑定了一个唯一的IP地址,因此一个服务器只能支持一个域名。但是随着虚拟主机技术的发展,一台物理主机上可能有多个虚拟主机,每个虚拟主机都有自己的域名。这些域名共用一个IP地址。因此HTTP/1.1中增加了Host字段,用来表示当前的域名地址。

除此之外,HTTP/1.1还增加了许多其他特性,比如对动态生成的内容提供了完美支持,设计HTTP/1.0时,需要在响应头中设置完整的数据大小。如Content-Length:903,这样浏览器可以根据设置的数据大小准备资源来接收数据。不过随着服务端的发展,许多页面内容是动态生成的,数据传输之前根本不知道最终的数据大小,这就导致浏览器不知道什么时候会接收完所有数据(不知道什么时候结束)。

HTTP1.1引入Chunk transfer机制来解决这个问题,服务器会把数据分割为任意大小的数据块,每个数据块发送的时候会被附加上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志。

HTTP1.1还引入了客户端Cookie和安全机制,详见我的其他文章聊聊浏览器缓存,这里不再赘述。

除了特性之外,HTTP/1.1还对网络性能做了大量优化,主要体现在:

  • 增加了持久连接
  • 浏览器为每个域名维护六个TCP持久连接
  • 使用CDN可以实现域名分片

尽管采用了这么多优化策略,但HTTP/1.1对带宽的利用率依然不理想,这是HTTP/1.1的一个核心问题。之所以说 HTTP/1.1 对带宽的利用率不理想,是因为 HTTP/1.1 很难将带宽用满。主要原因大概有三个:

  1. TCP的慢启动
  2. 多条TCP连接竞争固定的带宽
  3. 队头阻塞问题

(1) 慢启动是TCP为了避免网络拥塞的一种策略,在HTTP层没有办法改变。而之所以说慢启动会带来性能问题,是因为页面中常用的一些关键资源文件本来就不大,如HTML 文件、CSS 文件和 JavaScript 文件,通常这些文件在 TCP 连接建立好之后就要发起请求的,但这个过程是慢启动,所以耗费的时间比正常的时间要多很多,这样就推迟了宝贵的首次渲染页面的时长了。

关于什么是慢启动,详见我的文章浅谈传输层协议

(2) 系统同时建立了多条 TCP 连接,当带宽充足时,每条连接发送或者接收 速度会慢慢向上增加;而一旦带宽不足时,这些 TCP 连接又会减慢发送或者接收的速度。

这样就会出现一个问题,因为有的 TCP 连接下载的是一些关键资源,如 CSS 文件、 JavaScript 文件等,而有的 TCP 连接下载的是图片、视频等普通的资源文件,但是多条 TCP 连接之间又不能协商让哪些关键资源优先下载,这样就有可能影响那些关键资源的下载速度。

(3) 队头阻塞使得这些数据不能并行请求,很不利于浏览器优化。

在浏览器处理生成页面的过程中,是非常希望能提前接收到数据的,这样就可以对这些数据做预处理操作,比如提前接收到了图片,那么就可以提前进行编解码操作,等到需要使用该图片的时候,就可以直接给出处理后的数据了,这样能让用户感受到整体速度的提升。

2.4 HTTP/2.0

2009年,谷歌公开了自行研发的 SPDY 协议,主要解决 HTTP/1.1 效率不高的问题。它后来被当作HTTP2的基础,主要特性都在HTTP2中得到继承。

通过前面的分析我们已经知道,HTTP/1.1中的慢启动和TCP之间的相互竞争带宽是TCP协议本身的机制导致的,而队头阻塞是由于HTTP/1.1本身的机制导致的。我们没有办法换掉TCP,所以只能想办法规避。

基于此,HTTP2的思路是一个域名只使用一个TCP长连接来传输数据。这样整个页面就一次慢启动,同时也避免了TCP连接竞争的问题。

针对队头阻塞,HTTP/2使用二进制分帧实现了多路复用

在HTTP/1.1中,报文的头信息必须是ASCll码,数据体可以是文本,也可以是二进制。而HTTP/2是一个完全的二进制协议。头信息和数据体都是二进制。并且统称为”帧”,可以分为头部帧和数据帧。帧的概念是实现多路复用的基础。

HTTP/2中,将每个请求或回应的所有数据包。称为一个数据流。每个数据流都有独一无二的编号。每个数据包(帧)在发送的时候必须标记数据流ID,用于区分他属于哪一个数据流。在一个连接中,客户端和服务器可以同时发送多个请求和响应的帧,而且不用按照顺序,到了服务器端再根据ID拼接成对应的HTTP请求。这样就避免了队头阻塞的问题。

服务器端在收到这些请求之后,可以根据喜好优先返回某些内容。比如服务器可能早就缓存好了 index.html 和 bar.js 的响应头信息,那么当接收到请求的时候就可以立即把 index.html 和 bar.js 的响应头信息返回给浏览器,然后再将 index.html 和 bar.js 的响应体数据返回给浏览器。之所以可以随意发送,是因为每份数据都有对应的 ID,浏览器接收到之后,会筛选出相同 ID 的内容,将其拼接为完整的 HTTP 响应数据

HTTP/2的二进制分帧还实现了优先请求。可以在某些请求的帧中添加字段,比如对JavaScript或者CSS等关键资源,可以把它标记为优先级高的资源。服务器收到这些优先级比较高的请求的时候,可以暂停之前的请求来优先处理关键资源的请求。

现在我们知道HTTP/2使用多路复用解决了队头阻塞问题,那么多路复用具体是怎么实现的呢?

浅谈http 流程图

从图中可以看出,HTTP/2 添加了一个二进制分帧层,那我们就结合图来分析下 HTTP/2 的请求和接收过程。

  • 首先,浏览器准备好请求数据,包括了请求行、请求头等信息,如果是 POST 方法,那 么还要有请求体。
  • 这些数据经过二进制分帧层处理之后,会被转换为一个个带有请求 ID 编号的帧,通过协议栈将这些帧发送给服务器。
  • 服务器接收到所有帧之后,会将所有相同 ID 的帧合并为一条完整的请求信息。
  • 然后服务器处理该条请求,并将处理的响应行、响应头和响应体分别发送至二进制分帧层。
  • 同样,二进制分帧层会将这些响应数据转换为一个个带有请求 ID 编号的帧,经过协议栈发送给浏览器。
  • 浏览器接收到响应帧之后,会根据 ID 编号将帧的数据提交给对应的请求

浅谈http 流程图

HTTP/2还实现了一些其他的特性,比如头信息压缩服务器推送

HTTP/2 允许服务器未经请求,提前主动给客户端推送必要的资源 ,这样就可以相对减少一些延迟时间。这里需要 注意的是 http2 下服务器主动推送的是静态资源。这和 WebSocket 以及使用 SSE 等方式 向客户端发送即时数据的推送是不同的。

一般是客户端收到HTML文件之后,服务器知道该 HTML 页面会引用几个重要的 JavaScript 文件和 CSS 文件,那么在接收到 HTML 请求之后,附带将要使用的 CSS 文件和JavaScript文件一并发送给客户端。这样当浏览器解析完 HTML 文件之后,就能直接拿到需要的 CSS 文件和 JavaScript 文件(节省了下载的RTT),这对首次打开页面的速度起到了至关重要的作用。详见我的其他文章:页面的性能优化

由于 HTTP/1.1协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。

HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzipcompress压缩后再发送;另一方面, 客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。

2.5 HTTP/3.0

很久之前我看过一段对HTTP/3的描述,我觉得很恰当,现分享一下:

甩掉TCP、TLS包袱,构建高效网络。

HTTP/2.0中使用了多路复用,一个域名只使用一个TCP连接,由于多个数据流使用的是一个TCP连接,所以遵守一个流量控制和拥塞机制。只要一个数据流遭遇拥塞,剩下的数据流就没办法发送出去。后面所有的数据都会被阻塞。

到了传输层,一个二进制帧(Message)会被拆分为很多的报文段(segment)来传输。而TCP是一个按序交付的可靠传输协议。一旦某个报文段丢失,就会重传造成阻塞。这是TCP的队头阻塞,是TCP本身的机制问题,与HTTP/2.0本身实现无关。

除了TCP的队头阻塞外,TCP的建立连接也有一定的网络延迟(TCP三次握手 1.5个RTT,TLS 1~2个RTT)。

同时还有一点很重要的是,TCP协议比较僵化。虽然知道已有TCP队头阻塞网络延迟的问题,但想要通过改进TCP协议来规避这些问题确实几乎不可能的。主要有两个原因:

  • 中间设备的僵化

网络核心中的中间设备有很多类型,包括路由器、防火墙、NAT、交换机等。它们通常依赖一些很少升级的软件,这些软件使用了大量的 TCP 特性,这些功能被设置之后就很少更新了。

如果我们在客户端更新了TCP协议,这些中间设备很可能不理解包的内容,于是就会把数据包丢弃。

  • 操作系统的僵化

TCP 协议都是通过操作系统内核来实现的,应用程序只能使用不能修改。而操作系统的更新都滞后于软件的更新。因此要想自由地更新内核中的 TCP 协议也是非常困难的。

那难道就没有办法了吗?

答案还是有的,既然不能修改,那我绕过TCP协议不久好了吗?

但是这也面临着和修改 TCP 一样的挑战,因为中 间设备的僵化,这些设备只认 TCP 和 UDP,如果采用了新的协议,新协议在这些设备同样不被很好地支持。

因此,HTTP/3 选择了一个折衷的方法——UDP 协议,基于UDP实现了类似于TCP的多路数据流、传输可靠性等功能,我们把这套功能称为QUIC协议。

浅谈http 流程图

通过上图我们可以看出,HTTP/3 中的 QUIC 协议集合了以下几点功能。

  • 实现了类似 TCP 的流量控制、传输可靠性的功能。它再UDP的基础上提供了数据包重传、拥塞控制以及其他一些 TCP 中存在的特性。
  • 集成了 TLS 加密功能。目前 QUIC 使用的是 TLS1.3,相较于早期版本 TLS1.3 有更多的优点,其中最重要的一点是减少了握手所花费的 RTT 个数。
  • 实现了 HTTP/2 中的多路复用功能。和 TCP 不同,QUIC 实现了在同一物理连接上可以有多个独立的逻辑数据流。实现了数据流的单独传输,就解决了 TCP 中队头阻塞的问题。

浅谈http 流程图

相信在技术层面,HTTP/3 是个完美的协议。不过要将 HTTP/3 应用到实际环境中依然面临着诸多严峻的挑战,这些挑战主要来自于以下三个方面:

  1. 目前服务器和浏览器端都没有对 HTTP/3 提供比较完整的支持。Chrome 虽然在数年前就开始支持 Google 版本的 QUIC,但是这个版本的 QUIC 和官方的 QUIC 存在着非常大的差异。
  2. 部署 HTTP/3 也存在着非常大的问题。因为系统内核对 UDP 的优化远远没有达到TCP 的优化程度,这也是阻碍 QUIC 的一个重要原因。
  3. 中间设备僵化的问题。这些设备对 UDP 的优化程度远远低于 TCP,据统计使用QUIC 协议时,大约有 3%~7% 的丢包率。