To Be a HTTP Hero

165 阅读1小时+

To Be a HTTP Hero

HTTP发展过程

HTTP/0.9

  • 结构简单,为了便于服务器和客户端处理,采用了纯文本格式。
  • 只允许用“GET”动作从服务器上获取 HTML 文档,并且在响应请求之后立即关闭连接。

HTTP/1.0

  • 增加了 HEAD、POST 等新方法。
  • 增加了响应状态码,标记可能的错误原因。
  • 引入了协议版本号概念。
  • 引入了 HTTP Header(头部)的概念,让 HTTP 处理请求和响应更加灵活。
  • 传输的数据不再仅限于文本。

HTTP/1.0 并不是一个“标准”,只是记录已有实践和模式的一份参考文档,不具有实际的约束力,也就是大家可以不遵守这个标准。

HTTP/1.1

  • 增加了 PUT、DELETE 等新的方法。
  • 增加了缓存管理和控制。
  • 明确了连接管理,允许持久连接。
  • 允许响应数据分块(chunked),利于传输大文件。
  • 强制要求 Host 头,让互联网主机托管成为可能。

与HTTP/1.0有一个重要的区别是:它是一个“正式的标准”,而不是一份可有可无的“参考文档”。这意味着今后互联网上所有的浏览器、服务器、网关、代理等等,只要用到 HTTP 协议,就必须严格遵守这个标准。

HTTP/2

HTTP/2是以SPDY 为基础制定的新版本HTTP 协议,在高度兼容 HTTP/1.1 的同时在性能改善方面做了很大努力,主要的特点有:

  • 二进制协议,不再是纯文本。
  • 可发起多个请求,废弃了 1.1 里的管道。
  • 使用专用算法压缩头部,减少数据传输量。
  • 允许服务器主动向客户端推送数据。
  • 增强了安全性,“事实上”要求加密通信。

HTTP/3

HTTP/3 基于 Google 的 QUIC 协议,是将来的发展方向。

超文本传输协议

HTTP:超文本传输协议。

协议:表示这是一种规范,大家都需要遵守。

传输:1.HTTP 协议是一个“双向协议”,数据是在请求方和响应方之间双向流动而不是单向流动。2.允许传输过程中可以存在任意多个“中间人”,而这些中间人也都遵从 HTTP 协议,只要不打扰基本的数据传输,就可以添加任意的额外功能,例如安全认证、数据压缩、编码转换等,用来优化整个传输过程。

超文本:HTTP 传输的不是 TCP/UDP 这些底层协议里被切分的杂乱无章的二进制包,而是完整的、有意义的数据,可以被浏览器、服务器这样的上层应用程序处理。它是文字、图片、音频和视频等的混合体(HTML),含有“超链接”,能够从一个“超文本”跳跃到另一个“超文本”。

互联网是遍布于全球的许多网络互相连接而形成的一个巨大的国际网络,在它上面存放着各式各样的资源,也对应着不同的协议,例如超文本资源使用 HTTP,普通文件使用 FTP,电子邮件使用 SMTP 和 POP3 等,而HTTP 是构建互联网最重要的一部分。

HTTP 通常基于 TCP/IP 协议栈之上,依靠 IP 协议实现寻址和路由、TCP 协议实现可靠数据传输、DNS 协议实现域名查找、SSL/TLS 协议实现安全通信。此外,还有一些协议依赖于 HTTP,例如 WebSocket、HTTPDNS 等。

与HTTP相关的各种概念

浏览器

浏览器本质上是一个 HTTP 协议中的请求方,使用 HTTP 协议获取网络上的各种资源。

在 HTTP 协议里,浏览器的角色被称为“User Agent”即“用户代理”,意思是作为访问者的“代理”来发起 HTTP 请求。通常简单地称之为“客户端”。

Web 服务器

Web Server服务器作为协议另一端的响应方,主要有:Apache、Nginx等。

CDN

CDN,位于浏览器和服务器中间,它应用了 HTTP 协议里的缓存和代理技术,代替源站响应客户端的请求,除了基本的网络加速外,还提供负载均衡、安全防护、边缘计算、跨运营商网络等功能,。

它可以缓存源站的数据,让浏览器的请求不用“千里迢迢”地到达源站服务器,直接在“半路”就可以获取响应。如果 CDN 的调度算法很优秀,更可以找到离用户最近的节点,大幅度缩短响应时间。

Web Service

Web Service 是一种由 W3C 定义的应用服务开发规范,使用 client-server 主从架构,通常使用 WSDL 定义服务接口,使用 HTTP 协议传输 XML 或 SOAP 消息,也就是说,它是一个基于 Web(HTTP)的服务架构技术,既可以运行在内网,也可以在适当保护后运行在外网。

WAF

WAF意思是“网络应用防火墙”。与硬件“防火墙”类似,它是应用层面的“防火墙”,专门检测 HTTP 流量,是防护 Web 应用的安全技术。WAF 通常位于 Web 服务器之前,可以阻止如 SQL 注入、跨站脚本等攻击。

与HTTP相关的各种协议

TCP/IP

TCP/IP 协议实际上是一系列网络通信协议的统称,其中最核心的两个协议是 TCP 和 IP,其他的还有 UDP、ICMP、ARP 等等,共同构成了一个复杂但有层次的协议栈。

这个协议栈有四层,最上层是“应用层”,最下层是“链接层”,TCP 和 IP 则在中间:TCP 属于“传输层”,IP 属于“网际层”。

IP 协议主要目的是解决寻址和路由问题,以及如何在两点间传送数据包。IP 协议使用“IP 地址”的概念来定位互联网上的每一台计算机。

TCP 协议是“传输控制协议”,它位于 IP 协议之上,基于 IP 协议提供可靠的字节流形式的通信,是 HTTP 协议得以实现的基础。“可靠”是指保证数据不丢失,“字节流”是指保证数据完整。

HTTP 是一个"传输协议",但它不关心寻址、路由、数据完整性等传输细节,而要求这些工作都由下层来处理。HTTP 协议运行在 TCP/IP 上。

DNS

在 TCP/IP 协议中使用 IP 地址来标识计算机,数字形式的地址对于计算机来说是方便的,但对于人类来说却既难以记忆又难以输入。于是“域名系统”出现了,用有意义的名字来作为 IP 地址的等价替代。

在 DNS 中,“域名”(Domain Name)又称为“主机名”(Host),为了更好地标记不同国家或组织的主机,让名字更好记,所以被设计成了一个有层次的结构。域名用“.”分隔成多个单词,级别从左到右逐级升高,最右边的被称为“顶级域名”。对于顶级域名,例如表示商业公司的“com”、表示教育机构的“edu”,表示国家的“cn”uk”等。

但想要使用 TCP/IP 协议来通信仍然要使用 IP 地址,所以需要把域名做一个转换,“映射”到它的真实 IP,这就是所谓的“域名解析”。目前全世界有 13 组根 DNS 服务器,下面再有许多的顶级 DNS、权威 DNS 和更小的本地 DNS,逐层递归地实现域名查询。

URI/URL

DNS 和 IP 地址只是标记了互联网上的主机,但是主机上有许多的文本、图片、页面。所以出现了 URI( 统一资源标识符),使用它能够唯一地标记互联网上资源。URI 另一个更常用的表现形式是 URL( 统一资源定位符),也就是“网址”,它实际上是 URI 的一个子集,不过因为这两者几乎是相同的。

例如https://juejin.cn/user/237942497623806

URI 主要有三个基本的部分构成:

  • 协议名:即访问该资源应当使用的协议,在这里是“https”。
  • 主机名:即互联网上主机的标记,可以是域名或 IP 地址,在这里是“juejin.cn”。
  • 路径:即资源在主机上的位置,使用“/”分隔多级目录,在这里是“/user/237942497623806l”。

HTTPS

HTTPS 是运行在 SSL/TLS 协议上的 HTTP。 SSL/TLS是一个负责加密通信的安全协议,建立在 TCP/IP 之上,所以也是个可靠的传输协议,可以被用作 HTTP 的下层。

SSL 发展到 3.0 时被标准化,改名为 TLS,

SSL综合了对称加密、非对称加密、摘要算法、数字签名、数字证书等技术,能够在不安全的环境中为通信的双方创建出一个秘密的、安全的传输通道。如果浏览器地址栏有一个小锁头标志,那就表明网站启用了安全的 HTTPS 协议,而 URI 里的协议名,也从“http”变成了“https”。

代理

代理(Proxy)是 HTTP 协议中请求方和应答方中间的一个环节,作为“中转站”,既可以转发客户端的请求,也可以转发服务器的应答。代理有很多的种类,常见的有:

  • 匿名代理:完全“隐匿”了被代理的机器,外界看到的只是代理服务器.
  • 透明代理:顾名思义,它在传输过程中是“透明开放”的,外界既知道代理,也知道客户端.
  • 正向代理:靠近客户端,代表客户端向服务器发送请求.
  • 反向代理:靠近服务器端,代表服务器响应客户端的请求;

CDN,实际上就是一种代理,它代替源站服务器响应客户端的请求,通常扮演着透明代理和反向代理的角色。

由于代理在传输过程中插入了一个“中间层”,所以可以在这个环节做很多事情,比如:

  • 负载均衡:把访问请求均匀分散到多台机器,实现访问集群化.
  • 内容缓存:暂存上下行的数据,减轻后端的压力.
  • 安全防护:隐匿 IP, 使用 WAF 等工具抵御网络攻击,保护被代理的机器.
  • 数据处理:提供压缩、加密等额外的功能。

网络分层模型

TCP/IP 网络分层模型

TCP/IP 协议总共有四层:

  • 第一层:“链接层”,负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标记网络上的设备,所以有时候也叫 MAC 层。
  • 第二层:“网际层”或者“网络互连层”,IP 协议就处在这一层。因为 IP 协议定义了“IP 地址”的概念,所以就可以在“链接层”的基础上,用 IP 地址取代 MAC 地址,把许许多多的局域网、广域网连接成一个虚拟的巨大网络,在这个网络里找设备时只要把 IP 地址再“翻译”成 MAC 地址就可以了。
  • 第三层:“传输层”,这个层次协议的职责是保证数据在 IP 地址标记的两点之间“可靠”地传输,是 TCP 协议工作的层次,另外还有UDP。TCP 是一个有状态的协议,需要先与对方建立连接然后才能发送数据,而且保证数据不丢失不重复。而 UDP 则比较简单,它无状态,不用事先建立连接就可以任意发送数据,但不保证数据一定会发到对方。两个协议的另一个重要区别在于数据的形式。TCP 的数据是连续的“字节流”,有先后顺序,而 UDP 则是分散的小数据包,是顺序发,乱序收。
  • 第四层:“应用层”,有各种面向具体应用的协议。例如 Telnet、SSH、FTP、SMTP 等等,当然还有 HTTP。MAC 层的传输单位是帧,IP 层的传输单位是包,TCP 层的传输单位是段,HTTP 的传输单位则是消息或报文。但这些名词并没有什么本质的区分,可以统称为数据包。

OSI 网络分层模型

OSI 模型分成了七层:

  • 第一层:物理层,网络的物理形式,例如电缆、光纤、网卡、集线器等等。
  • 第二层:数据链路层,它基本相当于 TCP/IP 的链接层。
  • 第三层:网络层,相当于 TCP/IP 里的网际层。
  • 第四层:传输层,相当于 TCP/IP 里的传输层。
  • 第五层:会话层,维护网络中的连接状态,即保持会话和同步。
  • 第六层:表示层,把数据转换为合适、可理解的语法和语义。
  • 第七层:应用层,面向具体的应用传输数据。

TCP/IP 是一个纯软件的栈,没有网络应有的最根基的电缆、网卡等物理设备的位置。而 OSI 则补足了这个缺失,在理论层面上描述网络更加完整。

两个分层模型的映射关系

  • 第一层:物理层,TCP/IP 里无对应。
  • 第二层:数据链路层,对应 TCP/IP 的链接层。
  • 第三层:网络层,对应 TCP/IP 的网际层。
  • 第四层:传输层,对应 TCP/IP 的传输层。
  • 第五、六、七层:统一对应到 TCP/IP 的应用层。

“四层负载均衡”就是指工作在传输层上,基于 TCP/IP 协议的特性,例如 IP 地址、端口号等实现对后端服务器的负载均衡。

“七层负载均衡”就是指工作在应用层上,看到的是 HTTP 协议,解析 HTTP 报文里的 URI、主机名、资源类型等数据,再用适当的策略转发给后端服务器。

TCP/IP 协议栈的工作方式

HTTP 协议的传输过程就是这样通过协议栈逐层向下,每一层都添加本层的专有数据,层层打包,然后通过下层发送出去。

接收数据则是相反的操作,从下往上穿过协议栈,逐层拆包,每层去掉本层的专有头,上层就会拿到自己的数据。

但下层的传输过程对于上层是完全“透明”的,上层也不需要关心下层的具体实现细节,所以就 HTTP 层次来看,它不管下层是不是 TCP/IP 协议,看到的只是一个可靠的传输链路,只要把数据加上自己的头,对方就能原样收到。

二层转发,工作在二层的设备(i.e交换机)只认识MAC地址,所以建立MAC地址和端口的映射关系,来决定往哪个端口转发。

三层路由,工作在三层的设备(i.e路由器)利用ip地址和port,根据路由表选择最佳路径来转发包。

域名

域名的形式

域名是一个有层次的结构,是一串用“.”分隔的多个单词,最右边的被称为“顶级域名”,然后是“二级域名”,层级关系向左依次降低。

最左边的是主机名,通常用来表明主机的用途,比如“www”表示提供万维网服务、“mail”表示提供邮件服务。

例如“time.geekbang.org”,这里的“org”就是顶级域名,“geekbang”是二级域名,“time”则是主机名。使用这个域名,DNS 就会把它转换成相应的 IP 地址,你就可以访问网站了。

域名本质上还是个名字空间系统,使用多级域名就可以划分出不同的国家、地区、组织、公司、部门,每个域名都是独一无二的,可以作为一种身份的标识。

域名的解析

域名转换成 IP 地址,这个过程就是“域名解析”。

DNS 的核心系统是一个三层的树状、分布式服务,基本对应域名的结构:

  • 根域名服务器:管理顶级域名服务器,返回“com”“net”“cn”等顶级域名服务器的 IP 地址。
  • 顶级域名服务器:管理各自域名下的权威域名服务器,比如 com 顶级域名服务器可以返回 apple.com 域名服务器的 IP 地址。
  • 权威域名服务器:管理自己域名下主机的 IP 地址,比如 apple.com 权威域名服务器可以返回 www.apple.com 的 IP 地址。

例如,要访问“www.apple.com”,就要进行下面的三次查询:

  • 访问根域名服务器,它会告诉你“com”顶级域名服务器的地址。
  • 访问“com”顶级域名服务器,它再告诉你“apple.com”域名服务器的地址。
  • 最后访问“apple.com”域名服务器,就得到了“www.apple.com”的地址。

在核心 DNS 系统之外,还有两种手段用来减轻域名解析的压力,并且能够更快地获取结果,基本思路就是“缓存”:

  • 许多大公司、网络运行商都会建立自己的 DNS 服务器,作为用户 DNS 查询的代理,代替用户访问核心 DNS 系统。这些“野生”服务器被称为“非权威域名服务器”,可以缓存之前的查询结果,如果已经有了记录,就无需再向根服务器发起查询,直接返回对应的 IP 地址。这些 DNS 服务器的数量要比核心系统的服务器多很多,而且大多部署在离用户很近的地方。比较知名的 DNS 有 Google 的“8.8.8.8”,Microsoft 的“4.2.2.1”,还有 CloudFlare 的“1.1.1.1”等等。
  • 操作系统里也会对 DNS 解析结果做缓存,如果你之前访问过“www.apple.com”,那么下一次在浏览器里再输入这个网址的时候就不会再跑到 DNS 那里去问了,直接在操作系统里就可以拿到 IP 地址。
  • 操作系统里还有一个特殊的“主机映射”hosts文件,通常是一个可编辑的文本,如果操作系统在缓存里找不到 DNS 记录,就会找这个文件。

域名的一些用法

第一种,“重定向”。因为域名代替了 IP 地址,所以可以让对外服务的域名不变,而主机的 IP 地址任意变动。当主机有情况需要下线、迁移时,可以更改 DNS 记录,让域名指向其他的机器。

第二种,因为域名是一个名字空间,所以可以使用 bind9 等开源软件搭建一个在内部使用的 DNS,作为名字服务器。这样我们开发的各种内部服务就都用域名来标记,比如数据库服务都用域名“mysql.inner.app”,商品服务都用“goods.inner.app”,发起网络通信时也就不必再使用写死的 IP 地址了,可以直接用域名。

第三种包含了前两种,也就是基于域名实现的负载均衡。这种用法也有两种方式,两种方式可以混用。第一种方式,因为域名解析可以返回多个 IP 地址,所以一个域名可以对应多台主机,客户端收到多个 IP 地址后,就可以自己使用轮询算法依次向服务器发起请求,实现负载均衡。第二种方式,域名解析可以配置内部的策略,返回离客户端最近的主机,或者返回当前服务质量最好的主机,这样在 DNS 端把请求分发到不同的服务器,实现负载均衡。

此外有一些恶意的 DNS,那么它也可以在域名这方面“做手脚”,例如:

  • “域名屏蔽”,对域名直接不解析,返回错误,让你无法拿到 IP 地址,也就无法访问网站。
  • “域名劫持”,也叫“域名污染”,你要访问 A 网站,但 DNS 给了你 B 网站。

解释域名的 DNS 解析过程

比如有一个网站要上线,你在域名注册商那里申请了abc.com,那么你的域名A记录就保存在这个域名注册商的DNS服务器上,该DNS服务器称为权威域名服务器。当客户端访问abc.com时,先查找浏览器DNS缓存,没有则查找操作系统DNS缓存,在这一阶段是操作系统dnscache clinet 服务进行DNS缓存的,如果还是没有则查找hosts文件中的域名记录。然后依然没有的话则访问电脑上设置的DNS服务器IP,比如三大营运商的dns服务器或者谷歌的8.8.8.8,此时这一层的DNS服务器称为“野生DNS缓存服务器”,也就是非权威域名服务器。如果还是没有则非权威域名服务器会去查找根域名服务器-顶级域名服务器-二级域名服务器-权威域名服务器 ,这样客户端就在权威域名服务器上找到了abc.com对应的IP了,这个IP可以是多个,每次客户端请求的时候域名服务器会根据负载均衡算法分配一个IP给你。当DNS缓存失效了,则重新开始新一轮的域名请求。

浏览器缓存->操作系统dnscache ->hosts文件->非权威域名服务器->根域名服务器->顶级域名服务器->二级域名服务器->权威域名服务器。

DNS解析是一个包含迭代查询和递归查询的过程。

  • 递归查询指的是查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。使用递归 查询,用户只需要发出一次查询请求。
  • 迭代查询指的是查询请求后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。使用迭代查询,用户需要发出 多次的查询请求。

向本地 DNS 服务器发送请求的方式是递归查询,因为我们只需要发出一次请求,然后本地 DNS 服务器返回给我们最终的请求结果。

本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程,因为每一次域名服务器只返回单次 查询的结果,下一级的查询由本地 DNS 服务器自己进行。

HTTP报文

报文结构

HTTP 协议是一个“纯文本”的协议,所以头数据都是 ASCII 码的文本。

HTTP 协议的请求报文和响应报文的结构基本相同,由三大部分组成:

  • 起始行:描述请求或响应的基本信息。
  • 头部字段集合(header):使用 key-value 形式更详细地说明报文。
  • 消息正文:实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。

前两部分起始行和头部字段经常又合称为“请求头”或“响应头”,消息正文又称为“实体”,但与“header”对应,很多时候就直接称为“body”。

HTTP 协议规定报文必须有 header,但可以没有 body,而且在 header 之后必须要有一个空行。

请求行

请求报文里的起始行也就是请求行,它简要地描述了客户端想要如何操作服务器端的资源。

请求行由三部分构成:

  • 请求方法:是一个动词,如 GET/POST,表示对资源的操作。
  • 请求目标:通常是一个 URI,标记了请求方法要操作的资源。
  • 版本号:表示报文使用的 HTTP 协议版本。

状态行

响应报文里的起始行,在这里它不叫“响应行”,而是叫状态行,意思是服务器响应的状态。

  • 版本号:表示报文使用的 HTTP 协议版本。
  • 状态码:一个三位数,用代码的形式表示处理的结果,比如 200 是成功,500 是服务器错误。
  • 原因:作为数字状态码补充,是更详细的解释文字,帮助人理解原因。

头部字段

头部字段是 key-value 的形式,key 和 value 之间用“:”分隔,最后用 CRLF 换行表示字段结束。

HTTP 头字段非常灵活,不仅可以使用标准里的 Host、Connection 等已有头,也可以任意添加自定义头,这就给 HTTP 协议带来了扩展的可能。

使用头字段需要注意下面几点:

  • 字段名不区分大小写,例如“Host”也可以写成“host”,但首字母大写的可读性更好。
  • 字段名里不允许出现空格,可以使用连字符“-”,但不能使用下划线“_”。例如,“test-name”是合法的字段名,而“test name”“test_name”是不正确的字段名。
  • 字段名后面必须紧接着“:”,不能有空格,而“:”后的字段值前可以有多个空格。
  • 字段的顺序是没有意义的,可以任意排列不影响语义。
  • 字段原则上不能重复,除非这个字段本身的语义允许,例如 Set-Cookie。

常用头字段

HTTP 协议规定了非常多的头部字段,基本上可以分为四大类:

  • 通用字段:在请求头和响应头里都可以出现。
  • 请求字段:仅能出现在请求头里,进一步说明请求信息或者额外的附加条件。
  • 响应字段:仅能出现在响应头里,补充说明响应报文的信息。
  • 实体字段:它实际上属于通用字段,但专门描述 body 的额外信息。

Host 字段,它属于请求字段,只能出现在请求头里,它同时也是唯一一个 HTTP/1.1 规范里要求必须出现的字段。Host 字段告诉服务器这个请求应该由哪个主机来处理,当一台计算机上托管了多个虚拟主机的时候,服务器端就需要用 Host 字段来选择。例如在 127.0.0.1 上有三个虚拟主机:“www.chrono.com”“www.metroid.net”和“origin.io”。那么当使用域名的方式访问时,就必须要用 Host 字段来区分这三个 IP 相同但域名不同的网站,否则服务器就会找不到合适的虚拟主机,无法处理。

User-Agent 是请求字段,只出现在请求头里。它使用一个字符串来描述发起 HTTP 请求的客户端,服务器可以依据它来返回最合适此浏览器显示的页面。但由于历史的原因,User-Agent 非常混乱,每个浏览器都自称是“Mozilla”“Chrome”“Safari”,企图使用这个字段来互相“伪装”,导致 User-Agent 变得越来越长,最终变得毫无意义。不过有的比较“诚实”的爬虫会在 User-Agent 里用“spider”标明自己是爬虫,所以可以利用这个字段实现简单的反爬虫策略。

Date 字段是一个通用字段,但通常出现在响应头里,表示 HTTP 报文创建的时间,客户端可以使用这个时间再搭配其他字段决定缓存策略。

Server 字段是响应字段,只能出现在响应头里。它告诉客户端当前正在提供 Web 服务的软件名称和版本号。Server 字段不是必须要出现的,因为这会把服务器的一部分信息暴露给外界,如果这个版本恰好存在 bug,那么黑客就有可能利用 bug 攻陷服务器。所以,有的网站响应头里要么没有这个字段,要么就给出一个完全无关的描述信息。

Content-Length,它表示报文里 body 的长度,也就是请求头或响应头空行后面数据的长度。服务器看到这个字段,就知道了后续有多少数据,可以直接接收。如果没有这个字段,那么 body 就是不定长的,需要使用 chunked 方式分段传输。

请求方法

标准请求方法

目前 HTTP/1.1 规定了八种方法,单词都必须是大写的形式:

  • GET:获取资源,可以理解为读取或者下载数据。
  • HEAD:获取资源的元信息。
  • POST:向资源提交数据,相当于写入或上传数据。
  • PUT:类似 POST。
  • DELETE:删除资源。
  • CONNECT:建立特殊的连接隧道。
  • OPTIONS:列出可对资源实行的方法。
  • TRACE:追踪请求 - 响应的传输路径。

由客户端“请求”或者“指示”服务器来完成。客户端没有决定权,服务器掌控着所有资源,也就有绝对的决策权力。它收到 HTTP 请求报文后,看到里面的请求方法,可以执行也可以拒绝,或者改变动作的含义,HTTP 是一个协议。

GET/HEAD

GET的含义是请求从服务器获取资源。

GET 方法虽然基本动作比较简单,但搭配 URI 和其他头字段就能实现对资源更精细的操作。例如,在 URI 后使用“#”,就可以在获取页面后直接定位到某个标签所在的位置;使用 If-Modified-Since 字段就变成了“有条件的请求”,仅当资源被修改时才会执行获取动作;使用 Range 字段就是“范围请求”,只获取资源的一部分数据。

HEAD 方法与 GET 方法类似,也是请求从服务器获取资源,服务器的处理机制也是一样的,但服务器不会返回请求的实体数据,只会传回响应头,也就是资源的“元信息”。因为它的响应头与 GET 完全相同,所以可以用在很多并不真正需要资源的场合,避免传输 body 数据的浪费。比如,想要检查一个文件是否存在,只要发个 HEAD 请求就可以了,没有必要用 GET 把整个文件都取下来。再比如,要检查文件是否有最新版本,同样也应该用 HEAD,服务器会在响应头里把文件的修改时间传回来。

POST/PUT

GET 和 HEAD 方法是从服务器获取数据,而 POST 和 PUT 方法则是相反操作,向 URI 指定的资源提交数据,数据就放在报文的 body 里。

POST 也是一个经常用到的请求方法,只要向服务器发送数据,用的大多数都是 POST。

PUT 的作用与 POST 类似,也可以向服务器提交数据,但与 POST 存在微妙的不同,通常 POST 表示的是“新建”“create”的含义,而 PUT 则是“修改”“update”的含义。在实际应用中,PUT 用到的比较少。而且,因为它与 POST 的语义、功能太过近似,有的服务器甚至就直接禁止使用 PUT 方法,只用 POST 方法上传数据。

DELETE

DELETE 方法指示服务器删除资源,因为这个动作危险性太大,所以通常服务器不会执行真正的删除操作,而是对资源做一个删除标记。当然,更多的时候服务器就直接不处理 DELETE 请求。

CONNECT

CONNECT 是一个比较特殊的方法,要求服务器为客户端和另一台远程服务器建立一条特殊的连接隧道,这时 Web 服务器在中间充当了代理的角色。

OPTIONS

OPTIONS 方法要求服务器列出可对资源实行的操作方法,在响应头的 Allow 字段里返回。

TRACE

TRACE 方法多用于对 HTTP 链路的测试或诊断,可以显示出请求 - 响应的传输路径。它的本意是好的,但存在漏洞,会泄漏网站的信息,所以 Web 服务器通常也是禁止使用。

扩展方法

虽然 HTTP/1.1 里规定了八种请求方法,但它并没有限制我们只能用这八种方法,这也体现了 HTTP 协议良好的扩展性,我们可以任意添加请求动作,只要请求方和响应方都能理解就行。

安全与幂等

在 HTTP 协议里,所谓的“安全”是指请求方法不会“破坏”服务器上的资源,即不会对服务器上的资源造成实质的修改。

只有 GET 和 HEAD 方法是“安全”的,因为它们是“只读”操作。

而 POST/PUT/DELETE 操作会修改服务器上的资源,增加或删除数据,所以是“不安全”的。

所谓的“幂等”实际上是一个数学用语,被借用到了 HTTP 协议里,意思是多次执行相同的操作,结果也都是相同的,即多次“幂”后结果“相等”。

GET 和 HEAD 既是安全的也是幂等的,DELETE 可以多次删除同一个资源,效果都是“资源不存在”,所以也是幂等的。

POST 是“新增或提交数据”,多次提交数据会创建多个资源,所以不是幂等的。

PUT 是“替换或更新数据”,多次更新一个资源,资源还是会第一次更新的状态,所以是幂等的。

URI

URI,也就是统一资源标识符。因为它经常出现在浏览器的地址栏里,所以俗称为“网络地址”,简称“网址”。

严格地说,URI 不完全等同于网址,它包含有 URL 和 URN 两个部分,在 HTTP 世界里用的网址实际上是 URL——统一资源定位符。但因为 URL 实在是太普及了,所以常常把这两者简单地视为相等。

URI 的格式

URI 本质上是一个字符串,这个字符串的作用是唯一地标记资源的位置或者名字。

URI 最常用的形式,由 scheme、host:port、path 和 query 四个部分组成。

URI 的基本组成

URI 第一个组成部分叫 scheme,翻译成中文叫“方案名”或者“协议名”,表示资源应该使用哪种协议来访问。最常见的就是“http”了,表示使用 HTTP 协议。另外还有“https”,表示使用经过加密、安全的 HTTPS 协议。此外还有其他不是很常见的 scheme,例如 ftp、ldap、file、news 等。

浏览器或者你的应用程序看到 URI 里的 scheme,就知道下一步该怎么走了,会调用相应的 HTTP 或者 HTTPS 下层 API。如果一个 URI 没有提供 scheme,即使后面的地址再完善,也是无法处理的。

在 scheme 之后,必须是三个特定的字符“://”,它把 scheme 和后面的部分分离开。

在“://”之后,是被称为“authority”的部分,表示资源所在的主机名,通常的形式是“host:port”,即主机名加端口号。主机名可以是 IP 地址或者域名的形式,必须要有,否则浏览器就会找不到服务器。但端口号有时可以省略,浏览器等客户端会依据 scheme 使用默认的端口号,例如 HTTP 的默认端口号是 80,HTTPS 的默认端口号是 443。

有了协议名和主机地址、端口号,再加上后面标记资源所在位置的 path,浏览器就可以连接服务器访问资源了。URI 里 path 采用了类似文件系统“目录”“路径”的表示方式,采用了 “/”风格。它与 scheme 后面的“://”是一致的。URI 的 path 部分必须以“/”开始,也就是必须包含“/”,不要把“/”误认为属于前面 authority。

在 HTTP 报文里的 URI与浏览器里输入URI有很大的不同,协议名和主机名都不见了,只剩下了后面的部分。这是因为协议名和主机名已经分别出现在了请求行的版本号和请求头的 Host 字段里,没有必要再重复。当然,在请求行里使用完整的 URI 也是可以的。客户端和服务器看到的 URI 是不一样的。客户端看到的必须是完整的 URI,使用特定的协议去连接特定的主机,而服务器看到的只是报文请求行里被删除了协议名和主机名的 URI。

URI 的查询参数

使用“协议名 + 主机名 + 路径”的方式,已经可以精确定位网络上的任何资源了。但这还不够,很多时候我们还想在操作资源的时候附加一些额外的修饰参数。

URI 后面还有一个“query”部分,它在 path 之后,用一个“?”开始,但不包含“?”,表示对资源附加的额外要求。查询参数 query 有一套自己的格式,是多个“key=value”的字符串,这些 KV 值用字符“&”连接,浏览器和服务器都可以按照这个格式把长串的查询参数解析成可理解的字典或关联数组形式。

URI 的完整格式

URI 还有一个“真正”的完整形态。

第一个多出的部分是协议名之后、主机名之前的身份信息“user:passwd@”,表示登录主机时的用户名和密码,但现在已经不推荐使用这种形式了,因为它把敏感信息以明文形式暴露出来,存在严重的安全隐患。

第二个多出的部分是查询参数后的片段标识符“#fragment”,它是 URI 所定位的资源内部的一个“锚点”或者说是“标签”,浏览器可以在获取资源后直接跳转到它指示的位置。但片段标识符仅能由浏览器这样的客户端使用,服务器是看不到的。也就是说,浏览器永远不会把带“#fragment”的 URI 发送给服务器,服务器也永远不会用这种方式去处理资源的片段。

URI 的编码

URI 引入了编码机制,对于 ASCII 码以外的字符集和特殊字符做一个特殊的操作,把它们转换成与 URI 语义不冲突的形式。

URI 转义的规则有点“简单粗暴”,直接把非 ASCII 码或特殊字符转换成十六进制字节值,然后前面再加上一个“%”。

而中文、日文等则通常使用 UTF-8 编码后再转义。

有了这个编码规则后,URI 就更加完美了,可以支持任意的字符集用任何语言来标记资源。不过我们在浏览器的地址栏里通常是不会看到这些转义后的“乱码”的,这实际上是浏览器一种“友好”表现。

响应状态码

状态码分成五类,用数字的第一位表示分类,而 099 不用,这样状态码的实际可用范围就大大缩小了,由 000999 变成了 100~599。这五类的具体含义是:

  • 1××:提示信息,表示目前是协议处理的中间状态,还需要后续的操作。
  • 2××:成功,报文已经收到并被正确处理。
  • 3××:重定向,资源位置发生变动,需要客户端重新发送请求。
  • 4××:客户端错误,请求报文有误,服务器无法处理。
  • 5××:服务器错误,服务器在处理请求时内部发生了错误。

在 HTTP 协议中,客户端作为请求的发起方,获取响应报文后,需要通过状态码知道请求是否被正确处理,是否要再次发送请求,如果出错了原因又是什么。这样才能进行下一步的动作,要么发送新请求,要么改正错误重发请求。服务器端作为请求的接收方,也应该很好地运用状态码。在处理请求时,选择最恰当的状态码回复客户端,告知客户端处理的结果,指示客户端下一步应该如何行动。特别是在出错的时候,尽量不要简单地返 400、500 这样意思含糊不清的状态码。

状态码的定义是开放的,允许自行扩展。所以 Apache、Nginx 等 Web 服务器都定义了一些专有的状态码。如果你自己开发 Web 应用,也完全可以在不冲突的前提下定义新的代码。

1××

1××类状态码属于提示信息,是协议处理的中间状态,实际能够用到的时候很少。

“101 Switching Protocols”。它的意思是客户端使用 Upgrade 头字段,要求在 HTTP 协议的基础上改成其他的协议继续通信,比如 WebSocket。而如果服务器也同意变更协议,就会发送状态码 101,但这之后的数据传输就不会再使用 HTTP 了。

2××

2××类状态码表示服务器收到并成功处理了客户端的请求,这也是客户端最愿意看到的状态码。

“200 OK”是最常见的成功状态码,表示一切正常,服务器如客户端所期望的那样返回了处理结果,如果是非 HEAD 请求,通常在响应头后都会有 body 数据。

“204 No Content”是另一个很常见的成功状态码,它的含义与“200 OK”基本相同,但响应头后没有 body 数据。所以对于 Web 服务器来说,正确地区分 200 和 204 是很必要的。

“206 Partial Content”是 HTTP 分块下载或断点续传的基础,在客户端发送“范围请求”、要求获取资源的部分数据时出现,它与 200 一样,也是服务器成功处理了请求,但 body 里的数据不是资源的全部,而是其中的一部分。状态码 206 通常还会伴随着头字段“Content-Range”,表示响应报文里 body 数据的具体范围,供客户端确认,例如“Content-Range: bytes 0-99/2000”,意思是此次获取的是总计 2000 个字节的前 100 个字节。

3××

3××类状态码表示客户端请求的资源发生了变动,客户端必须用新的 URI 重新发送请求获取资源,也就是通常所说的“重定向”,包括著名的 301、302 跳转。

“301 Moved Permanently”称“永久重定向”,含义是此次请求的资源已经不存在了,需要改用新的 URI 再次访问。

“302 Found”,曾经的描述短语是“Moved Temporarily”,称“临时重定向”,意思是请求的资源还在,但需要暂时用另一个 URI 来访问。301 和 302 都会在响应头里使用字段 Location 指明后续要跳转的 URI,最终的效果很相似,浏览器都会重定向到新的 URI。两者的根本区别在于语义,一个是“永久”,一个是“临时”。

“304 Not Modified” 是一个比较有意思的状态码,它用于 If-Modified-Since 等条件请求,表示资源未修改,用于缓存控制。该状态码表示客户端发送附带条件的请求时,服务器端允许请求访问资源,但未满足条件的情况。304 虽然被划分在 3XX 类别中,但是和重定向没有关系。是告诉客户端有缓存,直接使用缓存中的数据。返回页面的只有头部信息,是没有内容部分的,这样在一定程度上提高了网页的性能。

4××

4××类状态码表示客户端发送的请求报文有误,服务器无法处理,它就是真正的“错误码”含义了。

“400 Bad Request”是一个通用的错误码,表示请求报文有错误,但具体是数据格式错误、缺少请求头还是 URI 超长它没有明确说,只是一个笼统的错误。

“403 Forbidden”实际上不是客户端的请求出错,而是表示服务器禁止访问资源。原因可能多种多样,例如信息敏感、法律禁止等。

“404 Not Found”的原意是资源在本服务器上未找到,所以无法提供给客户端。但现在已经被“滥用了”。

405 Method Not Allowed:不允许使用某些方法操作资源,例如不允许 POST 只能 GET。

406 Not Acceptable:资源无法满足客户端请求的条件,例如请求中文但只有英文。

408 Request Timeout:请求超时,服务器等待了过长的时间。

409 Conflict:多个请求发生了冲突,可以理解为多线程并发时的竞态。

413 Request Entity Too Large:请求报文里的 body 太大。

414 Request-URI Too Long:请求行里的 URI 太大。

429 Too Many Requests:客户端发送了太多的请求,通常是由于服务器的限连策略。

431 Request Header Fields Too Large:请求头某个字段或总体太大;

5××

5××类状态码表示客户端请求报文正确,但服务器在处理时内部发生了错误,无法返回应有的响应数据,是服务器端的“错误码”。

“500 Internal Server Error”与 400 类似,也是一个通用的错误码,服务器究竟发生了什么错误我们是不知道的。不过对于服务器来说这应该算是好事,通常不应该把服务器内部的详细信息,例如出错的函数调用栈告诉外界。虽然不利于调试,但能够防止黑客的窥探或者分析。

“501 Not Implemented”表示客户端请求的功能还不支持。

“502 Bad Gateway”通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的。

“503 Service Unavailable”表示服务器当前很忙,暂时无法响应服务,我们上网时有时候遇到的“网络服务正忙,请稍后重试”的提示信息就是状态码 503。503 是一个“临时”的状态,很可能过几秒钟后服务器就不那么忙了,可以继续提供服务,所以 503 响应报文里通常还会有一个“Retry-After”字段,指示客户端可以在多久以后再次尝试发送请求。

HTTP特点

灵活可扩展

HTTP 协议是一个“灵活可扩展”的传输协议。HTTP 协议最初诞生的时候就比较简单,只规定了报文的基本格式,比如用空格分隔单词,用换行分隔字段,“header+body”等,报文里的各个组成部分都没有做严格的语法语义限制,可以由开发者任意定制。

HTTP 对“可靠传输”的定义上,它不限制具体的下层协议,不仅可以使用 TCP、还可以使用 SSL/TLS,甚至是基于 UDP 的 QUIC,下层可以随意变化,而上层的语义则始终保持稳定。

可靠传输

HTTP 协议是一个“可靠”的传输协议。这个特点显而易见,因为 HTTP 协议是基于 TCP/IP 的,而 TCP 本身是一个“可靠”的传输协议,所以 HTTP 自然也就继承了这个特性,能够在请求方和应答方之间“可靠”地传输数据。它的具体做法与 TCP/UDP 差不多,都是对实际传输的数据(entity)做了一层包装,加上一个头,然后调用 Socket API,通过 TCP/IP 协议栈发送或者接收。

应用层协议

HTTP 协议是一个应用层的协议。在 TCP/IP 诞生后的几十年里,虽然出现了许多的应用层协议,但它们都仅关注很小的应用领域,局限在很少的应用场景。例如 FTP 只能传输文件、SMTP 只能发送邮件、SSH 只能远程登录等,在通用的数据传输方面比较差。

请求 - 应答

HTTP 协议使用的是请求 - 应答通信模式。这个请求 - 应答模式是 HTTP 协议最根本的通信模型,通俗来讲就是“一发一收”“有来有去”。请求 - 应答模式也明确了 HTTP 协议里通信双方的定位,永远是请求方先发起连接和请求,是主动的,而应答方只有在收到请求后才能答复,是被动的,如果没有请求时不会有任何动作。当然,请求方和应答方的角色也不是绝对的,在浏览器 - 服务器的场景里,通常服务器都是应答方,但如果将它用作代理连接后端服务器,那么它就可能同时扮演请求方和应答方的角色。

无状态

HTTP 协议是无状态的。“状态”其实就是客户端或者服务器里保存的一些数据或者标志,记录了通信过程中的一些变化信息。HTTP在整个协议里没有规定任何的“状态”,客户端和服务器永远是处在一种“无知”的状态。建立连接前两者互不知情,每次收发的报文也都是互相独立的,没有任何的联系。收发报文也不会对客户端或服务器产生任何影响,连接后也不会要求保存任何信息。“无状态”形象地来说就是“没有记忆能力”,每个请求都是互相独立,毫无关联的,两端都不会记录请求相关的信息。注意HTTP的“无状态”与响应头里的“状态码”是不同的概念。

因为服务器没有“记忆能力”,所以就不需要额外的资源来记录状态信息,不仅实现上会简单一些,而且还能减轻服务器的负担,能够把更多的 CPU 和内存用来对外提供服务。而且,“无状态”也表示服务器都是相同的,没有“状态”的差异,所以可以很容易地组成集群,让负载均衡把请求转发到任意一台服务器,不会因为状态不一致导致处理出错,使用堆机器的办法实现高并发高可用。

既然服务器没有“记忆能力”,它就无法支持需要连续多个步骤的“事务”操作。例如电商购物,首先要登录,然后添加购物车,再下单、结算、支付,这一系列操作都需要知道用户的身份才行,但“无状态”服务器是不知道这些请求是相互关联的,每次都得问一遍身份信息,不仅麻烦,而且还增加了不必要的数据传输量。

明文

明文传输。“明文”意思就是协议里的报文(准确地说是 header 部分)不使用二进制数据,而是用简单可阅读的文本形式。对比 TCP、UDP 这样的二进制协议,它的优点显而易见,不需要借助任何外部工具,用浏览器、Wireshark抓包后,直接用肉眼就可以很容易地查看或者修改,为我们的开发调试工作带来极大的便利。

明文的缺点也是一样显而易见,HTTP 报文的所有信息都会暴露,在传输链路的每一个环节上都毫无隐私可言,不怀好意的人只要侵入了这个链路里的某个设备,简单地“旁路”一下流量,就可以实现对通信的窥视。

不安全

安全有很多的方面,明文只是“机密”方面的一个缺点,在“身份认证”和“完整性校验”这两方面 HTTP 也是欠缺的。

“身份认证”简单来说就是“怎么证明你就是你”。HTTP 没有提供有效的手段来确认通信双方的真实身份。虽然协议里有一个基本的认证机制,但因为刚才所说的明文传输缺点,这个机制几乎可以说是“纸糊的”,非常容易被攻破。如果仅使用 HTTP 协议,很可能会连到一个页面一模一样但却是个假冒的网站,然后再被“钓”走各种私人信息。

HTTP 协议也不支持“完整性校验”,数据在传输过程中容易被篡改而无法验证真伪。黑客可以篡改信息。虽然银行可以用 MD5、SHA1 等算法给报文加上数字摘要,但还是因为“明文”这个致命缺点,黑客可以连同摘要一同修改,最终还是判断不出报文是否被篡改。

性能

“请求 - 应答”模式产生了 HTTP 的性能问题,也就是“队头阻塞”,当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一并被阻塞,会导致客户端迟迟收不到数据。

HTTP的实体数据

数据类型与编码

在 TCP/IP 协议栈里,传输数据基本上都是“header+body”的格式。但 TCP、UDP 因为是传输层的协议,它们不会关心 body 数据是什么,只要把数据发送到对方就算是完成了任务。而 HTTP 协议则不同,它是应用层的协议,数据到达之后工作只能说是完成了一半,还必须要告诉上层应用这是什么数据才行,MIME可以解决。

MIME 把数据分成了八大类,每个大类下再细分出多个子类,形式是“type/subtype”的字符串,符合了 HTTP 明文的特点,所以能够很容易地纳入 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,即不透明的二进制数据。

但仅有 MIME type 还不够,因为 HTTP 在传输时为了节约带宽,有时候还会压缩数据,所以还需要有一个“Encoding type”,告诉数据是用的什么编码格式,这样对方才能正确解压缩,还原出原始的数据。常用的有下面三种:

  • gzip:GNU zip 压缩格式,也是互联网上最流行的压缩格式。
  • deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip。
  • br:一种专门为 HTTP 优化的新压缩算法(Brotli)。

数据类型使用的头字段

HTTP 协议定义了两个 Accept 请求头字段和两个 Content 实体头字段,用于客户端和服务器进行“内容协商”。也就是说,客户端用 Accept 头告诉服务器希望接收什么样的数据,而服务器用 Content 头告诉客户端实际发送了什么样的数据。

Accept 字段标记的是客户端可理解的 MIME type,可以用“,”做分隔符列出多个类型,让服务器有更多的选择余地。

服务器会在响应报文里用头字段 Content-Type 告诉实体数据的真实类型。

Accept-Encoding 字段标记的是客户端支持的压缩格式,例如上面说的 gzip、deflate 等,同样也可以用“,”列出多个,服务器可以选择其中一种来压缩数据,实际使用的压缩格式放在响应头字段 Content-Encoding 里。不过这两个字段是可以省略的,如果请求报文里没有 Accept-Encoding 字段,就表示客户端不支持压缩数据;如果响应报文里没有 Content-Encoding 字段,就表示响应数据没有被压缩。

语言类型与编码

所谓的“语言类型”就是人类使用的自然语言,例如英语、汉语、日语等,而这些自然语言可能还有下属的地区性方言,所以在需要明确区分的时候也要使用“type-subtype”的形式,不过这里的格式与数据类型不同,分隔符不是“/”,而是“-”。例如:en 表示任意的英语,en-US 表示美式英语,en-GB 表示英式英语,而 zh-CN 就表示汉语。

关于自然语言的计算机处理还有一个东西叫做“字符集”。Unicode 和 UTF-8把世界上所有的语言都容纳在一种编码方案里,遵循 UTF-8 字符编码方式的 Unicode 字符集也成为了互联网上的标准字符集。

语言类型使用的头字段

HTTP 协议也使用 Accept 请求头字段和 Content 实体头字段,用于客户端和服务器就语言与编码进行“内容协商”。Accept-Language 字段标记了客户端可理解的自然语言,也允许用“,”做分隔符列出多个类型。

相应的,服务器应该在响应报文里用头字段 Content-Language 告诉客户端实体数据使用的实际语言类型。

字符集在 HTTP 里使用的请求头字段是 Accept-Charset,但响应头里却没有对应的 Content-Charset,而是在 Content-Type 字段的数据类型后面用“charset=xxx”来表示,这点需要特别注意。

不过现在的浏览器都支持多种字符集,通常不会发送 Accept-Charset,而服务器也不会发送 Content-Language,因为使用的语言完全可以由字符集推断出来,所以在请求头里一般只会有 Accept-Language 字段,响应头里只会有 Content-Type 字段。

内容协商的质量值

在 HTTP 协议里用 Accept、Accept-Encoding、Accept-Language 等请求头字段进行内容协商的时候,还可以用一种特殊的“q”参数表示权重来设定优先级,这里的“q”是“quality factor”的意思。权重的最大值是 1,最小值是 0.01,默认值是 1,如果值是 0 就表示拒绝。具体的形式是在数据类型或语言代码后面加一个“;”,然后是“q=value”。

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

它表示浏览器最希望使用的是 HTML 文件,权重是 1,其次是 XML 文件,权重是 0.9,最后是任意数据类型,权重是 0.8。服务器收到请求头后,就会计算权重,再根据自己的实际情况优先输出 HTML 或者 XML。

内容协商的结果

内容协商的过程是不透明的,每个 Web 服务器使用的算法都不一样。但有的时候,服务器会在响应头里多加一个 Vary 字段,记录服务器在内容协商时参考的请求头字段,给出一点信息。

Vary: Accept-Encoding,User-Agent,Accept

这个 Vary 字段表示服务器依据了 Accept-Encoding、User-Agent 和 Accept 这三个头字段,然后决定了发回的响应报文。

Vary 字段可以认为是响应报文的一个特殊的“版本标记”。每当 Accept 等请求头变化时,Vary 也会随着响应报文一起变化。

HTTP传输大文件的方法

数据压缩

通常浏览器在发送请求时都会带着“Accept-Encoding”头字段,里面是浏览器支持的压缩格式列表,例如 gzip、deflate、br 等,这样服务器就可以从中选择一种压缩算法,放进“Content-Encoding”响应头里,再把原数据压缩后发给浏览器。

不过这个方法有个缺点,gzip 等压缩算法通常只对文本文件有较好的压缩率,而图片、音频视频等多媒体数据本身就已经是高度压缩的,再用 gzip 处理也不会变小(甚至还有可能会增大一点),所以它就失效了。

分块传输

HTTP 协议可以用“chunked”分块传输编码,在响应报文里用头字段“Transfer-Encoding: chunked”来表示,意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。

Transfer-Encoding字段最常见的值是chunked,但也可以用gzip、deflate等,表示传输时使用了压缩编码。注意这与Content-Encoding

不同,Transfer-Encoding在传输后会被自动解码还原出原始数据,而Content-Encoding则必须由应用自行解码。

分块传输也可以用于“流式数据”,例如由数据库动态生成的表单页面,这种情况下 body 数据的长度是未知的,无法在头字段“Content-Length”里给出确切的长度,所以也只能用 chunked 方式分块发送。

“Transfer-Encoding: chunked”和“Content-Length”这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知。

分块传输的编码规则,采用了明文的方式,类似响应头。每个分块包含两个部分,长度头和数据块。

长度头用 16 进制数字表示长度;数据块紧跟在长度头后;最后用一个长度为 0 的块表示结束。

范围请求

HTTP 协议提出了“范围请求”(range requests)的概念,允许客户端在请求头里使用专用字段来表示只获取文件的一部分。

范围请求不是 Web 服务器必备的功能,所以服务器必须在响应头里使用字段“Accept-Ranges: bytes”明确告知客户端:“我是支持范围请求的”。如果不支持的话,服务器可以发送“Accept-Ranges: none”,或者干脆不发送“Accept-Ranges”字段,这样客户端就认为服务器没有实现范围请求功能,只能收发整块文件了。

请求头 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”。

服务器收到 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 发给客户端,一个范围请求就算是处理完了。

不仅看视频的拖拽进度需要范围请求,常用的下载工具里的多段下载、断点续传也是基于它实现的,要点是:

先发个 HEAD,看服务器是否支持范围请求,同时获取文件的大小。

开 N 个线程,每个线程使用 Range 字段划分出各自负责下载的片段,发请求传输数据。

下载意外中断也不怕,不必重头再来,只要根据上次的下载记录,用 Range 请求剩下的那一部分就可以了。

此外注意range是应用于原文件的。如果原来的文件是gzip的,则应用于压缩后的文件。如果原文件是文本,而是在传输过程中被压缩,那么就应用于压缩前的数据。

多段数据

刚才说的范围请求一次只获取一个片段,它还支持在 Range 头里使用多个“x-y”,一次性获取多个片段数据。这种情况需要使用一种特殊的 MIME 类型:“multipart/byteranges”,表示报文的 body 是由多段字节序列组成的,并且还要用一个参数“boundary=xxx”给出段之间的分隔标记。多段数据的格式与分块传输也比较类似,但它需要用分隔标记 boundary 来区分不同的片段。

每一个分段必须以“- -boundary”开始(前面加两个“-”),之后要用“Content-Type”和“Content-Range”标记这段数据的类型和所在范围,然后就像普通的响应头一样以回车换行结束,再加上分段数据,最后用一个“- -boundary- -”(前后各有两个“-”)表示所有的分段结束。

HTTP的连接管理

短连接

HTTP 协议最初(0.9/1.0)是个非常简单的协议,通信过程也采用了简单的“请求 - 应答”方式。它底层的数据传输基于 TCP/IP,每次发送请求前需要先与服务器建立连接,收到响应报文后会立即关闭连接。因为客户端与服务器的整个连接过程很短暂,不会与服务器保持长时间的连接状态,所以就被称为“短连接”。早期的 HTTP 协议也被称为是“无连接”的协议。短连接的缺点相当严重,因为在 TCP 协议里,建立连接和关闭连接都是非常“昂贵”的操作。TCP 建立连接要有“三次握手”,发送 3 个数据包;关闭连接是“四次挥手”。传输效率较低。

长连接

针对短连接暴露出的缺点,HTTP 协议就提出了“长连接”的通信方式,也叫“连接保活”(keep alive)。

连接相关的头字段

由于长连接对性能的改善效果非常显著,所以在 HTTP/1.1 中的连接都会默认启用长连接。不需要用什么特殊的头字段指定,只要向服务器发送了第一次请求,后续的请求都会重复利用第一次打开的 TCP 连接,也就是长连接,在这个连接上收发数据。

我们也可以在请求头里明确地要求使用长连接机制,使用的字段是 Connection,值是“keep-alive”。不过不管客户端是否显式要求长连接,如果服务器支持长连接,它总会在响应报文里放一个“Connection: keep-alive”字段。Connection字段还有一个取值:“Connection

:Upgrade“,配合状态码101表示协议升级,例如从HTTP切换到WebSocket。

不过长连接也有一些小缺点,问题就出在它的“长”字上。因为 TCP 连接长时间不关闭,服务器必须在内存里保存它的状态,这就占用了服务器的资源。如果有大量的空闲长连接只连不发,就会很快耗尽服务器的资源,导致服务器无法为真正有需要的用户提供服务。所以,长连接也需要在恰当的时间关闭,不能永远保持与服务器的连接,这在客户端或者服务器都可以做到。

在客户端,可以在请求头里加上“Connection: close”字段,告诉服务器:“这次通信后就关闭连接”。服务器看到这个字段,就知道客户端要主动关闭连接,于是在响应报文里也加上这个字段,发送之后就关闭 TCP 连接。

服务器端通常不会主动关闭连接,但也可以使用一些策略。拿 Nginx 来举例,它有两种方式:

  • 使用“keepalive_timeout”指令,设置长连接的超时时间,如果在一段时间内连接上没有任何数据收发就主动断开连接,避免空闲连接占用系统资源。
  • 使用“keepalive_requests”指令,设置长连接上可发送的最大请求次数。比如设置成 1000,那么当 Nginx 在这个连接上处理了 1000 个请求后,也会主动断开连接。

另外,客户端和服务器都可以在报文里附加通用头字段“Keep-Alive: timeout=value”,限定长连接的超时时间。但这个字段的约束力并不强,通信的双方可能并不会遵守,所以不太常见。

队头阻塞

“队头阻塞”与短连接和长连接无关,而是由 HTTP 基本的“请求 - 应答”模型所导致的。因为 HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求被最优先处理。如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本。

性能优化

并发连接”(concurrent connections),也就是同时对一个域名发起多个长连接,用数量来解决质量的问题。

但这种方式也存在缺陷。如果每个客户端都想自己快,建立很多个连接,用户数×并发数就会是个天文数字。服务器的资源根本就扛不住,或者被服务器认为是恶意攻击,反而会造成“拒绝服务”。所以,HTTP 协议建议客户端使用并发,但不能“滥用”并发。RFC2616 里明确限制每个客户端最多并发 2 个连接。不过实践证明这个数字实在是太小了,众多浏览器都“无视”标准,把这个上限提高到了 6~8。后来修订的 RFC7230 也就“顺水推舟”,取消了这个“2”的限制。

“域名分片”(domain sharding)技术,还是用数量来解决质量的思路。通过多开几个域名,而这些域名都指向同一台服务器,这样实际长连接的数量就又上去了。

HTTP的重定向和跳转

点击页面“链接”的跳转动作是由浏览器的使用者主动发起的,可以称为“主动跳转”,但还有一类跳转是由服务器来发起的,浏览器使用者无法控制,相对地就可以称为“被动跳转”,这在 HTTP 协议里有个专门的名词,叫做“重定向”。

重定向的过程

头字段“Location: /index.html”,它就是 301/302 重定向跳转的秘密所在。

“Location”字段属于响应字段,必须出现在响应报文里。但只有配合 301/302 状态码才有意义,它标记了服务器要求重定向的 URI。

浏览器收到 301/302 报文,会检查响应头里有没有“Location”。如果有,就从字段值里提取出 URI,发出新的 HTTP 请求,相当于自动替我们点击了这个链接。在“Location”里的 URI 既可以使用绝对 URI,也可以使用相对 URI。所谓“绝对 URI”,就是完整形式的 URI,包括 scheme、host:port、path 等。所谓“相对 URI”,就是省略了 scheme 和 host:port,只有 path 和 query 部分,是不完整的,但可以从请求上下文里计算得到。

重定向时如果只是在站内跳转,可以使用相对 URI。但如果要跳转到站外,就必须用绝对 URI。

重定向状态码

301 俗称“永久重定向”,意思是原 URI 已经“永久”性地不存在了,今后的所有请求都必须改用新的 URI。浏览器看到 301,就知道原来的 URI“过时”了,就会做适当的优化。比如历史记录、更新书签,下次可能就会直接用新的 URI 访问,省去了再次跳转的成本。搜索引擎的爬虫看到 301,也会更新索引库,不再使用老的 URI。

302 俗称“临时重定向”,意思是原 URI 处于“临时维护”状态,新的 URI 是起“顶包”作用的“临时工”。浏览器或者爬虫看到 302,会认为原来的 URI 仍然有效,但暂时不可用,所以只会执行简单的跳转页面,不记录新的 URI,也不会有其他的多余动作,下次访问还是用原 URI。

301/302 是最常用的重定向状态码,在 3××里剩下的几个还有:

  • 303 See Other:类似 302,但要求重定向后的请求改为 GET 方法,访问一个结果页面,避免 POST/PUT 重复操作。
  • 307 Temporary Redirect:类似 302,但重定向后请求里的方法和实体不允许变动,含义比 302 更明确。
  • 308 Permanent Redirect:类似 307,不允许重定向后的请求变动,但它是 301“永久重定向”的含义。

不过这三个状态码的接受程度较低,有的浏览器和服务器可能不支持,开发时应测试确认浏览器的实际效果后才能使用。

重定向的应用场景

“资源不可用”,需要用另一个新的 URI 来代替。至于不可用的原因那就很多了。例如域名变更、服务器变更、网站改版、系统维护,这些都会导致原 URI 指向的资源无法访问,为了避免出现 404,就需要用重定向跳转到新的 URI,继续为网民提供服务。

“避免重复”,让多个网址都跳转到一个 URI,增加访问入口的同时还不会增加额外的工作量。例如,有的网站都会申请多个名称类似的域名,然后把它们再重定向到主站上。

重定向的相关问题

第一个问题是“性能损耗”。很明显,重定向的机制决定了一个跳转会有两次请求 - 应答,比正常的访问多了一次。虽然 301/302 报文很小,但大量的跳转对服务器的影响也是不可忽视的。站内重定向还好说,可以长连接复用,站外重定向就要开两个连接,如果网络连接质量差,那成本就高多了,会严重影响用户的体验。所以重定向应当适度使用。

第二个问题是“循环跳转”。如果重定向的策略设置欠考虑,可能会出现“A=>B=>C=>A”的无限循环,不停地在这个链路里转圈圈,后果可想而知。所以 HTTP 协议特别规定,浏览器必须具有检测“循环跳转”的能力,在发现这种情况时应当停止发送请求并给出错误提示。

外部重定向和内部重定向

外部重定向,服务器会把重定向的地址给浏览器,然后浏览器再次的发起请求。

内部重定向,直接在服务器内部跳转URL,不会发出HTTP请求,服务器会直接把重定向的资源返给浏览器,不需要再次在浏览器发起请求。

HTTP的Cookie机制