HTTP 协议基础
HTTP 协议基础
HTTP 的作用是什么
网络中传输的,是一串01010101
这样的信号,网卡接受到的也是这样的一串信号,接收到这一串01010101
信号之后,,我们应该如何解析呢?
以什么样的方式解析网络中传输过来的二进制信息,就是网络协议规定的内容,发送方必须按照协议规定的格式发送信号,接收方才能正确地解析信号,从这个角度来说,网络协议是一种格式约定。
HTTP 协议,是众多网络协议中的一中。
其实这个问题,我们再学习 Netty 时,在《Netty 基础实践》的
通信协议
小节也提到过。
查看 HTTP 的版本
在 Chrome 浏览器中,右键点击网页空白处,选择检查
选项打开开发者工具
,打开Network
标签页,刷新页面,右键 name 所在行,勾选Protocol
即可看到每个请求的协议及其版本信息
其中 h2 表示HTTP/2.0
,http/1.1
就是字面意思,表示HTTP/1.1
,在当前这个时间点(2024 年 1 月 5 日)大部分的互联网网站使用的 HTTP 协议的版本都已经升级到了HTTP/2.0
。HTTP/3.0
因为刚推出不久,还没有经过市场的检验,使用的还不多。
点击Name列下的一个请求,在右侧弹出框的Headers
标签页下,勾选raw
复选框
即可看到请求详情,和响应详情,例如:
|
|
HTTP 协议的发展历史
参考博客:从 HTTP0.9 到 HTTP3.0 发展历程的万字回首与展望,这篇博客讲得很好,也非常的详细。
版本 | 推出年份 | 当前状态 |
---|---|---|
HTTP/0.9 | 1991 年 | 已过时 |
HTTP/1.0 | 1996 年 | 已过时 |
HTTP/1.1 | 1997 年 | 标准 |
HTTP/2.0 | 2015 年 | 标准 |
HTTP/3.0 | 2022 年 | 标准 |
HTTP/1.0
以前的版本太过古老,我们就不仔细研究了,我们直接从作为标准发布的HTTP/1.0
开始介绍。
HTTP/1.0
其实HTTP/1.0
已经具有我们现在所熟知的 HTTP 的框架了,HTTP 消息分为两部分,消息头和消息体,而且Content-Type
消息头可以让我们传输多种类型的消息体,而且支持多种 HTTP 请求方法:POST、PUT、PATCH、HEAD、OPTIONS、DELETE。还提供Cache-Control
消息头用来对网络资源进行缓存,当客户端在规定时间内访问相同的网页,首先客户端(大部分的时候是浏览器)缓存发挥作用,网页数据直接从浏览器缓存取出,不需要访问后端服务器,或者客户端没缓存,请求还是发到了后端,但是服务端(比如 Tomcat)对请求进行了缓存,于是直接从缓存中返回请求,而不会走 Servlet。
HTTP/1.0
还新增响应状态码,响应状态码会在响应开始时发送,使浏览器能了解服务器执行请求具体情况 (成功 or 失败),并及时根据返回状态码,来相应调整行为(如更新或使用本地缓存)。
此外还增加了多字符集支持,多部分发送,权限等。总的来说,其实HTTP/1.0
已经能满足我们大部分场景下的需求了。
不过HTTP/1.0
的缺点也很突出:HTTP/1.0
是无状态且无连接的应用层协议,浏览器 (客户端) 和服务器 (服务端) 保持短暂的连接 (短连接),对于发送过的请求或响应都不做持久化处理,浏览器每次请求都需要建立 tcp 连接,服务器处理完成后立即断开 TCP 连接(无连接的表现),服务器不跟踪也不记录每个客户端过去的请求(无状态的表现)。
HTTP/1.0的无连接特性也会导致一些性能缺陷:
-
客户端与服务端之间的连接无法复用,
HTTP/1.0
中每个 TCP 连接只能发送一个请求,数据发送完毕连接就关闭,如果还要请求其他资源,就必须重新建立连接。TCP 每次建立连接时,都需要进行三次握手和四次挥手,所以频繁地建立新连接的成本很高,网络的利用率较低 -
队头堵塞问题,由于HTTP/1.0规定下一个请求必须在前一个请求响应到达之前才能发送。假设一个请求因为网络的波动,产生的丢包和乱序问题,导致响应一直不到达,那么下一个请求就不发送,就到导致阻塞后面的请求;而且每次建立连接高成本,会更加重性能缺陷
HTTP/1.1
随着浏览器的普及,网页上的图片视频等文件越来越多,一个页面甚至能包含成千上万个超链接资源文件,HTTP/1.0
每下载一个文件,都需要重新建立连接、传输数据和断开连接这 3 个操作,HTTP/1.0
无连接特性导致连接无法复用以及队头堵塞性能的问题,越来越严重。
1997 年HTTP/1.1
正式发布,HTTP/1.1
正式支持长连接,它在请求头上新增Connection
字段,将其设置为Connection: keep-alive
,当然你不用去设置,因为这是默认开启的,来保持连接不断开,这样也就可以被多个请求复用,如果你不想开启长连接,也可以在 HTTP 请求头中加上Connection: close
。
注意,浏览器对与同一个域名同时建立的 TCP 连接数是有限制的,比如,谷歌浏览器中对于同一个域名,默认允许同时建立 6 个 TCP 长连接。
HTTP/1.1
还加入管道机制pipelining
(多个请求可以"并行",无需等待上一个请求返回),试图解决"队头阻塞"的问题;当然它的前提是 HTTP/1.1 支持长连接,但是管道的效果其实并不好,主要由于它要求服务器在返回响应的时候,响应的顺序还是必须按照其接收到的请求的顺序返回的,只有这样才能保证客户端能够区分出每次请求的响应内容,为什么这么规定呢?由于 HTTP/1.1
是个文本协议,同时返回的内容也并不能区分对应于哪个发送的请求,所以顺序必须维持一致。比如你向服务器发送了两个请求 GET/query?q=A
和 GET/query?q=B
,服务器返回了两个结果,浏览器是没有办法根据响应结果来判断响应对应于哪一个请求的。必须保证按照请求顺序返回响应也就导致了一个问题,在建立起一个 TCP 连接之后,假设客户端在这个连接连续向服务器发送了几个请求。按照标准,服务器应该按照收到请求的顺序返回结果,假设服务器在处理首个请求时花费了大量时间,那么后面所有的请求都需要等着首个请求结束才能响应,所以仍然存在队头阻塞问题。那还不如直接顺序发起请求算了,同时使用多个 TCP 来实现请求的并发,所以 Pipelining 实际用处不大,所以现代浏览器默认是不开启 HTTP Pipelining 的。FireFox、Chrome 都做过管线化的试验,但是由于各种原因,它们最终都放弃了管线化处理。
请求并发过程中的队头阻塞问题一直到
HTTP/2.0
才解决。这也是为什么HTTP/2.0
一经推出就被大范围采纳的原因
在HTTP/1.1
中增加请求头 Host 字段,用来表示当前的域名地址,这样浏览器可以使用 host 来明确表示要访问服务器上的哪个 Web 站点,服务器同时也可以根据不同的 Host 值做不同的处理,这就实现了多个请求发往同一台服务器上的不同网站;也就是说如果每个虚拟 web 站点都有自己的单独的域名,那么这些单独的域名可以公用同一个 IP 地址。
HTTP/1.1
还支持文件的断点续传,所谓的断点续传,就是文件在网络传输过程中时常面对网络中断的情况,上传到中途时,网突然断了,由于文件比较大,若重新开始上传的话,非常浪费时间,这个时候就需要有一种机制可以从上次网络中断的地方继续开始上传,那么HTTP/1.1
是如何支持文件的断点续传?HTTP/1.1
通过新增 Header 里两个参数,客户端发请求时对应的是Range
,服务器端响应时对应的是Content-Range
;再配合新引入的分块传输Chunk transfer
机制,服务器会将数据分割成若干个任意大小的数据块,每个数据块发送时会附上上个数据块的长度 (无论内容多大服务端都可以动态知晓),最后使用一个零长度的块作为发送数据完成的标志。
在《用电信号传输 TCP/IP 数据 —— 探索协议栈和网卡》的
对较大的数据进行拆分
小节,简单了解过这个机制
可能还会出现,当客户端再次发起续传请求时,此时 URL 对应服务器上的资源文件已经发生变化这种情况,HTTP/1.1
还新增Last-Modified
来标识文件的最后修改时间,如此就能够判断出续传文件时是否已经发生过改动;同时还是定义了一个ETag
的头,可以使用 ETag 头来放置文件的唯一标识
在《浏览器生成消息 —— 探索浏览器内部》的
生成 HTTP 请求消息
小节的图中,可以看到对这些请求头的解释。
HTTP/1.1
引入了Cookie技术,在请求头新增Cookie
字段,响应头新增Set-Cookie
字段。Cookie 使基于无状态的 HTTP 协议能够记录稳定的状态信息成为了可能。
Cookie
是服务器发送到用户浏览器并保存到本地的一小块数据,浏览器下次向同一服务器再发起请求时携带 (会带来额外的性能开销) 并一起发送到服务器上。一般,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态或者相关信息
HTTPS
HTTP/1.1
最大的安全问题,就是它是明文传输的,早期 web 和网络主要用于学术资料的传输与共享,但随着 web 流行于整个地球,使用者也越来越鱼龙混杂,所有经过 http 传输的信息都可以被抓包等其他手段看的清清楚楚,存在很大的安全隐患
于是在 1994 年,网景公司(Netscape Communication)在 http 协议依赖的 TCP/IP 协议栈的基础上,创建了一个额外的加密传输层:SSL 协议,后来发展为 TLS,一般被称为SSL/TLS协议。HTTP+SSL/TLS = HTTPS(超文本传输安全协议),所以 HTTPS并不是一个新协议。
HTTPS 连接建立,需要 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才能够进入加密报文传输通道。
HTTP 默认端口号是 80,HTTPS 默认端口号是 443。
HTTP/2.0
HTTP/2.0
在 2015 年正式发布,相较于HTTP/1.1
不仅仅是更新发展,更是里程碑式的突破,其主要解决了 HTTP 的队头阻塞问题,同时对原有的 HTTP 协议传输的问题还进行了很多的优化。
首先HTTP/2.0
相比HTTP/1.1
最大的变化就是传输内容彻底采用二进制,而HTTP1.x
头信息是文本(ASCII 编码),数据体可以是文本,也可以是二进制;HTTP/2.0
为了最终传输二进制,同时兼容HTTP/1.x
,它在应用层和传输层之间增加一个二进制分帧层,在不改动 HTTP/1.x
的语义、方法、状态码、URI 以及首部字段的情况下,重新设计了编码机制,它将所有传输信息分割为更小的帧 frame,并进行二进制编码将其封装;帧就是传输的消息的基本单位。其中帧可以分为:头信息帧(Headers Frame)和数据帧(Data Frame),HTTP/1.x
中的头信息 header 封装到 Headers 帧中,而数据体 body 将被封装到 Data 帧中
计算机中,为了兼容老的技术方案同时提出新的方案,经常的做法就是加一层
完全采用二进制传输唯一的坏处就是降低了 HTTP 消息的可读性,但是好处很多,比如占用的空间更小,同时服务端也不需要再进行2次解码等。
简单介绍一下HTTP/2.0
中的一些概念:
-
连接 Connection:1 个 TCP 连接,包含一个或者多个 Stream。
-
数据流 Stream:一个双向通讯数据流,包含 1 条或者多条 Message。
-
消息 Message:对应
HTTP/1.x
中的请求或者响应,包含一条或者多条 Frame。注意,请求的帧和其对应的响应的帧,在同一个数据流中。 -
数据帧 Frame:最小单位,以二进制压缩格式存放
HTTP/1.x
中的内容,包含类型 Type, 长度 Length, 标记 Flags, 流标识 Stream 和 frame payload 有效载荷。
基于二进制传输,HTTP/2.0
实现了在一个 TCP 连接中实现同时发起多个请求,也就是多路复用。而且不存在队头阻塞问题,每个请求及该请求的响应不需要等待其他的请求,某个请求任务耗时严重,不会影响到其它连接的正常执行。
一个 TCP 连接包含很多数据流(你可以把数据流简单地理解为一个通道,不同的通道通过通道编号进行区分),客户端发起的请求和服务端针对这个请求的响应在一个数据流中,而请求或者响应都可以被拆分成许多个二进制的帧,不过要注意,数据流在传输请求或者响应的帧的时候,必须是按照拆分结果顺序传输,比如一个消息被拆分成了一个 Headers 帧,两个 Data 帧,那在数据流中传输的时候就必须先发送 Headers 帧,然后再发送第一个 Data 帧,再发送第二个 Data 帧。我们可以把属于不同数据流的帧混在同一个连接里双向传输,这些二进制数据帧通过数据流 ID(stream id
)来区分自己属于哪一个数据流;也就是说只需要通过单个 TCP 连接就可以高速传输所有的请求数据,避免了"队头堵塞",这才是真正地实现 TCP 连接的并行,其实就是基于数据流来实现会话的串行传输。这样能够绕过浏览器限制同一个域名下的请求数量的限制,充分利用 TCP 带宽。
逻辑上:
实际上:
在一个 TCP 连接中可以并发多个流(逻辑上的,并不是物理存在的),每一个消息拆分成的所有帧都必须在一个流中顺序传输。
其实,看到这个设计,我想起来操作系统实现多线程也是通过CPU时间片来实现的,CPU时间片跟
HTTP/2.0
的二进制帧的概念很像,CPU按时间执行任务,就像TCP连接按顺序传输帧,属于不同任务/数据流的frame交织(interleave)在一起,实现多线程/多路复用。
类似的在Nginx博客中:
跟HTTP/1.x
相比,HTTP/2.0
核心变化就在于,不再通过多个 tcp 实现并发请求。到通过多个流来实现并发请求。即提高了单个 tcp 的利用率。
HTTP/1.x
的 pipelining 要求在同一个 tcp 中,服务端必须按照收到的请求的顺序来发送请求对应的响应,之所以这么做,是因为,如果有多个 http 响应同时到达,我们无法将响应和请求对应起来,因为响应中不存在 requestID 这种东西。但是HTTP/2.0
解决了这个问题,首先是将一个请求拆分成多个二进制帧,方便并行传输,而且每一个二进制帧都有一个 streamID,这个 streamID 一方面标识多个二进制帧属于同一个请求/响应,也标识了一个响应对应的请求,因为请求和响应是在同一个流中的,这个 streamID 正是HTTP/1.x
时代,我们需要的那个 requestID。
除了多路复用,HTTP/2.0
引入的优化还包括头部压缩(Header Compression)
由于 HTTP 协议是无状态协议,所以每次请求都必须附上所有信息;这就导致同时发出多个请求,往往他们的头信息是相似的,许多字段都是重复的,比如Cookie
和User Agent
等字段,会浪费很多带宽,有时候消息体占用的空间甚至都没有请求头占据的空间大,消息传输的效率很低。
HTTP/2.0
采用HPACK 算法,客户端和服务器同时维护一张头信息表,所有字段都会存入这个静态表,生成一个索引号,以后就不发送同样字段了,只发送索引号,减少传输重复数据的成本;另一方面头信息继续使用 gzip 或 compress 压缩后再发送,进一步压缩传输占用的带宽。
除此之外,HTTP/2.0
还引入了服务端推送
。
当一个网页有许多静态资源,HTTP/1.x
时代,客户端收到网页后,解析 html 源码,发现依赖其他静态资源,然后发起获取静态资源请求;而在HTTP/2.0
中,服务端如果提前预知客户端需要哪些资源,可以提前把这些静态资源和网页一起,主动推给客户端缓存中,也就是在客户端请求之前,服务器未经请求,主动向客户端发送资源,这叫做服务器推送(server push)。也就是说,HTTP/2.0
在一定程度上改变了传统的“请求 - 应答”模型,服务器不再是完全被动地响应请求,也可以新建“流”主动向客户端发送消息。
这个效果与基于HTTP/1.1
实现的Websocket 协议类似,服务端都可以主动给客户端发消息,但是不同的是HTTP/2.0
允许让服务器能够主动地推送资源到客户端缓存(一般就是浏览器缓存),并没有允许推送数据到客户端应用本身(推送到应用本身的意思就是可以被 JavaScript 脚本处理)。服务器的推送只是由浏览器来处理,并不会让应用代码介入,所以客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,客户端可以通过发送RST_STREAM
帧来拒收,这一切客户端的应用都是无感知的。这跟 Websocket 协议还是有很大的差别的。
而且服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行,即遵守同源策略。
关于 Websocket 协议,请看《WebSocket 理论知识》
HTTP/3.0
启用HTTP/2.0
后性能会有较大地提升,解决了 HTTP 这一层面的队头阻塞问题,但是因为所有传输数据的压力并没有消失,而是转移到底层依赖的一个 TCP 连接之上,而 TCP 又是个面向连接、可靠的传输层协议,TCP 建立连接时的三次握手还是耗时还是长的,其次 TCP 依旧存在"队头阻塞"问题。
要想进一步提升 HTTP 的性能,必须要优化其依赖的底层协议。TCP 很成熟意味着改造难度很大。谷歌相较于改进 TCP 协议,更倾向于基于 UDP来开发全新一代 HTTP 协议,HTTP/3.0
也叫QUIC协议 (Quick UDP Internet Connections)
-
TCP 面向连接,可靠,保证数据正确性,保证数据顺序,有队头阻塞问题,成本较高
-
UDP 无连接,不可靠,不保证数据正确性 (可能丢包),不保证数据顺序,无队头阻塞问题,成本较低
TCP 层的"队头阻塞"问题其实不是问题,本质上是因为 TCP 是一个可靠的传输协议,在传输过程中,一旦丢包(没有收到 ACK 信号),会触发 TCP重传机制,这个时候一个 TCP 连接中的其他所有的请求都必须等待,直到丢的包被重传回来;这就会出现因为丢包而阻塞整个连接的请求。
为了实现可靠性传输同时不存在队头阻塞问题,谷歌开发了 QUIC 协议:
-
QUIC
使用 UDP 来替换 TCP,基本上算是重写了 HTTP 协议,而 UDP 没有"队头阻塞"问题,QUIC
也实现了可靠传输,保证数据一定能够抵达目的地,还引入了类似HTTP/2.0
的数据流和多路复用 -
QUIC
协议可以同时运行多个数据流,每个数据流独立互不影响,并为每个流独立实现数据包丢失检测和重传,来保证数据的正确和有序性;当某个数据流丢包时,只会阻塞这个数据流,其他的数据流不会受到影响
而且HTTP/3.0
还缩短了建立连接的时间
我们一般用来衡量网络建立连接性能的,常用指标是 RTT(Round-Trip Time),往返时延,表示从发送端发送数据开始,到发送端收到来自接收端的确认,所经历的时延;通俗点讲就是数据包一来一回的消耗了多少时间。
我们可以通过 curl 命令来查看一个网址的耗时,具体请看《通过 curl 命令判断网络连接的耗时瓶颈》
我们知道,基于 TCP 的 HTTPS 要建立一个连接,需要进行 TCP 握手和 TLS 握手,需要 3 个 RTT:TCP 握手需要 1 个 RTT,TSL 握手需要 2 个 RTT;TSL 握手中,如果把第一次握手计算出来的对称密钥缓存起来,也需要 1 个 RTT,总体上那也需要 2 个 RTT。但是基于 QUIC 协议的 HTTPS,可以做到首次连接只需要 1RTT, 后面的连接只需要 0RTT。这是因为QUIC
可以让客户端发送给服务端的第一个包就可以包含有效的业务数据,这里使用DH 密钥交换算法,重新定义了 TLS 协议加密 QUIC 头部的方式,既提高了网络攻击成本,又降低了建立连接的速度。
关于 Diffie-Hellman 密钥交换协议,请看一文搞懂 Diffie-Hellman 密钥交换协议
HTTP/3.0
还支持连接迁移。
TCP 连接基于四元组 (源 IP, 源端口,目的 IP, 目的端口), 切换网络时,只要有一个因素 (一般是源 IP) 发生变化,就会导致原本的连接异常,这时需要重新创建 TCP 连接,才能正常传输数据,
在《多个客户端如何同时连接同一个服务器的同一个端口》中,我们介绍过唯一确定一个 tcp 连接的方法
在我们现实生活中,网络的切换是很常见的情况,比如从流量上网切换到 WIFI 上网。或者从 WIFI 上网切换到流量上网。如果此时你在进行王者荣耀,你就会掉线,然后需要重连。
而QUIC
不受四元组的影响,当这四个元素发生变化时,原来的连接依旧能够维持稳定;因为使用 64 位的随机数作为连接的Connection ID
,并使用该 ID 表示连接;举个例子,当我们用手机看视频,这个时候需要出门拿快递,手机的网络会从 WIFI 自动地切换到 4G,全程都不会感觉到视频卡顿,体验感直接拉满。
HTTP/3.0
还支持前向纠错。
前向纠错 FEC是一种在单向通信系统中控制传输错误的技术,通过连同数据发送额外的信息进行错误恢复,以降低比特误码率
因为 UDP 并不是个可靠协议,QUIC 使用前向纠错来增加协议的容错性,一段数据会被拆成多个数据包,传输途中某个包丢失,接收方收到数据包后,可以"校验"发现有丢包情况并通过其他包和 FEC,推算出丢失的那个包的数据来"纠错"。
HTTP3 还有许多有意思的特点,篇幅有限就不展开了,详情见Hypertext Transfer Protocol Version 3 (HTTP/3)
主流浏览器对HTTP各版本的支持情况
我们主要通过Can I use来查看浏览器对各种技术的支持情况。
Can I use不仅可以用来查看浏览器对HTTP技术的支持,还可以支持其他协议比如SSE,比如其他技术,比如CSS的各种效果比如Flexible Box Layout的支持情况
比如HTTP/2
,可以看到在当前时间点(2024年1月15日),总体上97.88%
的浏览器的最新版本都支持了HTTP/2.0
,这其中甚至包括了QQ浏览器、百度浏览器等小众的国产浏览器
对HTTP/3
的支持也已经上来了,默认支持的浏览器占到78.23%
,加上经过配置之后支持的浏览器,所有支持HTTP/3
的浏览器占到95.72%
,也很高了。