1.8万字深入学习HTTP

812 阅读59分钟

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.1RFC 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通信过程:

  1. 客户端通过一个浏览器应用发起HTTP请求,请求想要获取的文档。
  2. 请求通过TCP/IP加工,经过一个一个中间代理,最终到达服务器主机。
  3. 服务器主机接收请求,处理请求,检出被请求的文档或数据,放到响应中,把响应发送给客户端。
  4. 响应内容经过TCP/IP层加工,经过一个个中间代理,被送回到服务器。
  5. 服务器收到响应,把它交个浏览器,浏览器把文档呈现出来。

在这个过程中,我们忽略网络中的路由器,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还能用于提交简单信息,但这是不规范的用法。

HEAD

与GET同样用于获取资源,但只获取头部信息,也就是得到的响应与GET相比是没有主体的。

HEAD同样是安全的,幂等的,可缓存的。

HEAD主要用于获取某些资源的信息,前提是这些信息会体现在头部中。比如,请求大文件之前先获取头部查看文件大小再决定是否下载它,以此节约带宽。

POST

用于向指定资源提交信息,通常导致在服务器上的状态变化或副作用。

POST方法是不安全的,非幂等的,大部分时候不可缓存的。

POST用于向服务器发送信息,最主要的使用场景是HTML表单提交。

PUT

使用提交的信息创建或替换指定的资源。

PUT方法是不安全的,幂等的,不可缓存的。

PUT也是用于信息提交,PUT与POST最大的区别在于是否幂等。比如提交订单的时候,如果使用POST方法,多次提交一个订单,会生成多份订单,如果使用PUT,只会有一份,这种时候,PUT更符合。

DELETE

删除指定资源。

DELETE方法是不安全的,幂等的,不可缓存的。

删除服务器上的资源应该使用DELETE方法。

OPTIONS

用于获取目的资源支持的通信选项。

OPTION方法是安全的,幂等的,不可缓存的。

OPTIONS常常使用于预检请求,询问服务器能支持哪些选项的请求,然后再决定是否发起正式请求。

后面的请求方法很少用到,做个了解:

CONNECT

用于建立一个到指定的第三方服务器的通信隧道,把接收请求的服务器作为中间代理。

TRACE

向目标服务器发起回环测试,HTTP中间代理可能会修改请求,服务器收到TRACE请求后返回一个与请求一样的TRACE响应,就能知道最后达到服务器的请求内容,并且Via字段记录了中间经过的代理服务器。

TACE提供了一种调试机制,但存在安全漏洞,服务器默认不会开启TACE方法。

PATCH

用于向特定资源提交补丁内容,对资源进行一部分修改,不同于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的风格规范,它指出:

  1. 每个URL应该代表一种资源。
  2. 客户端使用GET, POST, PUT, DELETE对服务器上的资源进行操作,分别代表对资源的获取,更新,替换,删除。

使用RESTful风格,上面的例子应该这样表示:

GET /posts/3
POST /posts/3
DELETE /posts/3
PUT /posts/3

即使不知道服务器的处理,我们也能清楚请求进行的操作,这就是语义化。

RESTful只是风格规范,没有强制要求,但建议尽量使用RESTful风格。

5、状态码

状态码用于标识HTTP请求的响应状态。

响应分为五类:

  1. 1XX -- 信息响应
  2. 2XX -- 成功响应
  3. 3XX -- 重定向
  4. 4XX -- 客户端错误
  5. 5XX --服务器错误

状态码的意义、短语和可能出现的场景如下:

100 Continue

含义:目前为止的请求正常,客户端可以继续发送请求。

场景:客户端想要发送一个主体数据庞大的请求,但服务器可能不接受,这时,客户端会先发起一个带有Expect:100-continueContent-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-SinceIf-None-Match首部,服务器查询资源是否自缓存以来发生改变。如果没有,返回带304的响应(参考缓存验证小节)。

400 Bad Request

含义:错误请求。请求的语义出错,服务器无法理解,客户端不应该再重复发生该请求。

场景:发出了错误的请求,请求的语法错误,消息太大客户端无法处理。

403 Forbidden

含义:拒绝服务。服务器有能力但拒绝了客户端的请求,常常是客户端权限受限。

场景:IP被网站列为黑名单。你的账号权限不足以查看某些页面。拒绝往往是网站业务逻辑的一部分。

404 Not Found

含义:找不到资源。

场景:向不存在的URL发起请求。

500 Internal Server Error

含义:服务器内部异常。服务器发送异常无法继续正常处理请求。

场景:服务器处理请求过程中代码运行发送异常。

解决:调试修改网站代码。

501 Not Implemented

含义:请求的方法不被支持。这是基于服务器的,有些服务器只支持GETPOST请求。

场景:对不支持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

客户端收到请求后,需要执行以下操作:

  1. 验证请求范围是否合法,即是否越界等。如果范围错误,返回416 Range Not Satisfiable响应表示提示端范围错误。
  2. 如果范围合法,读取指定范围的数据内容。
  3. 把数据放到一个206 Partial Content的响应报文中,并设置Content-Range: bytes x-y/length,表示发送了文件的x-y范围,文件总大小为length
  4. 发送响应报文。

如果在Range请求头设置多个范围,客户端还能一次性请求多个范围的数据。如:

Range: bytes=0-499, -500 

表示获取前500个字节和后500个字节。

这种时候,对应的响应会设置Content-Type:multipart/beteranges; boundary=boundary来表明这是一个多段范围的响应, 并且每段数据使用--boundary隔开,注意前面的--

每段数据都由实体头Content-TypeContent-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.9HTTP/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的实现需要三个组件:

  1. 网站后端的Cookie数据库。网站需要记住对每个用户设置的Cookie, 以此根据Cookie值判断对应的用户。

  2. HTTP报文上的有关首部字段。Set-CookieCookie首部提供了对Cookie的支持。

  3. 客户端的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尽可能安全,提供了HttpOnlySecureSameSite指令。

  • 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-storeno-cache指令是控制如何使用缓存的通用指令:

  • no-store-- 不使用缓存。在响应上时,表示任何节点都不要保存该响应;在请求上时,表示该请求不接受任何缓存。
  • no-cache-- 缓存但重新验证。在响应上时,表示可以保存,但每次使用前必须验证缓存是新鲜的。在请求上时,表示可以接受缓存,但必须验证缓存内容是新鲜的。

另外两个属性,publicprivate是只用于响应上的,控制哪些缓存节点可以缓存该响应:

  • private -- 只允许私有缓存(浏览器缓存)保存该响应。默认值。
  • public -- 中间代理也可以缓存。

为了保证缓存的有效,还设置了max-ages-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-freshmax-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的资源标识,添加ETagIf-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-SinceIf-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:closeConnection: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消息,跟之前的认识完全不一样了。内容很多,以后可能会忘,做个笔记好点,忘了来翻翻。