[TOC]
http 2.0 新特性
二进制分帧
在应用层(HTTP2.0)和传输层(TCP、UDP)新增的二进制分帧层。在这层中,数据会被分割成更小的消息和帧,然后可以无序发,最后组装就行
head压缩
多路复用
做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。
因为请求都在同一个tcp连接上完成
服务器推送
服务器可以对一个客户端请求发送多个响应,之前是一个请求一个响应
实现原理大致: 客户端发送一次请求,服务端的请求并不会关闭,发完第一次,接着发第二次
目前NGINX的V1.13.9和tomcat已经支持,后端也能实现
关键字是
link
服务器推送有一个很麻烦的问题。所要推送的资源文件,如果浏览器已经有缓存,推送就是浪费带宽。即使推送的文件版本更新,浏览器也会优先使用本地缓存。
一种解决办法是,只对第一次访问的用户开启服务器推送。
来源: 服务器推送实现
HTTP协议的Keep-Alive
可以看到里面的请求头部和响应头部都有一个key-valueConnection: Keep-Alive
,这个键值对的作用是让HTTP保持连接状态(就是俗称的长链接),因为HTTP 协议采用“请求-应答”模式,当使用普通模式,即非 Keep-Alive 模式时,每个请求/应答客户和服务器都要新建一个连接,完成之后立即断开连接(HTTP 协议为无连接的协议);当使用 Keep-Alive 模式时,Keep-Alive 功能使客户端到服务器端的连接持续有效。
在HTTP 1.1版本后,默认都开启Keep-Alive模式,只有加入加入 Connection: close
才关闭连接,当然也可以设置Keep-Alive模式的属性,例如 Keep-Alive: timeout=5, max=100
,表示这个TCP通道可以保持5秒,max=100,表示这个长连接最多接收100次请求就断开。
Keep-Alive模式下如何知道某一次数据传输结束
如果不是Keep-Alive模式,HTTP协议中客户端发送一个请求,服务器响应其请求,返回数据。服务器通常在发送回所请求的数据之后就关闭连接。这样客户端读数据时会返回EOF(-1),就知道数据已经接收完全了。 但是如果开启了 Keep-Alive模式,那么客户端如何知道某一次的响应结束了呢?
以下有两个方法
- 如果是静态的响应数据,可以通过判断响应头部中的Content-Length 字段,判断数据达到这个大小就知道数据传输结束了。
- 但是返回的数据是动态变化的,服务器不能第一时间知道数据长度,这样就没有 Content-Length 关键字了。这种情况下,服务器是分块传输数据的,
Transfer-Encoding:chunk
,这时候就要根据传输的数据块chunk来判断,数据传输结束的时候,最后的一个数据块chunk的长度是0。
使用HTTP建立长连接
当需要建立 HTTP 长连接时,HTTP 请求头将包含如下内容:
Connection: Keep-Alive
如果服务端同意建立长连接,HTTP 响应头也将包含如下内容:
Connection: Keep-Alive
当需要关闭连接时,HTTP 头中会包含如下内容:
Connection: Close
慢速攻击:Http协议中规定,HttpRequest以
结尾来表示客户端发送结束。攻击者打开一个Http 1.1的连接,将Connection设置为Keep-Alive, 保持和服务器的TCP长连接。然后始终不发送
, 每隔几分钟写入一些无意义的数据流, 拖死机器。
强缓存和协商缓存
这里的服务器值是 资源服务器, 例如 NG
当我们向服务器请求资源后,会根据情况将资源 copy 一份副本存在本地,以方便下次读取。它与本地存储 localStorage 、cookie 等不同,本地存储更多是数据记录,存储量较小,为了本地操作方便。而缓存更多是为了减少资源请求,多用于存储文件,存储量相对较大。
就浏览器而言,一般缓存我们分为四类,按浏览器读取优先级顺序依次为:Memory Cache
、Service Worker Cache
、HTTP Cache
、Push Cache
。而本篇文章主要讲的就是 HTTP Cache
HTTP Cache
HTTP Cache 是我们开发中接触最多的缓存,它分为强缓存和协商缓存。优先级: 强缓存 > 协商缓存
强缓存:直接从本地副本比对读取,不去请求服务器,返回的状态码是 200。
协商缓存:会去服务器比对,若没改变才直接读取本地缓存,返回的状态码是 304。
因为需要问 服务器 看资源是否过期, 所以叫协商缓存
强缓存
强缓存主要包括 expires
和 cache-control
。
expires
expires 是 HTTP1.0 中定义的缓存字段。当我们请求一个资源,服务器返回时,可以在 Response Headers 中增加 expires 字段表示资源的过期时间。它是一个时间戳(准确点应该叫格林尼治时间),当客户端再次请求该资源的时候,会把客户端时间与该时间戳进行对比,如果大于该时间戳则已过期,否则直接使用该缓存资源。
例如: expires: Thu, 03 Jan 2019 11:43:04 GMT
但是,发送请求时是使用的客户端时间去对比。所以存在以下两个问题
- 客户端和服务端时间可能快慢不一致
- 客户端的时间是可以自行修改的(比如浏览器是跟随系统时间的,修改系统时间会影响到),所以不一定满足预期。
cache-control
正由于上面说的可能存在的问题,HTTP1.1 新增了 cache-control
字段来解决该问题,所以当 cache-control 和 expires 都存在时,cache-control 优先级更高。该字段是一个时间长度,单位秒,表示该资源过了多少秒后失效。当客户端请求资源的时候,发现该资源还在有效时间内则使用该缓存,它不依赖客户端时间。
cache-control 主要有 max-age 和 s-maxage、public 和 private、no-cache 和 no-store 等值。例如: cache-control: public, max-age=3600, s-maxage=3600
属性值 | 值 | 备注 |
---|---|---|
max-age | 3600 | 例如值为3600,表示(当前时间+3600秒)内不与服务器请求新的数据资源, |
s-maxage | 和max-age一样且优先级更高,在代理服务器中仍生效 | |
private | 内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存) | |
public | 所有内容都将被缓存(客户端和代理服务器都可缓存) | |
no-store | 不缓存任何数据, 直接向服务器请求最新 | |
no-cache | 表示的是不直接询问浏览器缓存情况,而是去向服务器验证当前资源是否更新(即走协商缓存) |
pragma
既然讲到了 no-cache 和 no-store,就顺便把 pragma 也讲了。他的值有 no-cache 和 no-store,表示意思同 cache-control,优先级高于 cache-control 和 expires,即三者同时出现时,先看 pragma -> cache-control -> expires
。
pragma: no-cache
那时候
Cache-control
(http1.1)还没出
协商缓存
上面的 expires 和 cache-control 都会访问本地缓存直接验证看是否过期,如果没过期直接使用本地缓存,并返回 200。但如果设置了 no-cache 和 no-store 则本地缓存会被忽略,会去请求服务器验证资源是否更新,如果没更新才继续使用本地缓存,此时返回的是 304,这就是协商缓存。协商缓存主要包括last-modified
和 etag
。
客户端携带上一次的
last-modified
和etag
值到服务端, 服务端进行比较这个两个值是否发生变化, 如果不携带,则默认为要获取新值
if-modified-since
存上次访问返回的last-modified
值
if-none-match
存上次访问返回的etag
值
NGINX 默认是开启
Etag
和last-modified
的, 所以es中自定义分词列表的热更新能力就是通过这两个字段实现的
last-modified
last-modified 记录资源最后修改的时间。启用后,请求资源之后的响应头会增加一个 last-modified 字段,如下:
例如: last-modified: Thu, 20 Dec 2018 11:36:00 GMT
当再次请求该资源时,请求头中会带有 if-modified-since
字段,值是之前返回的 last-modified
的值,如:if-modified-since:Thu, 20 Dec 2018 11:36:00 GMT
。服务端会对比该字段和资源的最后修改时间,若一致则证明没有被修改,告知浏览器可直接使用缓存并返回 304;若不一致则直接返回修改后的资源,并修改 last-modified
为新的值。
但 last-modified
有以下两个问题:
- 只要编辑了,不管内容是否真的有改变,都会以这最后修改的时间作为判断依据,当成新资源返回,从而导致了没必要的请求响应
- 时间的精确度只能到秒,如果在一秒内的修改是检测不到更新的,仍会告知浏览器使用旧的缓存。
ETag (EntityTags)
为了解决last-modified
上述问题,有了 etag
。 etag
会基于资源的内容编码生成一串唯一的标识字符串,只要内容不同,就会生成不同的 etag
。启用 etag 之后,请求资源后的响应返回会增加一个 etag 字段,如下: etag: "FllOiaIvA1f-ftHGziLgMIMVkVw_"
当再次请求该资源时,请求头会带有 if-none-match
字段,值是之前返回的 etag 值,如:if-none-match:"FllOiaIvA1f-ftHGziLgMIMVkVw_"
。服务端会根据该资源当前的内容生成对应的标识字符串和该字段进行对比,若一致则代表未改变可直接使用本地缓存并返回 304;若不一致则返回新的资源(状态码200)并修改返回的 etag 字段为新的值。
可以看出 etag 比 last-modified 更加精准地感知了变化,所以 etag 优先级也更高。不过从上面也可以看出 etag 存在的问题,就是每次生成标识字符串会增加服务器的开销。
弱etag
ETag: W/"630c1e6c-3485"
在商城中看到的是带了W/
的, 这表示弱校验 , 全称是 weak
, 强校验是字节级别的, 弱校验是语义级别的
在Apache服务器中 Apache默认通过FileEtag中FileEtag | Node Mtime Size
的配置自动生成ETag (节点/修改时间/文件大小 三个因素), 如果文件在一秒内频繁修改,并不改内容, 且etag的生成规则改为 Mtime
, 则etag在这一秒内的都是一样的, 此种情况下可以通过 弱etag 来减少刷新缓存 (毕竟强etag是全局唯一的)
etag 的生成规则 http并没有定义 , 因此具体的etag生成逻辑由服务端实现
一文讲透HTTP缓存之ETag - 掘金 (juejin.cn)
HTTP 条件请求 - HTTP |MDN (mozilla.org)
案例分析
假设当前有这么一个 index 页面,返回的响应信息如下:
cache-control: max-age=72000
expires: Tue, 20 Nov 2018 20:41:14 GMT
last-modified: Tue, 20 Nov 2018 00:41:14 GMT
标签进入、输入url回车进入
这种情况下会根据实际设计的缓存策略去判断。
- 由于该例没有设置 no-cache 和 no-store,所以默认先走强缓存路线。根据
cache-control
(expires
优先级低)判断缓存是否过期,若没有过期则此时返回 200(from cache)。 - 若本地缓存已经过期再走协商缓存路线,根据之前的 last-modified 值去与服务器比对,若这个时间之后没有改过则去读取本地缓存,返回 304(not modified)。
- 否则返回新的资源,状态码 200(ok),并更新返回响应的 last-modified 值。
按刷新按钮、F5 刷新、网页右键“重新加载”
不使用强缓存,直接使用 协商缓存
ctrl + F5 强制刷新
强缓存和协商缓存都不走, 直接获取最新资源
HTTP中的强缓存与协商缓存 - 漫思 - 博客园 (cnblogs.com)
http3
- HTTP3基于UDP协议重新定义了连接,在QUIC层实现了无序、并发字节流的传输,解决了队头阻塞问题(包括基于QPACK解决了动态表的队头阻塞);
- HTTP3重新定义了TLS协议加密QUIC(Quick UDP Internet Connection)头部的方式,既提高了网络攻击成本,又降低了建立连接的速度(仅需1个RTT就可以同时完成建链与密钥协商);
- HTTP3 将Packet、QUIC Frame、HTTP3 Frame分离,实现了连接迁移功能,降低了5G环境下高速移动设备的连接维护成本。
基于TCP实现的HTTP2遗留下3个问题:
- 有序字节流引出的队头阻塞(Head-of-line blocking),使得HTTP2的多路复用能力大打折扣;
- TCP与TLS叠加了握手时延,建链时长还有1倍的下降空间;
- 基于TCP四元组确定一个连接,这种诞生于有线网络的设计,并不适合移动状态下的无线网络,这意味着IP地址的频繁变动会导致TCP连接、TLS会话反复握手,成本高昂。
预检请求
发生跨域请求时, 浏览器不知道当前请求是否被服务端允许, 所以需要发送一个请求去验证一下, 此为预检请求.(浏览器自动发起)
预检请求长什么样?
可以看到有两个一样的请求, post请求是我们正常的业务请求, 而options就是预检请求, 但预检请求是不带body,也不会修改服务器的资源,也不会返回响应体.
什么时候会发生预检请求?
发生了跨域请求
协议 + 域名 + 端口 组成源, 当 起始源和目标源 不同时认为是跨域请求
该请求是非简单请求
浏览器将CORS请求分成两类:
简单请求(simple request)
和非简单请求(not-so-simple request
)。只要同时满足以下两大条件,就属于简单请求。
- 请求方法是以下三种方法之一:
HEAD GET POST
- HTTP的头信息不超出以下几种字段:
Accept Accept-Language Content-Language Last-Event-ID Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
也就
我们常用的业务接口, 一般是json格式传参, 或者加了自定义的请求头参数, 也就是说这些接口都是
非简单请求
预检缓存过期或者禁止了缓存 (浏览器控制台上可以禁用缓存)
详解
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。预请求实际上是对服务端的一种权限请求
请求头
“预检"请求用的请求方法是OPTIONS
,表示这个请求是用来询问的。头信息里面,
关键字段是Origin
,表示请求来自哪个源。
Access-Control-Request-Method
: 用来列出浏览器的CORS请求会用到哪些HTTP方法
Access-Control-Request-Headers
: 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,例如X-Custom-Header,authorization,saas-auth
响应头
Access-Control-Allow-Origin
: 表示允许的来源, * 表示允许任意来源
Access-Control-Allow-Methods
: 它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法(这样可以避免多次"预检"请求。)
Access-Control-Allow-Headers
: 它是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段
Access-Control-Max-Age
: 该字段可选,用来指定本次预检请求的有效期,单位为秒。
http CORS options请求(预检请求)详解 - 知乎 (zhihu.com)
5分钟看懂HTTP3_文化 & 方法_Mehdi Zed_InfoQ精选文章