HTTP 一、基础知识
一、概述
1、概述
- HTTP(Hyper Text Transfer Protocol): 全称超文本传输协议,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。
- HTTP 是一种应用层协议,是基于 TCP/IP 通信协议来传递数据的,其中 HTTP1.0、HTTP1.1、HTTP2.0 均为 TCP 实现,HTTP3.0 基于 UDP 实现。现主流使用 HTTP1.0 和 HTTP3.0
- 协议: 为了使数据在网络上从源头到达目的,网络通信的参与方必须遵循相同的规则,这套规则称为协议,它最终体现为在网络上传输的数据包的格式。
通俗点讲,协议就是要保证网络通信的双方,能够互相对接上号。就像是两个人传递纸条通过互相指定的暗号,如果发送天亮了,表示可以打游戏了等等
2、历史
(1)HTTP诞生:
1989 年,任职于欧洲核子研究中心(CERN)的蒂姆·伯纳斯 - 李(Tim Berners-Lee)发表了一篇论文,提出了在互联网上构建超链接文档系统的构想。这篇论文中他确立了三项关键技术。
- URI:即统一资源标识符,作为互联网上资源的唯一身份;
- HTML:即超文本标记语言,描述超文本文档;
- HTTP:即超文本传输协议,用来传输超文本。
(2)HTTP/0.9
最早版本是1991年发布的0.9版。该版本极其简单,只有一个命令GET。
TCP 连接(connection)建立后,客户端向服务器请求(request)网页,协议规定,服务器只能回应HTML格式的字符串,不能回应别的格式。服务器发送完毕,就关闭TCP连接。
(3)HTTP/1.0
1996年5月,HTTP/1.0 版本发布,内容大大增加。但 HTTP/1.0 并不是一个“标准”,只是记录已有实践和模式的一份参考文档,不具有实际的约束力,相当于一个“备忘录”。
增加了 HEAD、POST 等新方法;
增加了响应状态码,标记可能的错误原因;
引入了协议版本号概念;
引入了 HTTP Header(头部)的概念,让 HTTP 处理请求和响应更加灵活;
传输的数据不再仅限于文本。
(4)HTTP/1.1
1997年1月,HTTP/1.1 版本发布,只比 1.0 版本晚了半年。它进一步完善了 HTTP 协议,一直用到了20年后的今天,直到现在还是最流行的版本。HTTP/1.1 是对 HTTP/1.0 的小幅度修正,但它是一个“正式的标准”,而不是一份可有可无的“参考文档”。
增加了 PUT、DELETE 等新的方法;
增加了缓存管理和控制;
明确了连接管理,允许持久连接;
允许响应数据分块(chunked),利于传输大文件;
强制要求 Host 头,让互联网主机托管成为可能。
(5)SPDY 协议
2009年,谷歌公开了自行研发的 SPDY 协议,主要解决 HTTP/1.1 效率不高的问题。
这个协议在Chrome浏览器上证明可行以后,就被当作 HTTP/2 的基础,主要特性都在 HTTP/2 之中得到继承。
(6)HTTP/2
2015年,HTTP/2 发布。它不叫 HTTP/2.0,是因为标准委员会不打算再发布子版本了,下一个新版本将是 HTTP/3。HTTP/2 的制定充分考虑了现今互联网的现状:宽带、移动、不安全,在高度兼容HTTP/1.1 的同时在性能改善方面做了很大努力,主要的特点有:
二进制协议,不再是纯文本;
可发起多个请求,废弃了 1.1 里的管道;
使用专用算法压缩头部,减少数据传输量;
允许服务器主动向客户端推送数据;
增强了安全性,“事实上”要求加密通信。
虽然 HTTP/2 已经发布好几年,也衍生出了 gRPC 等新协议,但由于 HTTP/1.1 实在是太过经典和强势,目前它的普及率还比较低,大多数网站使用的仍然还是 20 年前的HTTP/1.1。
(7)HTTP/3
2022年6月6日,IETF (互联网工程任务小组) 正式发布了 HTTP/3 的 RFC。
在 HTTP/2 还处于草案之时,Google 又发明了一个新的协议,叫做 QUIC,而且还是相同的“套路”,继续在 Chrome 和自家服务器里试验着“玩”,依托它的庞大用户量和数据量,持续地推动 QUIC 协议成为互联网上的“既成事实”。
也就是 2018 年,互联网标准化组织 IETF 提议将“HTTP over QUIC”更名 为“HTTP/3”并获得批准,HTTP/3 正式进入了标准化制订阶段。
(8)总结
HTTP 协议始于三十年前蒂姆·伯纳斯 - 李的一篇论文;
HTTP/0.9 是个简单的文本协议,只能获取文本资源;
HTTP/1.0 确立了大部分现在使用的技术,但它不是正式标准;
HTTP/1.1 是目前互联网上使用最广泛的协议,功能也非常完善;
HTTP/2 基于 Google 的 SPDY 协议,注重性能改善,但还未普及;
HTTP/3 基于 Google 的 QUIC 协议,是将来的发展方向。
3、关联概念
(1)网络世界
互联网的正式名称是 Internet,里面存储着无穷无尽的信息资源,我们通常所说的“上 网”实际上访问的只是互联网的一个子集“万维网”(World Wide Web),它基于 HTTP 协议,传输 HTML 等超文本资源,能力也就被限制在 HTTP 协议之内。现在的互联网 90% 以上的部分都被万维网,也就是 HTTP 所覆盖。
(2)浏览器
浏览器的正式名字叫“Web Browser”,顾名思义,就是检索、查看互联网上网页资源的 应用程序,名字里的 Web,实际上指的就是“World Wide Web”,也就是万维网。
浏览器本质上是一个 HTTP 协议中的请求方,使用 HTTP 协议获取网络上的各种资源。
(3)Web 服务器
Web 服务器是一个很大也很重要的概念,它是 HTTP 协议里响应请求的主体,通常有软件和硬件两层含义,硬件就是提供web服务的硬件机器,软件就是提供 Web 服务的应用程序,常用的有Apache和Nginx。
(4)CDN
CDN,全称是“Content Delivery Network”,翻译过来就是“内容分发网络”。它应用 了 HTTP 协议里的缓存和代理技术,代替源站响应客户端的请求。它可以缓存源站的数据,大幅度缩短响应时间。
除了基本的网络加速外,还提供负载均衡、 安全防护、边缘计算、跨运营商网络等功能,能够成倍地“放大”源站服务器的服务能力。
(5)爬虫
爬虫实际上是一种可以自动访问Web资源的应用程序。绝大多数是由各大搜索引擎“放”出来的,抓取网页存入庞大的数据库,再建立关键字索 引,这样我们才能够在搜索引擎中快速地搜索到互联网角落里的页面。
爬虫也有不好的一面,它会过度消耗网络资源,占用服务器和带宽,影响网站对真实数据的 分析,甚至导致敏感信息泄漏。所以,又出现了“反爬虫”技术,通过各种手段来限制爬 虫。其中一项就是“君子协定”robots.txt,约定哪些该爬,哪些不该爬。
(6)DNS
在 TCP/IP 协议中使用 IP 地址来标识计算机,数字形式的地址对于计算机来说是方便了, 但对于人类来说却既难以记忆又难以输入。 于是“域名系统”(Domain Name System)出现了,用有意义的名字来作为 IP 地址的 等价替代。
域名用“.”分隔成多个单词,级别从左到右逐级升高,最右边的被称为“顶级域名”。
但想要使用 TCP/IP 协议来通信仍然要使用 IP 地址,所以需要把域名做一个转换,“映 射”到它的真实 IP,这就是所谓的“域名解析”。
(7)URI/URL
URI(Uniform Resource Identifier),中文名称是统一资源标识符,使用它就能够唯一地标记互联网上资源。
URI 另一个更常用的表现形式是 URL(Uniform Resource Locator), 统一资源定位符,也就是我们俗称的“网址”,它实际上是 URI 的一个子集,不过因为这两者几乎是相同的,差异不大,所以通常不会做严格的区分。
URN:Uniform Resource Name,统一资源名称,也是URI的一个子集。
URI 主要有三个基本的部分构成:协议名,即访问该资源应当使用的协议;主机名,即互联网上主机的标记,可以是域名或 IP 地址;路径,即资源在主机上的位置,使用“/”分隔多级目录。
(8)HTTPS
HTTPS全称是“HTTP over SSL/TLS”,也就是 运行在 SSL/TLS 协议上的 HTTP,它是一个负责加密通信的安全协议,建 立在 TCP/IP 之上,所以也是个可靠的传输协议,可以被用作 HTTP 的下层。
SSL 的全称是“Secure Socket Layer”,由网景公司发明,当发展到 3.0 时被标准化,改 名为 TLS,即“Transport Layer Security”,但由于历史的原因还是有很多人称之为 SSL/TLS,或者直接简称为 SSL。
SSL 使用了许多密码学最先进的研究成果,综合了对称加密、非对称加密、摘要算法、数字 签名、数字证书等技术,能够在不安全的环境中为通信的双方创建出一个秘密的、安全的传输通道。
(9)代理
代理(Proxy)是 HTTP 协议中请求方和应答方中间的一个环节,作为“中转站”,既可以 转发客户端的请求,也可以转发服务器的应答。
代理有很多的种类,常见的有:
匿名代理:完全“隐匿”了被代理的机器,外界看到的只是代理服务器;
透明代理:顾名思义,它在传输过程中是“透明开放”的,外界既知道代理,也知道客 户端;
正向代理:靠近客户端,代表客户端向服务器发送请求;
反向代理:靠近服务器端,代表服务器响应客户端的请求;
由于代理在传输过程中插入了一个“中间层”,所以可以在这个环节做很多有意思的事情, 比如:
负载均衡:把访问请求均匀分散到多台机器,实现访问集群化;
内容缓存:暂存上下行的数据,减轻后端的压力;
安全防护:隐匿 IP, 使用 WAF 等工具抵御网络攻击,保护被代理的机器;
数据处理:提供压缩、加密等额外的功能。
4、访问Web服务器
(1)IP地址访问Web服务器
简要叙述最简单的浏览器 HTTP 请求过程:
-
浏览器从地址栏的输入中获得服务器的 IP 地址和端口号;
-
浏览器用 TCP 的三次握手与服务器建立连接;
-
浏览器向服务器发送拼好的报文;
-
服务器收到报文后处理请求,同样拼好报文再发给浏览器;
-
浏览器解析报文,渲染输出页面。
(2)使用域名访问Web服务器
在浏览器地址栏里直接输入 IP 地址可以访问服务器,但绝大多数情况下,我们是不知道服务器IP 地址的,使用的是域名。浏览器看到了网址里的域名,发起域名解析动作,把这个域名翻译成 TCP/IP 协议里的 IP 地址。
不过因为域名解析的全过程实在是太复杂了,如果每一个域名都要大费周折地去网上查一下,那我们上网肯定会慢得受不了。所以,在域名解析的过程中会有多级的缓存,浏览器首先看一下自己的缓存里有没有,如果没有就向操作系统的缓存要,还没有就检查本机域名解析文件 hosts,也就是上一讲中我们修改的“C:\WINDOWS\system32\drivers\etc\hosts”。
刚好,里面有一行映射关系“127.0.0.1 www.chrono.com”,于是浏览器就知道了域名对应的 IP 地址,就可以愉快地建立 TCP 连接发送 HTTP 请求了。
(3)真实的网络世界
如果你用的是电脑台式机,那么你可能会使用带水晶头的双绞线连上网口,由交换机接入固定网络。如果你用的是手机、平板电脑,那么你可能会通过蜂窝网络、WiFi,由电信基站、无线热点接入移动网络。接入网络的同时,网络运行商会给你的设备分配一个 IP 地址,这个地址可能是静态分配的,也可能是动态分配的。静态 IP 就始终不变,而动态 IP 可能你下次上网就变了。
假设你要访问的是 Apple 网站,显然你是不知道它的真实 IP 地址的,在浏览器里只能使用域名“www.apple.com”访问,那么接下来要做的必然是域名解析。这就要用 DNS 协议开始从操作系统、本地 DNS、根 DNS、顶级 DNS、权威 DNS 的层层解析,当然这中间有缓存,可能不会费太多时间就能拿到结果。
DNS 解析可能会给出 CDN 服务器的 IP 地址,这样你拿到的就会是 CDN 服务器而不是目标网站的实际地址。因为 CDN 会缓存网站的大部分资源,比如图片、CSS 样式表,所以有的 HTTP 请求就不需要再发到 Apple,CDN 就可以直接响应你的请求,把数据发给你。
由 PHP、Java 等后台服务动态生成的页面属于“动态资源”,CDN 无法缓存,只能从目标网站获取。于是你发出的 HTTP 请求就要开始在互联网上的“漫长跋涉”,经过无数的路由器、网关、代理,最后到达目的地。
目标网站的服务器对外表现的是一个 IP 地址,但为了能够扛住高并发,在内部也是一套复杂的架构。通常在入口是负载均衡设备,例如四层的 LVS 或者七层的 Nginx,在后面是许多的服务器,构成一个更强更稳定的集群。
负载均衡设备会先访问系统里的缓存服务器,通常有 memory 级缓存 Redis 和 disk 级缓存 Varnish,它们的作用与 CDN 类似,不过是工作在内部网络里,把最频繁访问的数据缓存几秒钟或几分钟,减轻后端应用服务器的压力。
如果缓存服务器里也没有,那么负载均衡设备就要把请求转发给应用服务器了。这里就是各种开发框架大显神通的地方了,例如 Java 的 Tomcat/Netty/Jetty,Python 的 Django,还有 PHP、Node.js、Golang 等等。它们又会再访问后面的 MySQL、PostgreSQL、MongoDB 等数据库服务,实现用户登录、商品查询、购物下单、扣款支付等业务操作,然后把执行的结果返回给负载均衡设备,同时也可能给缓存服务器里也放一份。
应用服务器的输出到了负载均衡设备这里,请求的处理就算是完成了,就要按照原路再走回去,还是要经过许多的路由器、网关、代理。如果这个资源允许缓存,那么经过 CDN 的时候它也会做缓存,这样下次同样的请求就不会到达源站了。
最后网站的响应数据回到了你的设备,它可能是 HTML、JSON、图片或者其他格式的数据,需要由浏览器解析处理才能显示出来,如果数据里面还有超链接,指向别的资源,那么就又要重走一遍整个流程,直到所有的资源都下载完。
二、基本概念
1、报文结构
HTTP 协议的请求报文和响应报文的结构基本相同,由三大部分组成:
- 起始行(start line):描述请求或响应的基本信息;
- 头部字段集合(header):使用 key-value 形式更详细地说明报文;
- 消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。
这其中前两部分起始行和头部字段经常又合称为“请求头”或“响应头”,消息正文又称为“实体”,但与“header”对应,很多时候就直接称为“body”。
HTTP 协议规定报文必须有 header,但可以没有 body,而且在 header 之后必须要有一个“空行”,也就是“CRLF”,十六进制的“0D0A”。
如下是一个http报文实例
在这个浏览器发出的请求报文里,第一行“GET /HTTP/1.1”就是请求行,而后面的“Host”“Connection”等等都属于 header,报文的最后是一个空白行结束,没有 body。很多时候,特别是浏览器发送 GET 请求的时候都是这样,HTTP 报文经常是只有 header 而没 body。
完整的Http请求:
完整的 HTTP 响应:
2、请求行
请求行,也就是请求报文里的起始行,它简要地描述了客户端想要如何操作服务器端的资源。请求行由三部分构成:
- 请求方法:是一个动词,如 GET/POST,表示对资源的操作;
- 请求目标:通常是一个 URI,标记了请求方法要操作的资源;
- 版本号:表示报文使用的 HTTP 协议版本。
这三个部分通常使用空格(space)来分隔,最后要用CRLF 换行表示结束。
在如下的请求行里,“GET”是请求方法,“/”是请求目标,“HTTP/1.1”是版本号。
GET / HTTP/1.1
3、状态行
状态行,也就是响应报文里的起始行,意思是服务器响应的状态。状态行也有三部分构成:
- 版本号:表示报文使用的 HTTP 协议版本;
- 状态码:一个三位数,用代码的形式表示处理的结果,比如 200 是成功,500 是服务器错误;
- 原因:作为数字状态码补充,是更详细的解释文字,帮助人理解原因。
在如下的状态行里,这个报文使用的协议版本号是 1.1,状态码是 200,一切OK。
HTTP/1.1 200 OK
4、头部字段
请求行或状态行再加上头部字段集合就构成了 HTTP 报文里完整的请求头或响应头,如下:
头部字段是 key-value 的形式,key 和 value 之间 用“:”分隔,最后用 CRLF 换行表示字段结束。HTTP 头字段非常灵活,不仅可以使用标准里的 Host、 Connection 等已有头,也可以任意添加自定义头,这就给 HTTP 协议带来了无限的扩展可能。不过使用头字段需要注意下面几点:
- 字段名不区分大小写,例如“Host”也可以写 成“host”,但首字母大写的可读性更好;
- 字段名里不允许出现空格,可以使用连字符“-”,但不 能使用下划线“_”。例如,“test-name”是合法的字 段名,而“test name”“test_name”是不正确的字段 名;
- 字段名后面必须紧接着“:”,不能有空格,而“:”后的 字段值前可以有多个空格;
- 字段的顺序是没有意义的,可以任意排列不影响语义;
- 字段原则上不能重复,除非这个字段本身的语义允许,例 如 Set-Cookie。
5、常用头字段
HTTP 协议规定了非常多的头部字段,实现各种各样的功能,但基本上可以分为四大类:
- 通用字段:在请求头和响应头里都可以出现;
- 请求字段:仅能出现在请求头里,进一步说明请求信息或者额外的附加条件;
- 响应字段:仅能出现在响应头里,补充说明响应报文的信 息;
- 实体字段:它实际上属于通用字段,但专门描述 body 的 额外信息。
(1)Host(请求字段)(必填字段)
请求字段,只能出现在请求头里,它同时也是唯一一个 HTTP/1.1 规范里要求必须出现的字段,也就是说,如果请求头里没有 Host,那这就是 一个错误的报文。
Host 字段告诉服务器这个请求应该由哪个主机来处理,当 一台计算机上托管了多个虚拟主机的时候,服务器端就需要 用 Host 字段来选择,有点像是一个简单的“路由重定 向”。
例如在 127.0.0.1 上有三个虚拟主机,那么当使用域名的方式访问时,就必须要用 Host 字段来区分这三个 IP 相同但域名不同的网站,否则服务器 就会找不到合适的虚拟主机,无法处理。
(2)User-Agent(请求字段)
请求字段,只出现在请求头里。它使用一个字符串来描述发起 HTTP 请求的客户端,服务器可以依据它来返回最合适此浏览器显示的页面。
由于历史的原因,User-Agent 非常混乱,每个浏览器都自称是“Mozilla”“Chrome”“Safari”,企图使用这个 字段来互相“伪装”,导致 User-Agent变得越来越长,最终变得毫无意义。
不过有的比较“诚实”的爬虫会在 User-Agent 里用“spider”标明自己是爬虫,所以可以利用这个字段实现简单的反爬虫策略。
(3)Date(通用字段)
通用字段,但通常出现在响应头里,表示 HTTP 报文创建的时间,客户端可以使用这个时间再搭配其 他字段决定缓存策略。
(4)Server(响应字段)
响应字段,只能出现在响应头里。它告诉客户 端当前正在提供 Web 服务的软件名称和版本号,例如“Server: openresty/1.15.8.1”, 即使用的是 OpenResty 1.15.8.1。
Server 字段也不是必须要出现的,因为这会暴露服务器信息,如果这个版本恰好存在bug,那么黑客就有可能利用bug攻陷服务器。所以,有的网站响应头里要么没有这个字段,要么就给出一个完全无关的描述信息。比如 GitHub,它的 Server 字段里只是显示为“GitHub.com”。
(5)Content-Length(实体字段)
它表示报文里body 的长度,也就是请求头或响应头空行后面数据的长度。服务器看到这个字段,就知道了后续有多少数据,可以直接接收。如果没有这个字段,那么 body 就是不定长的,需要使用 chunked 方式分段传输。
6、请求方法
(1)标准请求方法
URI只能定位资源,但是怎么操作资源,需要有某种动作指令,所以,就这么出现了“请求方法”。它的实际含义就 是客户端发出了一个“动作指令”,要求服务器端对 URI 定 位的资源执行这个动作。
目前 HTTP/1.1 规定了八种方法,单词都必须是大写的形 式,我先简单地列把它们列出来,后面再详细讲解。
- GET:获取资源,可以理解为读取或者下载数据;
- HEAD:获取资源的元信息;
- POST:向资源提交数据,相当于写入或上传数据;
- PUT:类似 POST;
- DELETE:删除资源;
- CONNECT:建立特殊的连接隧道;
- OPTIONS:列出可对资源实行的方法;
- TRACE:追踪请求 - 响应的传输路径。
虽然客户端发出了这些明确的操作指令,但是执行的最终还是服务器,服务器不一定按照客户端的要求进行操作资源,比如,DELETE不一定删除资源,也可以提交数据。
(2)常用–Get(从服务器获取资源)
它的含义是请求从服务器获取资源,这个资源既可以是静态 的文本、页面、图片、视频,也可以是由 PHP、Java 动态 生成的页面或者其他格式的数据。
GET 方法虽然基本动作比较简单,但搭配 URI 和其他头字 段就能实现对资源更精细的操作。例如,在 URI 后使用“#”,就可以在获取页面后直接定位 到某个标签所在的位置;使用 If-Modified-Since 字段就变 成了“有条件的请求”,仅当资源被修改时才会执行获取动作;使用 Range 字段就是“范围请求”,只获取资源的一 部分数据。
(3)常用–HEAD(从服务器获取资源)
HEAD方法与 GET 方法类似,也是请求从服务器获取资源,服务器的处理机制也是一样的,但服务器不会返回请求 的实体数据,只会传回响应头,也就是资源的“元信息”。
HEAD 方法可以看做是 GET 方法的一个“简化版”或 者“轻量版”。因为它的响应头与 GET 完全相同,所以可以用在很多并不真正需要资源的场合,避免传输body数据的浪费。
比如,想要检查一个文件是否存在,只要发个 HEAD 请求 就可以了,没有必要用 GET 把整个文件都取下来。再比 如,要检查文件是否有最新版本,同样也应该用 HEAD,服 务器会在响应头里把文件的修改时间传回来。
(4)常用–POST(以向服务器提交数据)
向 URI 指定的资源提交数据,数据就放在报文的 body 里。POST应用的场景也非常多,只要向服务器发送数据,用的大多数都是 POST。
比如,你上论坛灌水,敲了一堆字后点击“发帖”按钮,浏览器就执行了一次 POST 请求,把你的文字放进报文的 body 里,然后拼好 POST 请求头,通过 TCP 协议发给服 务器。
(5)常用–PUT(以向服务器提交数据)
PUT 的作用与 POST 类似,也可以向服务器提交数据,但 与 POST 存在微妙的不同,通常 POST 表示的是“新建”“create”的含义,而 PUT 则是“修 改”“update”的含义。
在实际应用中,PUT 用到的比较少。而且,因为它与 POST 的语义、功能太过近似,有的服务器甚至就直接禁止使用 PUT 方法,只用 POST 方法上传数据。
(6)非常用–DELETE(删除资源)
DELETE方法指示服务器删除资源,因为这个动作危险性太 大,所以通常服务器不会执行真正的删除操作,而是对资源 做一个删除标记。当然,更多的时候服务器就直接不处理 DELETE 请求。
(7)非常用–CONNECT(建立特殊的连接隧道)
CONNECT是一个比较特殊的方法,要求服务器为客户端和 另一台远程服务器建立一条特殊的连接隧道,这时 Web 服 务器在中间充当了代理的角色。
(8)非常用–OPTIONS(列出可对资源实行的方法)
OPTIONS方法要求服务器列出可对资源实行的操作方法, 在响应头的 Allow 字段里返回。它的功能很有限,用处也不 大,有的服务器(例如 Nginx)干脆就没有实现对它的支持。
(9)非常用–TRACE(追踪请求 - 响应的传输路径)
TRACE方法多用于对 HTTP 链路的测试或诊断,可以显示 出请求 - 响应的传输路径。它的本意是好的,但存在漏洞, 会泄漏网站的信息,所以 Web 服务器通常也是禁止使用。
(10)扩展方法
虽然 HTTP/1.1 里规定了八种请求方法,但它并没有限制我 们只能用这八种方法,这也体现了 HTTP 协议良好的扩展 性,我们可以任意添加请求动作,只要请求方和响应方都能 理解就行。
有一些得到了实际应用的请求方法 (WebDAV),例如 MKCOL、COPY、MOVE、LOCK、 UNLOCK、PATCH 等。如果有合适的场景,你也可以把它 们应用到自己的系统里,比如用 LOCK 方法锁定资源暂时不 允许修改,或者使用 PATCH 方法给资源打个小补丁,部分更新数据。但因为这些方法是非标准的,所以需要为客户端 和服务器编写额外的代码才能添加支持。你也完全可以根据实际需求,自己发明新的方法。
(11)安全与幂等
所谓的“安全”是指请求方法不会“破 坏”服务器上的资源,即不会对服务器上的资源造成实质的修改。按照这个定义,只有 GET 和 HEAD 方法是“安全”的, POST/PUT/DELETE 操作会修改服务器上的资源,增加 或删除数据,所以是“不安全”的。
所谓的“幂等”实际上是一个数学用语,意思是多次执行相同的操作,结果也都是相同的, 即多次“幂”后结果“相等”。GET 和 HEAD 既是安全的也是幂等的,DELETE 可以多次删除同一个资源,效果都是“资源不存在”,所以 也是幂等的。POST 是“新增或提交数据”,多次 提交数据会创建多个资源,所以不是幂等的;而 PUT 是“替换或更新数据”,多次更新一个资源,资源还是会第 一次更新的状态,所以是幂等的。
7、URI
(1)URI的格式
URI 本质上是一个字符串,这个字符串的作用是唯一地标记资源的位置或者名字。
下面的这张图显示了 URI 最常用的形式,由 scheme、 host:port、path 和 query 四个部分组成,但有的部分可以 视情况省略。
(2)URI的基本组成
- scheme:翻译成中文叫“方案名”或者“协议名”,表示资源应该使用哪种协议来访问。最常见是“http”,另外还有“https”,表示经过加密、安全的 HTTPS协议。 此外还有其他的,例如 ftp、ldap、 file、news 等。如果一个 URI 没有提供 scheme,是无法处理的。
- “ :// ”:在 scheme 之后,必须是三个特定的字符“ :// ”,它把 scheme 和后面的部分分离开。
- authority:表示资源 所在的主机名,通常的形式是“host:port”,即主机名加端口号。主机名可以是 IP 地址或者域名的形式,必须要有,否则浏览器就会找不到服务器。但端口号有时可以省略,浏览器等客户端会依据 scheme 使用默认的端口号,例如 HTTP 的 默认端口号是 80,HTTPS 的默认端口号是 443。
- path:标记资源所在位置,有了协议名和主机地址、端口号再加上path,浏览器就可以连接服务器访问资源了。URI 里 path 采用了类似文件系统“目录”“路径”的表示 方式,因为早期互联网上的计算机多是 UNIX 系统,所以采用了 UNIX 的“/”风格。
- query:表示对资源附加的额外要求,它在 path 之后,用一个“?”开始,但不包含“?”。查询参数 query 有一套自己的格式,是多 个“key=value”的字符串,这些 KV 值用字符“&”连接,浏览器和客户端都可以按照这个格式把长串的查询参数 解析成可理解的字典或关联数组形式。
如下是几个实例:
http://nginx.org
http://www.chrono.com:8080/11-1
https://tools.ietf.org/html/rfc7230
file:///D:/http_study/www/
最后一个 URI 要注意了,它的协议名不是“http”,而是“file”,表示这是本地文件,而后面居然有三个斜杠,三个斜杠里的前两个属于 URI 特殊分隔符“😕/”,然后后面的“/D:/http_study/www/”是路径,而中间的主机名被“省略”了。这实际上是 file 类型 URI 的“特例”,它允许省略主机名,默认是本机 localhost。
(3)URI 的完整格式
URI 还有一个“真正”的完整形态,如下图所示,这个“真正”形态比基本形态多了两部分。
user:passwd@:表示登录主机时的用户名和密码, 但现在已经不推荐使用这种形式了(RFC7230),因为它把 敏感信息以明文形式暴露出来,存在严重的安全隐患。
#fragment:它是 URI 所定位的资源内部的一 个“锚点”或者说是“标签”,浏览器可以在获取资源后直接跳转到它指示的位置。
(4)URI 的编码
在 URI 里只能使用 ASCII 码,为了在URI中表示ASCII 码以外的字符集和“@&/”等特殊字符,URI 引入了编码机制,把它们转换成与 URI 语义不冲 突的形式。这在 RFC 规范里称 为“escape”和“unescape”,俗称“转义”。
URI 转义的规则有点“简单粗暴”,直接把非 ASCII 码或特殊字符转换成十六进制字节值,然后前面再加上一 个“%”。
例如,空格被转义成“%20”,“?”被转义成“%3F”。 而中文、日文等则通常使用 UTF-8 编码后再转义,例如“银河”会被转义成“%E9%93%B6%E6%B2%B3”。 有了这个编码规则后,URI 就更加完美了,可以支持任意的 字符集用任何语言来标记资源。
8、响应状态码
(1)状态码的位置
服务器收到请求报文,解析后需要进行处理,具体的业务逻辑多种多样,但最后必定是拼出一个响应报文发回客户端。响应报文由响应头加响应体数据组成,响应头又由状态行和头字段构成。状态行的结构如下:
- Version:是 HTTP 协议的版本号,通常是HTTP/1.1,用处不是很大。
- Reason:是原因短语,是状态码的简短文字描述,例如“OK”“Not Found”等等,也可以自定义。但它只是为了兼容早期的文本客户端而存在,提供的信息很有限,目前的大多数客户端都会忽略它。
- Status Code:状态码,它是一个十进制数字,以代码的形式表示服务器对请求的处理结果,就像我们通常编写程序时函数返回的错误码一样。意义在于表达HTTP 数据处理的“状态”,客户端可以依据代码适时转换处理状态,例如继续发送请求、切换协议,重定向跳转等。
(2)状态码
目前 RFC 标准里规定的状态码是三位数,所以取值范围就是从 000 到 999。RFC 标准把状态码分成了五类,用数字的第一位表示分类,而 0~99 不用,这样状态码的实际可用范围就大大缩小了,由000~999 变成了 100~599。这五类的具体含义是:
- 1××:提示信息,表示目前是协议处理的中间状态,还需要后续的操作;偶尔能够见到的是101;
- 2××:成功,报文已经收到并被正确处理,常用的有 200、204、206;
- 3××:重定向,资源位置发生变动,需要客户端重新发送请求,常用的有 301、302、304;
- 4××:客户端错误,请求报文有误,服务器无法处理,常用的有 400、403、 404;
- 5××:服务器错误,服务器在处理请求时内部发生了错误,常用的有 500、501、 502、503;
目前 RFC 标准里总共有 41 个状态码,但状态码的定义是开放的,允许自行扩展。所以 Apache、Nginx 等 Web 服务器都定义了一些专有的状态码。如果你自己开发 Web 应用,也完全可以在不冲突的前提下定义新的代码。
状态码 | 说明 |
100 Continue | 服务器仅接收到部分请求,但是服务器并没有拒绝该请求,客户端应该继续发送其余的请求。 |
101 Switching Protocols | 它的意思是客户端使用 Upgrade 头字段,要求在 HTTP 协议的基础上改成其他的协议继续通信,比如 WebSocket。而如果服务器也同意变更协议,就会发送状态码 101,但这之后的数据传输就不会再使用 HTTP 了。 |
103 Checkpoint | 用于 PUT 或者 POST 请求恢复失败时的恢复请求建议。 |
200 OK | 是最常见的成功状态码,表示一切正常,服务器如客户端所期望的那样返回了处理结果,如果是非 HEAD请求,通常在响应头后都会有 body 数据。 |
201 Created | 请求被创建完成,同时新的资源被创建。 |
202 Accepted | 供处理的请求已被接受,但是处理未完成。 |
203 Non-Authoritative Information | 请求已经被成功处理,但是一些应答头可能不正确,因为使用的是其他文档的拷贝。 |
204 No Content | 请求已经被成功处理,但是没有返回新文档。浏览器应该继续显示原来的文档。如果用户定期地刷新页面,而Servlet可以确定用户文档足够新,这个状态代码是很有用的。 |
205 Reset Content | 请求已经被成功处理,但是没有返回新文档。但浏览器应该重置它所显示的内容。用来强制浏览器清除表单输入内容。 |
206 Partial Content | 是 HTTP 分块下载或断点续传的基础,在客户端发送“范围请求”、要求获取资源的部分数据时出现,它与 200 一样,也是服务器成功处理了请求,但 body 里的数据不是资源的全部,而是其中的一部分。状态码 206 通常还会伴随着头字段“Content-Range”,表示响应报文里 body 数据的具体范围,供客户端确认,例如“Content-Range: bytes 0-99/2000”,意思是此次获取的是总计 2000 个字节的前 100 个字节。 |
300 Multiple Choices | 多重选择。链接列表。用户可以选择某链接到达目的地。最多允许五个地址。 |
301 Moved Permanently | 俗称“永久重定向”,含义是此次请求的资源已经不存在了,需要改用改用新的 URI再次访问。 |
302 Found | 俗称“临时重定向”,意思是请求的资源还在,但需要暂时用另一个 URI 来访问。301 和 302 都会在响应头里使用字段Location指明后续要跳转的 URI,最终的效果很相似,浏览器都会重定向到新的URI。两者的根本区别在于语义,一个是“永久”,一个是“临时”,所以在场景、用法上差距很大。比如,你的网站升级到了 HTTPS,原来的 HTTP 不打算用了,这就是“永久”的,所以要配置 301 跳转,把所有的HTTP 流量都切换到 HTTPS。再比如,今天夜里网站后台要系统维护,服务暂时不可用,这就属于“临时”的,可以配置成 302 跳转,把流量临时切换到一个静态通知页面,浏览器看到这个 302 就知道这只是暂时的情况,不会做缓存优化,第二天还会访问原来的地址。 |
303 See Other | 所请求的页面可在别的 URL 下被找到。 |
304 Not Modified | 未按预期修改文档。客户端有缓冲的文档并发出了一个条件性的请求(一般是提供If-Modified-Since头表示客户只想比指定日期更新的文档)。服务器告诉客户,原来缓冲的文档还可以继续使用。 |
305 Use Proxy | 客户请求的文档应该通过Location头所指明的代理服务器提取。 |
306 Switch Proxy | 目前已不再使用,但是代码依然被保留。 |
307 Temporary Redirect | 被请求的页面已经临时移至新的 URL 。 |
308 Resume Incomplete | 用于 PUT 或者 POST 请求恢复失败时的恢复请求建议。 |
400 Bad Request | 是一个通用的错误码,表示请求报文有错误,但具体是数据格式错误、缺少请求头还是 URI 超长它没有明确说,只是一个笼统的错误,客户端看到 400只会是“一头雾水”“不知所措”。所以,在开发 Web 应用时应当尽量避免给客户端返回 400,而是要用其他更有明确含义的状态码。 |
401 Unauthorized | 合法请求,但对被请求页面的访问被禁止。因为被请求的页面需要身份验证,客户端没有提供或者身份验证失败。 |
402 Payment Required | 此代码尚无法使用。 |
403 Forbidden | 实际上不是客户端的请求出错,而是表示服务器禁止访问资源。原因可能多种多样,例如信息敏感、法律禁止等,如果服务器友好一点,可以在 body 里详细说明拒绝请求的原因,不过现实中通常都是直接给一个“闭门羹”。 |
404 Not Found | 可能是我们最常看见也是最不愿意看到的一个状态码,它的原意是资源在本服务器上未找到,所以无法提供给客户端。但现在已经被“用滥了”,只要服务器“不高兴”就可以给出个 404,而我们也无从得知后面到底是真的未找到,还是有什么别的原因,某种程度上它比403 还要令人讨厌。 |
405 Method Not Allowed | 不允许使用某些方法操作资源,例如不允许 POST 只能 GET; |
406 Not Acceptable | 资源无法满足客户端请求的条件,例如请求中文但只有英文; |
407 Proxy Authentication Required | 用户必须首先使用代理服务器进行验证,这样请求才会被处理。 |
408 Request Timeout | 请求超出了服务器的等待时间。 |
409 Conflict | 多个请求发生了冲突,可以理解为多线程并发时的竞态; |
410 Gone | 被请求的页面不可用。 |
411 Length Required | “Content-Length” 未被定义。如果无此内容,服务器不会接受请求。 |
412 Precondition Failed | 请求中的前提条件被服务器评估为失败。 |
413 Request Entity Too Large | 由于所请求的实体太大,服务器不会接受请求。 |
414 Request-URI Too Long | 由于 URL 太长,服务器不会接受请求。当 POST 请求被转换为带有很长的查询信息的 GET 请求时,就会发生这种情况。 |
415 Unsupported Media Type | 由于媒介类型不被支持,服务器不会接受请求。 |
416 Requested Range Not Satisfiable | 客户端请求部分文档,但是服务器不能提供被请求的部分。 |
417 Expectation Failed | 服务器不能满足客户在请求中指定的请求头。 |
429 Too Many Requests | 客户端发送了太多的请求,通常是由于服务器的限连策略; |
431 Request Header Fields Too Large | 请求头某个字段或总体太大; |
500 Internal Server Error | 与 400 类似,也是一个通用的错误码,服务器究竟发生了什么错误我们是不知道的。不过对于服务器来说这应该算是好事,通常不应该把服务器内部的详细信息,例如出错的函数调用栈告诉外界。虽然不利于调试,但能够防止黑客的窥探或者分析。 |
501 Not Implemented | 表示客户端请求的功能还不支持,这个错误码比 500 要“温和”一些,和“即将开业,敬请期待”的意思差不多,不过具体什么时候“开业”就不好说了。 |
502 Bad Gateway | 通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的。 |
503 Service Unavailable | 表示服务器当前很忙,暂时无法响应服务,我们上网时有时候遇到的“网络服务正忙,请稍后重试”的提示信息就是状态码 503。503 是一个“临时”的状态,很可能过几秒钟后服务器就不那么忙了,可以继续提供服务,所以 503 响应报文里通常还会有一个“Retry-After”字段,指示客户端可以在多久以后再次尝试发送请求。 |
504 Gateway Timeout | 网关超时。服务器充当网关或者代理的角色时,未能从上游服务器收到一个及时的响应。 |
505 HTTP Version Not Supported | 服务器不支持请求中指明的HTTP协议版本。 |
511 Network Authentication Required | 用户需要提供身份验证来获取网络访问入口。 |
9、特点总结
- HTTP 是灵活可扩展的,可以任意添加头字段实现任意功能;
- HTTP 是可靠传输协议,基于 TCP/IP 协议“尽量”保证数据的送达;
- HTTP 是应用层协议,比 FTP、SSH 等更通用功能更多,能够传输任意数据;
- HTTP 使用了请求 - 应答模式,客户端主动发起请求,服务器被动回复请求;
- HTTP 本质上是无状态的,每个请求都是互相独立、毫无关联的,协议不要求客户端或 服务器记录请求相关的信息。
三、进阶
1、实体数据
(1)数据类型MIME type
HTTP是应用层的协议,数据到达客户端之后,必须要告诉上层应用是什么类型数据才能处理。
早在 HTTP 协议诞生之前就已经有了针对这种问题的解决方案,不过它是用在电子邮件系统里的,让电子邮件可以发送 ASCII 码以外的任意数据,方案的名字叫做“多用途互联网邮件扩展”(Multipurpose Internet Mail Extensions),简称为 MIME。
MIME 是一个很大的标准规范,但 HTTP 只取了其中一部分,用来标记 body 的数据类型,这就是我们平常总能听到的“MIME type”。
MIME 把数据分成了八大类,每个大类下再细分出多个子 类,形式是“type/subtype”的字符串。这里简单列举一下在 HTTP 里经常遇到的几个类别:
- text:即文本格式的可读数据,我们最熟悉的应该就是 text/html 了,表示超文本文档,此外还有纯文本 text/plain、样式表 text/css 等。
- image:即图像文件,有 image/gif、image/jpeg、 image/png 等。
- audio/video:音频和视频数据,例如 audio/mpeg、 video/mp4 等。
- application:数据格式不固定,可能是文本也可能是二进制,必须由上层应用程序来解释。常见的有 application/json,application/javascript、 application/pdf 等,另外,如果实在是不知道数据是什么类型,像刚才说的“黑盒”,就会是 application/octet-stream,即不透明的二进制数据。
(2)编码Encoding type
仅有 MIME type 还不够,因为 HTTP 在传输时为了节约带宽,有时候还会压缩数据,还需要有一个“Encoding type”,告诉数据是用的什么编码格式,这样对方才能正确解压缩,还原出原始的数据。常用的Encoding type只有下面三种:
- gzip:GNU zip 压缩格式,也是互联网上最流行的压缩 格式;
- deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip;
- br:一种专门为 HTTP 优化的新压缩算法(Brotli)。
(3)数据类型的头字段Accept&Content
HTTP 协议定义了两个 Accept 请求头字段和两个 Content 实体头字段,用于客户端和服务器进行“内容协商”。客户端用 Accept 头告诉服务器希望接收什么样的数据,而服务器用 Content 头告诉客户端实际发送了什么样的数据。
Accept:标记的是客户端可理解的 MIME type,可以用“,”做分隔符列出多个类型,让服务器有更多的选择余地,例如下面的这个头。
Accept: text/html,application/xml,image/webp,image/png
Content-Type:告诉实体数据的真实类型,浏览器看到类型是“text/html”就知道是 HTML 文件,会调用排版引擎渲染出页面,看到“image/png”就知道是一个 PNG 文件,就会在页面上显示出图像。
Content-Type: text/html
Content-Type: image/png
(4)编码的头字段Accept-Encoding&Content-Encoding
Accept-Encoding:是客户端支持的压缩格式, 例如上面说的 gzip、deflate 等,同样也可以用“,”列出多 个。
Content-Encoding:服务器可以选择其中一种来压缩数据,实际使用的压缩格式放在响应头字段Content-Encoding里。
Accept-Encoding: gzip, deflate, br
Content-Encoding: gzip
这两个字段是可以省略的,如果请求报文里没有 Accept-Encoding 字段,就表示客户端不支持压缩数据; 如果响应报文里没有 Content-Encoding 字段,就表示响应数据没有被压缩。
(5)语言类型
互联网遍布全球,不同国家不同地区的人使用了很多不同的语言,虽然都是 text/html,如何让浏览器显示出每个人都可理解可阅读的语言文字,就是国际化问题,HTTP 又引入了两个概念:语言类型与字符集。
语言类型就是人类使用的自然语言,例如英语、 汉语、日语等,而这些自然语言可能还有下属的地区性方言,所以在需要明确区分的时候也要使用“type-subtype”的形式,不过这里的格式与数据类型不同,分隔符不是“/”,而是“-”。
举几个例子:en 表示任意的英语,en-US 表示美式英语, en-GB 表示英式英语,而 zh-CN 就表示我们最常使用的汉语。
(6)字符集
在计算机发展的早期,各个国家和地区的人们“各自为政”,发明了许多字符编码方式来处理文字,比如英语世界用的 ASCII、汉语世界用的 GBK、BIG5,日语世界用的Shift_JIS 等。同样的一段文字,用一种编码显示正常,换另一种编码后可能就会变得一团糟。
所以后来就出现了 Unicode 和 UTF-8,把世界上所有的语言都容纳在一种编码方案里,UTF-8 也成为了互联网上的标准字符集。
(7)语言类型的头字段Accept-Language&Content-Language
Accept-Language:标记了客户端可理解的自然语言,也允许用“,”做分隔符列出多个类型,如下,这个请求头会告诉服务器:“最好给我 zh-CN 的汉语文字,如果没有就用其他的汉语方言,如果还没有就给英文”。
Accept-Language: zh-CN, zh, en
Content-Language:服务器在响应报文里用头字段Content-Language告诉客户端实体数据使用的实际语言类型:
Content-Language: zh-CN
(8)字符集的头字段Accept-Charset&Content-Type
Accept-Charset:标记了字符集在HTTP里使用的请求头字段。
Content-Type:响应头里却没有对应的 Content-Charset,而是在Content-Type字段用“charset=xxx”来表示,这点需要特别注意
Accept-Charset: gbk, utf-8 Content-Type: text/html; charset=utf-8
不过现在的浏览器都支持多种字符集,通常不会发送Accept-Charset,而服务器也不会发送 Content-Language,因为使用的语言完全可以由字符集推断出来,所以在请求头里一般只会有 Accept-Language 字段,响应头里只会有 Content-Type 字段。
(9)内容协商的质量值q
在 HTTP 协议里用 Accept、Accept-Encoding、Accept-Language 等请求头字段进行内容协商的时候,还可以用一种特殊的“q”参数表示权重来设定优先级,这里的“q”是“quality factor”的意思。
权重的最大值是 1,最小值是 0.01,默认值是 1,如果值是0 就表示拒绝。具体的形式是在数据类型或语言代码后面加一个“;”,然后是“q=value”。
这里要提醒的是“;”的用法,在大多数编程语言里“;”的断句语气要强于“,”,而在 HTTP 的内容协商里却恰好反了过来,“;”的意义是小于“,”的。
例如下面的 Accept 字段,它表示浏览器最希望使用的是 HTML 文件,权重是 1,其次是 XML 文件,权重是 0.9,最后是任意数据类型,权重是 0.8。服务器收到请求头后,就会计算权重,再根据自己的实际情况优先输出 HTML 或者 XML。
Accept: text/html,application/xml;q=0.9,*/*;q=0.8
(10)内容协商的结果Vary
Vary:内容协商的过程是不透明的,每个 Web 服务器使用的算法都不一样。但有的时候,服务器会在响应头里多加一个Vary字段,记录服务器在内容协商时参考的请求头字段。
如下,这个 Vary 字段表示服务器依据了 Accept-Encoding、User-Agent 和 Accept 这三个头字段,然后决定了发回的响应报文。
Vary: Accept-Encoding,User-Agent,Accept
Vary 字段可以认为是响应报文的一个特殊的“版本标记”。每当 Accept 等请求头变化时,Vary 也会随着响应报文一起变化。也就是说,同一个 URI 可能会有多个不同的“版本”,主要用在传输链路中间的代理服务器实现缓存服务。
2、传输大文件
早期互联网上传输的基本上都是只有几 K 大小的文本和小图片,现在的情况则大有不同。网页里包含的信息实在是太多了,随随便便一个主页 HTML 就有可能上百 K,高质量的图片都以 M 论,更不要说那些电影、电视剧了,几 G、几十 G 都有可能。http协议设计了很多有效手段用来实现在有限的带宽下高效快捷地传输这些大文件。
(1)数据压缩
Accept-Encoding:通常浏览器在发送请求时都会带着“Accept-Encoding”头字段,里面是浏览器支持的压缩格式列表,例如 gzip、deflate、br 等。
Content-Encoding:服务器就可以从中选择一种压缩算法,放进“Content-Encoding”响应头里,再把原数据压缩后发给浏览器。
缺点:gzip 等压缩算法通常只对文本文件有较好的压缩率,而图片、音频视频等多媒体数据本身就已经是高度压缩的,再用 gzip 处理也不会变小,所以它就失效了。
(2)分块传输
化整为零:如果大文件整体不能变小,那就把它“拆开”,分解成多个小块,把这些小块分批发给浏览器,浏览器收到后再组装复原。这样可以节省内存和带宽等资源。
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”。
不过浏览器在收到分块传输的数据后会自动按照规则去掉分块编码,重新组装出内容,所以想要看到服务器发出的原始报文形态就得用 Telnet 手工发送请求,因为 Telnet 只是收到响应报文就完事了,不会解析分块数据,所以可以很清楚地看到响应报文里的 chunked 数据格式:先是一行 16 进制长度,然后是数据,然后再是 16 进制长度和数据,如此重复,最后是 0 长度分块结束。
(3)范围请求
范围请求:有了分块传输编码,服务器就可以轻松地收发大文件了,但对于上 G 的超大文件,还有一些问题需要考虑。比如,你在看当下正热播的某穿越剧,想跳过片头,直接看正片,这实际上是想获取一个大文件其中的片段数据。范围请求允许客户端在请求头里使用专用字段只获取文件的一部分。
Accept-Ranges:范围请求不是 Web 服务器必备的功能,服务器必须在响应头里使用“Accept-Ranges: bytes”表示支持范围请求,使用“Accept-Ranges: none”,或者干脆不发送“Accept-Ranges”字段,表示不支持范围请求。
Range:是 HTTP 范围请求的专用字段,格式是“Range:bytes=x-y”, x 和 y 单位是字节,范围必须从 0 计数。例如前 10 个字节表示为“0-9”。
Range 的格式很灵活,起点 x 和终点 y 可以省略,能够很方便地表示正数或者倒数的范围。假设文件是 100 个字节,那么:
- “0-”表示从文档起点到文档终点,相当于“0-99”;
- “10-”是从第 10 个字节开始到文档末尾,相当于“10-99”;
- “-1”是文档的最后一个字节,相当于“99-99”;
- “-10”是从文档末尾倒数 10 个字节,相当于“90-99”。
服务器收到 Range 字段后,需要做四件事:
检查范围是否合法:比如文件只有 100 个字节,但请求“200-300”,这就是范围越界了。服务器就会返回状态码416,意思是“你的范围请求有误,我无法处理,请再检查一下”。
读取文件:如果范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码“206 PartialContent”,和 200 的意思差不多,但表示 body 只是原数据的一部分。
添加响应头字段Content-Range:告诉片段的实际偏移量和资源的总大小,格式是“bytes x-y/length”,与 Range 头区别在没有“=”,范围后多了总长度。例如,对于“0-10”的范围请求,值就是“bytes0-10/100”。
发送数据:直接把片段用 TCP 发给客户端,一个范围请求就算是处理完了。
例如下面的这个请求使用 Range 字段获取了文件的前 32个字节:
GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-31
返回的数据是(去掉了几个无关字段):
HTTP/1.1 206 Partial
Content Content-Length: 32
Accept-Ranges: bytes
Content-Range: bytes 0-31/96
// this is a plain text json doc
(4)多段数据
刚才说的范围请求一次只获取一个片段,其实它还支持在一次性获取多个片段数据,需要使用“Content-Type: multipart/byteranges”,表示报文的 body 是由多段字节序列组成的,并且还要用一个参数“boundary=xxx”给出段之间的分隔标记。
多段数据的格式与分块传输也比较类似,但它需要用分隔标记 boundary 来区分不同的片段,可以通过图来对比一下。
每一个分段必须以“- -boundary”开始,之后要用“Content-Type”和“Content-Range”标记这段数据的类型和所在范围,然后就像普通的响应头一样以回车换行结束,再加上分段数据,最后用一个“- -boundary- -”表示所有的分段结束。
例如,我们发出有两个范围的请求:
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=0000000000Content-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/96ext json d
--00000000001--
报文里的“- -00000000001”就是多段的分隔符,使用它客户端就可以很容易地区分出多段 Range 数据。
3、连接管理
(1)短连接
HTTP 协议最初(0.9/1.0)是个非常简单的协议,通信过程也采用了简单的“请求 - 应答”方式。它底层的数据传输基于 TCP/IP,每次发送请求前需要先与服务器建立连接,收到响应报文后会立即关闭连接。因为客户端与服务器的整个连接过程很短暂,不会与服务器保持长时间的连接状态,所以就被称为“短连接”(shortlived connections)。
短连接的缺点相当严重,因为在 TCP 协议里,建立连接和关闭连接都是非常“昂贵”的操作。TCP 建立连接要有“三次握手”,发送 3 个数据包,需要 1 个 RTT;关闭连接是“四次挥手”,4 个数据包需要 2 个 RTT。而 HTTP 的一次简单“请求 - 响应”通常只需要 4 个包,如果不算服务器内部的处理时间,最多是 2 个 RTT。这么算下来,浪费的时间就是“3÷5=60%”,有三分之二的时间被浪费掉了,传输效率低得惊人。
(2)长连接
针对短连接暴露出的缺点,HTTP 协议就提出了“长连接”的通信方式,也叫“持久连接”(persistentconnections)、“连接保活”(keep alive)、“连接复用”(connection reuse)。
其实解决办法也很简单,用的就是“成本均摊”的思路,既然 TCP 的连接和关闭非常耗时间,那么就把这个时间成本由原来的一个“请求 - 应答”均摊到多个“请求 - 应答”上。
短连接与长连接的对比示意图如下,在短连接里发送了三次 HTTP“请求 - 应答”,每次都会浪费 60% 的 RTT 时间。而在长连接的情况下,同样发送三次请求,因为只在第一次时建立连接,在最后一次时关闭连接,所以浪费率就是“3÷9≈33%”,降低了差不多一半的时间损耗。显然,如果在这个长连接上发送的请求越多,分母就越大,利用率也就越高。
(3)连接头字段
默认启用长连接:由于长连接对性能的改善效果非常显著,所以在 HTTP/1.1中的连接都会默认启用长连接。不需要用什么特殊的头字段指定,只要向服务器发送了第一次请求,后续的请求都会重复利用第一次打开的 TCP 连接,也就是长连接,在这个连接上收发数据。
Connection: keep-alive:表示在请求头里明确地要求使用长连接机制。不管客户端是否显式要求长连接,如果服务器支持长连接,它总会在响应报文里放一个“Connection: keep-alive”字段,告诉客户端是支持长连接。
长连接缺点:因为 TCP 连接长时间不关闭,服务器必须在内存里保存它的状态,占用了服务器的资源。如果有大量的空闲长连接只连不发,就会很快耗尽服务器的资源,导致服务不可用。所以,长连接也需要在恰当的时间关闭,不能永远保持与服务器的连接,这在客户端或者服务器都可以做到。
Connection: close:在客户端,在请求头里加上这个头字段表示这次通信后就关闭连接。服务器看到这个字段,就知道客户端要主动关闭连接,于是在响应报文里也加上这个字段,发送之后就调用 Socket API 关闭 TCP连接。
服务器端通常不会主动关闭连接,但也可以使用一些策略。拿 Nginx 来举例,它有两种方式:
- 使用“keepalive_timeout”指令,设置长连接的超时时间,如果在一段时间内连接上没有任何数据收发就主动断开连接,避免空闲连接占用系统资源。
- 使用“keepalive_requests”指令,设置长连接上可发送的最大请求次数。比如设置成 1000,那么当 Nginx 在这个连接上处理了 1000 个请求后,也会主动断开连接。
Keep-Alive: timeout=value:客户端和服务器都可以在报文里加上这个头字段限定长连接的超时时间。但这个字段的约束力并不强,通信的双方可能并不会遵守,所以不太常见。
(4)队头阻塞
“队头阻塞”与短连接和长连接无关,而是由 HTTP 基本的“请求 - 应答”模型所导致的。因为 HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求被最优先处理。如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待。
(5)性能优化
并发连接:因为“请求 - 应答”模型不能变,所以“队头阻塞”问题在HTTP/1.1 里无法解决,只能缓解,HTTP 里设计了并发连接(concurrentconnections),也就是同时对一个域名发起多个长连接,用数量来解决质量的问题。
并发连接缺陷:如果每个客户端都想自己快,建立很多个连接,用户数×并发数就会是个天文数字。服务器的资源根本就扛不住,或者被服务器认为是恶意攻击,反而会造成“拒绝服务”。所以,HTTP 协议建议客户端使用并发,但不能“滥用”并发。RFC2616 里明确限制每个客户端最多并发 2 个连接。不过实践证明这个数字实在是太小了,众多浏览器“无视”标准,把这个上限提高到了 6~8。后来修订的RFC7230 也就“顺水推舟”,取消了这个“2”的限制。
域名分片:但“并发连接”所压榨出的性能也跟不上高速发展的互联网无止境的需求,于是出现了域名分片(domain sharding)技术,还是用数量来解决质量的思路。多开几个域名,比如 shard1.baidu.com、shard2.baidu.com,而这些域名都指向同一台服务器,这样实际长连接的数量就又上去了。
4、重定向和跳转
点击页面“链接”时的跳转时,浏览器首先要解析链接文字里的 URI。再用这个 URI 发起一个新的 HTTP 请求,获取响应报文后就会切换显示内容,渲染出新 URI 指向的页面。
重定向:跳转动作是由浏览器的使用者主动发起的,可以称为“主动跳转”,但还有一类跳转是由服务器来发起的,浏览器使用者无法控制,相对地就可以称为“被动跳转”,这在 HTTP 协议里有个专门的名词,叫做重定向(Redirection)。
(1)重定向的过程
Location:属于响应字段,但只有配合 301/302 状态码才有意义,它标记了服务器要求重定向的 URI。
浏览器收到 301/302 报文,会检查响应头里有没有Location。如果有,就从字段值里提取出 URI,发出新的 HTTP 请求,相当于自动替我们点击了这个链接。
注意,在重定向时如果只是在站内跳转,你可以放心地使用相对 URI。但如果要跳转到站外,就必须用绝对 URI,不写全的话浏览器会当成相对URI去处理。
(2)重定向状态码
最常见的重定向状态码就是 301 和 302,另外还有几个不太常见的,例如 303、307、308 这三个状态码的接受程度较低,有的浏览器和服务器可能不支持,开发时应当慎重,测试确认浏览器的实际效果后才能使用。它们最终的效果都差不多,让浏览器跳转到新的 URI,但语义上有一些细微的差别,使用的时候要特别注意。
301:俗称“永久重定向”(Moved Permanently),意思是原 URI 已经“永久”性地不存在了,今后的所有请求都必须改用新的 URI。浏览器看到 301,就知道原来的 URI“过时”了,就会做适当的优化。比如历史记录、更新书签,下次可能就会直接用新的 URI 访问,省去了再次跳转的成本。搜索引擎的爬虫看到 301,也会更新索引库,不再使用老的 URI。
302:俗称“临时重定向”(“Moved Temporarily”),意思是原 URI 处于“临时维护”状态,新的 URI 是起“顶包”作用的“临时工”。浏览器或者爬虫看到 302,会认为原来的 URI 仍然有效,但暂时不可用,所以只会执行简单的跳转页面,不记录新的URI,也不会有其他的多余动作,下次访问还是用原 URI。
303 See Other:类似 302,但要求重定向后的请求改为GET 方法,访问一个结果页面,避免 POST/PUT 重复操作;
307 Temporary Redirect:类似 302,但重定向后请求里的方法和实体不允许变动,含义比 302 更明确;
308 Permanent Redirect:类似 307,不允许重定向后的请求变动,但它是 301“永久重定向”的含义。
(3)重定向的应用场景
重定向:一个最常见的原因就是“资源不可用”,需要用另一个新的URI 来代替。不可用的原因很多,例如域名变更、服务器变更、网站改版、系统维护,这些都会导致原 URI 指向的资源无法访问,为了避免出现 404,就需要用重定向跳转到新的URI,继续为网民提供服务。另一个原因就是“避免重复”,让多个网址都跳转到一个URI,增加访问入口的同时还不会增加额外的工作量。
永久:301 的含义是“永久”的。如果域名、服务器、网站架构发生了大幅度的改变,比如启用了新域名、服务器切换到了新机房、网站目录层次重构,这些都算是“永久性”的改变。原来的 URI 已经不能用了,必须用 301“永久重定向”,通知浏览器和搜索引擎更新到新地址,这也是搜索引擎优化(SEO)要考虑的因素之一。
临时:302 的含义是“临时”的。原来的 URI 在将来的某个时间点还会恢复正常,常见的应用场景就是系统维护,把网站重定向到一个通知页面,告诉用户过一会儿再来访问。另一种用法就是“服务降级”,比如在双十一促销的时候,把订单查询、领积分等不重要的功能入口暂时关闭,保证核心服务能够正常运行。
(4)重定向的相关问题
性能损耗:很明显,重定向的机制决定了一个跳转会有两次请求 - 应答,比正常的访问多了一次。虽然 301/302 报文很小,但大量的跳转对服务器的影响也是不可忽视的。站内重定向还好说,可以长连接复用,站外重定向就要开两个连接,如果网络连接质量差,那成本可就高多了,会严重影响用户的体验。所以重定向应当适度使用,决不能滥用。
循环跳转:如果重定向的策略设置欠考虑,可能会出现“A=>B=>C=>A”的无限循环,不停地在这个链路里转圈圈,后果可想而知。所以 HTTP 协议特别规定,浏览器必须具有检测“循环跳转”的能力,在发现这种情况时应当停止发送请求并给出错误提示。
5、Cookie机制
HTTP 是“无状态”的,这既是优点也是缺点。优点是服务器没有状态差异,可以很容易地组成集群,而缺点就是无法支持需要记录状态的事务操作。好在 HTTP 协议是可扩展的,后来发明的 Cookie 技术,给 HTTP 增加了“记忆能力”。
(1)什么是 Cookie
HTTP 的 Cookie 机制相当于是服务器给每个客户端都贴上一张小纸条,上面写了一些只有服务器才能理解的数据,需要的时候客户端把这些信息发给服务器,服务器看到 Cookie,就能够认出是哪个客户端。
(2)Cookie 的工作过程
Cookie的传递用到两个字段:响应头字段Set-Cookie和请求头字段Cookie。
浏览器第一次访问服务器:服务器肯定是不知道他的身份的。所以,就要创建一个独特的身份标识数据,格式是“key=value”,然后放进 Set-Cookie 字段里,随着响应报文一同发给浏览器。
浏览器收到响应报文:看到里面有 Set-Cookie,知道这是服务器给的身份标识,于是就保存起来,下次再请求的时候就自动把这个值放进 Cookie 字段里发给服务器。
浏览器第二次请求服务器:里面有了 Cookie 字段,服务器就知道这个用户不是新人,就可以拿出 Cookie 里的值,识别出用户的身份,然后提供个性化的服务。
不过因为服务器的“记忆能力”实在是太差,服务器有时会在响应头里添加多个 Set-Cookie,存储多个“key=value”。但浏览器这边发送时不需要用多个 Cookie 字段,只要在一行里用“;”隔开就行。
从这张图中我们也能够看到,Cookie 是由浏览器负责存储的,而不是服务器。如果你换个浏览器,新的浏览器里没有服务器对应的 Cookie,服务器也就认不出来了,只能再走一遍 Set-Cookie 流程。
(3)Cookie 的属性
Cookie 的属性:Cookie 就是服务器委托浏览器存储在客户端里的一些数据,而这些数据通常都会记录用户的关键识别信息。所以,就需要在“key=value”外再用一些手段来保护,防止外泄或窃取,这些手段就是 Cookie 的属性。
Cookie 的生存周期:也就是它的有效期,让它只能在一段时间内可用,一旦超过这个期限浏览器就认为是Cookie失效,在存储里删除,也不会发送给服务器。Cookie 的有效期可以使用 Expires 和 Max-Age 两个属性来设置。“Expires”俗称“过期时间”,用的是绝对时间点,可以理解为“截止日期”(deadline)。“Max-Age”用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间。Expires 和 Max-Age 可以同时出现,两者的失效时间可以一致,也可以不一致,但浏览器会优先采用 Max-Age 计算失效期。
Cookie 的作用域:让浏览器仅发送给特定的服务器和 URI,避免被其他网站盗用。作用域的设置比较简单,“Domain”和“Path”指定了 Cookie 所属的域名和路径,浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie。
(4)Cookie 的安全性
写过前端的同学一定知道,在 JS 脚本里可以用 document.cookie 来读写 Cookie 数据,这就带来了安全隐患,有可能会导致“跨站脚本”(XSS)攻击窃取数据。
HttpOnly属性:会告诉浏览器,此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问,浏览器的 JS 引擎就会禁用 document.cookie 等一切相关的 API,脚本攻击也就无从谈起了。
SameSite属性:可以防范“跨站请求伪造”(XSRF)攻击,设置成“SameSite=Strict”可以严格限定 Cookie 不能随着跳转链接跨站发送,而“SameSite=Lax”则略宽松一点,允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送。
Secure属性:表示这个 Cookie 仅能用 HTTPS 协议加密传输,明文的HTTP 协议会禁止发送。但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在。
(5)Cookie 的应用
身份识别:Cookie 最基本的一个用途,保存用户的登录信息,你用账号和密码登录网站,登录成功后网服务器就会发给浏览器一个Cookie,之后访问网站,浏览器都会自动把身份 Cookie 发给服务器,服务器就能知道你的身份,实现“状态保持”。
广告跟踪:你上网的时候,广告商网站会给你加上 Cookie ,你上其他的网站的时候,别的广告就能用 Cookie读出你的身份,然后做行为分析,再推给你广告。如果广告商势力很大,广告到处都是,那么就比较“恐怖”了,无论你走到哪里它都会通过 Cookie 认出你来,实现广告“精准打击”。为了防止滥用 Cookie 搜集用户隐私,互联网组织相继提出了 DNT(Do Not Track)和P3P(Platform for Privacy Preferences Project),但实际作用不大。
(6)Chrome 开发者工具
Chrome 开发者工具是查看 Cookie 的有力工具,在“Network-Cookies”里可以看到单个页面 Cookie 的各种属性,另一个“Application”面板里则能够方便地看到全站的所有Cookie。
6、缓存控制
缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器。由于链路漫长,网络时延不可控,浏览器使用 HTTP 获取资源的成本较高。缓存起来,下次再请求的时候尽可能地复用。这样,就可以避免多次请求 - 应答的通信成本,节约网络带宽,也可以加快响应速度。
基于“请求 - 应答”模式的特点,可以大致分为客户端缓存和服务器端缓存。
(1)服务器的缓存控制
缓存的工作流程如下:
- 浏览器发现缓存无数据,于是发送请求,向服务器获取资源;
- 服务器响应请求,返回资源,同时标记资源的有效期;
- 浏览器缓存资源,等待下次重用。
Cache-Control:是服务器标记资源有效期使用的头字段,里面的值“max-age=30”就是资源的有效时间,相当于告诉浏览器,这个页面只能缓存 30 秒,之后就算是过期,不能用。
max-age:是“生存时间”(又叫“新鲜度”“缓存寿命”,类似 TTL,Time-To-Live),时间的计算起点是响应报文的创建时刻(即 Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。比如,服务器设定“max-age=5”,但因为网络质量很糟糕,等浏览器收到响应报文已经过去了 4 秒,那么这个资源在客户端就最多能够再存 1 秒钟,之后就会失效。
此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存:
- no_store:不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面;
- no_cache:它的字面含义容易与 no_store 搞混,实际的意思并不是不允许缓存,而是可以缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;
- must-revalidate:又是一个和 no_cache 相似的词,它的意思是如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。
缓存控制的流程图如下:
(2)客户端的缓存控制
Cache-Control:浏览器也可以发“Cache-Control”,也就是说请求 - 应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。
刷新按钮:浏览器会在请求头里加一个“Cache-Control: max-age=0”。所以浏览器就不会使用缓存,而是向服务器发请求。服务器看到 max-age=0,也就会用一个最新生成的报文回应浏览器。
Ctrl+F5 的强制刷新:它其实是发了一个“Cache-Control: no-cache”,含义和“max-age=0”基本一样,就看后台的服务器怎么理解,通常两者的效果是相同的。
前进,后退:你就会惊喜地发现“from disk cache”的字样,意思是没有发送网络请求,而是读取的磁盘上的缓存。在“前进”“后退”“跳转”这些重定向动作中浏览器只用最基本的请求头,没有“Cache-Control”,所以就会检查缓存,直接利用之前的资源,不再进行网络通信。
(3)条件请求
浏览器可以用两个连续的请求组成“验证动作”:先是一个 HEAD,获取资源的修改时间等元信息,然后与缓存数据比较,如果没有改动就使用缓存,节省网络流量,否则就再发一个 GET 请求,获取最新的版本。
条件请求:但这样的两个请求网络成本太高了,所以 HTTP 协议就定义了一系列“If”开头的“条件请求”字段,专门用来检查验证资源是否过期,把两个请求才能完成的工作合并在一个请求里做。而且,验证的责任也交给服务器,浏览器只需“坐享其成”。
条件请求头字段:一共有 5 个头字段,最常用的是“if-Modified-Since”和“If-None-Match”这两个,其他三个头字段是“If-Unmodified-Since”“If-Match”和“If-Range”。需要第一次的响应报文预先提供“Last-modified”和“ETag”,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。如果资源没有变,服务器就回应一个“304 Not Modified”,表示缓存依然有效,浏览器就可以更新一下有效期,然后放心大胆地使用缓存了。
Last-modified:就是文件的最后修改时间。**ETag:**是“实体标签”(Entity Tag)的缩写,是资源的一个唯一标识,主要是用来解决修改时间无法准确区分文件变化的问题。比如,一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分。再比如,一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽。使用 ETag 就可以精确地识别资源的变动情况,让浏览器能够更有效地利用缓存。
ETag 还有“强”“弱”之分。强 ETag 要求资源在字节级别必须完全相符,弱 ETag 在值前有个“W/”标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)。
7、代理服务
HTTP 的“请求 - 应答”模型中只有两个互相通信的角色,分别是“请求方”浏览器(客户端)和“应答方”服务器。现在在这个模型里引入一个新的角色,那就是HTTP 代理。整体上来看,还是一个有顺序关系的链条,而且链条里相邻的两个角色仍然是简单的一对一通信,不会出现越级的情况。
链条的起点还是客户端(也就是浏览器),中间的角色被称为代理服务器(proxy server),链条的终点被称为源服务器(origin server),意思是数据的“源头”“起源”。
(1)代理服务
代理服务:就是在客户端和服务器原本的通信链路中插入的一个中间环节,也是一台服务器,但提供的是“代理服务”。服务本身不生产内容,而是处于中间位置转发上下游的请求和响应,具有双重身份,面向下游的用户时,表现为服务器,代表源服务器响应客户端的请求;而面向上游的源服务器时,又表现为客户端,代表客户端发送请求。
(2)代理的作用
负载均衡:因为在面向客户端时屏蔽了源服务器,客户端看到的只是代理服务器,代理服务器就可以决定由后面的哪台服务器来响应请求。常用的负载均衡算法比如轮询、一致性哈希等等,这些算法的目标都是尽量把外部的流量合理地分散到多台源服务器,提高系统的整体资源利用率和性能。
- 健康检查:使用“心跳”等机制监控后端服务器,发现有故障就及时“踢出”集群,保证服务高可用;
- 安全防护:保护被代理的后端服务器,限制 IP 地址或流量,抵御网络攻击和过载;
- 加密卸载:对外网使用 SSL/TLS 加密通信认证,而在安全的内网不加密,消除加解密成本;
- 数据过滤:拦截上下行的数据,任意指定策略修改请求或者响应;
- 内容缓存:暂存、复用服务器响应。
(3)代理相关头字段
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://www.chrono.com/21-1”,它会转而访问“http://origin.io”。这里的“origin.io”就是源站,它会在响应报文里输出“Via”“X-Forwarded-For”等代理头字段信息:
单从浏览器的页面上很难看出代理做了哪些工作,因为代理的转发都在后台不可见,所以我把这个过程用 Wireshark 抓了一个包:
从抓包里就可以清晰地看出代理与客户端、源服务器的通信过程:
- 客户端 55061 先用三次握手连接到代理的 80 端口,然后发送 GET 请求;
- 代理不直接生产内容,所以就代表客户端,用 55063 端口连接到源服务器,也是三次握手;
- 代理成功连接源服务器后,发出了一个 HTTP/1.0 的 GET 请求;
- 因为 HTTP/1.0 默认是短连接,所以源服务器发送响应报文后立即用四次挥手关闭连接;
- 代理拿到响应报文后再发回给客户端,完成了一次代理服务。
在这个实验中,你可以看到除了“X-Forwarded-For”和“X-Real-IP”,还出现了两个字段:“X-Forwarded-Host”和“X-Forwarded-Proto”,它们的作用与“X-Real-IP”类似,只记录客户端的信息,分别是客户端请求的原始域名和原始协议名。
(4)代理协议
代理协议:虽然服务器可以拿到客户端信息。但对于代理服务器来说它并不是一个最佳的解决方案。因为通过“X-Forwarded-For”操作代理信息必须要解析数据再修改数据,会降低代理的转发性能。而且使用 HTTPS 通信是不允许修改数据的,所以无法使用。所以就出现了一个专门的代理协议(The PROXY protocol),它由知名的代理软件HAProxy 所定义,也是一个“事实标准”,被广泛采用(注意并不是 RFC)。
代理协议版本:有 v1 和 v2 两个版本,v1 和 HTTP 差不多,也是明文,而 v2 是二进制格式。今天只介绍比较好理解的 v1,它在 HTTP 报文前增加了一行 ASCII 码文本,相当于又多了一个头。这一行文本其实非常简单,开头必须是“PROXY”五个大写字母,然后是“TCP4”或者“TCP6”,表示客户端的 IP 地址类型,再后面是请求方地址、应答方地址、请求方端口号、应答方端口号,最后用一个回车换行(\r\n)结束。
例如下面的这个例子,在 GET 请求行前多出了 PROXY 信息行,客户端的真实 IP 地址是“1.1.1.1”,端口号是 55555。
PROXY TCP4 1.1.1.1 2.2.2.2 55555 80
GET / HTTP/1.1
Host: www.xxx.com
服务器看到这样的报文,只要解析第一行就可以拿到客户端地址,不需要再去理会后面的HTTP 数据,省了很多事情。不过代理协议并不支持“X-Forwarded-For”的链式地址形式,所以拿到客户端地址后再如何处理就需要代理服务器与后端自行约定。
8、缓存代理
(1)缓存代理服务
没有缓存的时候,代理服务器只有最简单的中转功能。加入缓存后,代理服务器既可以中转数据,还可以缓存数据,下一次再有相同的请求,代理服务器就可以直接发送 304 或者缓存数据。这样就降低了客户端的等待时间,同时节约了源服务器的网络带宽。
在 HTTP 的缓存体系中,缓存代理十分特殊,它既是客户端,又是服务器,即可以用客户端的缓存控制策略也可以用服务器端的缓存控制策略,它可以同时使用各种“Cache-Control”属性。但它并不是真正的数据消费者和生产者,所以还需要有一些新的“Cache-Control”属性来对它做特别的约束。
(2)源服务器缓存控制
前面介绍了 4 种服务器端的“Cache-Control”属性:max-age、no_store、no_cache 和 must-revalidate,这 4 种缓存属性可以约束客户端,也可以约束代理。但客户端和代理是不一样的,客户端的缓存只是用户自己使用,而代理的缓存可能会为非常多的客户端提供服务。所以,需要对它的缓存再多一些限制条件。
区分客户端上的缓存和代理上的缓存:可以使用两个新属性“private”和“public”。
- private: 表示缓存只能在客户端保存,是用户“私有”的,不能放在代理上与别人共享。
- public:表示缓存完全开放,谁都可以存,谁都可以用。
比如你登录论坛,返回的响应报文里用“Set-Cookie”添加了论坛 ID,这就属于私人数据,不能存在代理上。不然,别人访问代理获取了被缓存的响应就麻烦了。
区分缓存失效后的重新验证:“must-revalidate”是只要过期就必须回源服务器验证,而新的“proxy-revalidate”只要求代理的缓存过期后必须验证,客户端不必回源,只验证到代理这个环节就行了。
缓存的生存时间:使用新的“s-maxage”(s 是 share 的意思),只限定在代理上能够存多久,而客户端仍然使用“max_age”。
no-transform:这是一个代理专用的属性。代理有时候会对缓存下来的数据做一些优化,比如把图片生成 png、webp 等几种格式,方便今后的请求处理,而“no-transform”就会禁止这样做。
下面的流程图是完整的服务器端缓存控制策略,可以同时控制客户端和代理:
源服务器在设置完“Cache-Control”后必须要为报文加上“Last-modified”或“ETag”字段。否则,客户端和代理后面就无法使用条件请求来验证缓存是否有效,也就不会有 304 缓存重定向。
(3)客户端缓存控制
客户端在 HTTP 缓存体系里要面对的是代理和源服务器,如下图:
缓存生存时间:多了两个新属性“max-stale”和“min-fresh”。
- max-stale:意思是如果代理上的缓存过期了也可以接受,但不能过期太多,超过 x 秒也会不要。
- min-fresh:意思是缓存必须有效,而且必须在 x 秒后依然有效。
比如,草莓上贴着标签“max-age=5”,现在已经在冰柜里存了 7 天。如果有请求“max-stale=2”,意思是过期两天也能接受,所以刚好能卖出去。但要是“min-fresh=1”,这是绝对不允许过期的,就不会买走。这时如果有另外一个菠萝是“max-age=10”,那么“7+1<10”,在一天之后还是新鲜的,所以就能卖出去。
only-if-cached:表示只接受代理缓存的数据,不接受源服务器的响应。如果代理上没有缓存或者缓存过期,就应该给客户端返回一个504(Gateway Timeout)。
(4)其他问题
Vary:同一个请求,经过内容协商后可能会有不同的字符集、编码、浏览器等版本。比如,“Vary: Accept-Encoding”“Vary: User-Agent”,缓存代理必须要存储这些不同的版本。当再收到相同的请求时,代理就读取缓存里的“Vary”,对比请求头里相应的“ Accept-Encoding”“User-Agent”等字段,如果和上一个请求的完全匹配,比如都是“gzip”“Chrome”,就表示版本一致,可以返回缓存的数据。
Purge:就是“缓存清理”,它对于代理也是非常重要的功能,过期的数据应该及时淘汰,避免占用空间;源站的资源有更新,需要删除旧版本,主动换成最新版(即刷新);有时候会缓存了一些本不该存储的信息,例如网络谣言或者危险链接,必须尽快把它们删除。清理缓存的方法有很多,比较常用的一种做法是使用自定义请求方法“PURGE”,发给代理服务器,要求删除 URI 对应的缓存数据。