一、概述
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 可能会有多个不同的“版本”,主要用在传输链路中间的代理服务器实现缓存服务。