HTTP,中文名为超文本传输协议。是一个在计算机世界中专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范。
GET:
GET是获取服务器上的指定资源,请求参数一般写在URL中。
URL只能支持ASCII,而且浏览器对URL长度有限制。
GET请求没有请求体。
POST:
POST是根据请求负荷对指定的资源做出处理,携带的请求数据一般写在请求体中。
请求体中数据格式可以任意,大小也没有限制。
POST请求也可以将请求参数写在URL中
安全:请求方法不会破坏服务器上的资源。
幂等:多次执行相同的请求,结果一样。
GET请求安全且幂等,POST请求既不安全也不幂等。
对于重复性的HTTP请求,每次获取到的数据都一样时,可以通过缓存技术减少获取服务器的响应。
HTTP缓存技术分为两种,强制缓存和协商缓存。
强制缓存是浏览器判断缓存是否过期,未过期则直接使用缓存。
通过服务端告知浏览器是否可以用缓存的方式,为协商缓存。只有在未命中强制缓存时,才能发起带有协商缓存字段的请求。
简单、灵活和易于扩展、应用广泛和跨平台
报文格式为
header
+body
,header
也是key-value类型;各类请求方法、URL、状态码等组成要求运行自定义扩充
无状态、明文传输、不安全
长链接、管道运输
客户端和服务端,只要任意一端没有明确提出断开连接,则保持TCP连接状态;
在同一个TCP连接中,客户端可以发送多个请求,不必等响应结果,但是服务器必须按照接收请求的顺序发送对这些管道化请求的响应。
HTTP 的信息是明文传输,HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。
HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443。
HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
信息加密:采用混合加密的方式实现信息的机密性;
校验机制:摘要算法来确保数据的完整性;
身份证书:数字证书解决被冒充的风险。
混合加密:
利用对称加密和非对称加密结合的混合加密方式,保证信息的机密性。
摘要算法+数字签名:
公钥加密,私钥解密。这个目的是为了保证内容传输的安全,因为被公钥加密的内容,其他人是无法解密的,只有持有私钥的人,才能解密出实际的内容;
私钥加密,公钥解密。这个目的是为了保证消息不会被冒充,因为私钥是不可泄露的,如果公钥能正常解密出私钥加密的内容,就能证明这个消息是来源于持有私钥身份的人发送的。
数字证书:
通过数字证书的方式保证服务器公钥的身份,解决冒充的风险。
客户端向服务器端索要并验证服务器的公钥,双方协商生产会话秘钥。这就是TLS握手阶段,涉及四次通信,常用RSA算法和ECDHE算法。
RSA:
ClinetHello:
客户端给服务端发起加密通信请求,请求中包含客户端支持的TLS版本、生成的随机数(client random)、支持的密码套件。
ServerHello:
服务端接收到客户端请求后,发送响应,包含确认TLS版本、生成的随机数(server random)、确认的密码套件、服务器的数字证书。
客户端回应:
客户端收到服务端的响应后,确认服务端的数字证书有效性,取出服务器的公钥,生成加密报文。再次向服务端发送信息,一个随机数(pre-master key)、加密通信算法改变通知(以后都用会话秘钥进行加密通信)、客户端握手结束通知。
有了三个随机数和双方协商的加密算法,客户端生成本次通信的会话秘钥。
服务端响应:
服务端接收到第三个随机数,通过协商的加密算法,生成本次通信的会话秘钥。再次发送响应,加密通信算法改变通知(之后的所有通信都进行加密)、服务端握手结束通知。
至此,整个TLS的握手结束。客户端和服务端进入了加密通信,使用HTTP协议,只不过用会话秘钥进行加密内容。
TLS在实现上分为握手协议和记录协议,握手协议是上面提到的四次握手(协商加密算法和生成对称秘钥),记录协议负责保护应用程序数据完整性。
记录协议完成后,将最终的报文数据传输到TCP层进行传输。
HTTPS 协议本身到目前为止还是没有任何漏洞的,即使你成功进行中间人攻击,本质上是利用了客户端的漏洞(用户点击继续访问或者被恶意导入伪造的根证书),并不是 HTTPS 不够安全。
简单:
HTTP基本的报文格式是header
+body
,头部信息是key-value
灵活:
HTTP协议里的各类请求方法、URL、状态码、头字段等每个组成要求都可自定义和扩充
跨平台
无状态:
每次请求都是新的请求,需要验证信息。
解决无状态的比较简单方式是cookie技术,在返回给浏览器的响应中记录cookie。浏览器就会存储该数据,以后该浏览器的每次请求都会携带该cookie,cookie相当于存储在浏览器的数据。
不安全:
HTTP的通信使用明文,内容会被窃听。
长连接:
HTTP1.0的每次请求都基于TCP的三次握手协议,增加了通信开销。
为此,HTTP1.1提出了长连接的通信方式,只要任意一端没有明确提出断开连接,则保持TCP连接状态。
管道网络运输:
采用长连接方式,使管道(pipeline)网络传输成为了可能。
在同一个TCP连接里,客户端可以发起多个请求,不必等第一个响应回来,可以发送第二个请求,减少整体的响应时间。
但服务端必须按照接收请求的顺序发送对这些管道化请求的响应,如果服务端在处理第一个请求时响应时间过长,后续的请求的处理都会被阻塞住,称之为队头阻塞。
总之 HTTP/1.1 的性能一般般,后续的 HTTP/2 和 HTTP/3 就是在优化 HTTP 的性能。
尽量避免发送 HTTP 请求;
在需要发送 HTTP 请求时,考虑如何减少请求次数;
减少服务器的 HTTP 响应的数据大小;
对于重复性的HTTP请求,每次请求获得到的数据一样时,就可以将数据缓存在本地,不必每次通过网络获取服务端的响应。
强制缓存和协商缓存。
减少重定向请求次数;
合并请求;
延迟发送请求;
无损压缩和有损压缩
针对非对称加密,密钥对=私钥+公钥
公钥一般用来加密,私钥用来签名。
公钥和私钥唯一对应,用某个公钥签名过得内容只能用对应的私钥才能解签验证;同样用某个私钥加密的内容只能用对应的公钥才能解密。
对要传输的数据进行hash计算
使用私钥对需要传输的数据的摘要进行加密
数据接收端利用自己的公钥对签名进行解密
客户端发一个Client Hello,消息里面有客户端使用的TLS版本、支持的密码套件列表,以及生成的随机数(client random)。client random会被服务端留下,以后用作生成对称加密秘钥的材料之一。
服务端收到客户端的Hello,会确认TLS版本号是否支持,从密码套件列表中选择一个密码套件,以及服务端生成的随机数(server random)。
客户端和服务端互相打招呼的过程,就互相确认了支持的TLS版本号和即将要使用的密码套件,并且各自生成一个随机数,将随机数传递给对方。
服务端为了证明自己的身份,会发送「Server Certificate」给客户端,这个消息里含有数字证书。
客户端拿到了服务端的数字证书,需要验证有效性。一个数字证书通常包含:
公钥;
持有者信息;
证书认证机构(CA)的信息;
CA 对这份文件的数字签名及使用的算法;
证书有效期;
还有一些其他额外信息;
数字证书的作用为认证公钥持有者的身份,防止第三方冒充。
客户端验证成功服务端的身份后,产生新的随机数(pre-master
),利用RSA公钥加密后传递给服务端。服务端用私钥解密后,此时client和server端都含有了三个随机数,client random
、server random
、
pre-master
。
根据这三个随机数,双方生成会话秘钥(master secret
)是对称秘钥,用于后续的HTTP请求/响应。
客户端发送一个Change Cipher Spec
,告诉服务端开始使用加密方式发送消息。
客户端再发一个encrypted handshake message(finisher)
,把之前所有发送的数据做个摘要,再用会话秘钥加密一下,让服务器做个验证。
Change Cipher Spec
之前传输的 TLS 握手数据都是明文,之后都是对称密钥加密的密文。
服务器也是同样的操作,发Change Cipher Spec
和Encrypted Handshake Message
消息,如果双方都验证加密和解密没问题,那么握手正式完成。
最后,就用「会话密钥」加解密 HTTP 请求和响应了。
使用 RSA 密钥协商算法的最大问题是不支持前向保密。
因为客户端传递随机数(用于生成对称加密密钥的条件之一)给服务端时使用的是公钥加密的,服务端收到后,会用私钥解密得到随机数。所以一旦服务端的私钥泄漏了,过去被第三方截获的所有 TLS 通讯密文都会被破解。
为了解决这个问题,后面就出现了 ECDHE 密钥协商算法,我们现在大多数网站使用的正是 ECDHE 密钥协商算法。
客户端首先会发一个「Client Hello」消息,消息里面有客户端使用的 TLS 版本号、支持的密码套件列表,以及生成的随机数(Client Random)。
服务端收到客户端的「打招呼」,同样也要回礼,会返回「Server Hello」消息,消息面有服务器确认的 TLS 版本号,也给出了一个随机数(Server Random),然后从客户端的密码套件列表选择了一个合适的密码套件。
接着,服务端为了证明自己的身份,发送「Certificate」消息,会把证书也发给客户端。
因为服务端选择了 ECDHE 密钥协商算法,所以会在发送完证书后,发送「Server Key Exchange」消息。
至此,TLS 两次握手就已经完成了,目前客户端和服务端通过明文共享了这几个信息:Client Random、Server Random 、使用的椭圆曲线、椭圆曲线基点 G、服务端椭圆曲线的公钥,这几个信息很重要,是后续生成会话密钥的材料。
客户端验证服务端的证书,生成一个随机数作为客户端椭圆曲线的私钥,再根据服务端前面给的信息,生成客户端的椭圆曲线公钥,用「Client Key Exchange」消息发给服务端。
最终的会话密钥,就是用「客户端随机数 + 服务端随机数 + x(ECDHE 算法算出的共享密钥) 」三个材料生成的。
算好会话密钥后,客户端会发一个「Change Cipher Spec」消息,告诉服务端后续改用对称算法加密通信。
接着,客户端会发「Encrypted Handshake Message」消息,把之前发送的数据做一个摘要,再用对称密钥加密一下,让服务端做个验证,验证下本次生成的对称密钥是否可以正常使用。
最后,服务端也会有一个同样的操作,发「Change Cipher Spec」和「Encrypted Handshake Message」消息,如果双方都验证加密和解密没问题,那么握手正式完成。于是,就可以正常收发加密的 HTTP 请求和响应了。
RSA 密钥协商算法「不支持」前向保密,ECDHE 密钥协商算法「支持」前向保密;
使用了 RSA 密钥协商算法,TLS 完成四次握手后,才能进行应用数据传输,而对于 ECDHE 算法,客户端可以不用等服务端的最后一次 TLS 握手,就可以提前发出加密的 HTTP 数据,节省了一个消息的往返时间(这个是 RFC 文档规定的,具体原因文档没有说明,所以这点我也不太明白);
使用 ECDHE, 在 TLS 第 2 次握手中,会出现服务器端发出的「Server Key Exchange」消息,而 RSA 握手过程没有该消息;
由裸数据传输的HTTP协议转成加密数据传输的HTTPS协议,提高安全性的同时也带来了性能消耗。HTTPS比HTTP协议多出了一个TLS协议握手过程,目的是为了通过非对称加密握手协商或者交换出对称加密秘钥。
产生性能消耗的两个环节:
TLS协议的握手过程
握手后的对称加密报文传输
硬件:CPU优化
软件:软件升级和协议优化
协议:密钥交换算法优化和TLS升级
证书:证书传输和证书验证优化
会话复用
HTTP/2 没有在 URI 里引入新的协议名,仍然用「http://」表示明文协议,用「https://」表示加密协议,于是只需要浏览器和服务器在背后自动升级协议,这样可以让用户意识不到协议的升级,很好的实现了协议的平滑升级。
只在应用层做了改变,还是基于 TCP 协议传输,应用层方面为了保持功能上的兼容,HTTP/2 把 HTTP 分解成了「语义」和「语法」两个部分,「语义」层不做改动,与 HTTP/1.1 完全一致,比如请求方法、状态码、头字段等规则保留不变。
HTTP协议的报文是由header
+body
构成的,HTTP1.1对于body部分进行了压缩,带对于header并未做出优化。
HTTP2对header部分进行了优化,使用HPACK算法进行压缩头部,HPACK算法主要包含三个部分:
静态字典;
动态字典;
Huffman 编码(压缩算法);
客户端和服务器两端都会建立和维护「字典」,用长度较小的索引号表示重复的字符串,再用 Huffman 编码压缩数据,可达到 50%~90% 的高压缩率。
静态表只包含了 61
种高频出现在头部的字符串,不在静态表范围内的头部字符串就要自行构建动态表,它的 Index 从 62
起步,会在编码解码的时候随时更新。
HTTP/2 厉害的地方在于将 HTTP/1 的文本格式改成二进制格式传输数据,极大提高了 HTTP 传输效率,而且二进制数据使用位运算能高效解析。
而 HTTP/2通过 Stream ,多个 Stream 复用一条 TCP 连接,达到并发的效果,解决了 HTTP/1.1 队头阻塞的问题,提高了 HTTP 传输的吞吐量。
HTTP/2 通过 Stream 实现的并发,比 HTTP/1.1 通过 TCP 连接实现并发要牛逼的多,因为当 HTTP/2 实现 100 个并发 Stream 时,只需要建立一次 TCP 连接,而 HTTP/1.1 需要建立 100 个 TCP 连接,每个 TCP 连接都要经过TCP 握手、慢启动以及 TLS 握手过程,这些都是很耗时的。
HTTP/2 还可以对每个 Stream 设置不同优先级,帧头中的「标志位」可以设置优先级,比如客户端访问 HTML/CSS 和图片资源时,希望服务器先传递 HTML/CSS,再传图片,那么就可以通过设置 Stream 的优先级来实现,以此提高用户体验。
客户端发起的请求,必须使用的是奇数号 Stream,服务器主动的推送,使用的是偶数号 Stream。服务器在推送资源时,会通过 PUSH_PROMISE
帧传输 HTTP 头部,并通过帧中的 Promised Stream ID
字段告知客户端,接下来会在哪个偶数号 Stream 中发送包体。
在 Stream 1 中通知客户端 CSS 资源即将到来,然后在 Stream 2 中发送 CSS 资源,注意 Stream 1 和 2 是可以并发的。
HTTP/2 协议其实还有很多内容,比如流控制、流状态、依赖关系等等。
这次主要介绍了关于 HTTP/2 是如何提升性能的几个方向,它相比 HTTP/1 大大提高了传输效率、吞吐能力。
第一点,对于常见的 HTTP 头部通过静态表和 Huffman 编码的方式,将体积压缩了近一半,而且针对后续的请求头部,还可以建立动态表,将体积压缩近 90%,大大提高了编码效率,同时节约了带宽资源。
不过,动态表并非可以无限增大, 因为动态表是会占用内存的,动态表越大,内存也越大,容易影响服务器总体的并发能力,因此服务器需要限制 HTTP/2 连接时长或者请求次数。
第二点,HTTP/2 实现了 Stream 并发,多个 Stream 只需复用 1 个 TCP 连接,节约了 TCP 和 TLS 握手时间,以及减少了 TCP 慢启动阶段对流量的影响。不同的 Stream ID 可以并发,即使乱序发送帧也没问题,比如发送 A 请求帧1-> B 请求帧1-> A 请求帧2 -> B 请求帧2,但是同一个 Stream 里的帧必须严格有序。
另外,可以根据资源的渲染顺序来设置 Stream 的优先级,从而提高用户体验。
第三点,服务器支持主动推送资源,大大提升了消息的传输性能,服务器推送资源时,会先发送 PUSH_PROMISE 帧,告诉客户端接下来在哪个 Stream 发送资源,然后用偶数号 Stream 发送资源给客户端。
HTTP/2 通过 Stream 的并发能力,解决了 HTTP/1 队头阻塞的问题,看似很完美了,但是 HTTP/2 还是存在“队头阻塞”的问题,只不过问题不是在 HTTP 这一层面,而是在 TCP 这一层。
HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。
HTTP/2 通过头部压缩、二进制编码、多路复用、服务器推送等新特性大幅度提升了 HTTP/1.1 的性能,而美中不足的是 HTTP/2 协议是基于 TCP 实现的,于是存在的缺陷有三个。
队头阻塞;
TCP 与 TLS 的握手时延迟;
网络迁移需要重新连接;
一个流中的数据丢失了,会影响到其他流的请求。
因为 TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且有序的,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据,从 HTTP 视角看,就是请求被阻塞了。
发起 HTTP 请求时,需要经过 TCP 三次握手和 TLS 四次握手(TLS 1.2)的过程,因此共需要 3 个 RTT 的时延才能发出请求数据。
另外, TCP 由于具有「拥塞控制」的特性,所以刚建立连接的 TCP 会有个「慢启动」的过程,它会对 TCP 连接产生"减速"效果。
一个 TCP 连接是由四元组(源 IP 地址,源端口,目标 IP 地址,目标端口)确定的,这意味着如果 IP 地址或者端口变动了,就会导致需要 TCP 与 TLS 重新握手,这不利于移动设备切换网络的场景,比如 4G 网络环境切换成 WIFI。
UDP 是一个简单、不可靠的传输协议, UDP 包之间是无序的,没有依赖关系。
UDP 是不需要连接的,也就不需要握手和挥手的过程,所以天然的就比 TCP 快。
HTTP/3 不仅仅只是简单将传输协议替换成了 UDP,还基于 UDP 协议在「应用层」实现了 QUIC 协议,它具有类似 TCP 的连接管理、拥塞窗口、流量控制的网络特性,相当于将不可靠传输的 UDP 协议变成“可靠”的了,所以不用担心数据包丢失的问题。
QUIC 协议的优点有很多,这里举例几个,比如:
无队头阻塞;
更快的连接建立;
连接迁移;
QUIC 连接上的多个 Stream 之间没有依赖,都是独立的,某个流发生丢包了,只会影响该流,其他流不受影响。
对于 HTTP/1 和 HTTP/2 协议,TCP 和 TLS 是分层的,分别属于内核实现的传输层、openssl 库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先 TCP 握手,再 TLS 握手。
但是 HTTP/3 的 QUIC 协议并不是与 TLS 分层,而是QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。
而 QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。
HTTP/2 虽然具有多个流并发传输的能力,但是传输层是 TCP 协议,于是存在以下缺陷:
队头阻塞,HTTP/2 多个请求跑在一个 TCP 连接中,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据,从 HTTP 视角看,就是多个请求被阻塞了;
TCP 和 TLS 握手时延,TCL 三次握手和 TLS 四次握手,共有 3-RTT 的时延;
连接迁移需要重新连接,移动设备从 4G 网络环境切换到 WIFI 时,由于 TCP 是基于四元组来确认一条 TCP 连接的,那么网络环境变化后,就会导致 IP 地址或端口变化,于是 TCP 只能断开连接,然后再重新建立连接,切换网络环境的成本高;
HTTP/3 就将传输层从 TCP 替换成了 UDP,并在 UDP 协议上开发了 QUIC 协议,来保证数据的可靠传输。
QUIC 协议的特点:
无队头阻塞,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,也不会有底层协议限制,某个流发生丢包了,只会影响该流,其他流不受影响;
建立连接速度快,因为 QUIC 内部包含 TLS1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与 TLS 密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。
连接迁移,QUIC 协议没有用四元组的方式来“绑定”连接,而是通过「连接 ID 」来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本;
另外 HTTP/3 的 QPACK 通过两个特殊的单向流来同步双方的动态表,解决了 HTTP/2 的 HPACK 队头阻塞问题。
既然有 HTTP 协议,为什么还要有 WebSocket?
看起来服务器主动发消息给客户端的场景,是怎么做到的?
在前端代码里不断定时发送HTTP请求到服务器,服务器接收到请求后给客户端响应,这是一种伪服务器推的形式。
但这样,会有两个比较明显的问题:
当你打开 F12 页面时,你会发现满屏的 HTTP 请求。虽然很小,但这其实也消耗带宽,同时也会增加下游服务器的负担。
最坏情况下,用户在扫码后,需要等个 1~2 秒,正好才触发下一次 HTTP 请求,然后才跳转页面,用户会感到明显的卡顿。
使用起来的体验就是,二维码出现后,手机扫一扫,然后在手机上点个确认,这时候卡顿等个 1~2 秒,页面才跳转。
每次HTTP请求几乎都会留给服务器一定的时间做响应,在这段时间内没有返回响应意味着超时。
如果将 HTTP 请求的超时设置的很大,比如 30 秒,在这 30 秒内只要服务器收到了扫码请求,就立马返回给客户端网页。如果超时,那就立马发起下一次请求。
像这种在较长时间内等待服务器响应的机制,就是所谓的长轮询机制。常用的消息队列RokcetMQ中,消费者去取数据时,也用到了这种方式。
TCP 连接的两端,同一时间里,双方都可以主动向对方发送数据。这就是所谓的全双工。
HTTP/1.1
,也是基于TCP协议的,同一时间里,客户端和服务器只能有一方主动发数据,这就是所谓的半双工。
客户端和服务器之间都要互相主动发大量数据的场景,新的应用层协议WebSocket就被设计出来了。
浏览器在TCP三次握手建立连接后,都统一使用HTTP协议先进行一次通信,如果想建立WebSocket连接,就会在HTTP请求头中带上一些特殊字段。
如果服务器正好支持升级成 WebSocket 协议,就会走 WebSocket 握手流程。
WebSocket完美继承了 TCP 协议的全双工能力,并且还贴心的提供了解决粘包的方案。
它适用于需要服务器和客户端(浏览器)频繁交互的大部分场景,比如网页/小程序游戏,网页聊天室,以及一些类似飞书这样的网页协同办公软件。
在使用 WebSocket 协议的网页游戏里,怪物移动以及攻击玩家的行为是服务器逻辑产生的,对玩家产生的伤害等数据,都需要由服务器主动发送给客户端,客户端获得数据后展示对应的效果。