# 👉 HTTP/HTTPS
# HTTP/1.0、HTTP/1.1、HTTP2 的发展
# HTTP1.1 的发展
# 加入了持久化连接
通过
Connection:keep-alive
开启持久连接,保证了在客户端和服务端首次建立 TCP 连接后,可以进行多个 HTTP 请求,而无需一个 HTTP 请求建立一次 TCP 连接。但这个仍遵守“请求-响应“的模式,即需要在当前请求得到响应后才能发送第二个请求。加入了管道化特性,允许客户端在发送 HTTP 请求得到响应之前,发送下一个请求,借此来减少等待时间提高吞吐。
但是,这个会存在对头阻塞的问题:
对头阻塞是指在 TCP 连接中,HTTP 请求必须等待前一个请求响应之后,才能发送,后面的依次类推,由此可以看出,如果在一个 TCP 通道中如果某个 http 请求的响应因为某个原因没有及时返回,后面的响应会被阻塞。(因为管道化要求服务端按照请求发送的顺序返回响应(FIFO),原因很简单,HTTP 请求和响应并没有序号标识,无法将乱序的响应与请求关联起来。)
在 HTTP1.1 版本如何解决队头阻塞:
1)减少请求数量
内联一些资源:css、base64 图片等,合并小文件减少资源数;2)并发连接
HTTP 协议建议客户端使用并发长连接,这个并发指的是 TCP 并发连接接。
一般浏览器会把并发连接数增加到 6 到 8 个,谷歌浏览器是 6 个,也就是页面中如果针对同一个域名有多个 HTTP 请求,谷歌浏览器会针对这个域名建立 6 个 tcp 长连接,在每个长连接里面再去处理 HTTP 请求,但是这种方案其实对服务器的挑战非常大。3)域名分片
有些 web 优化方案中还会突破 6 到 8 的限制,那就是域名切片,因为长连接是针对的同一个域名,那么如果开发人员将资源分布在不同的域名上,那么长连接的数量也是可以被突破的。 例如:一个域名可以并发 6 个长连接,那我就多分几个域名:比如 content1.tupian1.com 、content2.tupian1.com。例子
假设页面中有 100 张图片,基于这个案例,咱们用图示将 HTTP1.0 到 HTTP1.1 的变迁用三张图来表示一下:
1)HTTP1.0 时代:100 个 HTTP 请求建立 100 个 TCP 连接。2)HTTP1.1 时代:TCP 支持了长连接,每个 TCP 可以处理多个 HTTP 请求。同时,也可以借助域名切片来增大 TCP 的并发数,但这样做会增大服务器的连接数。
耗时的角度看:
1)如果在一个页面中只一个 TCP 长连接,现在 100 个资源所用时间为100 * RTT * n
,如果用 6 个 TCP 链接,那么时间缩短为100 * RTT * n/6
。2)在 http1.1 协议下,针对浏览器对每个域名最大并发数的限制,我们可以利用域名分片的技术来进一步缩短时间,使得对同一个网站的最大并发数就会随着域名的增加而增加。时间进一步缩短为:
100 * RTT * n / (6 * 域名个数)
。RTT(Round-Trip Time): 往返时延。在计算机网络中它是一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。
随着域名增多也会出现一些问题
- 服务器压力增大。
- 浏览器解析域名也会花费时间,域名越多花费时间也越多
- TCP 本身的一些不好的特性,慢启动,多条 TCP 链接竞争网速。
TCP 本身存在一些问题,如慢启动,当网速发生变化时,TCP 的速度也会随之波动,速度同样受影响。TCP 本身存在竞争关系,多个 TCP 同时连接会互相抢占网速,同时也可能会有对头阻塞的问题
# 增加缓存的字段
# 支持 Range 字段,可以进行断点续传
# 错误通知的管理
# HTTP2 的发展
# 头部压缩
在 HTTP/1.1 及之前的时代,请求体一般会有响应的压缩编码过程,通过 Content-Encoding 头部字段来指定。
HTTP/2 针对头部字段,也采用了对应的压缩算法——HPACK,对请求头进行压缩。
HPACK 算法压缩
通过在服务器和客户端之间建立哈希表,将常用的字段以及相关的值存储起来,通过索引(比如 0,1,2,...)传给对方即可,对方拿到索引查表就行了。这种传索引的方式,可以说让请求头字段得到极大程度的精简和复用。哈夫曼编码
对于整数和字符串进行哈夫曼编码,哈夫曼编码的原理就是先将所有出现的字符建立一张索引表,然后让出现次数多的字符对应的索引尽可能短,传输的时候也是传输这样的索引序列,可以达到非常高的压缩率。
# 二进制分帧
HTTP/2 把报文全部换成二进制格式,全部传输 01 串,方便了机器的解析。
在⾼层 HTTP API 与低层 TCP 套接字接口的之间引⼊⼆进制分帧层。⼀个请求报⽂会被切割成多个帧,并对它们采用二进制格式的编码将其封装。
其中,HTTP1.X 中的首部信息 header 封装到 Headers 帧中,而 request body 将被封装到 Data 帧中。
# 二进制分帧的结构
HTTP2.0 通信的最小单位为帧,所有帧都共享一个 9 字节的首部,其中包含帧的长度、类型、标志、还有一个保留位,并且至少有标识出当前帧所属的流的标识符(StreamID),帧承载着特定类型的数据,如 HTTP 首部、负荷等等。帧在流上发送的顺序非常重要,最后接收方会把相同 Stream Identifier (同一个流) 的帧重新组装成完整消息报文。
帧开头是 3 个字节的长度,这个长度表示的是帧体的长度。
长度后面的 1 个字节是帧类型,大致可以分成数据帧和控制帧两类;
DATA
帧和HEADERS
帧、PRIORITY
帧属于数据帧,存放的是 HTTP 报文(HEADERS 帧总是先于 DATA 帧发送);
而RST_STREAM
、SETTINGS
、PING
、GOWAY
、WINDOW_UPDATE
等则是用来管理流的控制帧。第 5 个字节是非常重要的帧标志信息,可以保存 8 个标志位,携带简单的控制信息。
常用的标志位有:
END_HEADERS
表示头数据结束,相当于 HTTP/1 里头后的空行(\r\n
);
END_STREAM
表示单方向数据发送结束(即 EOS,End of Stream),相当于 HTTP/1 里 Chunked 分块结束标志(0\r\n\r\n
);
PRIORITY
表示流的优先级;报文头里最后 4 个字节是流标识符,也就是帧所属的“流”。每个帧的 Stream Identifier 字段指明了它属于哪个流。
接收方使用它就可以从乱序的帧里识别出具有相同流 ID 的帧序列,按顺序组装起来就实现了虚拟的“流”。
分帧之后,服务器看到的不再是一个个完整的 HTTP 请求报文,而是一堆乱序的二进制帧。这些二进制帧不存在先后关系,因此也就不会排队等待,也就没有了 HTTP 的队头阻塞问题(TCP 队头阻塞依旧存在)。
PS:既然是乱序,那最后如何来处理这些乱序的数据帧呢?
首先所谓的乱序,指的是不同 ID 的 Stream 是乱序的,但同一个 Stream ID 的帧一定是按顺序传输的。二进制帧到达后对方会将 Stream ID 相同的二进制帧组装成完整的请求报文和响应报文。当然,在二进制帧当中还有其他的一些字段,实现了优先级和流量控制等功能。
# 过程
头部数据压缩之后,HTTP/2 就要把报文拆成二进制的帧准备发送。
HTTP2.0 通信都在一个 TCP 连接上完成,这个连接可以承载任意数量的双向数据流,相应的每个数据流以消息的形式发送。而消息由一或多个帧组成,这些帧可以乱序发送,然后根据每个帧首部的流标识符(streamId)重新组装。
TCP 连接在客户端、服务器端建⽴⼀条双向通道;
这个通道上会有多个双向通讯的流,流是连接中的一个虚拟信道,可以承载双向的消息;每个流都有一个唯一的整数标识符(1、2...N);
最开始两者都是空闲状态,当客户端发送 Headers 帧后,开始分配 Stream ID, 此时客户端的流打开, 服务端接收之后服务端的流也打开,两端的流都打开之后,就可以互相传递数据帧和控制帧了。
A 端将消息切割成多个帧;(消息是指逻辑上的 HTTP 消息,比如请求、响应等,由一或多个帧组成。)
A 端发起⼀个流,并在流中发送多个帧⾄ B 端;
B 端在同⼀个流中整合帧为消息;
# 多路复用
基于二进制分帧层,HTTP2.0 可以在共享一个 TCP 连接来进行多个数据帧的通信,这就是多路复用的概念。可以并行交错的发送请求和响应,这些请求和响应之间互不影响。
# 解决 HTTP 头阻塞
HTTP/2 的二进制分帧机制解决了 HTTP 1.x 中存在的队首阻塞问题,也消除了并行处理和发送请求及响应时对多个连接的依赖。结果,就是应用速度更快、开发更简单、部署成本更低。
# 设置请求优先级
每个流都可以带有一个 31 比特的优先值:0 表示最高优先级,231 -1 表示最低优先级。同时,也可以根据 PRIORITY 帧进行设置,让服务器生成优先级树。
客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。具体来讲,服务器可以根据流的优先级,控制资源分配(CPU、内存、带宽),而在响应数据准备好之后,优先将最高优先级的帧发送给客户端。
比如:客户端优先设置为.css>.js>.jpg,服务端按此顺序返回结果更加有利于高效利用底层连接,提高用户体验。
# 服务器推送
服务端根据客户端的请求,提前返回多个响应,推送额外的资源给客户端。
# 服务器推送如何工作
PUSH_PROMISE 帧是服务端向客户端有意推送资源的信号。
PUSH_PROMISE 帧中只包含预推送资源的首部。如果客户端对 PUSH_PROMISE 帧没有意见,服务端在 PUSH_PROMISE 帧后发送响应的 DATA 帧。如果客户端已经缓存了该资源,不需要推送,可以拒绝 PUSH_PROMISE 帧。
PUSH-PROMISE 必须遵循请求-响应原则,只能借着对请求的响应推送资源。因此客户端发起的流 id 是奇数,⽽服务器端发起的流 id 是偶数。
PUSH_PROMISE 帧必须在返回响应之前发送,以免客户端出现竞态条件(竞态条件是指在多线程的情况下不同的执行顺序会导致计算机执行出不同的结果正确性不同)
HTTP2.0 连接后,客户端与服务端交换 SETTINGS 帧,借此限定双向并发的最大数量。因此,客户端可以限定推送流的数量,或者通过把这个只设置为 0 来完全禁止服务器推送。
所有推送的资源都必须遵守同源策略。
# 流量控制
在同一个 TCP 连接上传输多个数据流,就意味着要共享带宽。标定数据流的优先级有助于按序交付,但只有优先级还不足以确定多个数据流或多个连接间的资源分配。为解决这个问题,HTTP/2 为数据流和连接的流量控制提供了一个简单的机制:
流量控制基于每一跳进行,而非端到端的控制;
流量控制基于 WINDOW_UPDATE 帧进行,即接收方广播自己准备接收某个数据流的多少字节,以及对整个连接要接收多少字节;
流量控制窗口大小通过 WINDOW_UPDATE 帧更新,这个字段指定了流 ID 和窗口大小递增值;
流量控制有方向性,即接收方可能根据自己的情况为每个流乃至整个连接设置任意窗口大小;
流量控制可以由接收方禁用,包括针对个别的流和针对整个连接。
HTTP/2 连接建立之后,客户端与服务器交换 SETTINGS 帧,目的是设置双向的流量控制窗口大小。除此之外,任何一端都可以选择禁用个别流或整个连接的流量控制。
参考文章:
https://www.jianshu.com/p/e57ca4fec26f
# HTTP2 一定会比 HTTP1.x 快吗?
不一定。但当这个连接中出现了丢包的情况,那就会导致 HTTP/2 的表现情况反倒不如 HTTP/1 了。
因为在出现丢包的情况下,整个 TCP 都要开始等待重传,也就导致了后面的所有数据都被阻塞了。但是对于 HTTP/1.1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。
# 建连延时
TCP 连接需要和服务器进行三次握手,即消耗完 1.5 个 RTT 之后才能进行数据传输。
TLS 连接有两个版本—— TLS1.2 和 TLS1.3,每个版本建立连接所花的时间不同,大致需要 1~2 个 RTT。
# 没有解决 TCP 队头阻塞
HTTP2 出现丢包时,整个 TCP 都要等待重传,那么就会阻塞该 TCP 连接中的所有请求。
# 多路复用 导致服务器压力上升 和 容易 Timeout
多路复用没有限制同时请求数。请求的平均数量与往常相同,但实际会有许多请求的短暂爆发,导致瞬时 QPS 暴增。
大批量的请求同时发送,由于 HTTP2 连接内存在多个并行的流,而网络带宽和服务器资源有限,每个流的资源会被稀释,虽然它们开始时间相差更短,但却都可能超时
总结,HTTP2 的缺陷:
- TCP 以及 TCP+TLS 建立连接的延时
- TCP 的队头阻塞并没有彻底解决
- 多路复用导致服务器压力上升
- 多路复用容易 Timeout
# HTTP3
Google 更起炉灶搞了一个基于 UDP 协议的 QUIC 协议,并且使用在了 HTTP/3 上,HTTP/3 之前名为 HTTP-over-QUIC,从这个名字中我们也可以发现,HTTP/3 最大的改造就是使用了 QUIC。
QUIC 虽然基于 UDP,但是在原本的基础上新增了很多功能。它真正“完美”地解决了“队头阻塞”问题。
https://zhuanlan.zhihu.com/p/102561034
https://www.cnblogs.com/Leo_wl/p/10530323.html
# HTTP 和 HTTPS 的区别
- HTTP 是明文传输,HTTPS 则是会加密传输;
- HTTP 是默认端口是 80,HTTPS 默认端口是 443;
- HTTPS 在 HTTP 和 TCP 通信的过程有一个安全层,这个安全层主要负责对数据的加密解密以及进行身份验证,以保证数据的不可篡改和来源的可靠性;
其中说的加密解密算法主要有两大类型:
- 对称加密,加密和解密用的都是同一把密钥,常见的对称加密算法有 AES、DES、SM1、SM4;
- 非对称加密,加解密不是同一把密钥,有公钥和私钥之分,公钥加密的只能用私钥来解,反之亦然。一般私钥只能自己拥有。常见的非对称加密算法有 RSA、SM2。
# HTTPS 的流程
通常 HTTP 直接和 TCP 通信,HTTPS 则先和安全层通信,然后安全层再和 TCP 层通信。也就是说 HTTPS 所有的安全核心都在安全层,它不会影响到上面的 HTTP 协议,也不会影响到下面的 TCP/IP。总的来说,安全层有两个主要的职责:对发起 HTTP 请求的数据进行加密操作和对接收到 HTTP 的内容进行解密操作。
# TLS 握手流程
# RSA 握手
Step1:客户端发送加密套件、TLS 协议、client_random
至服务端;
Step2:服务端回复它支持的加密套件、TLS 协议版本、sever_random
、服务端数字证书;
Step3:客户端端收到以后,校验数字证书的合法性和完整性;
Step4:校验通过以后,客户端生成一个pre_random
,并利用服务端公钥加密回传;
Step5:服务端收到以后,用私钥解密获取pre_random
;
Step6:此时两端都有三个参数pre_random, client_random, sever_random
,两端利用伪随机函数生成一个主密钥master_secret
。
根据以上流程可以看出,RSA 不具备前向安全性:一旦服务端私钥被破解,方可获取到 pre_random、client_random、server_random,而导致之前发送的报文信息都能被破解。
# TLS1.2
Step1:客户端发送加密套件、TLS 协议、client_random
、会话 ID(这个在 client hello 阶段是空,假设我们之前已经与服务端建立过 HTTPs 链接,那么这个有可能是一个有意义的数字) 至服务端;
Step2:服务端回复它支持的加密套件、TLS 协议版本、sever_random
、server_params
、会话 ID 服务端数字证书;
Step3:客户端端收到以后,校验数字证书的合法性和完整性;
Step4:校验通过以后,客户端回传client_params
;
Step5:两端都有四个参数client_params, sever_random, client_random, sever_random
;
两端都利用约定好的 ECDHE 算法,根据client_params, sever_random
生成pre_random
;
然后再根据pre_random, client_random, sever_random
,两端利用伪随机函数生成一个主密钥master_secret
。
TLS1.2 的优势:
相对于 RSA 握手具备前向安全性:服务端私钥不参与主密钥生成的协商过程,就算被破解以不影响之前发送过的报问。
一个完整的 TLS 握手流程涉及大量的加密解密校验过程,TLS1.2 为了克服这个困难,服务器维护一个以 session ID 为索引的结构体,用于临时存放 session key,并在 TLS handshake 阶段分享给浏览器。
当浏览器重新连接 HTTPs 服务器时,TLS handshake 阶段,出示自己的 session ID,服务器获得 session ID,以此为索引,可以获得和该浏览器共同拥有的 session key,使用 session key 可以直接对用户做加密/解密动作。
session ID 寿命通常只有 1 天,如果在此期间没有新的连接,会被删除。
- TLS1.2 中可以启用 False Start,只需要 1.5 RTT 就可以开始传输应用数据。
客户端生成主密钥master_secret
后,可提前抢跑发送收尾信息,告诉服务端自己已经生成了主密钥了并且把数据也一并发过去,不用等到服务端通知收尾以后才开始数据传输。
这个收尾的消息包括两部分,一部分是 Change Cipher Spec(客户端已切换到协商的加密套件状态了),另一个是 Finished 消息,这个消息是对之前所有发送的数据做的摘要,对摘要进行对称加密(用这个共同密钥作为对称密码算法如 DES AES 的密钥),让对方验证一下。
RSA 中需要当双方都验证通过之后,握手才正式结束。后面的 HTTP 才正式开始传输加密报文,需要耗费 2RTT。
TLS1.2 中提前抢跑则可以只需耗费 1.5RTT 在 TLS 握手过程上。
# TLS1.3
在 TLS1.3 中废除了非常多的加密算法,最后只保留五个加密套件:
TLS_AES_128_GCM_SHA256;
TLS_AES_256_GCM_SHA384;
TLS_CHACHA20_POLY1305_SHA256;
TLS_AES_128_GCM_SHA256;
TLS_AES_128_GCM_8_SHA256;
最后剩下的对称加密算法只有 AES 和 CHACHA20,之前主流的也会这两种。分组模式也只剩下 GCM 和 POLY1305, 哈希摘要算法只剩下了 SHA256 和 SHA384 了,默认密钥交换算法为 ECDHE。
在 TLS 1.3 中,客户端首先不仅发送 ClientHello 支持的密码列表,而且还猜测服务器将选择哪种密钥协商算法,并发送密钥共享,这可以节省很大一部分的开销,从而提高了速度。将客户端发送 ECDHE 临时公钥的过程提前到 ClientHello,同时删除了 ChangeCipherSpec 协议简化握手过程,使第一次握手时只需要 1-RTT,来看具体的流程:
Step1:客户端发送客户端支持的协议版本、ECDHE 密钥交换参数列表 KeyShare 至服务端;
Step2:服务端回复选定的加密套件、服务度证书;
使用证书对应的私钥对握手消息签名,将结果发送给客户端;
选用客户端提供的参数生成 ECDH 临时公钥,结合选定的 DH 参数计算出用于加密 HTTP 消息的共享密钥;
服务端生成的临时公钥通过 KeyShare 消息发送给客户端;
Step3:客户端接收到 KeyShare 消息后,使用证书公钥进行签名验证,获取服务器端的 ECDH 临时公钥,生成会话所需要的共享密钥;
Step4:双方使用生成的共享密钥对消息加密传输,保证消息安全。
大体的方式和 TLS1.2 差不多,不过和 TLS 1.2 相比少了一个 RTT,服务端不必等待对方验证证书之后才拿到 client_params,而是直接在第一次握手的时候就能够拿到, 拿到之后立即计算 secret,节省了之前不必要的等待时间。同时,这也意味着在第一次握手的时候客户端需要传送更多的信息,一口气给传完。
这种 TLS 1.3 握手方式也被叫做 1-RTT 握手。但还有进一步优化的空间,接下来看看优化的方式。
# TLS1.3 的优化
HTTPs://www.zhihu.com/search?type=content&q=TLS1.3
HTTPs://www.zhihu.com/search?type=content&q=TLS1.3