概述
HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol)
,超文本传输协议可以进行文字分割:超文本(Hypertext)、传输(Transfer)、协议(Protocol),
它们之间的关系如下:
超文本(HyperText)
:能传输图片、音频、视频,甚至点击文字或图片能够进行超链接的跳转。这种文本被称为超文本
传输(Transfer)
:两台计算机之间会形成互联关系进行通信,我们存储的超文本会被解析成为二进制数据包,由传输载体(例如同轴电缆,电话线,光缆)负责把二进制数据包由计算机终端传输到另一个终端的过程称为传输(transfer)
协议(Protocol)
:网络协议就是网络中(包括互联网)传递、管理信息的一些规范。如同人与人之间相互交流是需要遵循一定的规矩一样,计算机之间的相互通信需要共同遵守一定的规则,这些规则就称为网络协议
。
HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范
与HTTP相关的协议
在互联网中,任何协议都不会单独的完成信息交换,HTTP 也一样。虽然 HTTP 属于应用层
的协议,但是它仍然需要其他层次协议的配合完成信息的交换。
TCP/IP
- TCP: TCP 协议的全称是 Transmission Control Protocol 的缩写,意思是
传输控制协议
,HTTP 使用 TCP 作为通信协议,这是因为 TCP 是一种可靠的协议,而可靠能保证数据不丢失。 - IP: IP 协议的全称是 Internet Protocol 的缩写,它主要解决的是
通信双方寻址的问题
。IP 协议使用 IP 地址 来标识互联网上的每一台计算机,可以把 IP 地址想象成为你手机的电话号码,你要与他人通话必须先要知道他人的手机号码,计算机网络中信息交换必须先要知道对方的 IP 地址。
- TCP: TCP 协议的全称是 Transmission Control Protocol 的缩写,意思是
DNS
: DNS 的全称是域名系统(Domain Name System,缩写:DNS)
,它作为将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。URI/URL
- URI: URI的全称是(Uniform Resource Identifier),
中文名称是统一资源标识符
,使用它就能够唯一地标记互联网上资源。 - URL: URL的全称是(Uniform Resource Locator),
中文名称是统一资源定位符
,也就是我们俗称的网址,它实际上是 URI 的一个子集。
- URI: URI的全称是(Uniform Resource Identifier),
HTTPS
: HTTP 一般是明文
传输,很容易被攻击者窃取重要信息。HTTPS 的全称为 (Hyper Text Transfer Protocol over SecureSocket Layer)
,HTTPS 和 HTTP 有很大的不同在于 HTTPS 是以安全为目标的 HTTP 通道,在 HTTP 的基础上通过传输加密
和身份认证
保证了传输过程的安全性。HTTPS 在 HTTP 的基础上增加了 SSL 层,也就是说 HTTPS = HTTP + SSL。
HTTP模型
网络设计者以分层(layer)
的方式组织协议,每个协议属于层次模型之一。每一层都是向它的上一层提供服务(service)
,即所谓的服务模型(service model)
。每个分层中所有的协议称为 协议栈(protocol stack)
。因特网的协议栈由五个部分组成:物理层、链路层、网络层、运输层和应用层。我们采用自上而下的方法研究其原理,也就是应用层 -> 物理层的方式。
四层
第一层叫“链接层”(link layer)
:负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标记网络上的设备,所以有时候也叫 MAC 层。第二层叫“网际层”或者“网络互连层”(internet layer)
:IP 协议就处在这一层。因为 IP 协议定义了“IP 地址”的概念,所以就可以在“链接层”的基础上,用 IP 地址取代 MAC 地址,把许许多多的局域网、广域网连接成一个虚拟的巨大网络,在这个网络里找设备时只要把 IP 地址再“翻译”成 MAC 地址就可以了。第三层叫“传输层”(transport layer)
:这个层次协议的职责是保证数据在 IP 地址标记的两点之间“可靠”地传输,是 TCP 协议工作的层次,另外还有它的一个“小伙伴”UDP。第四层叫“应用层”(application layer)
:由于下面的三层把基础打得非常好,所以在这一层就“百花齐放”了,有各种面向具体应用的协议。例如 Telnet、SSH、FTP、SMTP 等等,当然还有我们的 HTTP。
MAC 层的传输单位是帧(frame
),IP 层的传输单位是包(packet)
,TCP 层的传输单位是段(segment)
,HTTP 的传输单位则是消息或报文(message)
。但这些名词并没有什么本质的区分,可以统称为数据包
。
七层(OSI网络分成模型)
第一层:物理层
,网络的物理形式,例如电缆、光纤、网卡、集线器等等;第二层:数据链路层
,它基本相当于 TCP/IP 的链接层;第三层:网络层
,相当于 TCP/IP 里的网际层;第四层:传输层
,相当于 TCP/IP 里的传输层;第五层:会话层
,维护网络中的连接状态,即保持会话和同步;第六层:表示层
,把数据转换为合适、可理解的语法和语义;第七层:应用层
,面向具体的应用传输数据。
四层与七层对比
第一层:物理层
,TCP/IP 里无对应;第二层:数据链路层
,对应 TCP/IP 的链接层;第三层:网络层
,对应 TCP/IP 的网际层;第四层:传输层
,对应 TCP/IP 的传输层;第五、六、七层
:统一对应到 TCP/IP 的应用层。
凡是由操作系统
负责处理的就是四层或四层以下,否则,凡是需要由应用程序
(也就是你自己写代码)负责处理的就是七层。
HTTP 请求响应过程
当你在浏览器中输入网址后,到底发生了什么事情?你想要的内容是如何展现出来的?让我们通过一个例子来探讨一下,我们假设访问的 URL 地址为 http://www.someSchool.edu/someDepartment/home.index
当我们输入网址并点击回车时,浏览器内部会进行如下操作:
- DNS服务器会首先进行
域名的映射
,找到访问www.someSchool.edu所在的地址,然后HTTP 客户端进程在 80 端口发起一个到服务器 www.someSchool.edu 的 TCP 连接(三次握手
)。在客户和服务器进程中都会有一个套接字与其相连。 - HTTP
客户端
通过它的套接字向服务器发送一个 HTTP请求报文
。该报文中包含了路径 someDepartment/home.index 的资源。 - HTTP
服务器
通过它的套接字接受该报文,进行请求的解析工作,并从其存储器(RAM 或磁盘)中检索出对象 www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到 HTTP 响应报文中,并通过套接字向客户进行发送。 - HTTP
服务器
随即通知 TCP 断开 TCP 连接,实际上是需要等到客户接受完响应报文后才会断开 TCP 连接。 - HTTP
客户端
接受完响应报文后,TCP 连接会关闭。HTTP 客户端从响应中提取出报文中是一个 HTML 响应文件,并检查该 HTML 文件,然后循环检查报文中其他内部对象。 - 检查完成后,HTTP 客户端会把对应的资源通过显示器呈现给用户。
三次握手
通过三次握手来建立一次连接
-
第一次握手
:建立连接时,客户端发送syn
包(syn=j)到服务器,并进入SYN_SEND
状态,等待服务器确认; -
第二次握手
:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK
包,此时服务器进入SYN_RECV
状态; -
第三次握手
:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED
状态,完成三次握手。 完成三次握手,客户端与服务器开始传送数据.
四次挥手
-
第一次挥手
: 客户端A发送一个FIN.用来关闭客户A到服务器B的数据传送 -
第二次挥手
: 服务器B收到这个FIN. 它发回一个ACK,确认序号为收到的序号+1。和SYN一样,一个FIN将占用一个序号 -
第三次挥手
: 服务器B关闭与客户端A的连接,发送一个FIN给客户端A -
第四次挥手
: 客户端A发回ACK报文确认,并将确认序号设置为序号加1
HTTP 请求特征
从上面整个过程中我们可以总结出 HTTP 进行分组传输是具有以下特征
- 支持
客户-服务器
模式 简单快速
:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有 GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于 HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信速度很快。灵活
:HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记。无连接
:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。无状态
:HTTP 协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
HTTP 报文
上面描述了一下 HTTP 的请求响应过程,流程比较简单,在客户端与服务端确立好TCP连接后,客户端发送HTTP 请求报文
来和服务端交互。那么接下来分析一下HTTP报文的组成
HTTP 请求/响应报文三大部分组成:
起始行(start line)
:描述请求或响应的基本信息
请求行
: 由请求方式,请求目标,版本号组成响应行
: 服务器响应状态
头部字段(header)
:使用 key-value 形式更详细地说明报文;
HTTP 的请求头分为四种: 通用标头、请求标头、响应标头 和 实体标头,依次来进行详解。
通用标头
Cache-Control
:可以出现在请求标头和响应标头中,Cache-Control 的种类比较多,虽然说这是一个通用标头,但是有一些特性是请求标头具有的,有一些是响应标头才有的。主要大类有 可缓存性、阈值性、 重新验证并重新加载 和其他特性max-age
: 资源被认为仍然有效的最长时间,与 Expires 不同,这个请求是相对于 request标头的时间,而 Expires 是相对于响应标头。(请求标头)s-maxage
: 重写了 max-age 和 Expires 请求头,仅仅适用于共享缓存,被私有缓存所忽略(这块不理解,看完响应头的 Cache-Control 再进行理解)(请求标头)max-stale
:表示客户端将接受的最大响应时间,以秒为单位。(响应标头)min-fresh
: 表示客户端希望响应在指定的最小时间内有效。(响应标头)
Connection
:定当前事务(一次三次握手和四次挥手)完成后,是否会关闭网络连接。Connection 有两种,一种是持久性连接
,即一次事务完成后不关闭网络连接- Connection:
keep-alive
持久性连接 - Connection:
close
一次事务完成后关闭网络连接
- Connection:
实体标头
实体标头是描述消息正文内容
的 HTTP 标头。实体标头用于 HTTP 请求和响应中。头部Content-Length、 Content-Language、 Content-Encoding 是实体头。
Content-Length
实体报头指示实体主体的大小,以字节为单位,发送到接收方。Content-Language
实体报头描述了客户端或者服务端能够接受的语言,例如- Content-Language: de-DE
- Content-Language: en-US
- Content-Language: de-DE, en-CA
Content-Encoding
这又是一个比较麻烦的属性,这个实体报头用来压缩媒体类型。Content-Encoding 指示对实体应用了何种编码。常见的内容编码有这几种: gzip、compress、deflate、identity ,这个属性可以应用在请求报文和响应报文中- Accept-Encoding: gzip, deflate //请求头
- Content-Encoding: gzip //响应头
请求标头
Host
:Host 请求头指明了服务器的域名(对于虚拟主机来说),以及(可选的)服务器监听的TCP端口号Referer
:HTTP Referer 属性是请求标头的一部分,当浏览器向 web 服务器发送请求的时候,一般会带上 Referer,告诉服务器该网页是从哪个页面链接过来的Upgrade-Insecure-Requests
:Upgrade-Insecure-Requests 是一个请求标头,用来向服务器端发送信号,表示客户端优先选择加密及带有身份验证的响应。If-Modified-Since
:HTTP 的 If-Modified-Since 使其成为条件请求:- 返回200,只有在给定日期的最后一次修改资源后,服务器才会以200状态发送回请求的资源。
- 如果请求从开始以来没有被修改过,响应会返回304并且没有任何响应体
If-None-Match
:If-None-Match HTTP请求标头使请求成为条件请求。对于 GET 和 HEAD 方法,仅当服务器没有与给定资源匹配的 ETag 时,服务器才会以200状态发送回请求的资源。对于其他方法,仅当最终现有资源的ETag与列出的任何值都不匹配时,才会处理请求。
响应标头
响应标头是可以在 HTTP 响应中使用的 HTTP 标头,。并不是所有出现在响应中的标头都是响应标头。有通用标头和实体标头也会出现在响应标头中,比如 Content-Length
就是一个实体标头,但是,在这种情况下,这些实体请求通常称为响应头。
消息正文(entity)
实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。
内容协商
内容协商机制是指客户端和服务器端就响应的资源内容进行交涉,然后提供给客户端最为适合的资源。内容协商会以响应资源的语言、字符集、编码方式等作为判断的标准。
内容协商主要有以下3种类型:
服务器驱动协商(Server-driven Negotiation)
:这种协商方式是由服务器端进行内容协商。服务器端会根据请求首部字段进行自动处理客户端驱动协商(Agent-driven Negotiation)
:这种协商方式是由客户端来进行内容协商。透明协商(Transparent Negotiation)
:是服务器驱动和客户端驱动的结合体,是由服务器端和客户端各自进行内容协商的一种方法。
内容协商的分类有很多种,主要的几种类型是 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Language。
Accept
: 标记的是客户端可理解的MIME type
,可以用“,”做分隔符列出多个类型,让服务器有更多的选择余地;Accept: text/html,application/xml,image/webp,image/png
MIME: MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的因特网标准。MIME 消息能包含文本、图像、音频、视频以及其他应用程序专用的数据。
Accept-Charset
:字符集;Accept-Charset: gbk, utf-8Accept-Language
:标记了客户端可理解的自然语言,也允许用“,”做分隔符列出多个类型;Accept-Language: zh-CN, zh, enAccept-Encoding
: 客户端支持的压缩格式,常见的内容编码有这几种: gzip、compress、deflate、identityq
:参数表示权重来设定优先级,这里的“q”是“quality factor”的意思。权重的最大值是 1,最小值是 0.01,默认值是 1,如果值是 0 就表示拒绝。具体的形式是在数据类型或语言代码后面加一个“;”,然后是“q=value”。Vary
字段:内容协商的过程是不透明的,每个 Web 服务器使用的算法都不一样。但有的时候,服务器会在响应头里多加一个 Vary 字段,记录服务器在内容协商时参考的请求头字段,给出一点信息,例如:Vary: Accept-Encoding,User-Agent,AcceptContent-Type
:告诉实体数据的真实类型;Content-Type 字段的数据类型后面用“charset=xxx”来表示字符集 Content-Type: text/html; charset=utf-8Content-Encoding
: 服务器可以选择其中一种来压缩数据,实际使用的压缩格式放在响应头字段Content-Language
: 告诉客户端实体数据使用的实际语言类型:Content-Language: zh-CN
客户端用 Accept 头告诉服务器希望接收什么样的数据,而服务器用 Content 头告诉客户端实际发送了什么样的数据。
HTTP传输大文件的方法
早期互联网上传输的基本上都是只有几 K 大小的文本和小图片,现在的情况则大有不同。网页里包含的信息实在是太多了,随随便便一个主页 HTML 就有可能上百 K,高质量的图片都以 M 论,更不要说那些电影、电视剧了,几 G、几十 G 都有可能。
下面探讨HTTP 协议里有哪些手段能解决这个问题。
数据压缩
通常浏览器在发送请求时都会带着“Accept-Encoding
”头字段,里面是浏览器支持的压缩格式列表,
例如 gzip、deflate、br 等,这样服务器就可以从中选择一种压缩算法,放进“Content-Encoding
”响应头里,
再把原数据压缩后发给浏览器。
如果压缩率能有 50%,也就是说 100K 的数据能够压缩成 50K 的大小,那么就相当于在带宽不变的情况下网速提升了一倍, 加速的效果是非常明显的。
不过这个解决方法也有个缺点,gzip 等压缩算法通常只对文本文件有较好的压缩率,而图片、音频视频等多媒体数据本身就已经是高度压缩的, 再用 gzip 处理也不会变小(甚至还有可能会增大一点),所以它就失效了。
分块传输
把大文件分解成多个小块,把这些小块分批发给浏览器,浏览器收到后再组装复原。 这样浏览器和服务器都不用在内存里保存文件的全部,每次只收发一小部分,网络也不会被大文件长时间占用, 内存、带宽等资源也就节省下来了。
这种“化整为零
”的思路在 HTTP 协议里就是“chunked
”分块传输编码,在响应报文里用头字段“Transfer-Encoding: chunked
”来表示,意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。分块传输也可以用于“流式数据
”,例如由数据库动态生成的表单页面,这种情况下 body 数据的长度是未知的,无法在头字段“Content-Length
”里给出确切的长度,所以也只能用 chunked 方式分块发送。
“Transfer-Encoding: chunked”和“Content-Length”这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked),
这一点你一定要记住。下面我们来看一下分块传输的编码规则,其实也很简单,同样采用了明文的方式,很类似响应头。
- 每个分块包含两个部分,长度头和数据块;
- 长度头是以 CRLF(回车换行,即\r\n)结尾的一行明文,用 16 进制数字表示长度;
- 数据块紧跟在长度头后,最后也用 CRLF 结尾,但数据不包含 CRLF;
- 最后用一个长度为 0 的块表示结束,即“0\r\n\r\n”。
范围请求
有了分块传输编码,服务器就可以轻松地收发大文件了,但对于上 G 的超大文件,还有一些问题需要考虑。
比如,你在看当下正热播的某穿越剧,想跳过片头,直接看正片,或者有段剧情很无聊,想拖动进度条快进几分钟,这实际上是想获取一个大文件其中的片段数据,而分块传输并没有这个能力。
HTTP 协议为了满足这样的需求,提出了“范围请求
”(range requests)的概念,允许客户端在请求头里使用专用字段来表示只获取文件的一部分,相当于是客户端的“化整为零
”。
服务器必须在响应头里使用字段“Accept-Ranges: bytes
”明确告知客户端:“我是支持范围请求的
”。
请求头 Range
是 HTTP 范围请求的专用字段,格式是“bytes=x-y
”,其中的 x 和 y 是以字节为单位的数据范围。
要注意 x、y 表示的是“偏移量”,范围必须从 0 计数,
例如前 10 个字节表示为“0-9”,第二个 10 字节表示为“10-19”,而“0-10”实际上是前 11 个字节。Range 的格式也很灵活,起点 x 和终点 y 可以省略,能够很方便地表示正数或者倒数的范围。
假设文件是 100 个字节,那么:
- “0-”表示从文档起点到文档终点,相当于“0-99”,即整个文件;
- “10-”是从第 10 个字节开始到文档末尾,相当于“10-99”;
- “-1”是文档的最后一个字节,相当于“99-99”;
- “-10”是从文档末尾倒数 10 个字节,相当于“90-99”。
GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-31
服务器收到 Range 字段后,需要做四件事。
- 第一,它必须检查范围是否合法,比如文件只有 100 个字节,但请求“200-300”,这就是范围越界了。服务器就会返回状态码
416
,意思是“你的范围请求有误,我无法处理,请再检查一下”。 - 第二,如果范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码“
206 Partial Content
”,和 200 的意思差不多,但表示 body 只是原数据的一部分。 - 第三,服务器要添加一个响应头字段
Content-Range
,告诉片段的实际偏移量和资源的总大小,格式是“bytes x-y/length
”,与 Range 头区别在没有“=”,范围后多了总长度。例如,对于“0-10”的范围请求,值就是“bytes 0-10/100”。最
后剩下的就是发送数据了,直接把片段用 TCP 发给客户端,一个范围请求就算是处理完了。
多段数据
刚才说的范围请求一次只获取一个片段,其实它还支持在 Range 头里使用多个“x-y”,一次性获取多个片段数据。
这种情况需要使用一种特殊的 MIME 类型:“multipart/byteranges
”,表示报文的 body 是由多段字节序列组成的,并且还要用一个参数“boundary=xxx
”给出段之间的分隔标记。
多段数据的格式与分块传输也比较类似,但它需要用分隔标记 boundary 来区分不同的片段,可以通过图来对比一下。
每一个分段必须以“- -boundary”开始(前面加两个“-”),之后要用“Content-Type”和“Content-Range”标记这段数据的类型和所在范围,然后就像普通的响应头一样以回车换行结束,再加上分段数据,最后用一个“- -boundary- -”(前后各有两个“-”)表示所有的分段结束。
HTTP的重定向和跳转
由浏览器的使用者主动发起的,可以称为“主动跳转
”,但还有一类跳转是由服务器来发起的,浏览器使用者无法控制,相对地就可以称为“被动跳转
”,这在 HTTP 协议里有个专门的名词,叫做“重定向
”(Redirection)。
重定向的过程
301 是“永久重定向
”,302 是“临时重定向
”,浏览器收到这两个状态码就会跳转到新的 URI。
用 Chrome 访问 URI “/18-1”,它会使用 302 立即跳转到“/index.html”。
可以看到,这一次“重定向
”实际上发送了两次 HTTP 请求,第一个请求返回了 302,然后第二个请求就被重定向到了“/index.html
”。但如果不用开发者工具的话,你是完全看不到这个跳转过程的,也就是说,重定向是“用户无感知
”的。
我们再来看看第一个请求返回的响应报文:
“Location
”字段属于响应字段,必须出现在响应报文里。但只有配合 301/302 状态码才有意义,它标记了服务器要求重定向的 URI,这里就是要求浏览器跳转到“index.html”。
浏览器收到 301/302 报文,会检查响应头里有没有“Location”。如果有,就从字段值里提取出 URI,发出新的 HTTP 请求,相当于自动替我们点击了这个链接。
重定向的相关问题
第一个问题是“性能损耗”
。很明显,重定向的机制决定了一个跳转会有两次请求 - 应答,比正常的访问多了一次。虽然 301/302 报文很小,但大量的跳转对服务器的影响也是不可忽视的。站内重定向还好说,可以长连接复用,站外重定向就要开两个连接,如果网络连接质量差,那成本可就高多了,会严重影响用户的体验。所以重定向应当适度使用,决不能滥用。第二个问题是“循环跳转”
。如果重定向的策略设置欠考虑,可能会出现“A=>B=>C=>A”的无限循环,不停地在这个链路里转圈圈,后果可想而知。所以 HTTP 协议特别规定,浏览器必须具有检测“循环跳转”的能力,在发现这种情况时应当停止发送请求并给出错误提示。
HTTP的Cookie机制
HTTP 的 Cookie 机制就是服务器给每个客户端都贴上一张小纸条,上面写了一些只有服务器才能理解的数据,需要的时候客户端把这些信息发给服务器,服务器看到 Cookie,就能够认出对方是谁了。
Cookie 的工作过程
这要用到两个字段:响应头字段 Set-Cookie
和请求头字段 Cookie
。
当用户通过浏览器第一次访问服务器的时候,服务器肯定是不知道他的身份的。所以,就要创建一个独特的身份标识数据,格式是“key=value
”,然后放进 Set-Cookie
字段里,随着响应报文一同发给浏览器。
浏览器收到响应报文,看到里面有 Set-Cookie
,知道这是服务器给的身份标识,于是就保存起来,下次再请求的时候就自动把这个值放进 Cookie
字段里发给服务器。
因为第二次请求里面有了 Cookie
字段,服务器就知道这个用户不是新人,之前来过,就可以拿出 Cookie 里的值,识别出用户的身份,然后提供个性化的服务。
不过因为服务器的“记忆能力”实在是太差,一张小纸条经常不够用。所以,服务器有时会在响应头里添加多个 Set-Cookie
,存储多个“key=value
”。但浏览器这边发送时不需要用多个 Cookie 字段,只要在一行里用“;
”隔开就行。
Cookie 的属性
Cookie 就是服务器委托浏览器存储在客户端里的一些数据,而这些数据通常都会记录用户的关键识别信息。 所以,就需要在“key=value”外再用一些手段来保护,防止外泄或窃取,这些手段就是 Cookie 的属性。
- “
Expires
”俗称“过期时间
”,用的是绝对时间点,可以理解为“截止日期”(deadline)。 - “
Max-Age
”用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间。 - “
Domain
”和“Path
”指定了 Cookie 所属的域名和路径,浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie。 - 属性“
HttpOnly
”会告诉浏览器,此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问, SameSite
”可以防范“跨站请求伪造”(XSRF)攻击,设置成“SameSite=Strict”可以严格限定 Cookie 不能随着跳转链接跨站发送,而“SameSite=Lax”则略宽松一点,允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送。- “
Secure
”,表示这个 Cookie 仅能用 HTTPS 协议加密传输,明文的 HTTP 协议会禁止发送。但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在。
HTTP的缓存控制
基于“请求 - 应答”模式的特点,可以大致分为客户端缓存和服务器端缓存,因为服务器端缓存经常与代理服务“混搭”在一起。
服务器的缓存控制
- 浏览器发现缓存无数据,于是发送请求,向服务器获取资源;
- 服务器响应请求,返回资源,同时标记资源的有效期;
- 浏览器缓存资源,等待下次重用。
看看具体的请求 - 应答过程。
服务器标记资源有效期使用的头字段是“Cache-Control
”,里面的值“max-age=30
”就是资源的有效时间,相当于告诉浏览器,“这个页面只能缓存 30 秒,之后就算是过期,不能用。”
“max-age
”是 HTTP 缓存控制最常用的属性,此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存:
no-store
:不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面;no-cache
:它的字面含义容易与 no-store 搞混,实际的意思并不是不允许缓存,而是可以缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;must-revalidate
:又是一个和 no-cache 相似的词,它的意思是如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。
客户端的缓存控制
其实不止服务器可以发“Cache-Contro
l”头,浏览器也可以发“Cache-Control”,也就是说请求 - 应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。
当你点“刷新”按钮的时候,浏览器会在请求头里加一个“Cache-Control: max-age=0
”。因为 max-age 是“生存时间
”,max-age=0 的意思就是没有缓存要重新获取,而本地缓存里的数据至少保存了几秒钟,所以浏览器就不会使用缓存,而是向服务器发请求。服务器看到 max-age=0
,也就会用一个最新生成的报文回应浏览器。
条件请求
浏览器用“Cache-Control”做缓存控制只能是刷新数据,不能很好地利用缓存数据,又因为缓存会失效,使用前还必须要去服务器验证是否是最新版。
浏览器可以用两个连续的请求组成“验证动作
”:先是一个 HEAD
,获取资源的修改时间等元信息,然后与缓存数据比较,如果没有改动就使用缓存,节省网络流量,否则就再发一个 GET
请求,获取最新的版本。
但这样的两个请求网络成本太高了,所以 HTTP 协议就定义了一系列“If
”开头的“条件请求
”字段,专门用来检查验证资源是否过期,把两个请求才能完成的工作合并在一个请求里做。而且,验证的责任也交给服务器,浏览器只需“坐享其成”。
条件请求一共有 5 个头字段,我们最常用的是“if-Modified-Since
”和“If-None-Match
”这两个。需要第一次的响应报文预先提供“Last-modified
”和“ETag
”,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。
如果资源没有变,服务器就回应一个“304 Not Modified
”,表示缓存依然有效,浏览器就可以更新一下有效期,然后放心大胆地使用缓存了。
“Last-modified”很好理解,就是文件的最后修改时间。
ETag 是“实体标签
”(Entity Tag)的缩写,是资源的一个唯一标识,主要是用来解决修改时间无法准确区分文件变化的问题。
比如,一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分。
再比如,一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽。
使用 ETag 就可以精确地识别资源的变动情况,让浏览器能够更有效地利用缓存。
ETag 还有“强
”“弱
”之分。
强 ETag 要求资源在字节级别必须完全相符,弱 ETag 在值前有个“W/”标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)。
HTTP的代理服务
HTTP 协议遵循了 HTTP 的“请求 - 应答”模型,协议中只有两个互相通信的角色,分别是“请求方
”浏览器(客户端)和“应答方
”服务器。在这个模型里引入一个新的角色,那就是HTTP 代理
。引入 HTTP 代理后,原来简单的双方通信就变复杂了一些,加入了一个或者多个中间人,但整体上来看,还是一个有顺序关系的链条,而且链条里相邻的两个角色仍然是简单的一对一通信,不会出现越级的情况。
链条的起点还是客户端
(也就是浏览器),中间的角色被称为代理服务器
(proxy server),链条的终点被称为源服务器
(origin server),意思是数据的“源头”“起源”
所谓的“代理服务
”就是指服务本身不生产内容,而是处于中间位置转发上下游的请求和响应,具有双重身份:面向下游的用户时,表现为服务器,代表源服务器响应客户端的请求;而面向上游的源服务器时,又表现为客户端,代表客户端发送请求。
代理的作用
- 由于代理处在 HTTP 通信过程的中间位置,相应地就对上屏蔽了真实客户端,对下屏蔽了真实服务器,简单的说就是“
欺上瞒下
”。在这个中间层的“小天地”里就可以做很多的事情,为 HTTP 协议增加更多的灵活性,实现客户端和服务器的“双赢”。 负载均衡
。因为在面向客户端时屏蔽了源服务器,客户端看到的只是代理服务器,源服务器究竟有多少台、是哪些 IP 地址都不知道。于是代理服务器就可以掌握请求分发的“大权”,决定由后面的哪台服务器来响应请求。健康检查
:使用“心跳”等机制监控后端服务器,发现有故障就及时“踢出”集群,保证服务高可用;安全防护
:保护被代理的后端服务器,限制 IP 地址或流量,抵御网络攻击和过载;加密卸载
:对外网使用 SSL/TLS 加密通信认证,而在安全的内网不加密,消除加解密成本;数据过滤
:拦截上下行的数据,任意指定策略修改请求或者响应;内容缓存
:暂存、复用服务器响应
代理相关头字段
首先,代理服务器需要用字段“Via”标明代理的身份。
Via
是一个通用字段,请求头或响应头里都可以出现。每当报文经过一个代理节点,代理服务器就会把自身的信息追加到字段的末尾,就像是经手人盖了一个章。如果通信链路中有很多中间代理,就会在 Via 里形成一个链表,这样就可以知道报文究竟走过了多少个环节才到达了目的地。
例如下图中有两个代理:proxy1
和 proxy2
,客户端发送请求会经过这两个代理,依次添加就是“Via: proxy1, proxy2”,等到服务器返回响应报文的时候就要反过来走,头字段就是“Via: proxy2, proxy1”。
Via 字段只解决了客户端和源服务器判断是否存在代理的问题,还不能知道对方的真实信息。
但服务器的 IP 地址应该是保密的,关系到企业的内网安全,所以一般不会让客户端知道。不过反过来,通常服务器需要知道客户端的真实 IP 地址,方便做访问控制、用户画像、统计分析。
可惜的是 HTTP 标准里并没有为此定义头字段,但已经出现了很多“事实上的标准”,
最常用的两个头字段是“X-Forwarded-For
”和“X-Real-IP
”。“X-Forwarded-For”的字面意思是“为谁而转发
”,形式上和“Via”差不多,也是每经过一个代理节点就会在字段里追加一个信息。但“Via”追加的是代理主机名(或者域名),而“X-Forwarded-For”追加的是请求方的 IP 地址。所以,在字段里最左边的 IP 地址就客户端的地址。
“X-Real-IP”是另一种获取客户端真实 IP 的手段,它的作用很简单,就是记录客户端 IP 地址,没有中间的代理信息,相当于是“X-Forwarded-For”的简化版。如果客户端和源服务器之间只有一个代理,那么这两个字段的值就是相同的。
缓存代理
缓存代理
,也就是支持缓存控制的代理服务。
HTTP 的服务器缓存功能主要由代理服务器来实现(即缓存代理
),而源服务器系统内部虽然也经常有各种缓存(如 Memcache、Redis、Varnish 等),但与 HTTP 没有太多关系,所以这里暂且不说
你可以看到:在没有缓存的时候,代理服务器每次都是直接转发客户端和服务器的报文,中间不会存储任何数据,只有最简单的中转功能。
代理服务收到源服务器发来的响应数据后需要做两件事。
- 第一个当然是把报文转发给客户端,
- 而第二个就是把报文存入自己的 Cache 里。
下一次再有相同的请求,代理服务器就可以直接发送 304 或者缓存数据,不必再从源服务器那里获取。这样就降低了客户端的等待时间,同时节约了源服务器的网络带宽。
在 HTTP 的缓存体系中,缓存代理的身份十分特殊,它“既是客户端,又是服务器
”,同时也“既不是客户端,又不是服务器
”。说它“即是客户端又是服务器
”,是因为它面向源服务器时是客户端,在面向客户端时又是服务器,所以它即可以用客户端的缓存控制策略也可以用服务器端的缓存控制策略,也就是说它可以“Cache-Control
”属性。但缓存代理也“即不是客户端又不是服务器”,因为它只是一个数据的“中转站”,并不是真正的数据消费者和生产者,所以还需要有一些新的“Cache-Control”属性来对它做特别的约束。
问题
- 第一个是“
Vary
”字段,它是内容协商的结果,相当于报文的一个版本标记。- 同一个请求,经过内容协商后可能会有不同的字符集、编码、浏览器等版本。比如,“Vary: Accept-Encoding”“Vary: User-Agent”,缓存代理必须要存储这些不同的版本。
- 当再收到相同的请求时,代理就读取缓存里的“
Vary
”,对比请求头里相应的“ Accept-Encoding”“User-Agent”等字段,如果和上一个请求的完全匹配,比如都是“gzip”“Chrome”,就表示版本一致,可以返回缓存的数据。
- 另一个问题是“
Purge
”,也就是“缓存清理
”,它对于代理也是非常重要的功能,例如:- 过期的数据应该及时淘汰,避免占用空间;
- 源站的资源有更新,需要删除旧版本,主动换成最新版(即刷新);
- 有时候会缓存了一些本不该存储的信息,例如网络谣言或者危险链接,必须尽快把它们删除。
理缓存的方法有很多,比较常用的一种做法是使用自定义请求方法“PURGE”,发给代理服务器,要求删除 URI 对应的缓存数据。
总结
- 1. HTTP是一个在计算世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范
- 2. HTTP 属于应用层的协议,需要其他层次的协议配合完成信息交换
- TCP/IP 是网络世界最常用的协议,HTTP 通常运行在 TCP/IP 提供的可靠传输基础上;
- DNS 域名是 IP 地址的等价替代,需要用域名解析实现到 IP 地址的映射;
- URI 是用来标记互联网上资源的一个名字,由“协议名 + 主机名 + 路径”构成,俗称 URL;
- HTTPS 相当于“HTTP+SSL/TLS+TCP/IP”,为 HTTP 套了一个安全的外壳;
- 3. 凡是由操作系统负责处理的就是四层或四层以下,否则,凡是需要由应用程序(也就是你自己写代码)负责处理的就是七层。
- 4. 由于HTTP协议是建立在TCP协议之上,所以在通过HTTP发送报文的时候,首先要进行TCP连接,采用三次握手来建立连接
第一次握手
:建立连接时,客户端发送syn
包(syn=j)到服务器,并进入SYN_SEND
状态,等待服务器确认;第二次握手
:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK
包,此时服务器进入SYN_RECV
状态;第三次握手
:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED
状态,完成三次握手。 完成三次握手,客户端与服务器开始传送数据.
- 5. 四次挥手
第一次挥手
: 客户端A发送一个FIN.用来关闭客户A到服务器B的数据传送第二次挥手
: 服务器B收到这个FIN. 它发回一个ACK,确认序号为收到的序号+1。和SYN一样,一个FIN将占用一个序号第三次挥手
: 服务器B关闭与客户端A的连接,发送一个FIN给客户端A第四次挥手
: 客户端A发回ACK报文确认,并将确认序号设置为序号加1
- 6. HTTP具有 客户-服务器模式、简单快速、灵活、无连接、无状态的特征
- 7. 在客户端与服务端确立好TCP连接后,HTTP发送报文与服务端进行交互:报文由起始行、请求头部、实体组成
- 8. HTTP传输大文件的方法
- 压缩
- 分块传输
- 范围请求
- 都断数据
- 9. HTTP 通过Location 字段来重定向
- 301 是“永久重定向”
- 302 是“临时重定向”
参考
《透析HTTP协议》