0、前言
作为网络部分学习的起点,从HTTP开始!
全文很长,建议收藏,防止迷路! 建议每次读一个小节。
1、HTTP概述
HTTP(HyperText Transfer Protocol)中文名称超文本传输协议,是一种用于传输超媒体文档(HTML,图片等)的应用层协议。HTTP的最初目的是为了发布和接收HTML文档,随着网页内容的丰富,HTTP消息也支持图片、音频等二进制内容,成为传输计算机资源的一种协议。
HTTP发展至今主要的版本是HTTP/0.9
, HTTP/1.0
, HTTP/1.1
, HTTP/2
, 发布了一系列的RFC
, 其中最著名的是规范了HTTP/1.1
的RFC 2616
。
作为一个传输协议,HTTP有很多特点:
-
应用层协议。这涉及到计算机网络分层模型的概念,HTTP是最顶层的应用层协议。
-
客户端-服务器模型。也称
C/S模型
,HTTP的通信过程是一个请求 - 应答模型,发起请求的主机为客户端,响应请求的主机称为服务器。 -
通常使用TCP/IP信道。事实上,HTTP可以建设在任何可靠传输的信道之上。
-
默认80端口。
-
无状态连接。HTTP是基于TCP(大多数时候)的连接协议,在HTTP/0.9和HTTP/1.0, 每次HTTP通信过程都会建立新连接,每次通信完成后断开连接,这种连接称为
非持续连接(短连接)
。因为连接的建立和断开成本较高,HTTP/1.1和HTTP/2默认使用持续连接
, 相同客户端和服务器之间的多次通信可以在同一条HTTP连接上进行。 -
明文协议。这个说法在HTTP/2上已经不成立,但在此之前,HTTP是基于文本的明文协议。
-
简单灵活。HTTP设计得十分简单,并且可以扩展,使得它非常使用。而且十分灵活,能传输任何类型的文件。
一个典型的HTTP通信过程:
- 客户端通过一个浏览器应用发起HTTP请求,请求想要获取的文档。
- 请求通过TCP/IP加工,经过一个一个中间代理,最终到达服务器主机。
- 服务器主机接收请求,处理请求,检出被请求的文档或数据,放到响应中,把响应发送给客户端。
- 响应内容经过TCP/IP层加工,经过一个个中间代理,被送回到服务器。
- 服务器收到响应,把它交个浏览器,浏览器把文档呈现出来。
在这个过程中,我们忽略网络中的路由器,HTTP通信系统的主要组件有:
- 客户端(client):发起请求的主机。
- 用户代理(user agent):客户端中发起请求的程序,经常是一个浏览器,HTTP最常见的是
B/S模型
。但它也可能是任何发起请求的程序,比如一个命令行,一个爬虫机器。 - 服务器(server):接收请求并发出响应的主机。
2、URL
HTTP中使用URL
来标识请求的资源。
URL
(Uniform Resource Location)统一资源定位符,是用于唯一标识计算机资源并指明其获取协议的一串字符。
2.1、URL和URI
在说明URL之前,不得不说URI
,经常听到这两个词被互用,有人说URL,又有人跳出来说不是叫URI吗?
其实两个都有。
URI
(Uniform Resource Identifier)统一资源标识符用于唯一标识计算机网络上的资源。
???这和前面的URL有区别吗?——是有的。
URI是用于唯一标识计算机资源的,关键在于唯一标识计算机资源的不只一种方式,URL是其中的一种,也是最常见的,另一种叫URN(Uniform Resource Name)。
如果我们假设一个地址只住一个人的话,并不考虑重名,那URL就像是一个人的地址,URN就像是一个人的名称。这二者都可以唯一确定一个人,都可以作为一个人的标识,不过地址给出了去哪里找到这个人,而名字不行。同样的,URL指明了通过何种方式获取到该资源。
明白了吧,当我们说URI的时候,它可能是一个URL,也往往是,所以很多时候,它们是可以互用的,除非你们在讨论URN。
URN的例子是urn:isbn:0451450523
, 或者urn:mpeg:mpeg7:schema:2001
,不用理会它们的命名规则,平时很少使用。
所以,我们基本上都是在讨论URL,又因为URL是一种URI,这种时候,URL和URI的说法都是正确的。就好像一个正整数,有人叫正整数,也有人叫整数,都没错。URL更准确,但URI容错率更高,平时没必要争论谁正确,但理解其中的区别还是有必要的。下文使用URL说法,尽管标准上使用的是URI。
2.2、URL格式
一个URL的完整格式如下:
[协议类型]://[凭证信息]@[服务器地址]:[端口号]/[资源层级UNIX文件路径][文件名]?[查询]#[片段ID]
其中[访问凭证信息]、[端口号]、[查询]、[片段ID]都属于选填项, 所以我们看到的一般格式为:
[协议类型]://[服务器地址]/[资源层级UNIX文件路径][文件名]?[查询]
端口号默认为80
,如果使用其它端口则不能省略。
查询使用key1=value1&key2=value2
的格式。
如URL:https://zh.wikipedia.org:443/w/index.php?page=2&title=Special:随机页面
,可以得到:
- 协议:
https
。 - 主机:
zh.wikipedia.org
。 - 端口:
443
。 - 文件路径:
/w/index.php
。 - 查询:
page=2&title=Special:随机页面
,这里有两个查询参数。
3、HTTP报文格式
HTTP协议有只有两种报文(消息):请求报文
和响应报文
。
HTTP是明文报文,很容易看懂。这是故意设计的,为的就是简单。即使HTTP/2使用”二进制帧“进行封装,仍然可以使用之前的方式理解报文。
从大整体上看,HTTP报文(请求报文和响应报文)总体格式:起始行 + 若干首部 + 空格 + 主体部分。
- 起始行:报文的第一行,使用
<CR><LF>
换行。 - 首部:控制通信过程和行为的键值对信息。首部之间使用
<CR><LF>
换行隔开,也就是一个首部一行。 - 空行:必须的,用于区分首部行和主体。只能有
<CR><LF>
,不能存在其他字符。 - 主体:也称实体数据,消息的数据部分。可选的,GET请求常常没有主体。
报文有请求报文和响应报文之分,具体格式与报文类型有关。
3.1、请求报文
<请求方法> <URL> <协议版本>
<首部1>
<首部2>
<....>
<主体>
请求报文的第一行也称为请求行,它包括请求方法
, URL
,协议版本
,中间使用空格隔开。
请求方法
:HTTP支持的请求方法有9种,常见的有GET
,POST
,PUT
,DELETE
等,大小写敏感的。URL
:不包含协议(肯定是HTTP)、主机和端口(由首部行host
给出)部分,如:/index.html
。协议版本
:HTTP的版本号,如HTTP/1.1
。
若干首部行,也被称为请求头。
规定只有Host
首部是必须的,不然无法知道请求的主机。所以,一个最简单的请求如下:
GET / HTTP/1.1
Host: www.google.com
注意后面是有空行的。
3.2、响应报文
<协议版本> <状态码> <状态短语>
<首部1>
<首部2>
<....>
<主体>
响应报文的第一行称为状态行,表示了响应的状态信息,由协议版本
,状态码
,状态短语
组成。
版本协议
:同请求报文,但是在响应报文中是第一个位置。状态码
:HTTP响应状态码,表示请求的处理情况,如200
,404
,500
。状态短语
:用于描述响应状态的短语,每个状态码有对应的默认短语,支持自定义,不过大多数都使用默认。
首部与实体数据跟请求报文类似。
不同于请求报文,响应报文的主体经常被使用。
维基上关于响应报文的一个例子:
HTTP/1.1 200 OK
Content-Length: 3059
Server: GWS/2.0
Date: Sat, 11 Jan 2003 02:44:04 GMT
Content-Type: text/html
Cache-control: private
Set-Cookie: PREF=ID=73d4aef52e57bae9:TM=1042253044:LM=1042253044:S=SMCc_HRPCQiqyX9j; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com
Connection: keep-alive
<html>...html文本内容...<html>
4、请求方法
HTTP定义一组请求方法,表示对指定资源的操作。
在介绍方法时,先了解一下请求方法的几个特性:
- 安全的:如果一个请求方法不会改变服务器的状态,也就是不会改变服务器的资源,它被认为是安全的。
- 幂等的:如果多次提交一个请求得到的结果是一样的,它被认为是幂等的。
- 可缓存的:如果一个请求方法的响应可以被缓存,在下次请求的时候使用,它被认为是可缓存的。
请求一个指定资源的表示形式. 使用GET的请求应该只被用于获取数据。
GET方法是安全的,幂等的,可缓存的。
GET是最常用的一个方法,客户端向服务器获取内容的请求基本都是GET请求。GET还能用于提交简单信息,但这是不规范的用法。
与GET同样用于获取资源,但只获取头部信息,也就是得到的响应与GET相比是没有主体的。
HEAD同样是安全的,幂等的,可缓存的。
HEAD主要用于获取某些资源的信息,前提是这些信息会体现在头部中。比如,请求大文件之前先获取头部查看文件大小再决定是否下载它,以此节约带宽。
用于向指定资源提交信息,通常导致在服务器上的状态变化或副作用。
POST方法是不安全的,非幂等的,大部分时候不可缓存的。
POST用于向服务器发送信息,最主要的使用场景是HTML表单提交。
使用提交的信息创建或替换指定的资源。
PUT方法是不安全的,幂等的,不可缓存的。
PUT也是用于信息提交,PUT与POST最大的区别在于是否幂等。比如提交订单的时候,如果使用POST方法,多次提交一个订单,会生成多份订单,如果使用PUT,只会有一份,这种时候,PUT更符合。
删除指定资源。
DELETE方法是不安全的,幂等的,不可缓存的。
删除服务器上的资源应该使用DELETE方法。
用于获取目的资源支持的通信选项。
OPTION方法是安全的,幂等的,不可缓存的。
OPTIONS常常使用于预检请求,询问服务器能支持哪些选项的请求,然后再决定是否发起正式请求。
后面的请求方法很少用到,做个了解:
用于建立一个到指定的第三方服务器的通信隧道,把接收请求的服务器作为中间代理。
向目标服务器发起回环测试,HTTP中间代理可能会修改请求,服务器收到TRACE请求后返回一个与请求一样的TRACE响应,就能知道最后达到服务器的请求内容,并且Via字段记录了中间经过的代理服务器。
TACE提供了一种调试机制,但存在安全漏洞,服务器默认不会开启TACE方法。
用于向特定资源提交补丁内容,对资源进行一部分修改,不同于PUT的覆盖操作,PATCH是非幂等的。
如果对以上方法有疑惑可以点击进入相应的MDN文档查看使用场景和案例。
一共9种HTTP方法,大部分情况下,我们只会用到GET
, POST
, PUT
, DELETE
。
有些网站,只使用GET
, POST
方法,处理获取,提交,删除,修改资源等操作。
不要惊讶,因为HTTP的请求方法是语义上的,对资源的实际操作取决于服务器如何处理请求。
举个极端的例子,有一篇编号为3
的文章资源,我们可以:
GET /getPosts/3 //获取文章
POST /editPosts/3 //修改文章
GET /deletePosts/3 //删除文章
POST /addPosts/3 //替换文章
服务器根据URL地址对请求进行处理,对数据库进行操作,这是完全可以的,事实上也是不少如此设计的。甚至只用一种请求方法也可以。
HTTP提供了这些方法和用途,却没有要求我们必须这么做。
作为补充的,有一种RESTful
的风格规范,它指出:
- 每个URL应该代表一种资源。
- 客户端使用
GET
,POST
,PUT
,DELETE
对服务器上的资源进行操作,分别代表对资源的获取,更新,替换,删除。
使用RESTful风格,上面的例子应该这样表示:
GET /posts/3
POST /posts/3
DELETE /posts/3
PUT /posts/3
即使不知道服务器的处理,我们也能清楚请求进行的操作,这就是语义化。
RESTful只是风格规范,没有强制要求,但建议尽量使用RESTful风格。
5、状态码
状态码用于标识HTTP请求的响应状态。
响应分为五类:
1XX
-- 信息响应2XX
-- 成功响应3XX
-- 重定向4XX
-- 客户端错误5XX
--服务器错误
状态码的意义、短语和可能出现的场景如下:
100 Continue
含义:目前为止的请求正常,客户端可以继续发送请求。
场景:客户端想要发送一个主体数据庞大的请求,但服务器可能不接受,这时,客户端会先发起一个带有Expect:100-continue
和Content-Length
等首部的请求,收到服务器回复100 Continue
状态的响应再发送请求体。
101 Switching Protocol
含义:应客户端要求,服务器正在切换协议。
场景:客户端要求升级应用协议时。比如使用WebSocket协议时,客户端会发送一个HTTP请求询问服务器是否支持升级到WebSocket协议,如果服务器支持,则回复带有101 Switching Protocol
的响应。
200 OK
含义:就是响应OK了
场景:随处可见
201 Created
含义:成功并创建,常在POST或PUT方法的响应报文中,表示响应成功并创建了新资源,新资源的地址是Location
首部行的值,或者是请求的URL。
场景:如上,对一个POST请求返回带有201 Created
的响应,并在响应Location
首部行指出新资源的地址。
202 Accepted
含义:请求已经被接受,但还未成功处理,而且处理结果不可知。
场景:请求被移交给其它应用或其它服务器处理,或者稍后进行批处理,先回复客户端一个202 Accept
报文告知请求被接受了,但处理结果未知。
204 No Content
含义:请求成功了,但没有响应内容。告诉客户端请求已经被处理了,但没有响应内容,客户端什么都不用做。
场景:客户点击了一个链接,默认行为是发送页面跳转,客户端可以回复一个204 No Content
响应告诉浏览器不用跳转。
206 Partial Content
含义:部分响应。该响应报文只包含了部分数据,用于传输大文件的一部分内容。
场景:返回一个大文件的一部分数据,使用状态码206 Partial Content
,并在响应首部中指明数据的范围和总文件大小(参考后面的范围请求小节)。
300 Multiple Choice
含义:请求的资源有多个可能响应,用户或客户代理需选择其中一个进行进行重定向。
场景:查看这个页面。这个很少用。
301 Moved Permanently
含义:永久性重定向。表示请求的资源已经永久移动到其它URL了,新URL在响应的Location
中。
场景:当请求的资源被转移到其它URL上后,服务器一般会给出301告诉用户代理以后使用新的URL。
302 Found
含义:暂时性重定向。资源被暂时转移到新URL,这次从新URL中获取(Location
给出),但下次依旧向旧URL请求,这就是Found强调的含义。
场景:服务器资源临时移动后配置的响应。
补充:重定向过程。一个重定向请求会经过两次请求再获取到目的资源。第一个请求发出之后,服务器响应一个3XX的重定向响应,在
Location
给出新URL,如果是永久性的(301或308),浏览器会保存新URL,并在以后每次请求旧URL时自动使用新的URL,搜索引擎也会相应更新;如果是暂时性的重定向(302和307),浏览器不会保存新URL,下次依然从旧URL请求。
304 Not Modified
含义:未修改。说明无须传输请求的内容,可以使用缓存内容。
场景:在使用缓存的时候发生,如果请求被缓存,缓存服务器会向目标服务器发起一个条件GET请求,带有If-Modified-Since
或If-None-Match
首部,服务器查询资源是否自缓存以来发生改变。如果没有,返回带304
的响应(参考缓存验证小节)。
400 Bad Request
含义:错误请求。请求的语义出错,服务器无法理解,客户端不应该再重复发生该请求。
场景:发出了错误的请求,请求的语法错误,消息太大客户端无法处理。
403 Forbidden
含义:拒绝服务。服务器有能力但拒绝了客户端的请求,常常是客户端权限受限。
场景:IP被网站列为黑名单。你的账号权限不足以查看某些页面。拒绝往往是网站业务逻辑的一部分。
404 Not Found
含义:找不到资源。
场景:向不存在的URL发起请求。
500 Internal Server Error
含义:服务器内部异常。服务器发送异常无法继续正常处理请求。
场景:服务器处理请求过程中代码运行发送异常。
解决:调试修改网站代码。
501 Not Implemented
含义:请求的方法不被支持。这是基于服务器的,有些服务器只支持GET
和POST
请求。
场景:对不支持DELETE
请求的服务器发送DELETE
请求。
解决:联系服务器管理员。
502 Bad GetWay
含义:网关错误。网关或代理的服务器,从上游服务器(如tomcat)收到的响应无效。错误原因是超时。
场景:同时太多请求到达,服务器无法处理,导致超时。
解决:客户端用户可以尝试刷新。服务方提高服务器的效率。
503 Service Unavailable
含义:服务不可用。表示服务器停机维护或已超载,目前不接受请求。
场景:停机维护。
以上状态码,平时最常见的有:200
, 204
, 206
, 301
, 302
, 400
, 403
, 404
, 500
, 501
, 502
, 503
。
你都懂它们的含义和出现情况了吗?反正我晕了。
6、HTTP首部
HTTP首部,也称HTTP消息头,是HTTP消息中的用于传递附加消息的可选项,它们对客户端和服务器理解HTTP消息有着重要作用。
根据首部可以出现的位置及其含义,可以把首部分成四种类型:
- 通用首部:可以出现在请求和响应中,而且与消息实体无关的首部。如
Date
,Cache-Control
。 - 请求首部(请求头):只会出现在请求中,而且与实体内容无关的首部。如
Cookie
,User-Agent
。 - 响应首部(响应头):只会出现在响应中, 而且与实体无关的部分。如
Age
,Location
。 - 实体首部:可以出现在请求和响应中,用于描述消息实体信息的首部,这类首部也可能出现在请求的实体部分(即body里)。如
Content-Length
,Content-Language
。
提示:也经常将请求中的首部称为请求头,响应中的首部称为响应头,而不理会它们是否与内容相关。
一些使用X-
开头的首部属于标准中没有规定的自定义首部。
多个首部之间使用<CR><LF>
换行隔开,其实就是每个首部一行,不管有多长,所以也叫首部行。
每个首部表示成首部名:首部值
的键值对形式。首部名采用大小写无关的带连字符写法。首部值的形式与首部的内容有关,有些首部值会是一个数字,如Content-Length:2000
,有些是一串字符串,如Cookie:id=22;option=3
, 有些则使用具有特定含义的指令,如Connection:keep-alive
。
补充:关于指令。首部行的值可以由一个或多个指令(directive)组成。指令之间使用
;
隔开。我无法找到指令的明确定义,但个人理解,是一些规定的表示指定意义的字符串,如keep-alive
,close
, 以及具有某个规定key的键值对,如max-age=60
。那些能使合法字符任意组合的字符串,并不被称为指令,如Cookie:useid=347;option=2
,其中,userid与option并不是Cookie
规定的属性,是用户自己设置的。指令只是一种叫法而已,不用太在意。
下面是某个请求的首部部分:
GET /home.html HTTP/1.1
Host: developer.mozilla.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: HTTPs://developer.mozilla.org/testpage.html
Connection: keep-alive
Upgrade-Insecure-Requests: 1
If-Modified-Since: Mon, 18 Jul 2016 02:36:04 GMT
If-None-Match: "c561c68d0ba92bbeb8b0fff2a9199f722e3a621a"
Cache-Control: max-age=0
同样的,一个响应头的例子:
200 OK
Access-Control-Allow-Origin: *
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Mon, 18 Jul 2016 16:06:00 GMT
Etag: "c561c68d0ba92bbeb8b0f612a9199f722e3a621a"
Keep-Alive: timeout=5, max=997
Last-Modified: Mon, 18 Jul 2016 02:36:04 GMT
Server: Apache
Set-Cookie: mykey=myvalue; expires=Mon, 17-Jul-2017 16:06:00 GMT; Max-Age=31449600; Path=/; secure
Transfer-Encoding: chunked
Vary: Cookie, Accept-Encoding
X-Backend-Server: developer2.webapp.scl3.mozilla.com
X-Cache-Info: not cacheable; meta data too large
X-kuma-revision: 1085259
x-frame-options: DENY
以上例子来自MDN。
你现在应该对这些首部一头雾水。让我们先拿几个最简单常用的练练手。
Host
-- 目的主机
请求头。用于表示请求的目的主机和端口号,端口号可选,默认使用请求服务的默认端口,如HTTP URL是80端口,HTTPS URL是443端口。
Host: developer.cdn.mozilla.net
Host: developer.cdn.mozilla.net:8080
Host
头是请求必须的,缺省或多个Host
可能造成400 Bad Request
响应。
Referer
-- 来源地址
请求头。表示该请求的来源地址,如果你点击了一个连接发送请求,那它的值就是你当前页面的URL。
Referer: https://developer.mozilla.org/en-US/docs/Web/JavaScript
提示:
Referer
是单词Refferer
的错误拼写
User-Agent
-- 用户代理
请求头。包含了一个特征字符串,用来让网络协议的对端来识别发起请求的用户代理软件的应用类型、操作系统、软件开发商以及版本号。
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36
User-Agent: Googlebot/2.1 (+HTTP://www.google.com/bot.html) //爬虫机器人
......
其它还有很多,但是,在了解相关原理和机制之前,根本说不明白,所以穿插在后面的相关内容里。
7、HTTP实体数据
现在,让我们把目光集中到另一部分——body,也就是实体数据部分。
7.1、MIME类型
计算机上的文件有各种类型,如.txt
文本文件,.jpg
图片文件,.mp3
音频文件,文件分类使得计算机能根据类型对文件进行相应的操作,正确的打开和处理文件。
同样的,在因特网上传输的数据也被分成多种类型,方便通信方理解数据并进行处理。不过,互联网上传输的文件采用与计算机上的不同的分类标准,使用一种称为MIME类型标准
的分类方式,它的全称是互联网媒体类型(Internet media type),简称媒体类型。
补充:MIME类型全称也有地方使用“多用途互联网邮件扩展”( Multipurpose Internet Mail Extensions),个人认为不是很妥,反正使用媒体类型或MIME类型不会引起即可。
媒体类表示格式:
类型名/类型名 [; 可选参数]
类型的表示是大小写不敏感的,经常使用小写表示。
类型名可以是以下6种类型之一,每种类型都有对应的多种子类型:
-
text
-- 人类可读的文本类型。如text/plain
,text/html
,text/css
,text/javascript
。 -
image
-- 图片类型。如image/gif
,image/png
,image/jpeg
,image/bmp
,image/webp
,image/x-icon
,image/vnd.microsoft.icon
。 -
audio
-- 音频文件。如audio/midi
,audio/mpeg
,audio/webm
,audio/ogg
,audio/wav
-
video
--视频文件。如video/webm
,video/ogg
. -
application
-- 二进制数据。如application/json
,application/octet-stream
,application/pdf
-
multipart
-- 多段数据类型。如multipart/form-data
,multipart/byteranges
提示:
multipart
并不属于MIME类型,是HTTP特有的多段数据类型,因为它与MIME类型一样,用于说明实体数据类型,所以放在这里。
下面是部分常见的格式:
text/plain
-- 通用文本格式
默认的文本格式,表示是文本格式文件,但不知道具体类型,浏览器只能当成文本解读,无法进一步确定成css
, javascript
之类的类型。
application/json
-- json格式
一种广泛用于表示轻量数据的数据格式,在web开发中经常使用。
application/octet-steam
-- 未知的应用程序文件
应用程序文件的默认值。接收方把它当成二进制文件,不知道如何执行,一般会保存到用户本地。
multipart/form-data
-- 表单数据格式
HTML表单提交的数据格式。
multipart/byteranges
-- 多部分文件的特定范围
指出这个文件由多部分组成,每个部分是一个范围。
补充:这里可以了解更多MIME类型细节。
7.2、主体压缩
为了缩小消息体的大小,HTTP在传输消息时,往往会将主体内容进行压缩,它的原理与文件压缩的原理类似。
几种主要的压缩格式,也称编码方式:**gzip
, deflate
, br
**,compress
,后面会用到。
为了区分未经压缩的实体内容,使用**identity
**表示数据未经压缩。
7.3、实体首部
现在,我们可以看一些跟实体相关的首部。它们一般都以Content-
的名称出现。请求和响应都可以有实体首部。
-
Content-Type:<MIME-type>
-- 实体的MIME类型,后面有一个可选的字符集类型,如utf-8
,bgk
,iso-8859-15
。Content-Type: text/html; charset=utf-8
-
Content-Length:<length>
-- 实体的字节数。Content-Length: 39749
-
Content-Encoding:<compress-type>
-- 实体的编码类型(压缩方式),上小节提到的几个。Content-Encoding: br
-
Content-Language:<language>
-- 实体内容的自然语言。如zh-CN
,en
,en-US
等。Content-Language: de, en
上面只是一些通用和常用的,还有更多的实体首部。
7.4、内容协商机制
按照RESTful规范,一个URL应该对应一个计算机资源,不过,一个资源可以有多种表现形式。如:同一个页面的英文版和中文版;移动端和桌面端获取的同一页面,内容不一样。服务器会在资源的变体中选择最适合用当前用户代理的版本。
这是内容协商的结果。浏览器在发送请求的时候,会带上Accept-*
系列首部行,表示自己希望接受到哪些形式的内容。这种希望是宽松的,因为用户代理并不知道服务器上的资源形式。服务器收到请求后,根据Accept-*
首部,选择合适的形式,并在响应中明确标明,告诉用户代理我最后发送了这种形式的资源,用户代理才能知道自己最终接收到的资源类型。
Accept-*
是一系列的请求头,只会出现在请求中,包括:
-
Accept:<MIME-type>
-- 客户端希望接收哪些MIME类型的响应。对应响应的Content-Type
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8
-
Accept-Encoding:<压缩方式>
-- 客户端能理解的编码方式。对应响应的Content-Encoding
Accept-Encoding: deflate, gzip;q=1.0, *;q=0.5
-
Accept-Language:<language>
-- 客户端可以理解的自然语言。对应响应的Content-Language
Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5
-
Accept-Charset:<charset>
-- 客户端可以处理的字符集类型。对应响应Content-Type
中的charset
属性。Accept-Charset: utf-8, iso-8859-1;q=0.5
这几个首部值都是相同的形式:<type1>[;q=<factor>, <type2>[;q=<factor>]
,多个类型之间使用,
隔开。每种类型都由类型名称和对应的质量因子表示。通配符*
代表其它类型,也就是没有被列出来的类型。质量因子代表该类型的优先级,最高是1
,最低为0.01
,0
表示不接受,默认是1
。习惯上优先级高的在前面,但顺序并不影响优先级。
注意:这种表示形式很容易让人误解。习惯上我们认为
;
的分隔语气要大于,
,这里却相反。记住质量因子代表其前面类型的优先级。
另外,User-Agent
也是会影响到实际发送的资源形式。
内容协商首部的值是浏览器自己决定的,用户并不知道,一些操作系统信息或浏览器设置可能会影响。
尽管客户端会使用这些请求首部表达自己希望接收的资源类型,但服务器可以选择忽略,这取决于网站开发者是否进行这方面的适配工作。
最后,在响应中也会使用一个Vary
首部,它的值是内容协商中出现的首部名。用来表示服务器选择资源时参考了哪些请求头。它主要用于缓存区分,在缓存部分再说明。
Vary: User-Agent, Accept-Language
7.5、分块传输
某些场景下,需要动态生成大量的数据,如果服务器可以在处理数据的同时,把已经完成的部分传输过去,就可以提高响应的速度。HTTP/1.1
支持了这种技术,允许一个响应中的实体数据分成一块一块传输,然后在客户端完成组装,形成一个完整的报文。这种适合流式传输技术称为分块传输。
分块传输的响应需要用Transfer-Encoding: chunked
首部表示实体部分被分块,因为数据是动态生成的,不然确定大小,所以不能出现Content-Length
。
分块传输的报文实体由多个块组成,每块的第一行是一个表是长度的数字,表示这个数据块的字节数。长度与数据之间通过换行区分,每个块之间也通过换行区分。
最后使用一个长度为0的数据块作为数据发送完毕的信号。
如MDN上的一个分块传输的例子:
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
7\r\n
Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
\r\n
这只有一个响应,但实体数据是分几次传输的,最后,客户端合并,提取出里面的数据内容为MozillaDeveloperNetwork
。
分块技术只在HTTP/1.1
上使用,HTTP/2
使用了更强大的流结构。
7.6、范围请求与多段数据
对于一些大型数据,我们可能只想获取某个特定的范围。比如一个两小时的电影,你可能只想看最终决战,这种时候应该直接向服务器请求最后20分钟左右的视频数据。这要归功于范围请求功能。下载文件经常使用的断点传输原理也是范围请求。
支持范围请求的服务器,会在发送响应的时候使用Accept-Range:bytes
响应头,告诉客户端可以进行范围请求。Accept-Rang:none
或没有Accept-Range
首部则表示不支持。
首次请求时,客户端可以先发起一个HEAD请求获取头部信息,这样就能知道服务器是否支持范围请求。
如果服务器支持范围请求,客户端可以发起一个带Range:bytes=x-y
请求头的请求,表示请求偏移量从x到y(包括x和y)的数据范围。
数据范围的表示非常灵活,对于一个100字节的数据,可以使用:
Range: bytes=0-10
-- 第1个字节到第11个字节,共11个字节。Range:bytes=10-
-- 从偏移量为10的字节开始到末尾,相当于10-99
。Range:bytes=-10
-- 倒数十个字节,相当于90-99
。
客户端收到请求后,需要执行以下操作:
- 验证请求范围是否合法,即是否越界等。如果范围错误,返回
416 Range Not Satisfiable
响应表示提示端范围错误。 - 如果范围合法,读取指定范围的数据内容。
- 把数据放到一个
206 Partial Content
的响应报文中,并设置Content-Range: bytes x-y/length
,表示发送了文件的x-y
范围,文件总大小为length
。 - 发送响应报文。
如果在Range
请求头设置多个范围,客户端还能一次性请求多个范围的数据。如:
Range: bytes=0-499, -500
表示获取前500个字节和后500个字节。
这种时候,对应的响应会设置Content-Type:multipart/beteranges; boundary=boundary
来表明这是一个多段范围的响应, 并且每段数据使用--boundary
隔开,注意前面的--
。
每段数据都由实体头Content-Type
和Content-Range
表示该数据段的MIME类型和范围,接着是一个空行,然后是数据段。
最后一个数据后面使用一个--boundary--
表示后面没有数据了,注意前后都有--
。
根据上面的请求,一个可能的响应部分:
(其它首部内容)
Content-Type:multipart/beteranges; boundary=ThisISABoundary
--ThisISABoundary
Content-Type: text/plain
Content-Range: bytes 0-499/2000
(前500个字符内容)
--ThisISABoundary
Content-Type: text/plain
Content-Range: bytes 1500-1999/2000
(后500个字符内容)
--ThisISABoundary--
8、HTTP连接管理
8.1、短连接与长连接
在HTTP/0.9
和HTTP/1.0
时期,客户端一次TCP连接只能进行一次请求-应答通信。这种连接被称为短连接,或非持续连接。从整过过程来看,每次通信都是独立的,所以也有一种无连接的说法。
这种设计在早起是合理的,因为当时的页面往往就只有一个HTML文档,用户一般需要很久才会再次发起请求。
但随着页面内容丰富,一个HTML文档往往还会引用多个图片,样式,脚本等,这意味着某个客户端发起一旦请求,在短时间内大概率会再次请求。而TCP连接建立与断开需要经历三次握手与四次挥手,频繁地建立和断开TCP连接消耗大量成本。
于是,在HTTP/1.0
后期,提出了长连接(持续连接)的模型,允许通信双方将连接保持以进行下次通信。
长连接通过首部段Connection
控制,它有两个值,如下:
Connection: keep-alive //客户端或服务器想要保持连接,HTTP/1.1默认
Connection: close //客户端或服务器主动请求在本次通信后关闭连接,HTTP/1.0默认
因为长连接对性能的改进,HTTP/1.1
默认开启长连接,但一般还是会带上Conneciton: keep-alive
。
但是,长连接也有缺点,连接会消耗服务器资源,即使用户短时间内不再发送请求,服务器资源也无法及时释放,并发处理能力减小。
Keep-Alive
首部字段是一种保证长连接及时断开的有效方法,它限制某个连接能保持的最长时间(单位秒)或最长通信次数。如:
Keep-Alive: timeout=5, max=1000
8.2、流水线
默认情况下,长连接中的请求是按第一个请求--收到响应--第二个请求...这样的顺序进行的,也就是必须等到上一个请求的响应到达后再发送下一个请求,这是长连接的又一个缺陷。这样后面的请求会被阻塞到前面所有请求都完成,这种情况叫做队头阻塞。
为了解决队头阻塞的问题,HTTP/1.1
还提出了流水线机制,即一个请求可以在上一个请求发送之后,但无需等待响应到来,就可以发送第二个请求。流水线只能支持幂等的请求(下图来自MDN)。
流水线能明显降低请求的时延,但被证明是很难实现的,尽管RFC2616
提出所有支持HTTP/1.1
的设备都应该支持流水线,但主流的浏览器都没有实现,所以流水线在实际上是几乎不用的,HTTP2
使用了更好的替代者--多路复用。
8.3、并发连接与域名分片
流水线不现实,并不意味着没有改进的空间了。相反的,开发者们在长连接的基础上,使用了并发连接和域名分片技术缓解队头阻塞问题。
并发连接是客户端的行为,即客户端浏览器向同一个域名发送建立多个连接。这样,每个连接的队列变短,阻塞就不明显了。但是,过多的连接数量会被服务器认为是DDoS攻击的可疑者,限制该客户机的请求(403警告)。一般允许的最大并发数是6。
域名分片则是服务器端的改良行为。因为并发连接数是限制了客户端到同一域名的数量,所以,可以把一个域名,分割成多个域名,让它们指向同一个服务器(DNS知识)。如果一个分成了3个这样的域名,就意味着每个客户端与服务器上的最大并发数量为(6 * 3 = )18。
尽管上述两种方法行之有效,但它们都是在HTTP规范之外的,由开发人员额外进行的改良工作。现在,如果有这个需要,应该投入到HTTP/2
的怀抱中(HTTP/2
在下面)。
9、Cookie
9.1、什么是Cookie
Cookie
是服务器发送到客户端并保存在客户端本地的一小块文本数据,使用Cookie
可以实现身份认证、个性化服务、行为追踪等功能,Cookie
技术已经被应用到许多网站中。
Cookie
的出现是为了弥补HTTP协议的无状态连接特点。无状态连接是指不会记录双方的通信过程。比如,客户端A与服务器B进行了一次通信之后断开连接。稍后,A再次向B发起请求,但HTTP是无状态的,并不会知道A跟B在之前是否通信过。
所以,服务器无法通过HTTP判断请求者的身份。用户登录是最典型的场景,如果一个用户提交了登录操作,网站后端认证通过,可是因为无状态的特点,后端根本无法得知后来的哪个请求来自已登录的用户,所以用户登录功能自然也就谈不上了。
一个简单的方案是在用户登录之后,在响应中附带一个口令,让客户端在每次请求的时候带上口令,这样,服务器就能知道请求的用户了。这个口令就是Cookie
。
9.2、Cookie的原理
Cookie
的实现需要三个组件:
-
网站后端的
Cookie
数据库。网站需要记住对每个用户设置的Cookie
, 以此根据Cookie
值判断对应的用户。 -
HTTP报文上的有关首部字段。
Set-Cookie
和Cookie
首部提供了对Cookie的支持。 -
客户端的
Cookie
本地存储文件。客户端需要保存服务器发送过来的Cookie
,在下次请求的时候带上Cookie
值,一般由浏览器进行保存。一个Cookie往往对应一个网站,Cookie文件大多是以键值对的方式保存。因为Cookie文件是浏览器保存的,所以Cookie的识别对象其实是浏览器,如果一个客户端上安装了多个浏览器,你在一个浏览器上的Cookie不会被另一个浏览器使用。同样,如果共用电脑,不同的人使用同一个浏览器也会使用相同的Cookie,除非更改Cookie。
在使用Cookie服务的网站服务器上,如果一个请求没Cookie
首部行,发送请求的客户端会被认为是一个新用户,服务器会为该用户分配并记录一个cookie值,在发送响应的同时,会设置一个Set-Cookie
首部行。用户代理(浏览器)收到响应后,提取首部行中的Set-Cookie
的值,保存到本地Cookie文件。浏览器在每次发送请求前,都会查询本地的Cookie文件,如果存在目标网站的Cookie,就把它添加到请求的Cookie
首部行中,服务器收到请求,发现这个请求带有Cookie
,就能认出这个用户了。
提示:以下是Cookie的高阶内容,可选看,你可以直接跳到Cookie的局限性一节。
Set-Cookie
首部行的全部语法是:
Set-Cookie:name=value[; expires=<date>][; max-age=<second>][; domain=<domain>][; path=<path>][; secure]
[; expires=<date>]
之类的用中括号括住的写法是可选项的意思。
下面截自某次使用百度的收到的Cookie:
第一个分号前是Cookie
的内容,它是一个name=value
的形式, 我也不知道其中的含义,这个估计只有后端开发者才知道吧。
后面的domain=.baidu.com
, path=/
的内容不属于cookie值,是该Cookie
的属性,它们告诉浏览器在哪些域名和路径下携带该Cookie。
一个Set-Cookie
只能设置一个Cookie,不过可以在一个报文中包含多个Set-Cookie
设置多个Cookie。
然后,这是某次请求携带的Cookie:
可以看到其中有一些cookie是来自上面的,有些则是来自其他响应或是浏览器之前保存的。
总之,浏览器发送的Cookie
首部格式如下:
Cookie: name1=value1; name2=value2; name3=value3;
9.3、Cookie的属性
Cookie
有自身的一些属性。如,有效期限、作用域、安全性等。这些属性一般在服务器设置Cookie的时候一同设置。
上面提到的domain
, path
表示了Cookie的作用域,即规定了在哪些域名和路径下该Cookie会被发送。
-
domain
表示cookie可送达的域名,如果没有指定,默认为当前访问路径所在的域名,不包括子域名。如果指定,则会表示cookie作用在指定的域名及其子域名上。前面例子中
domain=.baidu.com
,根据规范, 域名之前的.
被忽略,则它会作用于baidu.com
,news.baidu.com/
,zhidao.baidu.com/
等。而忽略domain
属性的,只会作用于baidu.com
。服务器设置的
domain
值必须包含当前主机才是合法的,否则,cookie会被忽略。不然肯定乱套。 -
path
表示cookie作用的路径,一般情况下被设置为根目录path=/
,表示该域名下任何路径都使用。
有效期是cookie一个及其重要的属性,因为过期的cookie会被逐渐清除,cookie文件一般不会太大,降低了浏览器的cookie管理压力,更重要的,从安全性来说,公用电脑应该在每次关闭浏览器时清除cookie,网站也应该在一段时间后重新验证用户的身份。
Cookie中使用expires=<date>
和max-age=<second>
来设置cookie的有效期。
expires=<date>
表示cookie的到期时间。date
表示一个时间点。max-age=<second>
表示cookie的有效时长(单位秒),从浏览器收到响应开始计算。
如果没有设置以上二者,默认表示这是一个Session Cookie,会在浏览器关闭后清除。如果两者同时出现,优先使用max-age
。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Max-Age=36000
Cookie的一个巨大问题是安全性。为了使Cookie尽可能安全,提供了HttpOnly
,Secure
和SameSite
指令。
HttpOnly
表示该cookie只能HTTP浏览器使用,无法使用脚本和各种API获取,防止跨站脚本攻击(XSS)。Secure
表示该cookie只在HTTPS协议下有效,否则被忽略。主要是防止cookie盗窃。如果使用HTTPS协议,即使别人获取到请求,因为内容被加密,也无法得知Cookie内容。SameSite
设置了跨站请求策略。SameSite=Strict
表示所有跨站请求不能发送Cookie。SameSite=Lax
则较宽松,允许GET/HEAD等安全方法,但不允许POST等方法。
尽管Cookie尽力保证其安全性,但是,敏感信息依然不应该保存在Cookie中,因为其整个机制就是不安全的。
9.4、使用Cookie
大多数后端框架的HTTP相关API中都有对Cookie的使用,我就不说了(主要是我也不会)。这里主要说前端部分。
前端对Cookie的操作,主要都通过documnet.cookie
属性。
1.获取cookie
document.cookie
会获取当前路径下的所有cookie值, 使用;空格
隔开。当然,具有HttpOnly
指令的cookie不会出现。一个简单的分割处理:
const cookies = document.cookie.split("; ");
2.写入Cookie
对document.cookie
进行赋值会执行与Set-Cookie
等效的结果。
document.cookie = "user=czpcalm; max-age=60" //添加了一个60秒后过期的cookie
注意,这种赋值是添加操作,字符串内的等号两边不要有空格。
注意:空格,
:
,;
等字符出现在cookie中必须被转码,可以使用encodeURIComponent()
方法对cookie内容进行编码。
3.删除Cookie
可以使用Set-Cookie
的特点删除Cookie。为cookie设置一个过去时间点的expires
,或者max-age=0
。
document.cookie = "user=czpcalm; max-age=0"; //如果存在,删除该cookie。
十分简单,对吧。
题外话:Cookie曾一度被用于在客户端上保存信息,如保存某个本网站登录的客户名称,因为每个网页都可以通过
document.cookie
获取到该信息,所以这成为一种在客户端不同页面之间传递信息的方式。但这些内容也会随着请求被发送,浪费带宽,而且Cookie有数量与大小的限制。这是一种极其不推荐的做法,现在应该使用localStorage
等方法。
9.5、Cookie的局限性
Cookie为我们提供了很多便利,身份认证、定制化服务、会话事务等等,同时也存在着许多的问题。
浏览器在使用cookie的时候有些限制,每个域名的cookie大小不能超过4K,大多数浏览器对cookie的数量也有限制,不同浏览器同一域名下允许的Cookie最大量不等,在20-60直接,为了保证cookie有效,应该尽量不多于20。
根据Cookie的原理,识别的是浏览器与服务器,不能实现用户与服务器的识别,是一种不那么准确的识别方式。
Cookie需要在传输过程中发送,增加了网络传输压力,而且是明文传输的,可能发生Cookie盗窃。
最大的问题在于Cookie的安全性。黑客可能试图通过跨站脚本攻击窃取用户的Cookie实现合法登录。用户自己也可能通过Cookie修改内容以达到欺骗服务器的目的。
最后,身份认证的功能为我们提供便利,同时也存在隐私泄露的问题,你的行为都会被服务器记录,从而投放针对性广告。甚至有的网站通过第三方合作,在搜索引擎等网站投放自家的Cookie,记录在自己的服务器,当用户访问自己的网站时,向用户推送定制的广告。这种Cookie称为第三方Cookie,如果某个企业大范围投放Cookie,就几乎能跟踪到你的一举一动。
10、HTTP缓存
缓存机制是HTTP的重要部分,使用HTTP缓存能有效提高HTTP请求的响应速度。
10.1、HTTP缓存概述
HTTP缓存,也称Web缓存,类似计算机缓存,把HTTP响应缓存在离用户更近的节点,在用户再次请求时将缓存内容发送给用户。好处是很明显的:更快的响应速度,更少的网络传输,更小的服务器压力。
最容易理解的是客户端缓存,也叫浏览器缓存,每个人都应该知道浏览器会把刚刚访问过的页面暂时保存起来,你在后退的时候页面会很快加载完成。很多人以为是浏览器回退到刚刚的状态,其实这里也发送了HTTP请求的过程,不过,浏览器发送请求前,会先检查本地缓存,如果缓存命中,就直接使用。HTTP请求发生了,但没有发送出去,这个过程甚至不需要网络,响应过程也是最快的。
另外,在请求-响应的过程中,网络上还存在众多的代理服务器,代理服务器是架设在客户端与源服务器(目的服务器)之间的中间服务器。客户端的请求都会先发送到代理服务器,代理服务器根据请求内容,可能转发给源服务器处理,可能使用缓存响应请求,甚至可能直接丢弃。服务器上发送的响应也会经过众多代理再转交给用户,具有缓存功能的代理会缓存内容,有的代理也可能对响应内容作出一定的优化改动。许多代理节点具有缓存响应的功能,也称缓存代理。
ISP(网络服务供应商)或企业等机构经常会设置本地Web代理,缓存特定局域网内的用户响应,提高Web响应速度。
有些网站为了提高响应速度,降低源服务器压力,也会使用代理服务器,这种称为反向代理。反向代理可以实现负载均衡,健康检查,数据过滤等功能,不过,我们这里只关心它的内容缓存功能。
请求发送到缓存代理时,会先检查有没有缓存,如果存在,使用缓存,否则把请求发往下一代理,如此直到源服务器。响应发送的过程中,缓存代理在转发的同时会将其缓存下来。
代理缓存属于公共缓存,你使用的缓存可能来自其它用户。客户端缓存则属于私有缓存。
下文中,当使用
缓存
一词时,同时代表客户端缓存和服务器缓存。
一个缓存资源在一段时间之后可能会失效,因为服务器上的资源可能发生了变化,这时,需要重新验证缓存是否可用。请求到达某个缓存节点时,发现存在缓存,但缓存过期了,这时候,缓存节点会在请求中附上If-Modified-Since:缓存时间
首部发送给服务器,服务器收到请求后,检查缓存时间到现在的过程中,资源有没有发生变化,如果发生变化,回复一个200 OK
的带有新资源的响应。如果没有变化,回复一个304 Not Modified
不带资源的响应,表示没有修改过,可以使用缓存,同时缓存会被更新为新鲜的。
然而,整个详细过程比这复杂得多,主要原因是资源的新鲜度不一致。有些请求的响应是不能被缓存的,如大部分POST请求,不难理解,只有安全请求(不会引起服务器状态变化的请求)才可以被缓存,事实是哪个,绝大多数缓存都是GET或HEAD请求;变化频繁的资源缓存有效时间很短,静态资源如图片等可以缓存很长时间。我们必须使用缓存策略有效管理整个缓存过程。同样,我放在进阶内容中。
下面属于进阶内容,选看。
10.2 缓存控制
HTTP的缓存控制几乎全部由Cache-Control
通用首部行实现。在请求中出现时,代表客户端能接受哪些缓存,在响应中出现时,代表服务器希望哪些缓存节点能缓存以及缓存多久等。
通用首部行:请求和响应中都能出现的首部行。
Cache-Control
首部有很多的指令,有些指令是通用的,有些则只能客户端设置,有些只能服务器设置。
no-store
和no-cache
指令是控制如何使用缓存的通用指令:
no-store
-- 不使用缓存。在响应上时,表示任何节点都不要保存该响应;在请求上时,表示该请求不接受任何缓存。no-cache
-- 缓存但重新验证。在响应上时,表示可以保存,但每次使用前必须验证缓存是新鲜的。在请求上时,表示可以接受缓存,但必须验证缓存内容是新鲜的。
另外两个属性,public
和private
是只用于响应上的,控制哪些缓存节点可以缓存该响应:
private
-- 只允许私有缓存(浏览器缓存)保存该响应。默认值。public
-- 中间代理也可以缓存。
为了保证缓存的有效,还设置了max-age
和s-maxage
指令表示缓存的寿命:
-
max-age=<second>
-- 缓存的有效时长,从请求发送的时间算起。出现在响应上时,表示可以缓存的保鲜时长,超过这个时长,缓存被认为是过期的;在请求上时,表示在请求前多少秒之内缓存的响应可以接受。首部行
Expires:<date>
也可以设置缓存过期时间,不过它是设置过期时间,max-age
是有效时长,而且前者是一个首部行,后者只是Cache-Control
首部行的一个指令。 -
s-maxage=<second>
-- 仅对代理缓存有效,覆盖max-age
,作用相同,但优先级高。
一般情况下,新鲜的缓存会被认为是可用的,过期的缓存需要重新验证或更新才能使用。
不过,要明白,新鲜度只是我们对资源副本有效(与源服务器相同)的一种估计,对于服务器上的资源何时会被修改,我们无法预测。
所以,有一些其它指令来控制缓存的使用规则。结果就是,新鲜的缓存不一定可用,过期的缓存不一定不可用,取决于我们对缓存资源的要求。
比如前面提过的no-store
指令,它要求必须每次验证缓存,即使缓存是新鲜的。它的效果跟max-age=0
同样。
提示:浏览器的刷新实际上是发送了设有
Cache-Control: max-age=0
的请求。
此外,还有一个must-revalidate
指令,它表示在缓存过期后,必须验证,如果缓存是新鲜的则不用验证。注意它与no-store
的区别。同样的有一个只对代理缓存有效的proxy-revalidate
指令。
去超时买东西的时候,对食品类的东西新鲜度要求比较高,可能不会买两个月内过期的食品,而像垃圾袋这种,即使过期一个月也可以接受。客户端对缓存也存在类似的要求,这就是min-fresh
和max-stale
指令的作用。
-
min-fresh=<second>
-- 缓存必须在second秒后还是新鲜的才使用。 -
max-stale=<second>
-- 缓存过期不超过second秒也能使用。 -
only-if-cache
-- 只要缓存,并且不用验证是否新鲜。
最后,看两个缓存控制的例子:
Cache-Control: no-store //不使用缓存,适用于变化频繁的资源,比如秒杀界面
Cache-Control:public, max-age=31536000 //所有节点都可以缓存,且有效期很长,适用于静态资源
10.3、缓存验证
上面我们知道,使用缓存前,经常需要验证服务器是上的资源有没有发生改变。比如,使用了no-cache
指令的缓存,带有must-revalidate
指令的缓存过期了等。
最简单的判断文件是否一致的办法是比较它们的最后修改时间。在验证请求的首部行带上If-Modified-Since:<缓存副本的最后修改时间>
首部行,源服务器收到请求后,对比缓存副本与服务器资源的最后修改时间,就能确定缓存是否是新鲜的。
这些验证使用的是时间戳对比,根据HTTP时间格式,只能精确到秒。对一秒内的多次修改无法区别。比如,一个资源在被修改之后得到版本1,记作版本1-最后修改时间1
,在一秒的时间内,被再次修改,得到版本2,因为时间只能精确到秒,所以还是记作版本2-最后修改时间1
。通过比较最后修改时间,我们得到了错误的结果。所以时间戳验证是一种较弱的验证。
为了弥补这点,HTTP提供了更强的验证方式,使用一种称为ETag
的资源标识,添加ETag
,If-None-Match
首部行实现。
ETag是一种服务器资源特定版本的标识,同一资源的不同版本,其ETag不同。也就是说,资源发生变化,其ETag也会改变。这样,根据ETag就能确定服务器上的资源是否发生变化。ETag有强弱之分,强ETag要求每个字节都相同,而弱ETag只要求语义相同,比如html文档上多了几个空格被认为没有改变。弱ETag以W/
开头。
使用ETag为验证器的服务器在发送可缓存响应时,会带上ETag
首部行,标明资源的ETag,如:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" //一个强ETag
ETag: W/"0815" //一个弱ETag
注意:ETag内容使用""括住。
这样,在验证缓存的时候,缓存服务器发送验证请求时,会带上If-None-Match:<Etag>
首部行,服务器收到后,根据ETag匹配,如果没有匹配到资源,说明资源发生了变化,缓存需要更新。
所以,一个缓存验证请求,可能带有If-Modified-Since
或If-None-Match
首部,也可能二者皆有(此时使用ETag验证方式)。这些验证请求被称为条件GET请求。
服务器在收到条件GET请求后,使用ETag或时间戳验证,如果服务器资源没有变更,则返回一个304 Not Modified
的响应通知缓存节点可以使用缓存,这个时候,资源不需要重新发送,减少了带宽。只有资源发生变化时,才发送一个200 OK
的响应消息,缓存节点接收后,更新缓存,并把响应发送给客户端。
10.4、Vary首部行
由于用户协商机制,对同一个资源的请求,可能因为请求头的不同而获取到不同的资源,比如移动端浏览器和桌面端获取淘宝首页,使用同样的GET方法和URL,得到的页面是不同的。如果移动端不小心使用了一个桌面端的缓存,这种体验是不好的。
Vary
首部行就用于区分这种匹配场景,它只能出现在响应中。Vary
的值是一个首部行的列表,出现在上面的首部行会成为请求匹配的一部分。
这样可能很难理解。以用户代理(浏览器)为例,假如一个浏览器发出一个请求,带有如下请求头:
GET / HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0
它的响应带有一个Vary:User-Agent
首部行,响应被缓存下来,后面,又有一个请求来到该缓存节点:
GET / HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0
它并不能使用,因为User-Agent
出现在缓存的Vary
首部中,而且两个请求User-Agent
不同。
11、HTTP/2
既然前面多次提到HTTP/2
对连接的改进,我想是时候介绍一下HTTP/2
了。
HTTP/2标准于2015年5月以RFC 7540正式发表,根据w3techs的报告,截止到2020年9月30号,全球有48.4%
的网站使用了HTTP/2
。
11.1、兼容HTTP/1
HTTP/2
是向下兼容的,从HTTP/1
升级到HTTP/2
并不需要客户端和网站应用进行额外的工作,也就是对用户是透明的,这个特性大大加快了HTTP/2
的应用推广。
其次,从协议上来看,HTTP/2
继承了HTTP/1
的语义,之前使用的请求方法,状态码,首部行等都是有效的。
但HTTP/2
在报文语法上进行了颠覆性改进,不再保持明文报文,而是使用二进制封装,再使用帧
这种结构进行分片。不过,我们仍然从语义上理解HTTP/2
,而协议的解析方从语法层进行解析。
HTTP/2
上的诸多改进都基于这种二进制格式的改动。
11.2、二进制分帧、流、多路复用、优先级
报文格式上,HTTP/2
不再采用传统的的明文格式,而是使用一种更小的二进制结构帧(frame)。对人而言不再语义可读,但更利于计算解析,减少了很多语法分析的过程。
二进制分帧之后,帧成为消息传输的单位。接收方如何确定哪些帧是来自同一个消息呢?
HTTP引入和一种称为“流
”的虚拟结构。一个连接被分成多个流,它是二进制帧的双向传输序列,来自同一消息的帧会被分配相同的流ID,接收方把具有相同流ID的帧组合在一起就是一个消息了。可以简单理解为,流是消息的一次传输。
一个连接分成多个流,可同时传输多个消息,实现了多路复用,也就解决了队头阻塞的问题。理解这一点的关键,在于理解“流”实际上是虚拟的,从连接上看,只是一个一个帧传输,不要求这些帧来自同一个消息。流ID的作用是让接收方正确接收消息,连接并不知道它的存在。
有了多路复用,一个客户端-服务器通信只需要一个长连接,消除了建立多个连接的资源浪费。
为了使重要消息更快发送,HTTP/2还首次引入了流优先级的概念,高优先级的流会被优先发送。
11.3、头部压缩
一个HTTP消息经常会伴随着十几个首部,这些首部一定程度上增大了消息的大小,特别是在一些没有消息主体的时候,经常发生“头重尾轻”的情况。并且,像User-Agent
这类通信过程固定的首部,也需要每次请求时都重复发送。为了进一步缩小消息,HTTP/2
对消息头部进行了压缩。
头部压缩使用专门的HPACK算法
, 其大致原理是:用索引号标记所有首部和首部值可能出现的组合,比如,Connection:close
和Connection:keep-alive
会不同的索引号标识。在客户端和服务器双方上维护一份共同的静态表和动态表,作为索引与首部的字典。发送方查找字典,找到每个要设置的首部的索引号,在消息中只需要发送索引号。接收方接收到消息后,根据索引号找到对应的首部,就能知道发送的是什么首部,以及对应什么值。另外,还使用哈夫曼编码进一步对数字和字符进行压缩。
11.4、服务器推送
HTTP/2也在一定程度上改动了请求-应答模型,引入服务器推送机制:特定条件下,服务器不需要等待请求到来,可以主动向客户端发送资源。比如,一个客户端请求了一个HTML页面,很明显,它会很快请求页面上引用的脚本、CSS、图片等内容,这个时候,服务器可以主动向客户端发送这些内容,加快响应速度。
11.5、安全加强
从HTTP本身的设计上来说,是没有安全性可言的,HTTP/2之前是明文的,HTTP/2只是使用二进制格式,并没有进行内容的加密。为了保证安全,实际上大多数情况下我们使用的是HTTPS
协议。它在HTTP消息传输之前(进入TCP之前),使用安全协议SSL/TLS
加密消息。
因为HTTPS的流行,HTTP/2工作组也把部分安全性包含进规范之中,强制使用TLS 1.2
以上版本,因为更早的版本都已经被发现存在漏洞。
不过,HTTP/2工作组在加密方面存在分歧。有人认为加密工作不应该成为HTTP的一部分,这样对不需要加密的通信是一种浪费。所以,HTTP/2
实际上有了两个版本,安全的h2
和未加密的h2c
。
番外:HTTP进化史
最后,让我们回顾一下HTTP的几个版本,以及它们的改进历程,也当做完总结。
HTTP/0.9 -- 单行协议
1991年发布,极其简单,只有GET方法,用于请求HTML文档。使用GET URL
命令进行资源请求,响应整体就是一个HTML文档。已过时。
HTTP/1.0 -- 可扩展协议
1996年发布,做了很大的改进。
-
增加POST和HEAD方法。
-
在消息中增加版本号,状态码,首部行,使用与现在基本一致的起始行+首部+实体格式。
-
增加了缓存,内容编码,多段数据传输等功能。
HTTP/1.0没有官方标准,标准化混乱。少量网站现在还在使用。
HTTP/1.1 -- 标准化协议
1997年发布,消除HTTP1.0的混乱并做出许多改进。
-
增加PUT、DELETE等方法。
-
默认使用长连接减少连接建立断开开销,并提出流水线机制,尽管很少被使用。
-
引入内容协商。
-
引入缓存控制。
-
支持响应分块。(HTTP/2)不支持。
-
增加
Host
头,不同域名可以在同一IP的服务器上。
非常强大的一个协议,仍然是目前使用最广泛的协议。
HTTP/2 -- 二进制协议
2015年发布,使用了二进制的报文结构,进行许多优化。
-
把消息划分成更小的二进制结构“帧”
-
引入虚拟“流”结构实现多路复用,基本解决队头阻塞问题,不再需要建立多个连接。
-
压缩头部缩小消息。
-
引入服务器主动推送机制。
-
要求安全协议TLS使用
1.2
以上版本。
HTTP/3 -- 未来的HTTP
未发布。现在,HTTP/3还处于草案阶段,它主要从传输层对HTTP进行了深入的改进。
- 不再使用TCP,而是使用专门为HTTP定制的
QUIC协议
, 解决TCP建立缓慢和报文段阻塞的问题。 - QUIC内置STL1.3,只能加密传输,保证安全性。
- 不再需要指定端口,使用更灵活的服务发现机制。
后记
终于学完了。深入学习了一下,即使这样,HTTP的内容是太多了,想要全面又深入实在很难。期间阅读了很多文档和博客,看了很多的HTTP消息,跟之前的认识完全不一样了。内容很多,以后可能会忘,做个笔记好点,忘了来翻翻。