HTTP 协议(HyperText Transfer Protocol,超文本传输协议)是用于 WWW 服务器与用户代理之间的通信协议,是互联网上应用最为广泛的一种网络协议,学习它可以帮助我们更好的理解 Web 的通信过程。在前端的实际工作中,接触 HTTP 的情况比较少,也许只有在使用 Ajax 时涉及到少量的相关内容,但知识的储备有深度也有广度,了解一些底层的东西并没有什么坏处,也许有一天就会用到它。

HTTP 通信过程

首先要明确一些概念:

HTTP 是一个基于请求与响应模式的、无状态的、应用层协议,它可以在任何其他互联网协议上,或者在其他网络上实现,通常是基于 TCP 的连接方式。

HTTP 是一个客户端和服务器端请求和应答的标准。服务器端很明确,就是网站的服务器,它通常是 IIS、Apache、Nginx 等服务程序实现的。客户端我们叫它用户代理,那么什么是用户代理呢?有人说就是浏览器,其实并不全面,用户代理介于用户和服务器之间,帮助用户简化与服务器之间的通信过程,它可以是浏览器,也有可能是搜索引擎蜘蛛或者其它抓取网页的工具程序。

当使用浏览器浏览网站时,通过在地址栏输入域名或者在一个网页上点击一个链接,浏览器就为我们呈现了想要浏览的页面。那么浏览器与网站的服务器是怎么进行通信的呢?

不考虑 TCP 握手和域名解析等问题,HTTP 通信的过程简单来说是这样的:

  1. 用户代理与服务器建立连接(通常是通过 TCP);
  2. 用户代理向服务器发送请求报文;
  3. 服务器解析请求报文,向用户返回响应报文;
  4. 用户代理接收响应报文,并进行后续操作,比如浏览器的解析和渲染。

由此可见,用户代理与服务器是通过一定格式的 HTTP 报文交换信息的,并且,HTTP 报文分为用户代理向服务器发送的请求报文和服务器向用户代理发送的响应报文。用户在输入网址或点击链接时实际上只提供了一个 URL,但是在实际的通信过程中,通过 HTTP 报文,用户代理与服务器之间交换了很多必要的信息。下面就分别讲解一下这两种报文的格式和内容。

HTTP 请求报文

HTTP 请求报文分为三个部分:请求行、请求头部、请求正文,格式如下:

HTTP请求报文格式

下面是一个 HTTP 请求报文的实例:

GET /index.html HTTP/1.1
Host: www.swj.name
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: pgv_pvid=233137994; safedog-flow-item=CD9D59D11F3B23DC7F61AB211A1B97F2
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache

请求行和请求正文

报文的第一行即请求行,其中以空格间隔的方式包含了三种信息:请求方法、URL、协议版本。URL 标明了要请求的页面,协议版本用来协调通信规则,这都很容易理解,重点要说的是请求方法。

HTTP 协议的请求方法有 GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT,其中最常用的是 GET 和 POST。

上面给出的报文实例使用的就是 GET 方法,它用于从指定的资源请求数据。我们知道 GET 方法如果传递信息是把内容放在 URL 中的,所以如果要发送数据,那么它的请求行是这样的:

GET /index.php?contentId=11 HTTP/1.1

使用 GET 方法发送数据安全性较差,因为所发送的内容是 URL 的一部分,所以一般不用于处理敏感信息(比如密码)。并且由于 URL 长度有一定的限制,GET 方法一般只用于传递少量的信息。另外需要强调的是,GET 方法是用于请求数据的,使用 GET 方法发送数据可以理解为通过一定的参数请求数据。

POST 方法用于向指定的资源提交要被处理的数据,与 GET 方法相比,POST 方法更安全,因为提交的数据不是放在 URL 中的。并且,由于 POST 方法对数据长度没有要求,所以可以用来向服务器传递大量的数据。一个完整的 POST 请求报文实例如下:

POST /login.php HTTP/1.1
Host: www.swj.name
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: pgv_pvid=233137994; safedog-flow-item=CD9D59D11F3B23DC7F61AB211A1B97F2
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache

username=admin&password=123456

与之前给的 GET 实例进行对比,会发现这个报文除了使用 POST 方法外,最下面还多出了用空行间隔的一段数据,这就是之前说的请求正文。在网页中使用的 POST 方法提交的表单,都是通过这样的方式发送给服务器的。

HEAD 方法与 GET 类似,不同之处是服务器对 HEAD 请求方法返回的响应报文不会包含响应正文,通常这种方法用来测试某个资源的状态。

PUT 方法用于完整地更新或替换一个现有资源,也可以用客户端指定的 URI 来创建一个新资源。它与 POST 方法类似,不同之处在于:POST 的数据如何存放是由服务器决定的,而 PUT 方法通常指定了资源的存放位置。

DELETE 方法用于请求服务器删除指定资源。

TRACE 方法会回显服务器收到的请求报文,用于测试或诊断通信过程,它的响应方式会在后面讲到。

CONNECT 方法:HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。

OPTIONS 方法用于获取当前 URL 所支持的 HTTP 方法,它的响应方式会在后面讲到。

由于 OPTIONS 方法的存在,我们可以知道,服务器并不一定支持所有的请求方法,通常它们只要支持 GET 和 POST 就已经能够完成基本的功能了。另外需要说明的是,由于具体的业务需要,有些服务器还可能会有一些自定义的扩展方法。

请求头部

请求头部从报文的第二行开始,由关键字/值对组成,每行一对,关键字和值用英文冒号分隔。请求头部通知服务器有关于客户端请求的信息。一些常用的关键字如下:

Host

Host: www.swj.name

用于标明用户代理访问的网站域名。这个有些人不理解,因为在请求报文发送之前,用户代理已经与服务器建立了 TCP 连接,为什么请求头部还要包含一个 Host 字段呢?实际情况是这样的,一台服务器可能会架设很多网站,也是就虚拟主机,这些网站的域名指向服务器的同一个 IP 地址的同一个端口,那么服务器为了区分用户访问的是哪个网站,就要用到 Host 字段了。

User-Agent

User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0

用于表明用户代理的身份,比如浏览器的类型和版本信息,基本的意思就是用户代理向服务器喊了句:队长!别开枪,是我!User-Agent 的值一直比较混乱,这是由于一些历史问题和各浏览器厂商之间竞争造成的,有兴趣的可以去查找下相关内容。

User-Agent 的意义在于,服务器可能会根据不同的用户代理返回不同的页面。举个例子,服务器通过 User-Agent 字段知道用户代理是一个手机浏览器,那么就可以向其返回或跳转到一个移动版的页面,增强用户体验。再举个例子,服务器发现用户代理是搜索引擎蜘蛛,于是返回了一个经过优化的页面,这样就可以更好的被搜索引擎收录了,当然,这是可耻的作弊行为,不提倡!

Accept

用户代理可支持的 MIME 类型列表,它的作用是告知服务器用户代理支持哪些格式的文件,例如用户代理小兔子告诉服务器它只吃青菜,那么服务器肯定不能给它丢一块肉。

MIME 的英文全称是 Multipurpose Internet Mail Extensions (多功能 Internet 邮件扩充服务),它最早应用于电子邮件系统,后来也应用到浏览器。MIME 的格式为:类型/子类型,多种类型之间用英文逗号分隔,优先顺序是它们从左到右的排列顺序。以下面的 Accept 字段为例:

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

text 表示支持的类型,html 表示支持的子类型,text/html 即表示支持标准文本格式的 html 文档。

application 表示应用程序数据或者二进制数据,application/xhtml+xml 即表示支持 xhtml 文档,同理,application/xml 表示支持 xml 文档。

*/* 表示任何类型。如果出现 type/* 的情况,说明支持大类型下面的所有子类型,例如:text/*。

在上面的例子中会发现有的类型后面还有一段用分号间隔的数据,比如:q=0.9,它的作用是用来表示类型的权重系数,取值范围是:0 =< q <= 1,值越大优先级越高,若没有指定 q 值,则其值为 1,即默认的从左到右的优先级顺序,若指定 q 值为 0,就表示用户代理不支持该类型的内容。这种权重系数表示方法在其他的头部字段中也会出现。

Accept-Language

Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3

用于表示用户代理支持的自然语言,它的意义在于,如果网站有多种语言版本,那么服务器可能会根据该字段自动为用户切换到优先级高的语言版本。

Accept-Encoding

Accept-Encoding: gzip, deflate

用于表示用户代理可接受的编码压缩格式。通信过程中为了提高传输效率,数据在发送前可能会经过一定格式的压缩,用户代理接收数据后再对其进行解压缩,所以,用户代理要用该字段告知服务器自己能够支持的压缩格式。

Accept-Charset

Accept-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7

用于标明用户代理可支持的字符集。它的作用是防止服务器响应的数据因为编码方式不能被用户代理识别而造成乱码。但实际上,Accept-Charset 只是一个标识位,服务器还要在程序上保证声明的字符集类型 和 HTTP 正文中所使用的字符集是一致的。例如在 HTTP 报文中声明编码方式为 UTF-8,可是正文的 HTML 使用的是 GB2312 编码,那么就会产生乱码了。

Referer

Referer: http://www.swj.name/index.html

用于表明产生请求的网页 URL。例如,在 A 网站的一个页面中有一个指向 B 网站的链接,当用户点击时,就跳转到了 B 网站的页面,这个点击产生的 HTTP 请求报文中会有一个 Referer 字段记录这个 A 网站页面的 URL。这是一个比较有用的字段,用 AB 网站的情况举两个例子:

第一个例子:B 网站通过商业合作在 A 网站投放广告,A 网站页面上的链接指向 B 网站的一个产品,B 网站想知道这次投放广告的效果,就可以通过 Referer 字段进行统计,确定有多少次点击来自 A 网站。

第二个例子:B 网站开发了一款软件,放在了自己的网站上向用户提供下载,一段时间后 B 网站发现 A 网站的某个页面上提供了一个指向 B 网站软件的链接,但是 B 网站从商业角度考虑,只希望用户从自己的网站上下载软件,这个时候就可以通过请求报文 Referer 字段判断请求的来路,屏蔽掉来自其它网站的请求。

上面的两个例子说明的是跨域跳转中 Referer 的作用,实际上,即使在同一个域中,页面之间的跳转也会带有 Referer 字段,下面是一个它在域内应用的实例:

一个用户登录功能,login.html 页面包含输入账号和密码的表单,表单提交指向 loginCheck.php,用于验证用户名和密码。当 loginCheck.php 收到请求时会首先读取报文的 Referer 字段,判断提交是否来自 login.html 页面,如果是,那么继续执行登录验证的过程,否则,不会进行登录验证。这么做的原因是出于安全性的考虑,系统将 login.html 页面视为用户登录的唯一入口,来自其它途径的请求有可能被用来破解密码。当然,想要绕过这种机制可以通过伪造请求报文的 Referer 字段来实现。这就像用木板在院子外筑了一道篱笆,虽然无法抵挡坦克的冲撞,但至少能拦住一些贪吃的小鸡小鸭。

Connection

Connection: keep-alive

用于通知服务器处理完本次请求后是否断开连接,有以下两个可选值:close 和 keep-alive,close 表示断开连接,keep-alive 表示继续保持连接。HTTP/1.1 使用 keep-alive 为默认值。

保持持续连接有什么意义呢?我们知道,网页想要实现丰富多彩的展现效果和更好的用户体验,除了使用 HTML 本身外,还要有许多附加的资源,比如:图片、视频、音频、Flash、CSS 文档、JS 文档等等,当浏览器收到响应报文后,会对返回 HTML 进行分析,找出需要哪些附加资源,然后立即向服务器请求这些资源。如果服务器在返回了响应报文后就关闭了与浏览器的连接,那么这些附加资源请求就需要与服务器重新建立连接,使用 keep-alive 让连接保持一段时间,可以减少重新建立连接的开销,提高通信效率。

Cookie

Cookie: username=admin; password=123456

用于向服务器发送属于该域的 Cookie 数据。这个很容易理解,举个例子,有些网站带有用户自动登录的功能,这个功能最简单的实现方法就是通过请求报文中的 Cookie 字段将浏览器在本地 Cookie 中存储的用户名、密码等相关数据发送到了服务器,服务器对数据验证通过后,就实现了自动登录。

Cookie 字段值的格式是一个个的键值对,它们之间用英文分号分隔。

Content-Type 和 Content-Length

Content-Type: application/x-www-form-urlencoded
Content-Length: 39

在请求报文中,这两个字段通常在使用 POST 方法发送数据时出现,它们都与请求正文有关。

Content-Type 用于表明请求正文数据的 MIME 类型,MIME 类型前面介绍过,就不再详述了。

Content-Length 用于表明请求正文数据的长度,如果数据经过压缩,那么就是压缩后的长度,总之,这个长度与实际发送的数据长度是一致的。Content-Length 的作用是用来保证数据的完整性。在实际的传输过程中,报文可能被拆分并封装成多个数据包发送,那么接收的一方在收到第一个数据包的时候通过 Content-Length 的值知道了总体的数据长度,就可以判断接收是否完整,是否还要继续接收。

Cache-Control 和 Pragma

Pragma: no-cache
Cache-Control: no-cache

Cache-Control 用于指定请求和响应遵循的缓存机制,它只有在 HTTP/1.1 下才被支持。请求报文中的 Cache-Control 指令有:no-cache、no-store、max-age、max-stale、min-fresh、only-if-cached,简单的介绍下它们的功能:

Pragma 用来包含实现特定的指令,最常用的是 Pragma: no-cache,它的作用与 Cache-Control: no-cache 一样。Pragma 是在 HTTP/1.0 中实现的,Pragma 和 Cache-Control 一起出现通常是为了兼容 HTTP/1.0 和 HTTP/1.1 两种协议版本。

If-Modified-Since 和 If-None-Match

If-Modified-Since: Wed, 16 Nov 2016 06:09:28 GMT
If-None-Match: "1293d9e824bd21:0"

If-Modified-Since 用于回传浏览器端缓存页面的最后修改时间,If-None-Match 用于回传浏览器端缓存页面的 Etag 值,它们的作用会在后面的响应报文中讲到。

Range

Range: bytes=0-9

用于分段请求中规定字节范围,具体作用会在后面的响应报文中讲到。

Origin 和 Access-Control-Request-*

此部分请求头用于跨域资源共享,要了解更多相关内容,请参考:跨域资源共享 (CORS) 详解

HTTP 响应报文

HTTP 响应报文由状态行、响应头部和响应正文部分组成,格式如下:

HTTP响应报文格式

同样,先给出一个实例:

HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Encoding: gzip
Content-Length: 1820
Content-Type: text/html
Date: Wed, 16 Nov 2016 06:09:28 GMT
Etag: "5127fb1892cd21:0"
Last-Modified: Sat, 22 Oct 2016 17:28:24 GMT
Server: IIS
Vary: Accept-Encoding
x-powered-by: WAF/2.0

<!doctype html>
<html>
...
</html>

响应行

响应报文的第一行即响应行,响应行由协议版本字段、状态码和状态码描述文本组成,用空格进行间隔。协议版本很简单,重点说一下状态码和状态码描述。

状态码由三位数字组成,用于表示对请求报文响应的结果,一般用于机器自动识别。状态码描述是一段描述状态码的原因短语,主要用于帮助用户理解。状态码的第一个数字定义响应的类别,有以下五种类型:

详细的状态码列表网上有很多,就不在这里列出了。

响应头部

与请求头部一样,响应头部从响应报文的第二行开始,由关键字/值对组成,每行一对,关键字和值用英文冒号分隔。一些常用的关键字如下:

Server

Server: Microsoft-IIS/7.5

用于表明服务器程序的类型和版本,这个字段与请求报文中的 User-Agent 对应,它们都用于向对方表明自己的身份。Server 字段其实没有什么用,通常只是为了体现信息的对等,很少有用户代理关心是什么软件在提供服务。当然,这么说有点太绝对了,有些非常热情的访问者会通过这个字段的信息在网上检索服务器程序的已知漏洞,从而实现攻击和渗透。

Date

Date: Wed, 16 Nov 2016 06:09:28 GMT

用于表示报文发送的时间,Date 值最后的 GMT 表明它是世界标准时格式,换算成本地时间,需要知道当前所在的时区,例如想要换算为北京时间,需要在这个标准时间上加八个小时。

Last-Modified

Last-Modified: Thu, 01 Dec 2016 03:24:36 GMT

表示资源的最后修改时间。浏览器缓存一个文件时,会把响应报文中的 Last-Modified 值也记录下来,当下一次请求该文件时,会使用在请求报文中提到的 If-Modified-Since 字段将记录的该文件最后修改时间发送给服务器,服务器接收后会将该值与文件的实际最后修改时间做对比,如果时间一致,那么返回 304 状态码,浏览器接收后,直接使用本地缓存的文件,如果时间不一致,就使用响应正文返回新的文件内容,浏览器接到之后,会丢弃旧文件,把新文件缓存起来。

利用 Last-Modified 判断是否使用缓存看似完美,但实际上有比较大的缺陷。例如,对于一个频繁更新但内容不发生变化的文件,浏览器会经常因为 Last-Modified 变更而放弃缓存下载没有变化的新文件,造成缓存的利用率很低。

另一个问题是这种方法本身的缺陷,Last-Modified 的最小粒度为秒级,也就是说只能精确到秒,当响应报文发出后,在小于一秒的时间内对文件进行修改,就可能导致最后修改时间没有变更,当浏览器再次请求时,会使用本地缓存,而实际上服务器上的文件已经改变了。提高 Last-Modified 的精度可以减少这种问题发生的概率,但并不能完全杜绝,因为文件的改变总是有可能发生在最小粒度之内。

Etag

Etag: "1293d9e824bd21:0"

Etag 是一个和服务器资源相关的校验标记,在 HTTP/1.1 中被引入,它的作用与 Last-Modified 类似,可以看做是对 Last-Modified 字段的升级和补充。Etag 的工作过程与 Last-Modified 相同,缓存时保存,再次访问时通过请求报文的 If-None-Match 回传,服务器接收并验证,判断是否使用缓存。

HTTP/1.1 只规定了 Etag 字段的值需要放在""内,并没有规定它的内容和实现方法,并且,服务器是 Etag 的生产者和消费者,浏览器只保证存储和回传,并不关心其内容,这都使 Etag 的取值有很大的灵活性,不同的服务程序会都会有自己的一些算法,通过选取不同的参数,可以解决很多 Last-Modified 无法解决的问题。

Etag 取值最常用的是哈希算法,这种算法产生的校验标记类似于用于验证文件是否被篡改的 MD5 值,在文件内容发生变化的时候其值也会改变。使用 Etag 可以弥补 Last-Modified 的缺陷,更准确高效的使用缓存,提高通信效率。

Cache-Control

Cache-Control: max-age=3600

响应报文中的 Cache-Control 指令有:public、private、no-cache、no-store、no-transform、max-age、must-revalidate、proxy-revalidate。分别介绍下它们的功能:

public

表示响应可被任何缓存区缓存,包括一些需要 HTTP 认证才能访问的内容。

private

表示缓存只能用于单个用户,不能被共享缓存处理,也就是说缓存是私有的。

no-cache 和 no-store

这两个指令在之前的请求报文中已经介绍过了,不再详述。

no-transform

表示资源被第三方缓存时不能修改其内容。这个不太容易理解,举个例子:当我们在手机浏览器中使用百度搜索时,搜索结果中的一些页面会被百度转码,这是因为这些页面可能不适合使用手机浏览器浏览,搜索引擎在收录过程中自动对其进行了优化,如果在响应报文中使用了 Cache-Control: no-transform,那么搜索引擎就不会对其进行优化转码。

max-age

表示缓存的生存周期,以秒为单位,即请求时间到过期时间之间的秒数,例如:

Cache-Control: max-age=3600

它表示此报文的缓存可以使用 3600 秒,也就是一个小时,在这个时间范围内,再次请求这个资源时可以直接使用缓存。需要注意的是,响应报文中的 max-age 和 请求报文中的 max-age 意义是不同的,如果上面这个例子出现在请求报文中,那么它表示客户端可以接收生存周期不大于一小时的响应。同理,请求报文中的 max-stale 和 min-fresh 也都是与生存周期有关的指令。

must-revalidate

表示服务器希望客户端严格遵守其缓存规则,例如:

Cache-Control: max-age=3600, must-revalidate
proxy-revalidate

功能与 must-revalidate 类似,但它只对缓存代理服务器起作用。

Expires

Expires: Tue, 08 Feb 2022 11:35:14 GMT

用于表示缓存的过期时间,超过这个时间后,浏览器会认为缓存过期,重新获取资源。在一个响应报文中,如果 Expires 和 Cache-Control 的 max-age 指令同时存在,max-age 的优先级要高于 Expires。

Connection

Connection: close

响应报文中的 Connection 字段是对请求报文中同名字段的响应。是否保持持续连接,服务器和客户端都有主动权。在通信过程中,浏览器为了提高效率,更希望保持持续连接,但这只是一种建议或请求,是否保持连接通常是由服务器决定的。

Web 服务器具有处理许多并发请求的能力,但这种能力受到硬件资源的限制,所以服务器程序为了保证稳定工作,通常会设定连接数的上限,超过这个上限会拒绝服务。维持一个持续连接看似微不足道,但在处理大量并发请求时就意味着要占用连接并消耗许多系统资源,所以有些服务器为了降低负载会设置为不支持持续连接,在响应报文中返回 Connection: close 通知客户端。

Location

Location: http://www.swj.name/index.html

用于重定向客户端到一个新的位置。Location 的值是一个 URL,客户端收到带有 Location 字段的响应报文后就会重新向这个 URL 发出请求,实现重定向。Location 是一个非常有用的字段,很多基础功能都是通过它实现的。在前面的请求报文中讲过一个例子,服务器通过 User-Agent 判断用户设备为手机,将页面重定向到移动版本,这个重定向就可以通过 Location 实现。

Content-Encoding

Content-Encoding: gzip

用于向用户代理通知响应正文的压缩格式。在请求报文中我们说过用户代理会通过 Accept-Encoding 字段向服务器告知自己支持的压缩格式,如果服务器同样支持其中某种格式的话就可能对响应正文进行压缩处理,并用响应报文中的 Content-Encoding 字段通知用户代理正文的压缩方法。Accept-Encoding 是一个列表,而 Content-Encoding 是一个明确的方法,并且 Content-Encoding 声明的方法一定包含在请求报文的 Accept-Encoding 列表中。

Content-Type 和 Content-Length

响应报文的 Content-Type 和 Content-Length 字段与请求报文的同名字段功能相同,它们用来描述响应正文的 MIME 类型和数据长度。

Content-Location

Content-Location: http://www.swj.name/index.html 

用于表示资源实际所处的位置。资源的实际所处位置通常会通过 URL 体现出来,但也有一些特殊情况,例如当你使用 http://www.swj.name/ 地址访问网站时,实际的访问资源是 index.html,但这个资源名并不会显示在浏览器的地址栏里,它是通过服务器的默认文档功能实现的,这个时候通过 Content-Location 字段就可以知道资源名和它所处的位置了。

Allow

Allow: OPTIONS, TRACE, GET, HEAD, POST

在前文的请求报文部分我们提到过一种 OPTIONS 请求方法,响应报文中的 Allow 字段即是对这种请求的响应,它的作用是告知用户代理当前服务器支持的请求方法。当然,服务器首先要支持 OPTIONS 方法。

Set-Cookie

用于设置客户端的 Cookie。对于前端来说,操作 Cookie 是很简单的,可以轻松的使用 JavaScript 实现,这种方式是在客户端本地操作,并不涉及网络通信,但是如果想要用 PHP、Java 等后台语言操作客户端的 Cookie,就需要使用到响应报文的 Set-Cookie 字段了。

在前面的请求报文中讲过一个通过发送 Cookie 数据实现自动登陆的例子,我们只讲了请求报文通过 Cookie 字段上传本地数据到服务器,并没有说明这些数据之前是怎么存储在本地的,实际上,这些数据就是通过 Set-Cookie 字段从服务器发送过来的,当用户通过输入用户名和密码实现一次成功的手动登录时,服务器就使用响应报文中的 Set-Cookie 字段将用户名和密码返回给浏览器,浏览器将这些数据存储在本地,当下一次访问时,浏览器发送这些 Cookie 数据,实现自动登陆。

Set-Cookie 与请求报文中的 Cookie 在格式上是不同的,它的过个键值对之间用回车分隔,例如:

Set-Cookie: username=admin
password=123456

Set-Cookie 的另一个重要功能是通过定义选项的值来进一步确定 Cookie 的功能:

有效期选项

用于规定 Cookie 的有效期,有两种设置方式:expires、Max-Age。

expires 表示绝对过期时间,超过这个时间,该 Cookie 就会失效,例如:

Set-Cookie: username=admin; Expires=Tue, 15 Jan 2016 21:47:38 GMT

Max-Age 表示相对过期时间,即 Cookie 在多长时间之后而失效,以秒为单位,例如:

Set-Cookie: username=admin; Max-Age=36000
范围选项

用于表示该 Cookie 的生效范围,有两个属性:Domain、Path。

Domain 表示当前 Cookie 所属域或子域,例如:

Set-Cookie: username=admin; Domain=.swj.name

Path 表示当前 Cookie 的生效路径,例如:

Set-Cookie: username=admin; path=/; Domain=.swj.name

只有当域名和路径同时满足的时候,浏览器才会将 Cookie 回传给服务器。如果没有设置 Domain 和 Path 的话,他们会被默认为当前请求页面对应值。

安全选项

用于规定如何安全的使用 Cookie,有两个选项:Secure、HttpOnly,它们都是没有值的。

Secure 表示该 Cookie 只能用 HTTPS 传输,传输时会进行安全加密,提高安全性,例如:

Set-Cookie: username=admin; path=/; Domain=.swj.name; Secure

HttpOnly 表示此 Cookie 必须用于 HTTP 或 HTTPS 传输,对浏览器脚本是不可见的,例如:

Set-Cookie: username=admin; path=/; Domain=.swj.name; HttpOnly

Accept-Ranges 和 Content-Range

这两个字段用于实现 HTTP 的分段请求。什么是分段请求呢?简单的说,就是把一个大的数据分割成许多小的分段进行传输,可以全部下载后重新拼装,也可以单独的下载其中的一部分分段。通过分段请求可以实现多线程下载和断点续传。

Accept-Ranges 用于表明服务器是否支持分段请求及哪种类型的分段请求,它有两个可选值:none 和 bytes,none 表示服务器不支持分段请求,bytes 表示服务器支持以字节为单位的分段请求,字节数也是目前唯一可选的分段单位。

要实现分段请求,要使用请求报文中的 Range 字段,它的作用就是表明请求的分段范围,例如:

Range: bytes=500-999

这个例子表明当前的请求报文只请求一个分段,字节数从 0 开始计数,请求分段范围是从第 500 个字节到第 999 个字节,共 500 个字节。

Content-Range 用于表明响应正文所属的分段范围,例如:

HTTP/1.1 206 Partial Content
Content-Range: bytes 500-999/2000
Content-Length: 500

这个例子的响应报文状态码为 206,表示当前的响应正文是一个分段,文件总长度为 2000 字节,当前响应分段范围是从第 500 个字节到第 999 个字节。我们在前文中讲过,Content-Length 用于描述正文的实际长度,在这里同样适用,在一个分段响应报文中,Content-Length 的值是该分段的长度,并不是完整文件的长度。

结合上面的内容,用一个简单的实例来说明多线程下载和断点续传的过程:

例如,现在要从服务器上通过多线程下载一个文件,URL 为:/demo.zip,文件大小是 1500 字节。但是在下载开始前,客户端只知道文件的 URL,对文件的大小是不清楚的,对服务器是否支持分段请求也是不清楚的,甚至对服务器上是否存在这个文件都是不清楚的,这个时候,客户端先要获取这些基本信息,这个步骤,使用 GET 方法显然是不合适的,因为它的响应报文会包含响应正文。在前文中我们说过 HEAD 请求方法,用它来完成这个过程就很实用了:

HEAD /demo.zip HTTP/1.1

这个时候不能使用 Range,因为要获取的是完整的文件大小,服务器响应报文如下:

HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 1500
Content-Type: application/x-zip-compressed
Etag: "5127fb1892cd21:0"
Last-Modified: Sat, 22 Oct 2016 17:28:24 GMT

状态码 200 说明文件存在,Accept-Ranges: bytes 说明服务器支持分段请求,Content-Length: 1500 表示文件大小为 1500 字节,通过这些信息,客户端知道进行多线程下载的条件都具备了,决定将数据分为三个分段进行下载,于是开启三个线程,分别发送分段请求:

# 分段请求报文 1
GET /demo.zip HTTP/1.1
Range: bytes=0-499

# 分段请求报文 2
GET /demo.zip HTTP/1.1
Range: bytes=500-999

# 分段请求报文 3
GET /demo.zip HTTP/1.1
Range: bytes=1000-

每个分段请求 500 个字节的数据,第三个请求的 Range: bytes=1000- 是个简化的写法,表示第 1000 个字节到最后,相当于 1000-1499,当三个线程都收到服务器的分段响应报文后,客户端对响应正文重新组装,得到完整的文件,多线程下载就完成了。

如果在下载过程中由于某些原因造成下载中断,就会导致部分数据缺失,以后想要完成下载,需要进行断点续传。断点续传的原理也是进行分段请求,缺失哪部分就请求哪部分,但是在开始分段请求前,需要进行另一个重要的步骤:验证文件信息。

这是因为开始断点续传时距离上一次下载可能过了一段时间,服务器可能对要下载的文件进行了修改或删除操作,所以要对相关信息进行一次核对,如果文件已经被删除,那么就无法完成下载了;如果文件出现了变更,那么已经完成下载的那部分数据也没有用了,需要重新下载。这个步骤也可以通过 HEAD 请求完成,验证的方法是通过 Etag 和 Last-Modified 的比对。

Access-Control-*

此部分响应头用于跨域资源共享,要了解更多相关内容,请参考:跨域资源共享 (CORS) 详解

X-Powered-By

X-Powered-By: ASP.NET

用于表明网站是用何种语言或框架编写的。

响应正文

响应正文即服务器返回给客户端的信息内容,与请求报文一样,响应报文中如果存在响应正文,那么响应正文位于整个报文的最底部,与响应头部之间用空行分隔。

响应正文不但能包含正常的信息内容,当服务器在响应过程中发生错误时,还可以通过响应正文将错误信息发送给客户端,方便程序员进行调试。

另外一种特殊的响应正文包含在服务器对 TRACE 请求方法的响应中,下面是一个简单的例子:

# HTTP 请求报文
TRACE /test HTTP/1.1
Host: localhost
Accept: text/html

# HTTP 响应报文
HTTP/1.1 200 OK
Content-Type: message/http

TRACE /test HTTP/1.1
Host: localhost
Accept: text/html

可以看到,请求报文中使用了 TRACE 方法,在响应报文中,服务端将收到的请求报文作为响应正文返回了,这就是回显请求的响应方式。这并非没有意义,实际上,当一个请求被发出后,它可能要经过很多网络设备或应用程序,每个节点都可能对其进行修改,回显的意义就是告诉客户端,服务端收到的报文变成了什么样子,这通常用于测试或诊断通信过程。

写在最后

这篇内容断断续续的写了一个多月的时间,大概有 1 万字了,最初没有想到会这么长,我的洪荒之力都快耗尽了。网站的更新频率一直比较低,这次玩了个大的。

对于我来说这其实也是个学习的过程,在网上找了很多资料,加上自己的理解。内容的结构并不是很死板,像 Cache 头域、Client 头域、Entity 头域等等这些分类都没有出现,因为我觉得这些都没什么必要,还是根据功能来编排结构比较容易让人理解。另外一些不重要或者非标准的东西也没有写,太多了。

由于是我个人的理解,难免会有些错误和遗漏,欢迎批评指正。