HTTP进阶
这一章,我们来详细了解一下HTTP协议中的各种头字段、包括定义、功能、使用方法、注意事项等
HTTP的实体数据
之前我们已经学习了HTTP报文的结构,由header+body组成,但是我们前面主要研究的是header,所以这一节,我们就了解一下body
数据类型与编码
TCP/IP协议中,传输的数据基本上都是header+body的格式,但是TCP和UDP由于是传输层的协议,并不关心body数据是什么,只要把数据发送到对方算是完成任务了
但是HTTP不行,他是应用层协议,数据到达之后必须告诉上层这是什么数据
如果HTTP没有告知浏览器数据类型,那么浏览器看到的就相当于是一个黑盒子,浏览器可以通过猜的方式来检查文件类型,但是这种方式十分低效,而且很大几率检查不出类型
但是,HTTP在诞生之初就已经解决了这个问题,不过是用在电子邮件系统中的,可以发送ASCII码以外的任何数据,这个方法的名字叫做多用途互联网邮件扩展,简称MIME
HTTP只取了这个方法的一部分,用来标记body的数据类型,也就是我们常常听见的MIME type
MIME把数据分成了八大类,每个大类下再细分出多个子类,形式是**type/subtype的字符串**
这里列举一下HTTP中常遇到的几个类别:
- text:文本格式的可读数据,其下的子类有:
text/html表示超文本文档,text/plain表示纯文本、text/css表示样式表 - image:图像文件,其下的子类有:
image/gif、image/jpeg、image/png等 - audio/video:音频和视频数据,其下的子类有:
audio/mpeg、video/mp4等 - application:数据格式不固定,可能是文本也可能是二进制,必须由上层应用程序来解释,其下的子类有:
applicatoin/json、application/javascript、application/pdf等,另外,如果实在不知道是什么数据,就会是application/octet-stream,即不透明的二进制数据
由于HTTP在传输时为了解决带宽,所以有时候会压缩数据,这时候就会用Encoding type告诉数据是用什么编码格式,让对方正确解压
Encoding type的类型有:
- gzip:GNU zip压缩格式,最流行
- deflate:zlib压缩格式,流行程度仅次于gzip
- br:专门为HTTP优化的新压缩算法
数据类型使用的头字段
现在已经有了MIME type 和 Encoding type,所以浏览器和服务器都可以识别出body的数据类型了,但是我们还没将其应用上去,现在我们看看HTTP如何处理
HTTP协议定义了两个Accept请求头字段和两个Content实体头字段,用于客户端和服务器进行内容协商
接下来我们对这四个字段进行详细的分析:
-
Accept
标记的是客户端可以理解的MIME type,可以用
,做分隔符列出多个类型,如:Accept: text/html,application/xml,image/png -
Content-Type:
服务器会在响应报文中用该字段告诉实体数据的真实类型,如:
Content-Type: text/html -
Accept-Encoding(可选):
该字段标记的是客户端支持的压缩格式,同样可以用
,列出多个,服务器选择其中一种压缩即可Accept-Encoding: gzip, br -
Content-Encoding(可选):
该字段表示服务器选择的压缩数据方法
Content-Content: gzip
语言类型和编码
上面已经解决了计算机理解body数据的问题,但是现在还有一个问题,就是不同国家的人使用不同的语言,虽然都是text/html,但是如何让浏览器显示出不同的语言呢
为了解决这个问题,HTTP引入了语言类型和字符集
语言类型就是使用的自然语言,要使用type-subtype的形式,分隔符是-,如:en是任意英语,en-US是美式英语,zh-CN是汉语
由于各个国家会采用不同的字符编码方式,所以这就会导致同样的一段文字,用一种编码显示正常,换另一种编码就出现乱码的情况
为了解决这个情况,后来出现了Unicode和UTF-8,把世界上所有语言都容纳在一种编码方案中,UTF-8也是标准字符集
语言类型使用的头字段
同样,HTTP也使用了Accept请求字段和Content实体字段,用于客户端和服务器语言编码进行内容协商
下面详细分析一下这四个字段:
-
Accept-Language(常用):
标记了客户端可理解的自然语言,也可以用
,做分隔符列出多个类型:Accept-Language: zh-CN, zh, en -
Content-Language:
告诉客户端实体数据使用的实际语言类型
Content-Language: zh-CN -
Accept-Charset:
标记了客户端可理解的字符集
Accept-Charset: gbk, utf-8 -
Content-Type(常用):
这里并没有与Accept-Charset对应的Content-Charset,而是使用Content-Type字段,在其后面使用
charset = xxx来表示Content-Type: text/html; charset=utf-8
内容协商的质量值
在HTTP中用Accept等请求头字段进行内容协商的时候,还可以使用特殊的q参数表示权重来设置优先级,这里的q是quality factor的意思
权重的最大值是1,最小值是0.01,默认值是1,如果是0则表示拒绝,具体的形式是在数据类型或语言代码后面加一个;,然后q=value
还要注意的是;的用法,在大多数编程语言中,;的断句语气要强于,,而在HTTP的内容协商中反了过来,;的意义小于,
例如:
Accept: text/html,application/xml;q=0.9,*/*;q=0.8
在上述的字段中,表示了浏览器最希望接收到HTML文件,权重是1,其次是XML文件,权重是0.9,最后是任意数据类型,权重是0.8
服务器接收到请求头后,就会计算权重,再根据自己的实际情况优先输出HTML和XML
内容协商的结果
有时候服务器会在响应头中多加一个Vary字段,记录服务器在内容协商时参考的请求字段,给出一点信息
Vary: Accept-Encoding,User-Agent,Accept
Vary字段也可以认为是响应报文的一个特殊的版本标记,同一个URI可能会有不同的版本,主要用在传输链路中间的代理服务器实现缓存服务
HTTP传输大文件的方法
由于现在互联网传输的数据已经可能很大了,所以我们要实现在有限的带宽下高速快捷地传输这些大文件
数据压缩
通常浏览器在发送请求地时候都会带着Accept-Encoding头字段,服务端可以选择一种压缩算法,把源数据压缩
这种方法对于文本文件有较好地压缩率,但是对于图片、视频这一种多媒体数据,本身就是高度压缩了,在压缩也不会变小(甚至会变大一点),所以这种方法不是万能的
不过这种方式压缩文本的效果还是很好的
分块传输
压缩是把大文件变小,而分块传输的思路是化整为零,把大文件分解成很多小块,把小块分批发给浏览器,浏览器收到后在组装复原
这样浏览器和服务器都不用在内存里保存文件的全部,只需要收发一小部分,网络也不会被大文件长时间占用,内存、带宽等资源也就节省了
这种思路在HTTP协议中就是chunked分块传输,在响应报文中用头字段Transfer-Encoding: chunked来表示,意思是报文中的body是分块发送的
分块传输也可以用于流式数据,例如数据库动态生成表单页面,这种情况下body长度位置,无法在Content-Length给出确切的长度,所以只能用chunked方式发送
这里有一个注意点:Transfer-Encoding: chunked和Content-Length这两个字段不能同时存在,因为一个是长度未知,一个是长度已知
接下来我们看一下分块传输的编码规则:
- 每个分块包含两个部分:长度头和数据块
- 长度头是以CRLF(回车换行,即
\r\n)结尾的一行明文,用16进制表示长度 - 数据块紧跟在长度头后,也用CRLF结尾,但是数据不包括CRLF
- 最后用一个长度为0的块表示结束,即
0\r\n\r\n
范围请求
通过上述的分块传输编码,服务器就可以轻松的收发大文件了
但是假如现在有一个场景,我在看一个电影,我要直接拖动进度条快进,那么这样就相当于获取大文件其中的片段数据,而分块传输并没有这个能力
基于这个问题,HTTP提出了范围请求的概念,允许客户端在请求头中使用专用字段来表示只获取文件的一部分,相当于客户端的化整为零
由于请求范围不是Web服务器必须的,所以服务器必须在响应头中使用字段Accept-Ranges: bytes来告诉客户端该服务器支持范围请求,如果不支持,服务器则可以发送Accept-Ranges: none,或者干脆不发该字段
而对于客户端,则需要使用请求头Range,格式是bytes=x-y,其中x和y是以字节为单位的数据范围
这里需要注意,x、y表示的是偏移量,需要从零开始计数,例如前十个字节表示为0-9
并且,Range的格式也很灵活,可以省略x或y,这里举个例子,假设文件是100字节:
0-:表示文档起点到终点,即0-9910-:表示从第10个字节到文档末尾,即10-99-1:文档的最后一个字节,相当于99-99-10:文档末尾倒数的10个字节,相当于90-99
服务器接收到Range字段后,需要做四件事:
- 检查范围是否合法,不合法返回状态码416,表示范围请求有误
- 如果范围正确,则根据Range头计算偏移量,读取文件的片段,返回状态码206,表示body只是原数据的一部分
- 服务器要添加一个响应头字段Content-Range,告诉片段的实际偏移量和资源的总大小,格式是**
bytes x-y/length**,与Range头区别在于没有=,返回后面多了总长度 - 最后就是发送数据了,直接把片段用TCP发给客户端,一个范围请求就算是处理完了
请求使用Range字段获取了文件的前32个字节
GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-31
返回的数据
HTTP/1.1 206 Partial Content
Content-Length: 32
Accept-Ranges: btyes
Content-Range: btyes 0-31/96
// this is a plain text json doc
有了范围请求,我们就可以根据视频得时间点计算出文件得Range,不用下载整个文件,直接精确获取分片所在得视频数据
范围请求不仅用在视频进度拖拽,还有常用下载工具中得多段下载、断点续传也是基于范围请求实现的,具体要点:
- 先发个HEAD,看服务器是否支持范围请求,同时获取文件的大小
- 开N个线程,每个线程使用Range字段划分出各自负责下载的片段,发请求传输数据
- 意外中断只需要根据上次的下载记录,用Range请求剩下的那一部分就可以了
多段数据
上面我们说了范围请求,请求一次只获取一个片段,其实还支持在Range头中使用多个x-y,一次性获取多个片段数据
这种情况需要一种特殊的MIME类型:multipart/byteranges,表示报文的body是由多段字节序列组成的,并且还要用一个参数**boundary=xxx给出段之间的分隔标记**
多段数据的格式如图:
请求使用Range字段获取了文件的前10个字节和20到29个字节:
GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-9, 20-29
响应方会返回:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=000000000001
Content-Length: 189
Connection: keep-alive
Accept-Ranges: btyes
--000000000001
Content-Type: text/plain
Content-Range: bytes 0-9/96
//this is
--000000000001
Content-Type: text/plain
Content-Range: bytes 20-29/96
//ext json d
--000000000001--
HTTP的连接管理
短连接
HTTP协议(1.0)的通信通过采用了简单的请求应答模式
底层传输数据需要基于TCP/IP,每次发请求前需要与服务器建立连接,收到响应报文后会立即关闭连接
所以客户端与服务器不会保持长时间连接,也就叫做短连接,早期的HTTP协议被称为无连接协议
而且短链接的缺点很明显,就是传输效率很低,因为要建立连接要三次握手,关闭连接要四次挥手,造成效率降低
长连接
为了解决短连接的缺点,HTTP提出了长连接,也叫做持久连接,连接保活,连接复用
解决方法采用了成本均摊的思路,把本来TCP连接关闭的时间由原来的一个请求应答均摊到多个请求应答上
这样明显,减少了建立连接断开连接的次数,效率自然也就提高了
连接相关的头字段
由于长连接提升性能十分显著,所以在HTTP/1.1中的连接都默认开启长连接
只要服务器发送了第一次请求,后续的请求都会用第一次打开的TCP连接
我们不用手动开启,在请求头里对应的字段是**Connection: keep-alive**
长连接也有缺点,由于TCP连接长时间不关闭,服务器必须在内存中保持他的状态,所以占用了服务器的资源,如果有大量的空闲长连接只连不发,那么很快就会耗尽服务器资源
所以长连接也需要在恰当的时间关闭,不能永远保持与服务器的连接
为了关闭长连接,在客户端中,我们可以在请求头中加上Connection: close字段来关闭连接,服务器收到该字段后,也会在响应报文中加上这个字段,发送之后就调用Socket API关闭TCP连接
而对于服务器,通常不会主动关闭连接,但是也有一些策略,如Nginx:
- 使用**
keepalive_timeout指令,设置超时时间**,超时就会主动断开连接 - 使用**
keepalive_requests指令,设置可发送的最大请求次数**,处理了该请求次数后,就会主动断开连接
另外,还可以头字段中加上**Keep-Alive: timeout=value设置超时时间**,不过该字段约束力不强,所以不常见
队头阻塞
队头阻塞与短连接和长连接无关,而是由HTTP基本的请求应答模型导致的
由于HTTP规定报文必须一收一发,构成一个先进先出的串行队列
所以如果队首的请求因为太慢耽误了时间,那么队列里后面的请求也不得不跟着一起等待
性能优化
由于队头阻塞问题,造成效率低下,所以HTTP又提出一个解决方案:并发连接
对同一个域名发起多个长连接,用数量来解决质量问题
这样确实能够提高效率,但是又带来另外的问题,因为每个客户端都会建立很多个连接,这样服务器资源面对这么多的连接,可能会造成服务器崩溃
对于这个缺陷,HTTP又提出了一个新的技术——域名分片
就是让一台服务器多开几个域名,这样长连接的数量就可以增加了
HTTP的重定向和跳转
重定向的过程
如果需要重定向,第一个发送的请求返回302,表示需要重定向,就会自动发送第二个请求,重定向到Location字段
Location字段属于响应字段,必须出现在响应报文中,但是只有配合301和302状态码才有意义,标记了服务器要求重定向的URI
Location中的URI可以使用绝对URI,也可以使用相对URI,如果是站内跳转,就可以使用相对URI,如果是站外跳转,则必须使用绝对URI
重定向状态码
301:永久重定向,如果浏览器看到301,就会知道原来的URI已经过时了,就会做一些优化,像历史记录,下次可能直接用新的URI访问了,省去再次跳转的成本
302:临时重定向,浏览器只会执行简单的页面跳转,不会有其他的多余动作
303:类似302,但是重定向后请求改为GET方法,避免POST和PUT重复操作
307:类似302,但是重定向后请求中的方法和实体不允许变动
308:类似307,不允许重定向后的请求变动,但是他是301的含义
重定向的相关问题
- 性能损耗:一个跳转会有两次请求-应答,比正常的访问多了一次
- 循环跳转:如果重定向的策略出问题,如
A->B->C->A,这样就会无限循环,浏览器必须要有检测循跳转的能力,发现这种情况应该立即停止请求
HTTP的Cookie机制
因为服务器在请求完只会就会清理资源,所以不会记住这个请求和相关信息
但是随着现在HTTP应用领域扩大,对记忆能力的需求也越来越强,所以就引入了Cookie机制
服务器会给每个客户端贴上一张小纸条,上面写了只有服务器才能理解的数据,需要的时候客户端把这些信息把给服务器,服务器看到Cookie,就能认出对方了
Cookie的工作过程
这个过程其实就是上述小纸条的传输过程
这里有两个字段:响应头字段Set-Cookie和请求头字段Cookie
用户第一次访问服务器时,服务器会创建一个独特的身份标识,格式是**key=value,然后放进Set-Cookie中,随着响应报文发送给浏览器**
浏览器收到响应报文,看到有Set-Cookie字段,就会保存起来,下次请求就把该字段放进Cookie字段发送
第二次请求服务器看到请求头中有Cookie,就可以进行个性化服务了
除此之外,服务器还可以在响应头中添加多个Set-Cookie,存储多个key=value,但是浏览器发送不需要多个Cookie字段,只需要用;隔开就可以了
Cookie存储在浏览器中,而不是操作系统上
Cookie的属性
-
Cookie生存周期:超过这个期限浏览器认为Cookie失效,则直接在存储中删除
可以通过两种方式设置:
-
Expires:过期时间
用的是绝对时间的,也就是截止日期
Expires: Thu, 18-Aug-22 15:01:40 GMT -
Max-Age:相对时间
单位是秒,浏览器用收到报文的时间点再加上Max-Age,就可以得到失效的绝对时间
Max-Age=10 //有效期10秒
-
-
Cookie作用域:让浏览器仅发送给特定的服务器和URI
Domain和path表示Cookie所属的域名和路径
浏览器在发送Cookie前会从URI中提取出host和path部分,对比Cookie属性,如果不满足条件,则不会发送Cookie
-
Cookie安全性:尽量不让服务器外的人看到
这里可以设置三个属性:
- HttpOnly:该属性会告诉浏览器,此Cookie只能通过浏览器HTTP协议传输,禁止其他方式访问(如
document.cookie等API) - SameSite:防范请求跨站伪造(XSRF)攻击,设置成**
Same=Strict可以严格限定Cookie不能随着跳转链接跨站发送**,SameSite=Lax则允许GET/HEAD等安全方法,但是禁止POST跨站发送 - Secure:表示这个Cookie仅能用HTTPS协议加密传输,明文的HTTP协议会禁止发送,但是Cookie本身不是加密的,控制台还是能看到Cookie的各种属性
- HttpOnly:该属性会告诉浏览器,此Cookie只能通过浏览器HTTP协议传输,禁止其他方式访问(如
Cookie的应用
-
身份识别
可以保存用户的登录信息,实现会话事务
-
广告跟踪
可以读出身份做行为分析,再精准推送广告
HTTP的缓存控制
基于请求-应答模式的特点,大致可以分为客户端缓存和服务器端缓存
服务端的缓存控制
缓存的流程可以简单概括如下:
- 浏览器发现缓存无数据,于是发送请求,向服务器获取资源
- 服务器响应请求,返回资源,同时标记资源的有效期
- 浏览器缓存资源,等待下次重用
服务器标记资源的有效期使用的头字段是**Cache-Control,其中的值可以是max-age=30,这里的意思是只能缓存30s**,之后就不能用了
这里的**max-age是指生存时间**,时间的计算起点是响应报文的创建时刻,而不是客户端收到报文的时刻
这是HTTP缓存控制的最常用手段,此外还有其他属性能够控制缓存:
- no_store:不允许缓存,用于某些变化频繁的数据
- no_cache:这里的意思是可以缓存,但是使用之前必须去服务器验证是否过期,是否有最新版本
- must-revalidate:如果缓存不过期就可以继续使用,但是过期了还想用就必须去服务器验证
客户端的缓存控制
客户端也可以使用Cache-Control进行缓存控制,也就是说请求-应答双方都可以使用这个字段进行缓存控制,互相协商缓存的使用策略
刷新页面其实会重新发送请求,请求头中的**Cache-Control为0**
强制刷新页面重新发送请求,请求头中的**Cache-Control为no-cache**
而对于前进、后退、跳转这几个重定向动作,浏览器则用最基本的请求头,所以会检查缓存,直接利用以前的资源,不会进行网络通信
条件请求
通过上述的客户端缓存控制,我们可能会认为浏览器用Cache-Control做缓存控制只能刷新数据,并不能利用缓存数据
实际上,浏览器可以用两个连续的请求组成验证动作,先是一个HEAD,获取资源修改的元信息,然后与缓存数据比较,如果没有改动就使用缓存,节省网络流量,否则就再发一个GET请求,获取最新版本
这样发送了两个请求,成本有点高,所以HTTP协议定义了一系列If开头的条件请求,专门用来检查资源是否过期,把两个请求合并在一个请求中
条件请求有5个头字段,最常用的是**If-Modified-Since和If-None-Match,这两个需要第一次响应报文预先提供Last-modified和ETag,然后第二次请求时就可以带上缓存中的原值,验证资源是否是最新的**
如果资源没有变,服务器则回应一个304,表示缓存依然有效,浏览器可以更新一下有效期,然后直接使用
Last-modified就是文件最后的修改时间
**ETag**是实体标签的缩写,是资源的唯一一个标识,主要用来解决修改时间无法准确区分文件变化的问题
HTTP的代理服务
HTTP代理就相当于一个中间人:
代理服务
代理服务就是服务本身不生成内容,而是处于中间位置转发上下游的请求和响应,具有双重身份
面对下游的用户,则表现为服务器,面对上游的源服务器,表现为客户端
这里主要讲最常见的反向代理,它在传输链路中更靠近源服务器,为源服务器提供代理服务
代理的作用
在计算机领域中的任何问题,都可以通过引入一个中间层来解决
由于代理处在HTTP通信过程的中间位置,相应的就对上屏蔽了真实客户端,对下屏蔽了真实服务器,中间层可以为HTTP协议增加更多的灵活性
最基本的功能就是负载均衡,因为面向客户端时屏蔽了源服务器,客户端看到的只是代理服务器,源服务器有多少台并不知道,所以代理服务器可以分发请求,决定由后面的哪台服务器来响应请求
对于代理常用的负载均衡算法,比如轮询、一致性哈希等等,这些算法的目标都是尽量把外部的流量合理地分散到多台源服务器,提高系统地整体资源利用率和性能
代理服务还能执行更多地功能:
- 健康检查:利用心跳机制监控服务器,有故障立即踢出集群
- 安全防护:保护被代理地后端服务器,防止过载和抵御网络攻击
- 加密卸载:外网使用SSL/TLS加密通信认证,内网则不加密
- 数据过滤:拦截上下行的数据,对其作相应修改
- 内容缓存:暂存、复用服务器响应
代理相关头字段
由于代理隐藏了真实客户端和服务器,如果想要这些信息,则需要先用字段Via标明代理的身份
Via这个通用字段可以在请求头或响应头中出现,每经过一个代理节点,代理服务器就会把自身信息追加到字段的末尾
如果有多个中间代理,就会在Via中形成一个链表:
但是这个字段只是解决了判断是否有代理的问题,并不知道源服务器的信息
其实这么做应该是正确的,服务器的IP应该是保密的,不会让用户知道,但是服务器需要知道客户端的真实信息
现在的HTTP标准被没有定义该头字段,但出现了很多事实上的标准,如最常用的X-Forwarded-For和X-Real-IP
- X-Forwarced-For:形式和Via差不多,但是会在每经过一个代理节点时,追加请求方的IP地址,所以,字段的最左边的IP地址就是客户端的地址
- X-Real-IP:相当于简单的X-Forwarded-For字段,记录客户端IP地址,没有中间的代理信息
此外,还有两个字段:X-Forwarded-Host和X-Forwarded-Proto作用于X-Real-IP类似,只记录客户端的信息,分别是客户端请求的原始域名和原始协议名
代理协议
上面讲过,X-Forwarded-For可以拿到客户端的信息,但是这个操作代理信息必须要解析HTTP报文头,所以成本比较高,因为代理只需要转发消息就好,不需要解析数据,会降低转发性能
并且,X-Forwarded-For甚至要修改原始报文,但是这在有些情况下是不被允许的(HTTPS)
所以现在出现了专门的代理协议,该协议有v1和v2两个版本,v1和HTTP差不多,也是明文,但是在HTTP报文前面加上了一行ASCII码文本,而v2则是二进制格式
这一行ASCII文本开头是PROXY五个大写字母,然后是TCP4或TCP6,表示客户端IP地址类型,再后面是请求方地址、应答方地址、请求方端口、应答方端口,最后一个回车换行结束,如:
PROXY TCP4 1.1.1.1 2.2.2.2 55555 80\r\n
总之,代理协议可以在不改动原始报文的情况下传递客户端的真实IP
HTTP的缓存代理
缓存代理服务
之前我们的代理只是简单的转发请求,不会存储任何数据,只有最简单的缓存功能
但是我们如果加入缓存,那么代理服务器收到源服务器的响应数据后就要做两件事:转发报文给客户端、将报文存储到自己的Cache中
下次再有相同的请求,代理服务器就可以直接发送304或者缓存数据,不必在从源服务器那里获取,降低了客户端的等待时间,同时节约了源服务器的网络带宽
源服务器的缓存控制
之前服务器端的Cache-Control属性的值为max-age、no_store、no_cache、must-revalidate,这四种缓存属性既可以约束客户端,也可以约束代理
所以,为了区分客户端的缓存和代理的缓存,可以使用两个新属性private和public
- private表示缓存只能在客户端保存,不能放在代理上于别人共享
- public表示缓存完全开放,谁都可以存和用
还要区分缓存失效后的重新验证,使用must-revalidate和proxy-revalidate
- must-revalidate表示只要过期就必须回源服务器验证
- proxy-revalidate表示只要求代理的缓存过期后必须验证,客户端不必回源,只验证到代理即可
还要区分缓存的生存时间,使用s-maxage和max_age
- s-maxage表示限定在代理上存多久
- max_age表示在客户端上存多久
还有一个代理专有的属性no-transform,表示不用对缓存下来的数据优化
客户端的缓存控制
关于缓存生存时间,多了两个新属性max-stale和min-fresh
- max-stale:表示如果代理商的缓存过期了也可以接收,但是不能过期太多,超过x秒也会不要
- min-fresh:表示缓存必须有效,并且必须在x秒后依然有效
客户端还有一个only-if-cached属性,表示只接受代理缓存的数据,不接受源服务器的响应,所以如果代理上没有缓存或者缓存过期,则应该给客户端返回504