各协议的特性
HTTP/1.0 —— 无状态无连接的应用层协议
- 无状态:服务器不跟踪不记录请求过的状态
- 无连接:浏览器每次请求都需要建立tcp连接
HTTP/1.0规定浏览器和服务器保持短暂的连接。浏览器的每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)
无状态导致的问题可以借助cookie/session机制来做身份认证和状态记录解决。
然而,无连接特性将会导致以下性能缺陷:
- 无法复用连接。每次发送请求的时候,都需要进行一次TCP连接,而TCP的连接释放过程又是比较费事的。这种无连接的特性会导致网络的利用率非常低。
- 队头堵塞(head of line blocking)。由于HTTP/1.0规定下一个请求必须在前一个请求响应到达之前才能发送。假设一个请求响应一直不到达,那么下一个请求就不发送,就到导致阻塞后面的请求。
为了解决这些问题,HTTP/1.1出现了。
HTTP/1.1
- 长连接。HTTP/1.1增加了一个Connection字段,通过设置Keep-alive(默认已设置)可以保持连接不断开,避免了每次客户端与服务器请求都要重复建立释放TCP连接,提高了网络的利用率。如果客户端想关闭HTTP连接,可以在请求头中携带
Connection:false
来告知服务器关闭请求 - 支持请求管道化(pipelining)。基于HTTP/1.1的长连接,使得请求管线化成为可能。管线化使得请求能够“并行”传输。举个例子来说,假如响应的主体是一个html页面,页面中包含了很多img,这个时候keep-alive就起了很大的作用,能够进行“并行”发送多个请求。(注意这里的“并行”并不是真正意义上的并行传输,具体解释如下。)
需要注意的是,服务器必须按照客户端请求的先后顺序依次回送相应的结果,以保证客户端能够区分出每次请求的响应内容。
也就是说,HTTP管道化可以让我们把先进先出队列从客户端(请求队列)迁移到服务端(响应队列)。
如图所示,客户端同时发了两个请求分别来获取html和css,假如说服务器的css资源先准备就绪,服务器也会先发送html再发送css。
换句话来说,只有等到html响应的资源完全传输完毕后,css响应的资源才能开始传输。也就是说,不允许同时存在两个并行的响应。
可见,HTTP/1.1还是无法解决队头阻塞(head of line blocking)的问题。同时“管道化”技术存在各种各样的问题,所以很多浏览器要么根本不支持它,要么就直接默认关闭,并且开启的条件很苛刻...而且实际上好像并没有什么用处。
那我们在谷歌控制台看到的并行请求又是怎么一回事呢?
如图所示,绿色部分代表请求发起到服务器响应的一个等待时间,而蓝色部分表示资源的下载时间。按照理论来说,HTTP响应理应当是前一个响应的资源下载完了,下一个响应的资源才能开始下载。而这里却出现了响应资源下载并行的情况。这又是为什么呢?
虽然HTTP/1.1支持管道化,但是服务器也必须进行逐个响应的送回,这个是很大的一个缺陷。实际上,现阶段的浏览器厂商采取了另外一种做法,它允许我们打开多个TCP的会话。也就是说,上图我们看到的并行,其实是不同的TCP连接上的HTTP请求和响应。这也就是我们所熟悉的浏览器对同域下并行加载6~8个资源的限制。而这,才是真正的并行!
此外,HTTP/1.1还加入了缓存处理,新的字段如cache-control,支持断点传输,以及增加了Host字段(使得一个服务器能够用来创建多个Web站点)。
HTTP/2.0
先来理解几个概念:
- 帧: HTTP/2 数据通信的最小单位消息:指 HTTP/2 中逻辑上的 HTTP 消息。例如请求和响应等,消息由一个或多个帧组成。
- 流: 存在于连接中的一个虚拟通道。流可以承载双向消息,每个流都有一个唯一的整数ID。
- 消息: 与逻辑消息对应的完整的一系列数据帧。
1.二进制分帧
HTTP/2 采用二进制格式传输数据,而非 HTTP/1.x 的文本格式,二进制协议解析起来更高效。 HTTP / 1 的请求和响应报文,都是由起始行,首部和实体正文(可选)组成,各部分之间以文本换行符分隔。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。
2.多路复用
- 同域名下的所有通信都在单个连接中完成。
- 单个连接可以承载任意数量的双向数据流。
- 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装 这一特性,使性能有了很大的提升:
- 同个域名只需要占用一个TCP连接,消除了因多个TCP连接而带来的延时和内存消耗。
- 单个连接上可以并行交错地请求和响应,之间互不干扰。
- 在HTTP/2中,每个请求都可以带一个31bit的优先值,0表示最高优先级, 数值越大优先级越低。有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。 可见,HTTP/2.0实现了真正的并行传输,它能够在一个TCP上进行任意数量HTTP请求。而这个强大的功能则是基于“二进制分帧”的特性。
3.服务器推送
服务端可以在发送页面HTML时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如服务端可以主动把JS和CSS文件推送给客户端,而不需要客户端解析HTML时再发送这些请求。
服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST_STREAM帧来拒收。主动推送也遵守同源策略,服务器不会随便推送第三方资源给客户端。
4.头部压缩
在HTTP/1.x中,头部元数据都是以纯文本的形式发送的,通常会给每个请求增加500~800字节的负荷。
比如说cookie
,默认情况下,浏览器会在每次请求的时候,把cookie
附在header
上面发送给服务器。(由于cookie
比较大且每次都重复发送,一般不存储信息,只是用来做状态记录和身份认证)
HTTP/2.0使用encoder
来减少需要传输的header
大小,通讯双方各自cache
一份header fields
表,既避免了重复header
的传输,又减小了需要传输的大小。高效的压缩算法可以很大的压缩header
,减少发送包的数量从而降低延迟。
各个协议的区别
HTTP/1.x keep-alive 与 HTTP/2 多路复用区别
- HTTP/1.x 是基于文本的,只能整体去传;HTTP/2 是基于二进制流的,可以分解为独立的帧,交错发送
- HTTP/1.x
keep-alive
必须按照请求发送的顺序返回响应;HTTP/2 多路复用不按序响应 - HTTP/1.x
keep-alive
为了解决队头阻塞,将同一个页面的资源分散到不同域名下,开启了多个 TCP 连接;HTTP/2 同域名下所有通信都在单个连接上完成 - HTTP/1.x
keep-alive
单个 TCP 连接在同一时刻只能处理一个请求(两个请求的生命周期不能重叠);HTTP/2 单个 TCP 同一时刻可以发送多个请求和响应
补充知识
1. cookie和session
1.1什么是 Cookie
HTTP是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。
而这个状态需要通过 cookie 或者 session 去实现。
cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)。
cookie重要属性:
属性 | 说明 |
---|---|
name=value | 键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型 - 如果值为 Unicode 字符,需要为字符编码。 - 如果值为二进制数据,则需要使用 BASE64 编码。 |
domain | 指定 cookie 所属域名,默认是当前域名 |
path | 指定 cookie 在哪个路径(路由)下生效,默认是 '/'。 如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read。 |
maxAge | cookie 失效的时间,单位秒。如果为整数,则该 cookie 在 maxAge 秒后失效。如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。如果为 0,表示删除该 cookie 。默认为 -1。 比 expires 好用。 |
expires | 过期时间,在设置的某个时间点后该 cookie 就会失效。 一般浏览器的 cookie 都是默认储存的,当关闭浏览器结束这个会话的时候,这个 cookie 也就会被删除 |
secure | 该 cookie 是否仅被使用安全协议传输。安全协议有 HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。 当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。 |
httpOnly | 如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全 |
1.2 什么是session
- session 是另一种记录服务器和客户端会话状态的机制
- session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中
- session 认证流程:
- 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
- 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
- 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
- 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。 根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。
1.3 Cookie 和 Session 的区别
- 安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
- 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
- 有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时(一般30分钟无操作)都会失效。
- 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。
2. TCP连接
2.1 三次握手
所谓三次握手(Three-Way Handshake)即建立TCP连接。就是指建立一个TCP连接时,需要客户端和服务端总共发送三个包以确认连接的建立。整个流程如下图所示:
- 第一次握手:Client将标志
SYN=1
,随机产生一个值seq=J
,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。 - 第二次握手:Server收到数据包后由标志位
SYN=1
知道Client请求建立连接,Server将标志位SYN
和ACK
都置为1,ack=J+1
,随机产生一个值seq=K
,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。 - 第三次握手:Client收到确认后,检查
ack
是否为J+1,ACK
是否为1,如果正确则将标志位ACK
置为1,ack=K+1
,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
简单来说,就是
- 建立连接时,客户端发送SYN包(SYN=i)到服务器,并进入到
SYN-SEND
状态,等待服务器确认 - 服务器收到SYN包,必须确认客户的SYN(ack=i+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器进入
SYN-RECV
状态 - 客户端收到服务器的SYN+ACK包,向服务器发送确认报ACK(ack=k+1),此包发送完毕,客户端和服务器进入
ESTABLISHED
状态,完成三次握手,客户端与服务器开始传送数据。
2.2 四次挥手
所谓四次挥手(Four-Way Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送四个包来确定连接的断开。整个流程如下图所示:
由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN
来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。
- 第一次挥手:Client发送一个
FIN
,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。 - 第二次挥手:Server收到
FIN
后,发送一个ACK
给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。 - 第三次挥手:Server发送一个
FIN
,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。 - 第四次挥手:Client收到
FIN
后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
2.3 为什么建立连接是三次握手,而关闭连接却是四次挥手呢?
这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。
3. Keep-Alive
HTTP/1.0 引入了 keep-alive
长连接,HTTP/1.0 中是默认关闭的,可以通过 Connection: keep-alive
; 开启 ,HTTP/1.1 默认是开启的,无论加没加 Connection: keep-alive
;
keep-alive
为了解决队头阻塞,将同一个页面的资源分散到不同域名下,开启了多个 TCP 连接;
所谓长连接,即在 HTTP 请求建立 TCP 连接时,请求结束,TCP 连接不断开,继续保持一段时间(timeout),在这段时间内,同一客户端向服务器发送请求都会复用该 TCP 连接,并重置 timeout 时间计数器,在接下来 timeout 时间内还可以继续复用 TCP 。这样无疑省略了反复创建和销毁 TCP 连接的损耗。
timeout 时间到了之后,TCP会立即断开连接吗?
若两小时(timeout)没有收到客户的数据,服务器就发送一个探测报文段,以后则每隔 75 秒发送一次。若一连发送 10 个探测报文段后仍无客户的响应,服务器就认为客户端出了故障,接着就关闭这个连接。
——摘自谢希仁《计算机网络》
4. http的缓存处理
4.1 浏览器缓存
缓存过程分析:
由上图我们可以知道:
- 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
- 浏览器每次从服务器端拿到返回的请求结果,都会将该结果和缓存标识存入浏览器缓存中
浏览器缓存分为本地缓存(强缓存),协商缓存(再验证) 两个阶段。
其中,常见的HTTP 缓存首部字段:
强缓存:向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程
- Expires(HTTP/1.0)
- Cache-Control(HTTP/1.1)
协商缓存:强缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程
- Last-Modified 和 If-Modified-Since(HTTP/1.0)
- ETag 和 If-None-Match(HTTP/1.1)
4.2 强缓存(缓存控制)
强缓存表示在缓存期间是否使用缓存(缓存是否有效),需不需要重新发送HTTP请求
控制强缓存的字段分别是 Expires
和 Cache-Control
,其中 Cache-Control
优先级比 Expires
高
Expires(HTTP/1.0) 值为服务器返回该请求结果缓存的到期时间:
Expires: Wed, 22 Oct 2018 08:41:00 GMT
表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。
并且 Expires 受限于客户端时间,如果修改了客户端时间,可能会造成缓存失效。
所以现在 HTTP/1.1中新增了 Cache-Control
Cache-Control(HTTP/1.1)
Cache-control: max-age=30
该属性值表示资源会在 30 秒后过期,需要再次请求。也就是说在 30 秒内如果再次发起该请求,则会直接使用缓存,强缓存生效。
它与 Expires 相比:
- HTTP响应报文中 Expires 的时间值,是一个绝对值
- HTTP响应报文中 Cache-Control为max-age=600 ,是相对值(解决 Expires 受限于客户端时间) 除了 max-age ,它还有以下取值:
注意下面的 no-cache
,资源依然会被缓存,并且这个缓存要服务器验证后才可以使用
max-age=0 和 no-cache 等价吗?
从规范的字面意思来说,max-age 到期是 应该(SHOULD) 重新验证,而 no-cache 是 必须(MUST) 重新验证。但实际情况以浏览器实现为准,大部分情况他们俩的行为还是一致的。(如果是 max-age=0, must-revalidate 就和 no-cache 等价了)
总结
自从 HTTP/1.1 开始,Expires
逐渐被 Cache-Control
取代。Cache-Control
是一个相对时间,即使客户端时间发生改变,相对时间也不会随之改变,这样可以保持服务器和客户端的时间一致性。而且 Cache-Control
的可配置性比较强大。
Cache-Control
的优先级高于 Expires
,为了兼容 HTTP/1.0
和 HTTP/1.1
,实际项目中两个字段我们都会设置。
4.3 协商缓存(缓存校验)
如果缓存过期了:
-
没有 Cache-Control 和 Expires
-
Cache-Control 和 Expires 过期
-
设置了 no-cache 需要发起请求验证服务器资源是否有更新:
-
有更新,返回200,更新缓存
-
无更新,返回304,更新浏览器缓存有效期
Last-Modified 和 If-Modified-Since(HTTP/1.0)
- Last-Modified(响应头)
- If-Modified-Since(请求头)
Last-Modified
表示本地文件最后修改日期,If-Modified-Since
会将 Last-Modified
的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回 304 状态码。
但是这种方式存在着一些缺点,例如:
- 负载均衡的服务器,各个服务器生成的 Last-Modified 可能有所不同
- GMT 格式有最小单位,例如,如果在一秒内有更改将不能被识别
ETag 和 If-None-Match(HTTP/1.1)
为了解决上面的那个问题, HTTP/1.1 加了这组标记
- ETag(响应头)
- If-None-Match(请求头)
ETag
类似于文件指纹,是文件的一个唯一标识序列,当资源有变化时,Etag
就会重新生成,If-None-Match
会将当前ETag
发送给服务器,询问该资源ETag
是否变动,有变动的话就将新的资源发送回来。并且ETag
优先级比Last-Modified
高
使用 ETag 就可以精确地识别资源的变动情况,就算是秒内的更新,也会让浏览器感知,能够更有效地利用缓存
ETag 强弱之分
ETag 机制同时支持强校验和弱校验。它们通过ETag标识符的开头是否存在“W/”来区分,如:
"123456789" -- 一个强ETag验证符
W/"123456789" -- 一个弱ETag验证符 强 ETag 要求资源在字节级别必须完全相符,弱 ETag 在值前有个“W/”标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)
4.4 Vary 响应
服务器通过指定 Vary: Accept-Encoding
,告知代理服务器,对于这个资源,需要缓存两个版本:
- 压缩
- 未压缩
这样老式浏览器和新的浏览器, 通过代理, 就分别拿到了未压缩和压缩版本的资源,避免了都拿同一个资源的尴尬。
Vary: Accept-Encoding, User-Agent
如上设置,代理服务器将针对是否压缩和浏览器类型两个维度去缓存资源。如此一来,同一个url,就能针对 PC 和 Mobile 返回不同的缓存内容。