HTTP 超文本传输协议是位于 TCP/IP 体系结构中的应用层协议,它是万维网数据通到信的基础。 当我们访问一个网站时,需要通过统一资源定位符(uniform resource locator,URL)来定位服务器并获取资源。
http建立连接过程
- 先通过域名系统(Domain Name System,DNS)查询将域名转换为 IP 地址。即将 *.com 转换为 221.xxx.100.xx 这一过程。
- 通过三次握手(稍后会讲)建立 TCP 连接。
- 发起 HTTP 请求。
- 目标服务器接收到 HTTP 请求并处理。
- 目标服务器往浏览器发回 HTTP 响应。
- 浏览器解析并渲染页面(详见前端组成树渲染的介绍)。### rtt(round-trip time)
从发送端(客户端)发送数据开始,到发送端接收到接收端的确认,总共经历的时延
http连接拆除过程
所有 HTTP 客户端(浏览器)、服务器都可在任意时刻关闭 TCP 连接。通常会在一条报文结束时关闭连接,但出错的时候,也可能在首部行的中间或其他任意位置关闭连接。
TCP 三次握手和四次挥手
首先,我们需要了解一些 TCP 报文段的字段和标志位:
- 32 比特的序号字段和确认号字段,TCP 字节流每一个字节都按顺序编号。确认号是接收方期望从对方收到的下一字节的序号。
- ACK 标志位,用于指示确认字段中的值是有效的 ACK=1 有效,ACK=0 无效。
- SYN 标志位,用于连接建立,SYN 为 1 时,表明这是一个请求建立连接报文。
- FIN 标志位,用于连接拆除,FIN 为 1 时,表明发送方数据已发送完毕,并要求释放连接。
http报文格式
http报文(请求和相应报文类似)由请求行,请求头,请求体组成,之间由回车换行分隔开
请求行和首部是由 ASCII 文本组成的,请求体是可选的,可以为空也可以是任意二进制数据。
// 请报文格式
<method> <request-URL> <version>
<headers>
<entity-body>
// 响应报文格式
<version> <status> <reason-phrase>
<headers>
<entity-body>
请求报文组成
- 请求方法,客户端希望服务器对资源执行的动作。(get, post,put,delete,option)
- 请求 URL,命名了所请求的资源。
- 协议版本,报文所使用的 HTTP 版本。(如1.x)
- 状态码,这三位数字描述了请求过程中所发生的情况。(200, 404)
- 原因短语,数字状态码的可读版本(例如上面的响应示例跟在 200 后面的 OK,一般按规范写最好)。
- 首部,可以有零或多个首部。(请求头信息)
- 实体的主体部分,可以为空也可以包含任意二进制数据。(请求参数部分)#### 请求方法总览
状态码总览
http2
http2是http1的拓展,语义,功能方法,状态码,url,头部字段等这些核心字段基本不变。 它的升级主要在于更改了客户端和服务器之间交换数据的方式。增加了额新的二进制分帧数据层。这一层不兼容之前的1.x版本
http1.x的问题
1. 队头阻塞
在 HTTP 请求应答过程中,如果出现了某种情况,导致响应一直未能完成,那后面所有的请求就会一直阻塞着,这种情况叫队头阻塞。
2. 低效的 TCP 利用
由于 TCP 慢启动机制,导致每个 TCP 连接在一开始的时候传输速率都不高,在处理多个请求后,才会慢慢达到“合适”的速率。对于请求数据量很小的 HTTP 请求来说,这种情况就是种灾难。
3. 臃肿的消息首部
HTTP/1.1 的首部无法压缩,再加上 cookie 的存在,经常会出现首部大小比请求数据大小还大的情况。
4. 受限的优先级设置
HTTP/1.1 无法为重要的资源指定优先级,每个 HTTP 请求都是一视同仁。 在继续讨论 HTTP/2 的新功能之前,先把 HTTP/1.1 的问题列出来是有意义的。因为 HTTP/2 的某些新功能就是为了解决上述某些问题而产生的。
HTTP/2 连接建立过程
现在的主流浏览器 HTTP/2 的实现都是基于 SSL/TLS 的,也就是说使用 HTTP/2 的网站都是 HTTPS 协议的,所以本文只讨论基于 SSL/TLS 的 HTTP/2 连接建立过程。 基于 SSL/TLS 的 HTTP/2 连接建立过程和 HTTPS 差不多。在 SSL/TLS 握手协商过程中,客户端在 ClientHello 消息中设置 ALPN(应用层协议协商)扩展来表明期望使用 HTTP/2 协议,服务器用同样的方式回复。通过这种方式,HTTP/2 在 SSL/TLS 握手协商过程中就建立起来了。
二进制分帧层
HTTP/2 是基于帧的协议。采用分帧是为了将重要信息封装起来,让协议的解析方可以轻松阅读、解析并还原信息。 而 HTTP/1.1 是以文本分隔的。解析 HTTP/1.1 不需要什么高科技,但往往速度慢且容易出错。你需要不断地读入字节,直到遇到分隔符 CRLF 为止,同时还要考虑不守规矩的客户端,它只会发送CRLF。 解析 HTTP/1.1 的请求或响应还会遇到以下问题:
- 一次只能处理一个请求或响应,完成之前不能停止解析。
- 无法预判解析需要多少内存。HTTP/2 有了帧,处理协议的程序就能预先知道会收到什么,并且 HTTP/2 有表示帧长度的字段。
由于 HTTP/2 是分帧的,请求和响应都可以多路复用,有助于解决类似类似队头阻塞的问题。
多路复用
在 HTTP/1.1 中,如果客户端想发送多个并行的请求,那么必须使用多个 TCP 连接。 而 HTTP/2 的二进制分帧层突破了这一限制,所有的请求和响应都在同一个 TCP 连接上发送:客户端和服务器把 HTTP 消息分解成多个帧,然后乱序发送,最后在另一端再根据流 ID 重新组合起来。 这个机制为 HTTP 带来了巨大的性能提升,因为:
- 可以并行交错地发送请求,请求之间互不影响;
- 可以并行交错地发送响应,响应之间互不干扰;
- 只使用一个连接即可并行发送多个请求和响应;
- 消除不必要的延迟,从而减少页面加载的时间;
- 不必再为绕过 HTTP 1.x 限制而多做很多工作;
流
HTTP/2 规范对流的定义是:HTTP/2 连接上独立的、双向的帧序列交换。如果客户端想要发出请求,它会开启一个新流,然后服务器在这个流上回复。 由于有分帧,所以多个请求和响应可以交错,而不会互相阻塞。流 ID 用来标识帧所属的流。 客户端到服务器的 HTTP/2 连接建立后,通过发送 HEADERS 帧来启动新的流。如果首部需要跨多个帧,可能还会发送 CONTINUATION 帧。该 HEADERS 帧可能来自请求或响应。 后续流启动的时候,会发送一个带有递增流 ID 的新 HEADERS 帧。
消息
HTTP 消息泛指 HTTP 请求或响应,消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流 ID 重新组装。
一个消息至少由 HEADERS 帧(它初始化流)组成,并且可以另外包含 CONTINUATION 和 DATA 帧,以及其他的 HEADERS 帧。
HTTP/1.1 的请求和响应部分都分成消息首部和消息体两部分;HTTP/2 的请求和响应分成 HEADERS 帧和 DATA 帧。
优先级
把 HTTP 消息分解为很多独立的帧之后,就可以通过优化这些帧的交错和传输顺序,进一步提升性能。 通过 HEADERS 帧和 PRIORITY 帧,客户端可以明确地和服务器沟通它需要什么,以及它需要这些资源的顺序。具体来讲,服务器可以根据流的优先级,控制资源分配(CPU、内存、带宽),而在响应数据准备好之后,优先将最高优先级的帧发送给客户端。
流量控制
在同一个 TCP 连接上传输多个数据流,就意味着要共享带宽。标定数据流的优先级有助于按序交付,但只有优先级还不足以确定多个数据流或多个连接间的资源分配。 为解决这个问题,HTTP/2 为数据流和连接的流量控制提供了一个简单的机制:
- 流量控制基于每一跳进行,而非端到端的控制;
- 流量控制基于 WINDOW_UPDATE 帧进行,即接收方广播自己准备接收某个数据流的多少字节,以及对整个连接要接收多少字节;
- 流量控制窗口大小通过 WINDOW_UPDATE 帧更新,这个字段指定了流 ID 和窗口大小递增值;
- 流量控制有方向性,即接收方可能根据自己的情况为每个流乃至整个连接设置任意窗口大小;
- 流量控制可以由接收方禁用,包括针对个别的流和针对整个连接。HTTP/2 连接建立之后,客户端与服务器交换 SETTINGS 帧,目的是设置双向的流量控制窗口大小。除此之外,任何一端都可以选择禁用个别流或整个连接的流量控制。
服务器推送
HTTP/2 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。
为什么需要这样一个机制呢?通常的 Web 应用都由几十个资源组成,客户端需要分析服务器提供的文档才能逐个找到它们。那为什么不让服务器提前就把这些资源推送给客户端,从而减少额外的时间延迟呢?服务器已经知道客户端下一步要请求什么资源了,这时候服务器推送即可派上用场。 另外,客户端也可以拒绝服务器的推送。
首部压缩
HTTP/1.1 存在的一个问题就是臃肿的首部,HTTP/2 对这一问题进行了改进,可以对首部进行压缩。 在一个 Web 页面中,一般都会包含大量的请求,而其中有很多请求的首部往往有很多重复的部分。 例如有如下两个请求:
:authority: unpkg.zhimg.com
:method: GET
:path: /za-js-sdk@2.16.0/dist/zap.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36
:authority: zz.bdstatic.com
:method: GET
:path: /linksubmit/push.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36
从上面两个请求可以看出来,有很多数据都是重复的。如果可以把相同的首部存储起来,仅发送它们之间不同的部分,就可以节省不少的流量,加快请求的时间。 HTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送。 下面再来看一个简化的例子,假设客户端按顺序发送如下请求首部:
Header1:foo
Header2:bar
Header3:bat
当客户端发送请求时,它会根据首部值创建一张表:
如果服务器收到了请求,它会照样创建一张表。 当客户端发送下一个请求的时候,如果首部相同,它可以直接发送这样的首部块(请求头):
62 63 64
服务器会查找先前建立的表格,并把这些数字还原成索引对应的完整首部。
取消合并资源
在 HTTP/1.1 中要把多个小资源合并成一个大资源,从而减少请求。而在 HTTP/2 就不需要了,因为 HTTP/2 所有的请求都可以在一个 TCP 连接发送。
取消域名拆分
取消域名拆分的理由同上,再多的 HTTP 请求都可以在一个 TCP 连接上发送,所以不需要采取多个域名来突破浏览器 TCP 连接数限制这一规则了。 参考资料: 半小时搞懂 HTTP、HTTPS和HTTP2