OSI 网络分层(7 层)

1 物理层 -> 2 数据链路层 -> 3 网络层(ip)-> 4 传输层(tcp) -> 5 会话层 –> 6 表示层 –> 7 应用层(http)
| 协议 | 描述 | |
|---|---|---|
| 第七层 | 应用层 | 支持网络应用,应用协议仅仅是网络应用的一个组成部分,运行在不同主机上的进程则使用应用层协议进行通信。主要的协议有:http、ftp、dns、telnet、smtp、pop3等。 |
| 第六层 | 表示层 | 把数据转换为合适、可理解的语法和语义 |
| 第五层 | 会话层 | 维护网络中的连接状态,即保持会话和同步,有 SSL |
| 第四层 | 传输层 | 负责为信源和信宿提供应用程序进程间的数据传输服务,这一层上主要定义了两个传输协议,即传输控制协议TCP和用户数据报协议UDP。 |
| 第三层 | 网络层 | 负责将数据报独立地从信源发送到信宿,主要解决路由选择、拥塞控制和网络互联等问题。IP 在这一层 |
| 第二层 | 数据链路层 | 负责将IP数据报封装成合适在物理网络上传输的帧格式并传输,或将从物理网络接收到的帧解封,取出IP数据报交给网络层。 |
| 第一层 | 物理层 | 负责将比特流在结点间传输,即负责物理传输。该层的协议既与链路有关也与传输介质有关。 |
HTTP 是应用层协议,而 TCP 是传输层协议
TCP
TCP、UDP都是是传输层协议:
- 用户数据报协议 UDP(User Datagram Protocol):
- 无连接;
- 尽最大努力的交付;
- 面向报文;
- 无拥塞控制;
- 支持一对一、一对多、多对一、多对多的交互通信;
- 首部开销小(只有四个字段:源端口、目的端口、长度、检验和)。
- 传输控制协议 TCP(Transmission Control Protocol):
- 面向连接;
- 每一个TCP连接只能是点对点的(一对一);
- 提供 可靠交付 服务;
- 提供 全双工 通信;
- 面向字节流。
另外,UDP是面向报文的传输方式是应用层交给UDP多长的报文,UDP发送多长的报文,即一次发送一个报文。因此,应用程序必须选择合适大小的报文
应用程序和 TCP 的交互是一次一个数据块(大小不等),但 TCP 把应用程序看成是一连串的无结构的字节流。TCP有一个缓冲,当应该程序传送的数据块太长,TCP就可以把它划分短一些再传送
当网络通信时采用 TCP 协议时,在真正的读写操作之前,客户端与服务器端之间必须建立一个连接,当读写操作完成后,双方不再需要这个连接时可以释放这个连接。连接的建立依靠“三次握手”,而释放则需要“四次握手”,所以每个连接的建立都是需要资源消耗和时间消耗的
TCP连接过程(三次握手)

第一次握手
客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。
第二次握手
服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。
第三次握手
当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。
为什么需要三次握手,2次不行吗?
喂喂喂,我是A,你听的到吗?B:在在在,我能听到,我是B,你能听到我吗? A:(听到了,老子不想理你) B:喂喂喂?听不听到?我X,对面死了,我挂了。。
如果只有 2 次的话,B 并不清楚 A 是否收到他发过去的信息。
TCP断开链接(四次挥手)

第一次挥手
若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。
第二次挥手
B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。
第三次挥手
B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。
PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。
第四次挥手
A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最长报文段寿命,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。
TCP 为什么是四次挥手,而不是三次?
当 A 给 B 发送 FIN 报文时,代表 A 不再发送报文,但仍可以接收报文。
B 可能还有数据需要发送,因此先发送 ACK 报文,告知 A “我知道你想断开连接的请求了”。这样 A 便不会因为没有收到应答而继续发送断开连接的请求(即 FIN 报文)。
B 在处理完数据后,就向 A 发送一个 FIN 报文,然后进入 LAST_ACK 阶段(超时等待)。
A 向 B 发送 ACK 报文,双方都断开连接。
HTTP
HTTP 是建立在 TCP 上的应用层协议,超文本传送协议。
当我们访问一个网站时,需要通过统一资源定位符(uniform resource locator,URL)来定位服务器并获取资源。
<协议>://<域名>:<端口>/<路径>
一个 URL 的一般形式通常如上所示(http://test.com/index.html ),现在最常用的协议就是 HTTP,HTTP 的默认端口是 80,通常可以省略。
HTTP 连接建立过程
- 先通过域名系统(Domain Name System,DNS)查询将域名转换为 IP 地址。即将 test.com 转换为 221.239.100.30 这一过程。
- 通过三次握手(稍后会讲)建立 TCP 连接。
- 发起 HTTP 请求。
- 目标服务器接收到 HTTP 请求并处理。
- 目标服务器往浏览器发回 HTTP 响应。
- 浏览器解析并渲染页面。
HTTP 连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。
http1.0 :客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。
http1.1 :可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后就可以再发送一个新的请求
http2.0 :支持多路复用,一个 TCP 可同时传输多个 http 请求,头部数据还做了压缩
http3.0 :使用了 QUIC,开启多个 TCP 连接,在出现丢包的情况下,只有丢包的 TCP 等待重传,剩余的 TCP 连接还可以正常传输数据
HTTP特点
- 无状态:协议对客户端没有状态存储,对事物处理没有“记忆”能力,比如访问一个网站需要反复进行登录操作。
- 无连接:HTTP/1.1之前,由于无状态特点,每次请求需要通过TCP三次握手四次挥手,和服务器重新建立连接。比如某个客户机在短时间多次请求同一个资源,服务器并不能区别是否已经响应过用户的请求,所以每次需要重新响应请求,需要耗费不必要的时间和流量。
- 基于请求和响应:基本的特性,由客户端发起请求,服务端响应。
- 简单快速、灵活。
- 通信使用明文、请求和响应不会对通信方进行确认、无法保护数据的完整性。
HTTP 报文格式
HTTP 报文由请求行、首部、实体主体组成,它们之间由 CRLF(回车换行符) 分隔开。
注意:实体包括首部(也称为实体首部)和实体主体,sp 即是空格 space。

请求行和首部是由 ASCII 文本组成的,实体主体是可选的,可以为空也可以是任意二进制数据。
请求报文和响应报文的格式基本相同。
请求报文格式:
<method> <request-URL> <version>
<headers>
<entity-body>
响应报文格式:
<version> <status> <reason-phrase>
<headers>
<entity-body>
一个请求或响应报文由以下字段组成:
- 请求方法,客户端希望服务器对资源执行的动作。
- 请求 URL,命名了所请求的资源。
- 协议版本,报文所使用的 HTTP 版本。
- 状态码,这三位数字描述了请求过程中所发生的情况。
- 原因短语,数字状态码的可读版本(例如上面的响应示例跟在 200 后面的 OK,一般按规范写最好)。
- 首部,可以有零或多个首部。
- 实体的主体部分,可以为空也可以包含任意二进制数据。
- 一个 HTTP 请求示例:
GET /2.app.js HTTP/1.1
Host: 118.190.217.8:3389
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36
Accept: */*
Referer: http://118.190.217.8:3389/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
一个 HTTP 响应示例:
HTTP/1.1 200 OK
X-Powered-By: Express
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Last-Modified: Sat, 07 Mar 2020 03:52:30 GMT
ETag: W/"253e-170b31f7de7"
Content-Type: application/javascript; charset=UTF-8
Vary: Accept-Encoding
Content-Encoding: gzip
Date: Fri, 15 May 2020 05:38:05 GMT
Connection: keep-alive
Transfer-Encoding: chunked
Method
- GET 方法请求一个指定资源的表示形式. 使用GET的请求应该只被用于获取数据.
- HEAD 方法请求一个与GET请求的响应相同的响应,只返回请求头,没有响应体,多数由 JavaScript 发起
- POST 方法用于将实体提交到指定的资源,通常导致状态或服务器上的副作用的更改.
- PUT 方法用请求有效载荷替换目标资源的所有当前表示。
- DELETE 方法删除指定的资源。
- CONNECT 方法建立一个到由目标资源标识的服务器的隧道,多用于 HTTPS 和 WebSocket 。
- OPTIONS 方法,预检,用于描述目标资源的通信选项。通过该请求来知道服务端是否允许跨域请求。
- TRACE 方法沿着到目标资源的路径执行一个消息环回测试,多数线上服务都不支持
- PATCH 方法用于对资源应用部分修改。
GET 和 HEAD
其中 GET 和 HEAD 被称为安全方法,因为它们是幂等的(如果一个请求不管执行多少次,其结果都是一样的,这个请求就是幂等的),类似于 POST 就不是幂等的。
HEAD 方法和 GET 方法很类似,但服务器在响应中只返回首部。这就允许客户端在未获取实际资源的情况下,对资源的首部进行检查。使用 HEAD,可以:
- 在不获取资源的情况下了解资源的情况。
- 通过查看响应状态码,看看某个对象是否存在。
- 通过查看首部,了解测试资源是否被修改了。
- 服务器开发者必须确保返回的首部与 GET 请求所返回的首部完全相同。遵循 HTTP/1.1 规范,就必须实现 HEAD 方法。
PUT
与 GET 方法从服务器读取文档相反,PUT 方法会向服务器写入文档。PUT 方法的语义就是让服务器用请求的主体部分来创建一个由所请求的 URL 命名的新文档。 如果那个文档已存在,就覆盖它。
TRACE
客户端发起一个请求时,这个请求可能要穿过路由器、防火墙、代理、网关等。每个中间节点都可能会修改原始的 HTTP 请求,TRACE 方法允许客户端在最终发起请求时,看看它变成了什么样子。
TRACE 请求会在目的服务器端发起一个“环回”诊断。行程最后一站的服务器会弹回一条 TRACE 响应,并在响应主体中携带它收到的原始请求报文。 这样客户端就可以查看在所有中间 HTTP 应用程序组成的请求/响应链上,原始报文是否被毁坏或修改过。

TRACE 方法主要用于诊断,用于验证请求是否如愿穿过了请求/响应链。它也是一种工具,用来查看代理和其他应用程序对用户请求所产生的效果。 TRACE 请求中不能带有实体的主体部分。TRACE 响应的实体主体部分包含了响应服务器收到的请求的精确副本。
其他
- POST 方法通常用来向服务器发送表单数据。
- OPTIONS 方法请求 Web 服务器告知其支持的各种功能。
- DELETE 方法就是让服务器删除请求 URL 所指定的资源。
状态码

300~399 重定向状态码
重定向状态码要么告诉客户端使用替代位置来访问他们感兴趣的资源,要么提供一个替代的响应而不是资源的内容。 如果资源已被移动,可以发送一个重定向状态码和一个可选的 Location 首部来告知客户端资源已被移走,以及现在在哪里可以找到它。这样,浏览器可以在不打扰使用者的情况下,透明地转入新的位置。
400~499 客户端错误状态码
有时客户端会发送一些服务器无法处理的东西,例如格式错误的请求报文、一个不存在的 URL。
500~599 服务器错误状态码
有时客户端发送了一条有效请求,服务器自身却出错了。
首部
首部和方法共同配合工作,决定了客户端和服务器能做什么事情。
首部分类:
- 通用首部,可以出现在请求或响应报文中。
- 请求首部,提供更多有关请求的信息。
- 响应首部,提供更多有关响应的信息。
- 实体首部,描述主体的长度和内容,或者资源自身。
- 扩展首部,规范中没有定义的新首部。
通用首部
有些首部提供了与报文相关的最基本信息,它们被称为通用首部。以下是一些常见的通用首部:

请求首部
请求首部是只在请求报文中有意义的首部,用于说明请求的详情。以下是一些常见的请求首部:

响应首部
响应首部让服务器为客户端提供了一些额外的信息。
实体首部
实体首部提供了有关实体及其内容的大量信息,从有关对象类型的信息,到能够对资源使用的各种有效的请求方法。
例如内容首部,提供了与实体内容有关的特定信息,说明了其类型、尺寸以及处理它所需的其他有用信息。 另外,通用的缓存首部说明了如何或什么时候进行缓存。实体的缓存首部提供了与被缓存实体有关的信息。

性能优化
减少 HTTP 请求
每发起一个 HTTP 请求,都得经历三次握手建立 TCP 连接,如果连接只用来交换少量数据,这个过程就会严重降低 HTTP 性能。所以我们可以将多个小文件合成一个大文件,从而减少 HTTP 请求次数。
其实由于持久连接(重用 TCP 连接,以消除连接及关闭时延;HTTP/1.1 默认开启持久连接)的存在,每个新请求不一定都需要建立一个新的 TCP 连接。但是,浏览器处理完一个 HTTP 请求才能发起下一个,所以在 TCP 连接数没达到浏览器规定的上限时,还是会建立新的 TCP 连接。从这点来看,减少 HTTP 请求仍然是有必要的。
静态资源使用 CDN
内容分发网络(CDN)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。
善用缓存
为了避免用户每次访问网站都得请求文件,我们可以通过添加 Expires 头来控制这一行为。Expires 设置了一个时间,只要在这个时间之前,浏览器都不会请求文件,而是直接使用缓存。
不过这样会产生一个问题,当文件更新了怎么办?怎么通知浏览器重新请求文件?
可以通过更新页面中引用的资源链接地址,让浏览器主动放弃缓存,加载新资源。
具体做法是把资源地址 URL 的修改与文件内容关联起来,也就是说,只有文件内容变化,才会导致相应 URL 的变更,从而实现文件级别的精确缓存控制。什么东西与文件内容相关呢?我们会很自然的联想到利用数据摘要要算法对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。
压缩文件
压缩文件可以减少文件下载时间,让用户体验性更好。
gzip 是目前最流行和最有效的压缩方法。可以通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。当然,服务器也得支持这一功能。
通过 max-age 和 no-cache 实现文件精确缓存
通用消息头部 Cache-Control 其中有两个选项:
- max-age: 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。在这个时间前,浏览器读取文件不会发出新请求,而是直接使用缓存。
- no-cache: 指定 no-cache 表示客户端可以缓存资源,每次使用缓存资源前都必须重新验证其有效性。
我们可以将那些长期不变的静态资源设置一个非常长的缓存时间,例如设置成缓存一年。
然后将 index.html 文件设置成 no-cache。这样每次访问网站时,浏览器都会询问 index.html 是否有更新,如果没有,就使用旧的 index.html 文件。如果有更新,就读取新的 index.html 文件。当加载新的 index.html 时,也会去加载里面新的 URL 资源。
例如 index.html 原来引用了 a.js 和 b.js,现在更新了变成 a.js 和 c.js。那就只会加载 c.js 文件。
HTTP 与 TCP 区别
TCP 协议对应于传输层,而 HTTP 协议对应于应用层,从本质上来说,二者没有可比性:
HTTP 对应于应用层,TCP 协议对应于传输层
HTTP 协议是在 TCP 协议之上建立的,HTTP 在发起请求时通过 TCP 协议建立起连接服务器的通道,请求结束后,立即断开 TCP 连接
HTTP 是无状态的短连接,而 TCP 是有状态的长连接
TCP是传输层协议,定义的是数据传输和连接方式的规范,HTTP是应用层协议,定义的是传输数据的内容的规范
说明:从HTTP/1.1起,默认都开启了Keep-Alive,保持连接特性,简单地说,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。
HTTPS
HTTPS 是最流行的 HTTP 安全形式,由网景公司首创,所有主要的浏览器和服务器都支持此协议。 使用 HTTPS 时,所有的 HTTP 请求和响应数据在发送之前,都要进行加密。加密可以使用 SSL 或 TLS。

SSL/TLS 协议作用在 HTTP 协议之下,对于上层应用来说,原来的发送/接收数据流程不变,这就很好地兼容了老的 HTTP 协议。由于 SSL/TLS 差别不大,下面统一使用 SSL。
要想了解 HTTPS 为何安全,还得继续了解一下这些概念:加密算法、摘要算法、数字签名和数字证书。
加密算法
对称密钥密码体制
对称密钥密码体制,即加密密钥和解密密钥是使用相同的密码体制。对称密钥加密技术的缺点之一就是发送者和接收者在对话之前,一定要有一个共享的密钥,所以不太安全。
公钥密码体制
公钥密码体制使用不同的加密密钥与解密密钥。公钥密码体制产生的主要原因有两个:一是对称密钥密码体制的密钥分配问题,二是对数字签名的需求。
在公钥密码体制中,加密密钥是公开的,解密密钥是需要保密的,加密算法和解密算法也是公开的。
公钥密码体制的加密和解密有如下特点:
- 密钥对产生器产生出接收者 B 的一对密钥,即加密密钥 PK 和解密密钥 SK。
- 发送者 A 用 B 的公钥 PK 作为加密密钥来加密信息,B 接收后用解密密钥 SK 解密。

使用对称密钥时,由于双方使用同样的密钥,因此在通信信道上可以进行一对一的双向保密通信,双方都可以用同一个密钥加密解密。
使用公开密钥时,在通信信道上可以是多对一的单向保密信道。即可以有多人持有 B 的公钥,但只有 B 才能解密。
摘要算法
摘要算法的主要特征是加密过程不需要密钥,并且经过加密的数据无法被解密,目前可以被解密逆向的只有CRC32算法,只有输入相同的明文数据经过相同的消息摘要算法才能得到相同的密文。
数字签名
用加密系统对报文进行签名,以说明是谁编写的报文,同时证明报文未被篡改过,这种技术称为数字签名。
数字签名是附加在报文上的特殊加密校验码。使用数字签名的好处有:
- 签名可以证明是作者编写了这条报文。只有作者才会有最机密的私有密钥,因此,只有作者才能计算出这些校验和。
- 签名可以防止报文被篡改,如果有人在报文传输过程中对其进行了修改,校验和就不再匹配了。
数字签名通常是用非对称公开密钥技术产生的。

看上图,任何人都能用 A 的公钥 PK 对密文进行 E 运算后得到 A 发送的明文。可见这种通信并非为了保密,而是为了进行签名和核实签名,即确认此信息是 A 发送的(使用 A 的密钥进行加密的报文,只有使用 A 的公钥才能正确解密)。 但上述过程仅对报文进行了签名,对报文 X 本身却未保密,所以要采用下图的方法,同时实现秘密通信和数字签名。

数字证书
假如你想访问一个网站,怎么确保对方给你的公钥是你想访问的网站的公钥,而不是被中间人篡改过的?
数字证书的出现就是为了解决这个问题,它是由数字证书认证机构颁发的,用来证明公钥拥有者的身份。换句话说,数字证书的作用就相当于人的身份证,身份证证明了张三就是张三,而不是别人。
数字证书一般包含以下内容:
- 对象的名称(人、服务器、组织等);
- 过期时间;
- 证书发布者(由谁为证书担保);
- 来自证书发布者的数字签名;
- 对象的公钥;
- 对象和所用签名算法的描述性信息。
- 任何人都可以创建一个数字证书,但由谁来担保才是重点。
数字证书的数字签名计算过程:
- 用摘要算法对数字证书的内容计算出摘要;
- 用数字证书的私钥对摘要进行加密得到数字签名。

当浏览器收到证书时,会对签名颁发机构进行验证,如果颁发机构是个很有权威的公共签名机构,浏览器可能就知道其公开密钥了(浏览器会预装很多签名颁发机构的证书)。如果对签名颁发机构一无所知,浏览器通常会向用户显示一个对话框,看看他是否相信这个签名发布者。
因为数字证书的公钥是公开的,任何人都可以用公钥解密出数字证书的数字签名的摘要,然后再用同样的摘要算法对证书内容进行摘要计算,将得出的摘要和解密后的摘要作对比,如果内容一致则说明这个证书没有被篡改过,可以信任。
这个过程是建立在被大家所认可的证书机构之上得到的公钥,所以这是一种安全的方式。

HTTPS 连接建立过程
HTTPS 连接建立过程和 HTTP 差不多,区别在于 HTTP(默认端口 80) 请求只要在 TCP 连接建立后就可以发起,而 HTTPS(默认端口 443) 在 TCP 连接建立后,还需要经历 SSL 协议握手,成功后才能发起请求。


HTTP/2
HTTP/2 是 HTTP/1.x 的扩展,而非替代。所以 HTTP 的语义不变,提供的功能不变,HTTP 方法、状态码、URL 和首部字段等这些核心概念也不变。
之所以要递增一个大版本到 2.0,主要是因为它改变了客户端与服务器之间交换数据的方式。HTTP 2.0 增加了新的二进制分帧数据层,而这一层并不兼容之前的 HTTP 1.x 服务器及客户端——是谓 2.0。
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/1.1 的问题
- 队头阻塞
在 HTTP 请求应答过程中,如果出现了某种情况,导致响应一直未能完成,那后面所有的请求就会一直阻塞着,这种情况叫队头阻塞。
- 低效的 TCP 利用
由于 TCP 慢启动机制,导致每个 TCP 连接在一开始的时候传输速率都不高,在处理多个请求后,才会慢慢达到“合适”的速率。对于请求数据量很小的 HTTP 请求来说,这种情况就是种灾难。
- 臃肿的消息首部
HTTP/1.1 的首部无法压缩,再加上 cookie 的存在,经常会出现首部大小比请求数据大小还大的情况。
- 受限的优先级设置
HTTP/1.1 无法为重要的资源指定优先级,每个 HTTP 请求都是一视同仁。
在继续讨论 HTTP/2 的新功能之前,先把 HTTP/1.1 的问题列出来是有意义的。因为 HTTP/2 的某些新功能就是为了解决上述某些问题而产生的。
二进制分帧层
HTTP/2 是基于帧的协议。采用分帧是为了将重要信息封装起来,让协议的解析方可以轻松阅读、解析并还原信息。
而 HTTP/1.1 是以文本分隔的。解析 HTTP/1.1 不需要什么高科技,但往往速度慢且容易出错。你需要不断地读入字节,直到遇到分隔符 CRLF 为止,同时还要考虑不守规矩的客户端,它只会发送 LF。
解析 HTTP/1.1 的请求或响应还会遇到以下问题:
- 一次只能处理一个请求或响应,完成之前不能停止解析。
- 无法预判解析需要多少内存。
HTTP/2 有了帧,处理协议的程序就能预先知道会收到什么,并且 HTTP/2 有表示帧长度的字段。

帧结构
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+

由于 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/2 代替 HTTP/1.1,本身就是一种巨大的性能提升。 这小节要聊的是在 HTTP/1.1 中的某些优化手段,在 HTTP/2 中是不必要的,可以取消的。
取消合并资源
在 HTTP/1.1 中要把多个小资源合并成一个大资源,从而减少请求。而在 HTTP/2 就不需要了,因为 HTTP/2 所有的请求都可以在一个 TCP 连接发送。
取消域名拆分
取消域名拆分的理由同上,再多的 HTTP 请求都可以在一个 TCP 连接上发送,所以不需要采取多个域名来突破浏览器 TCP 连接数限制这一规则了。
林秀栋的技术博客