前言
前面说到HTTP
的组成是起始行 + 头部 + 空行 + 实体
。头部由key: value
组成,这些内容有很大的意义。进阶篇中主要讲述了头部中常见的字段。
Accept和Content-Type
数据类型使用的头字段
之前提到过,HTTP
之前只支持文本数据,后来随着互联网的发展,数据类型越来越多。于是参考MIME
将数据分为了几大类:
text
: 文本格式的数据,常见的有text/html
、text/plain
等;image
: 图像格式,常见的有image/gif
、image/jpeg
等;audio/video
: 音频或者视频数据,常见的有audio/mpeg
、video/mp4
等;application
:数据格式不固定,常见的有application/json
,application/javascript
、application/pdf
等。
另外在传输的过程中,会将数据进行压缩,压缩方式有多种:
gzip
:GNU zip
压缩格式,也是互联网上最流行的压缩格式;deflate
:zlib(deflate)
压缩格式,流行程度仅次于gzip
;br
: 一种专门为 HTTP 优化的新压缩算法。
头部
中使用了Accept
和Content
字段,用于客户端和服务器进行内容协商。客户端用Accept
头告诉服务器希望接收什么样的数据,而服务器用Content
头告诉客户端实际发送了什么数据。
请求头设置Accept
告诉服务器,我(浏览器)可以接收application/json
和text/plain
或者任意数据类型,允许的数据压缩类型为gzip、deflate和br
。
响应头设置Content
告诉浏览器,自己发送了什么类型的数据:
还可以通过Content-Encoding
告诉传递的内容是用什么方式来压缩的:
Content-Encoding: gzip
不过这两个字段是可以省略的,如果请求报文里没有 Accept-Encoding
字段,就表示客户端不支持压缩数据;如果响应报文里没有 Content-Encoding
字段,就表示响应数据没有被压缩。
语言类型与编码
语言类型指的是人类使用的自然语言,例如英语、汉语和日语等。
在计算机发展的早期,各个国家发明了许多种字符编码,比如英语世界用的ASCII
、汉语世界用的GBK、BIG5
,日语世界用的Shift_JIS
等,后来就出现了 Unicode
和UTF-8
,把世界上所有的语言都容纳在一种编码方案里,UTF-8
也成为了互联网上的标准字符集。
在HTTP
中可以使用Accept-Language
标记客户端可以理解的自然语言;用Content-Language
来指定服务器响应报文中的实际语言类型:
Accept-Language: zh-CN, zh, en
Content-Language: zh-CN
对于字符集,客户端可以使用Accept-Charset
,但是服务器的响应头需要使用Content-Type
:
Accept-charset: gbk, utf-8
Content-Type: text/html; charset=utf-8
不过现在的浏览器都支持多种字符集,通常不会发送Accept-Charset
,而服务器也不会发送Content-Language
,因为使用的语言完全可以由字符集推断出来,所以在请求头里一般只会有Accept-Language
字段,响应头里只会有Content-Type
字段。
内容协商的权重
用字母q
代表权重,最大值是1,最小值是0.01:
Accept: text/html,application/xml;q=0.9,*/*;q=0.8
它表示浏览器最希望使用的是 HTML 文件,权重是 1,其次是 XML 文件,权重是 0.9,最后是任意数据类型,权重是 0.8。
内容协商的结果
内容协商的过程是不透明的,每个 Web 服务器使用的算法都不一样。但有的时候,服务器会在响应头里多加一个Vary字段,记录服务器在内容协商时参考的请求头字段,给出一点信息,例如:
Vary: Accept-Encoding,User-Agent,Accept
这个 Vary 字段表示服务器依据了 Accept-Encoding、User-Agent 和 Accept 这三个头字段,然后决定了发回的响应报文。
有关传输大文件的字段
随着互联网的发展,数据传输的种类越来越多,大小也越来越大。
Accept-Encoding和Content-Encoding进行压缩
前面我们提到了Accept-Encoding
和Content-Encoding
,这两个字段一个是客户端允许接收的数据压缩类型,一个是服务器可以响应的数据压缩类型。通过数据压缩,我们可以将数据压缩。
使用Gzip
压缩有个缺点就是,只对文本文件有较好的压缩率,而图片、音频等多媒体数据本身就已经是高度压缩的,再使用Gzip
处理也不会变小。
Transfer-Encoding分块传输
除了数据压缩之外,可以将大文件拆分成小文件传递给客户端。
Transfer-Encoding: chunked
由于拆分的长度是未知的,所以无法跟Content-Length
一起使用。
举个例子,从GitHub上下载源码包,GitHub要实时压缩实时发送,而不是一下子压缩好再发送,这样body的长度一开始就是未知的。所以就要用分块编码,压缩一部分,就发一部分,这部分的长度是已知的,但总长度只有压缩完才能知道。chunked编码用在“流式”收发数据的时候,通常数据是即时生成的,也就是动态数据。
Range、Accept-Ranges和Content-Range范围请求
除了压缩、分块,对于大文件处理我们还可以使用范围请求。在看电视剧的适合,我们想跳过片头直接看正片,这实际上就是想获取大文件其中的片段数据。
想跟服务器请求某范围的数据的,我们需要先让服务器支持范围请求:
# 响应头
HTTP/1.1 206 Partial Content
Content-Length: 32
Accept-Ranges: none # 不支持
Accept-Ranges: bytes # 支持
然后客户端发起一个请求,告诉浏览器要请求多少数据,可以借助Range
:
# 请求头
GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-31
服务器接收到Range
之后,需要做4件事情:
-
检查范围是否合法,如果越界,则返回416,意思是你的范围请求有无,我无法再处理;
-
如果范围正确,则返回206 Partial Content,返回了0-31这部分的数据;
-
服务器要添加一个响应头字段
Content-Range
,告诉片段的实际偏移量和资源的总大小,格式是“bytes x-y/length”:HTTP/1.1 206 Partial Content Content-Length: 32 Accept-Ranges: bytes Content-Range: bytes 0-31/96
不仅看视频的拖拽进度需要范围请求,常用的下载工具的多段下载、断点续传也是基于它实现的,要点是:
- 先发HEAD,看服务器是否支持范围请求,同时获取文件的大小;
- 开N个线程,每个线程用Range字段划分各自负责下载的片段,发请求传输数据;
- 下载意外中断也不怕,不必重头再来一遍,只要根据上次的下载记录,用Range请求剩下的那一部分就行。
多段数据
范围请求一般只获得一次数据,如果需要支持获取多个片段,可以在Range
中使用多个x-y
。这时响应报文中需要使用特殊的MIMT类型,还需要使用boundary=xxx
给出段直接分隔标记。
# 请求头
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=00000000001
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes
--00000000001
Content-Type: text/plain
Content-Range: bytes 0-9/96
// this is
--00000000001
Content-Type: text/plain
Content-Range: bytes 20-29/96
ext json d
--00000000001--
报文里的“- -00000000001”就是多段的分隔符,使用它客户端就可以很容易地区分出多段 Range 数据。
跟链接有关的Connection
前面提到过HTTP/1.1
比HTTP/1.0
,明确了链接管理,允许持久链接。
HTTP/1.0
时使用的是短链接,在发起请求时,客户端和服务端需要先建立起TCP
链接,收到响应报文之后会立即关闭。这种客户端与服务器不会保持长时间的链接状态成为短链接。另外TCP协议还有慢启动和拥塞窗口等特性,新建立的冷链接会比打开了一段时间的热链接要更慢一些。这些原因导致了HTTP的链接性能问题。
后来HTTP/1.1
采取了长连接,也叫持久链接。在 HTTP/1.1 中的连接都会默认启用长连接。不需要用什么特殊的头字段指定,只要向服务器发送了第一次请求,后续的请求都会重复利用第一次打开的 TCP 连接。
我们也可以使用Connection
字段:
Connection: keep-alive # 使用长连接
Connection: close # 断开链接
重定向
当返回的数据的响应头的状态码是301(永久重定向)或者302(临时重定向),会在响应头的首部返回一个Location
字段,告诉浏览器,应该重定向到哪里:
Location: /index.html
在“Location”里的 URI 既可以使用绝对 URI,也可以使用相对 URI。所谓“绝对 URI”,就是完整形式的 URI,包括scheme、host:port、path
等。所谓“相对 URI”,就是省略了 scheme 和 host:port
,只有path 和 query
部分,是不完整的,但可以从请求上下文里计算得到。
重定向是服务器发起的跳转,要求客户端改用新的 URI 重新发送请求,通常会自动进行,用户是无感知的。
补充:
301 的含义是“永久”的。
如果域名、服务器、网站架构发生了大幅度的改变,比如启用了新域名、服务器切换到了新机房、网站目录层次重构,这些都算是“永久性”的改变。原来的 URI 已经不能用了,必须用 301“永久重定向”,通知浏览器和搜索引擎更新到新地址,这也是搜索引擎优化(SEO)要考虑的因素之一。
302 的含义是“临时”的。
原来的 URI 在将来的某个时间点还会恢复正常,常见的应用场景就是系统维护,把网站重定向到一个通知页面,告诉用户过一会儿再来访问。另一种用法就是“服务降级”,比如在双十一促销的时候,把订单查询、领积分等不重要的功能入口暂时关闭,保证核心服务能够正常运行。
记住用户身份的Cookie
由于HTTP
是无状态的,所以不会记住访问的用户是谁,所以每次发送一个请求,得先判断是哪个用户再执行相应的操作。借助Cookie
可以让服务器认出对方是谁。
在响应头上我们可以根据Set-Cookie
设置Cookie
,这样在请求头中就会自动携带Cookie
。
如过服务器响应时设置了两个Cookie
:
Set-Cookie: sid=9436
Set-Cookie: system=Windows_x64
那么下一次客户端发送请求时,浏览器就会在请求头中自动送出Cookie
:
Cookie: sid=9436;system=Windows_x64
由于Cookie服务器委托浏览器存储在客户端的一些数据,所以需要一些手段来保护,防止外泄或者窃取:
- 设置
Cookie
的生命周期:使用Expires
设置截止日期,使用Max-Age
得到失效的秒数,Expires
和Max-Age
可以同时出现,两者的失效时间可以一致,也可以不一致,但浏览器会优先采用Max-Age
计算失效期。 - 设置
Cookie
的作用域,使用Domain
和Path
指定了域名和路径,浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie; - 设置
HttpOnly
,禁止JS脚本使用document.cookie
来读写Cookie
数据,阻止XSS攻击(即注入恶意脚本); - 设置
SameSite=Strict
严格限制Cookie
不能随着跳转链接跨站发送,防范CSRF
(跨站请求伪造)。
Cookie
可以用域身份识别,保存用户的登录信息;另一个作用是广告追踪。
你上网的时候肯定看过很多的广告图片,这些图片背后都是广告商网站(例如 Google),它会“偷偷地”给你贴上 Cookie 小纸条,这样你上其他的网站,别的广告就能用 Cookie 读出你的身份,然后做行为分析,再推给你广告。
跟缓存相关的Cache-Control
由于浏览器使用HTTP获取资源的成本较高,所以将数据缓存起来的话,下次再请求就可以尽可能地服用。这样,就可以避免多次请求 - 应答的通信成本,节约网络带宽,也可以加快响应速度。所以发起HTTP请求前,会先做出这样的行为:
- 浏览器发现缓存无数据,则发起请求,向服务器获取资源,否则直接从缓存中获得数据;
- 服务器响应请求,返回资源,同时标记资源的有效期;
- 浏览器缓存资源,等待下次重用。
服务器标记资源缓存用的是Cache-Control
字段:
值 | 描述 |
---|---|
max-age | 从响应被创建的时间起,资源的生存时间。 |
no_store | 不允许缓存。 |
no_cache | 在使用缓存之前,先去服务器验证一下资源是否过期,是否有最新版本。 |
must-revalidate | 如果缓存不过期就继续使用,否则就去跟服务器验证, |
不止服务器可以设置Cache-Control
,浏览器也可以:
# 刷新
Cache-Control: max-age=0
## 强制刷新
Cache-Control: no-cache
如果我们在请求资源的时候先去询问资源是否过期,然后获得返回结果,再根据结果进行下一步操作,比如过期,则去请求服务器,那么就发送了两个请求,对此HTTP
定义了条件请求字段。
# 响应报文
Last-Modified: Wed, 10 Mar 2021 02:28:56 GMT
ETag: "60482ee8-19f"
Last-Modified
是文件最后的修改时间;ETag
是实体标签,资源的唯一标识。两者相辅相成,ETag
主要用来解决修改时间无法精确区分文件变化的问题;而如果对一些图片文件的修改每次都扫描内容生成ETAG
来比较,显然比修改时间慢很多。
接下来每次请求同一个资源的时候,就会携带这个:
# 请求报文
# 与Last-Modified对应
if-Modified-Since: Wed, 10 Mar 2021 02:28:56 GMT
# 与ETag对应
If-None-Match: "60482ee8-19f"
此时如果能找到缓存的话,当前返回的状态码是304 Not Modified
。
跟代理相关的字段
所谓的“代理服务”就是指服务本身不生产内容,而是处于中间位置转发上下游的请求和响应,具有双重身份:面向下游的用户时,表现为服务器,代表源服务器响应客户端的请求;而面向上游的源服务器时,又表现为客户端,代表客户端发送请求。
代理最基本的一个功能是负载均衡。因为在面向客户端时屏蔽了源服务器,客户端看到的只是代理服务器,源服务器究竟有多少台、是哪些 IP 地址都不知道。于是代理服务器就可以掌握请求分发的“大权”,决定由后面的哪台服务器来响应请求。
在负载均衡的同时,代理服务还可以执行更多的功能,比如:
- 健康检查:使用“心跳”等机制监控后端服务器,发现有故障就及时“踢出”集群,保证服务高可用;
- 安全防护:保护被代理的后端服务器,限制
IP
地址或流量,抵御网络攻击和过载; - 加密卸载:对外网使用
SSL/TLS
加密通信认证,而在安全的内网不加密,消除加解密成本; - 数据过滤:拦截上下行的数据,任意指定策略修改请求或者响应;
- 内容缓存:暂存、复用服务器响应。
HTTP
中跟代理相关的字段我们一般接触不到,只有代理服务器转发发起的请求才会携带:
via
: 标明代理的身份,添加的是代理主机名(或者域名);X-Forwarded-For
:形式上和“Via”差不多,也是每经过一个代理节点就会在字段里追加一个信息,追加的是请求方的 IP 地址,所以,在字段里最左边的 IP 地址就客户端的地址;X-Real-IP
:只记录客户端 IP 地址。
缓存代理
前面提到了客户端缓存,代理也是可以做缓存的。在没有缓存的时候,代理服务器每次都是直接转发客户端和服务端的报文,中间不会存储任何数据,只有最简单的中转功能。加入缓存之后,代理服务受到源服务器发来的响应数据后需要做两件事,第一个是将报文转发给客户端,另一个就是把报文存入自己的Cache
中。
前面提到了Cache-Control
其实还有两个属性值:private
和public
。private
表示只能在客户端上保存,不能放在代理上跟其他人共享;而public
表示缓存完全开放,谁都可以存可以使用。
Cache-Control
上还有几个跟代理缓存相关的字段:
值 | 描述 |
---|---|
s-maxage | 缓存生存时间 |
no-transform | 禁止对缓存数据进行转换 |
proxy-revalidate | 只要代理的缓存过期后就必须验证,客户端不必回源,只验证到这个环节就行。 |
总结
本节主要讲述了HTTP
头部中主要的几个字段:
- 浏览器可以提供
Accept
告诉服务端自身可以接受的文件类型,而服务端在发送时可以在响应头使用Content-Type
告知浏览器具体发送了什么文件。Content-Type
也可用于浏览器提交数据时告知服务器提交了什么类型的数据。 Accept-Encoding
可以指定浏览器可接受的数据压缩类型,不提供则不支持;Content-Encoding
则是服务器提供的数据压缩类型,不提供则代表没有压缩数据;- 由于互联网发展迅猛,传输的文件类型越来越复杂,大小越来越大,为了提高传输效率,可以使用
Accept-Encoding
和Content-Encoding
来指定接受/发送的压缩格式、Transfer-Encoding: chunked
来进行分块传输、Range
、Accept-Ranges
和Content-Range
来进行范围传输和多端传输; - 浏览器只返回一部分传输内容时,状态码应为206;
HTTP/1.0
中使用的是短链接,HTTP/1.1
使用和默认的是长连接,字段为Connection: keep-alive
;- 301是永久重定向,302是临时重定向,当响应头中指定
Location
时,浏览器会自动跳转到指定页面; HTTP
是无状态的,可以借助Cookie
来记录用户身份并进行广告追踪;Cookie
是服务器生成并传送给客户端的,客户端接受到之后会在下一次请求中携带(只要Cookie
不过时);- 使用
Cache-Control
可以将资源进行缓存,如果设置max-age
,则跟强缓存有关,在未过期之前直接读取缓存,如果设置是no_cache
则跟协商缓存有关,先请求服务器再根据是否过期进行下一步操作,no_store
则表示不缓存; - 协商缓存服务器会返回
Last-Modified
和ETag
来表面资源的缓存修改时间和唯一标志,下次请求时,则会在请求头中加入If-Modified-Since
和If-None-Match
来判断是否要请求新资源; - 强缓存不用请求服务器,直接根据
max-age
判断是否读取浏览器缓存;而协商缓存需要请求服务器,询问缓存是否过期再判断是否读取浏览器缓存; - 代理服务指服务本身不生产内容,而是处于中间位置转发上下游的请求和响应。代理最基本的一个功能是负载均衡;
- CDN就是一种代理服务;
- 代理也是可以做缓存的,使用
Cache-Control
可以控制资源是否可以被代理缓存,priviate
表示资源只能被客户端缓存,public
表示客户端和代理都能对该资源进行缓存,另外还可以设置代理缓存的过期时间等字段。