Skip to content

不要关注

透视 HTTP 协议笔记

课程目录

破冰篇

时势与英雄:HTTP 协议的前世今生

HTTP 是什么?HTTP 又不是什么

HTTP 世界全览(上):与 HTTP 相关的各种概念

HTTP 世界全览(下):与 HTTP 相关的各种协议

”七层“和”四层“是什么?”五层“”六层“哪去了?

域名里有哪些门道?

自己动手,搭建 HTTP 实验环境

基础篇

键入网址后按下回车,后面究竟发生了什么?

HTTP 报文是什么样子的?

应该如何理解请求方法?

你能写出正确的网址吗?

响应状态码该怎么用?

HTTP 协议有哪些特点?

HTTP 有哪些优点和缺点?

进阶篇

海纳百川:HTTP 的实体数据

把大象装进冰箱:HTTP 传输大文件的方法

排队也要讲效率:HTTP 的连接管理

四通八达:HTTP 重定向和跳转

让我知道你是谁:HTTP 的 Cookie 机制

生鲜速递:HTTP 的缓存控制

良心中间商:HTTP 的代理服务

冷链周转:HTTP 的缓存代理

安全篇

HTTPS 是什么?SSL/TLC 又是什么?

固若金汤的根本(上):对称加密与非对称加密

固若金汤的根本(下):数字签名与证书

信任始于握手:TLS1.2 连接过程解析

更好更快的握手:TLS1.3 特性解析

连接太慢该怎么办:HTTPS 的优化

我应该迁移到 HTTPS 吗

飞翔篇

时代之风(上):HTTP/2 特性概览

时代之风(下):HTTP/2 内核剖析

未来之路:HTTP/3 展望

我应该迁移到 HTTP/2 吗?

探索篇

Nginx:高性能的 Web 服务器

OpenResty:更灵活的 Web 服务器

WAF:保护我们的网络服务

CDN:加速我们的网络服务

WebSocket:沙盒里的 TCP

总结篇

HTTP 性能优化面面观(上)

HTTP 性能优化面面观(下)

HTTP 的前世今生

史前年代

20 世纪 60 年代,美国国防部高等研究计划署(ARPA)建立了 ARPA 网,它有四个分布在各地的节点,被认为是如今互联网的“始祖”。

70 年代,基于对 ARPA 网的实践和思考,研究人员发明出了著名的 TCP/IP 协议。并在 80 年代中期进入了 UNIX 系统内核,促使更多的计算机接入了互联网

创世纪

1989 年,任职于欧洲核子研究中心(CERN)的蒂姆·博纳斯·李发表了一篇论文,提出了在互联网上构建超链接文档系统的构想。这篇论文中他确立了三项关键技术

  1. URI:即统一资源标识符,作为互联网上资源的唯一身份;
  2. HTML:即超文本标记语言,描述超文本文档;
  3. HTTP:即超文本传输协议,用来传输超文本;

基于它们,就可以把超文本系统完美地运行在互联网上,让各地的人们能够自由地共享信息,蒂姆把这个系统称为“万维网”(World Wide Web),也就是我们现在所熟知的 Web

HTTP/0.9

它只支持纯文本格式,以及只允许“GET”请求,并且在相应请求之后立即关闭连接,功能有限

“把简单的系统变复杂”,要比“把复杂的系统变简单”容易的多

HTTP/1.0

1993 年,NCSA(美国国家超级计算应用中心)开发出了 Mosaic,是第一个可以图文混排的浏览器,随后又在 1995 年开发出了服务器软件 Apache,简化了 HTTP 服务器的搭建工作

同一时期,1992 年发明了 JPEG 图像格式,1995 年发明了 MP3 音乐格式

HTTP/1.0 版本在 1996 年正式发布。它在多方面增强了 0.9 版,形式上已经和我们现在的 HTTP 差别不大了,例如:

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

但 HTTP/1.0 并不是一个“标准”,只是记录已有实践和模式的一份参考文档,不具有实际的约束力,相当于一个“备忘录”

HTTP/1.1

网景与微软的“浏览器大战”推动了 Web 的发展,在“浏览器大战”结束之后的 1999 年,HTTP/1.1 发布了 RFC 文档,编号为 2616,成为“正式的标准”。这意味着今后互联网上所有的浏览器、服务器、网关、代理等等,只要用到 HTTP 协议,就必须严格遵守这个标准,相当于是互联网世界的一个“立法”

HTTP/1.1 的主要变更有:

  1. 增加了 PUT、DELETE 等新的方法;
  2. 增加了缓存管理和控制;
  3. 明确了连接管理,允许持久连接;
  4. 允许相应数据分块(chunked),李渔传输大文件;
  5. 强制要求 Host 头,让互联网主机托管成为可能

HTTP/2

2015 年发布 HTTP/2,RFC 编号 7540,由 Google 推动,将自家的 SPDY 协议腿上标准的宝座,称为了 HTTP/2 协议

HTTP/2 的做东充分考虑了现今互联网的现状:宽带、移动、不安全,在高度兼容 HTTP/1.1 的同时在性能上做了很大的改善,主要特点有:

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

虽然 HTTP/2 推出已经 5 个年头了,但因为 HTTP/1.1 实在太过经典和强势,目前普及率还比较低

HTTP/3

Google 发明了一个新的协议,叫做 QUIC,目前 HTTP/3 在标准化指定阶段,也许要两三年后正式发布

历史回顾

  1. HTTP 协议始于三十年前蒂姆·博纳斯·李的一篇论文
  2. HTTP/0.9 是个简单的文本协议,只能获取文本资源;
  3. HTTP/1.0 确立了大部分现有使用的技术,但它不是正式标准;
  4. HTTP/1.1 是目前互联网上使用最广泛的协议,功能也非常完善;
  5. HTTP/2 基于 Google 的 SPDY 协议,注重性能改善,但普及度不高
  6. HTTP/3 基于 Google 的 QUIC 协议,是将来的发展方向

HTTP 是什么?HTTP 又不是什么?

HTTP 就是超文本传输协议。它是协议、也是传输、又是超文本。但它不是互联网、不是编程语言、不是 HTML、不是一个孤立的协议

  1. HTTP 是一个用在计算机世界里的协议,它确立了一种计算机之间交流通信的规范,以及相关的各种控制和错误处理方式。
  2. HTTP 专门用来在两点之间传输数据,不能用于广播、寻址或路由。
  3. HTTP 传输的是文字、图片、音频、视频等超文本数据。
  4. HTTP 是构建互联网的重要基础技术,它没有实体,依赖许多其他的技术来实现,但同时许多技术也都依赖于它。

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

但毫无疑问,HTTP 是构建互联网的一块重要拼图,而且是占比最大的那一块。

HTTP 世界全览:与 HTTP 相关的各种概念

HTTP世界

这张图左边的部分是与 HTTP 有关系的各种协议,比较偏向于理论;而右边的部分是与 HTTP 有关系的各种应用技术,偏向于实际应用。

先看右边这部分

与HTTP相关的各种概念

互联网的正式名称是 Internet,里面存储着无穷无尽的信息资源,我们通常所说的 上网 实际上访问的只是互联网的一个子集 「万维网(World Wide Web)」,它基于 HTTP 协议 ,传输 HTML 等超文本资源,能力也就被限制在 HTTP 协议之内。

互联网上还有许多万维网之外的资源,例如常用的电子邮件、BT 和 Magnet 点对点下载、FTP 文件下载、SSH 安全登录、各种即时通信服务等等,它们需要用各自的专有协议来访问。

  1. 互联网上绝大部分资源都使用 HTTP 协议传输;
  2. 浏览器是 HTTP 协议里的请求方,即 User Agent;
  3. 服务器是 HTTP 协议里的应答方,常用的有 Apache 和 Nginx;
  4. CDN 位于浏览器和服务器之间,主要起到缓存加速的作用;
  5. 爬虫是另一类 User Agent,是自动访问网络资源的程序

HTTP 世界全览:与 HTTP 相关的各种协议

在上一讲中,我介绍了与 HTTP 相关的浏览器、服务器、CDN、网络爬虫等应用技术。

今天要讲的则是比较偏向于理论的各种 HTTP 相关协议,重点是 TCP/IP、DNS、URI、HTTPS 等,希望能够帮你理清楚它们与 HTTP 的关系。

HTTP 相关协议

TCP/IP

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

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

IP 协议Internet Protocol 的缩写,主要目的是解决寻址和路由问题,以及如何在两点间传送数据包 。IP 协议使用 IP 地址 的概念来定位互联网上的每一台计算机。可以对比一下现实中的电话系统,你拿着的手机相当于互联网上的计算机,而要打电话就必须接入电话网,由通信公司给你分配一个号码,这个号码就相当于 IP 地址。

现在我们使用的 IP 协议大多数是 v4 版,地址是四个用 . 分隔的数字,例如 192.168.0.1 ,总共有 2^32,大约 42 亿个可以分配的地址。看上去好像很多,但互联网的快速发展让地址的分配管理很快就捉襟见肘。所以,就又出现了 v6 版,使用 8 组 : 分隔的数字作为地址,容量扩大了很多,有 2^128 个,在未来的几十年里应该是足够用了。

TCP 协议Transmission Control Protocol 的缩写,意思是 传输控制协议 ,它位于 IP 协议之上,基于 IP 协议提供可靠的、字节流形式的通信,是 HTTP 协议得以实现的基础。

DNS

DNS 域名是 IP 地址的等价替代,需要用域名解析实现到 IP 地址的映射;

URI/URL

DNS 和 IP 地址只是标记了互联网上的主机,但主机上有那么多文本、图片、页面,到底要找哪一个呢?

所以就出现了 URI(Uniform Resource Identifier),中文名称是 统一资源标识符 ,使用它就能够唯一地标记互联网上资源。

URI 另一个更常用的表现形式是 URL(Uniform Resource Locator), 统一资源定位符 ,也就是我们俗称的「网址」,它实际上是 URI 的一个子集,不过因为这两者几乎是相同的,差异不大,所以通常不会做严格的区分。

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

  1. 协议名:即访问该资源应当使用的协议,在这里是 http
  2. 主机名:即互联网上主机的标记,可以是域名或 IP 地址,在这里是 nginx.org
  3. 路径:即资源在主机上的位置,使用 / 分隔多级目录,在这里是 /en/download.html

HTTPS

HTTPS 就相当于这个比喻中的「火星文」,它的全称是 HTTP over SSL/TLS ,也就是运行在 SSL/TLS 协议上的 HTTP。

注意它的名字,这里是 SSL/TLS,而不是 TCP/IP,它是一个 负责加密通信的安全协议 ,建立在 TCP/IP 之上,所以也是个可靠的传输协议,可以被用作 HTTP 的下层。

因为 HTTPS 相当于 HTTP+SSL/TLS+TCP/IP ,其中的 HTTPTCP/IP 我们都已经明白了,只要再了解一下 SSL/TLS,HTTPS 也就能够轻松掌握。

SSL 的全称是 Secure Socket Layer ,由网景公司发明,当发展到 3.0 时被标准化,改名为 TLS,即 Transport Layer Security,但由于历史的原因还是有很多人称之为 SSL/TLS,或者直接简称为 SSL。

SSL 使用了许多密码学最先进的研究成果,综合了对称加密、非对称加密、摘要算法、数字签名、数字证书等技术,能够在不安全的环境中为通信的双方创建出一个秘密的、安全的传输通道,为 HTTP 套上一副坚固的盔甲。

代理

代理(Proxy)是 HTTP 协议中请求方和应答方中间的一个环节,作为 中转站 ,既可以转发客户端的请求,也可以转发服务器的应答。

代理有很多的种类,常见的有:

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

这次我介绍了与 HTTP 相关的各种协议,在这里简单小结一下今天的内容。

  1. TCP/IP 是网络世界最常用的协议,HTTP 通常运行在 TCP/IP 提供的可靠传输基础上;
  2. DNS 域名是 IP 地址的等价替代,需要用域名解析实现到 IP 地址的映射;
  3. URI 是用来标记互联网上资源的一个名字,由 协议名 + 主机名 + 路径 构成,俗称 URL;
  4. HTTPS 相当于 HTTP+SSL/TLS+TCP/IP ,为 HTTP 套了一个安全的外壳;
  5. 代理是 HTTP 传输过程中的中转站,可以实现缓存加速、负载均衡等功能。

常说的四层和七层到底是什么?五层、六层哪去了?

TCP/IP 网络分层模型

TCP/IP 网络分层模型

TCP/IP 协议总共有四层,就像搭积木一样,每一层需要下层的支撑,同时又支撑着上层,任何一层被抽掉都可能会导致整个协议栈坍塌。

第一层叫 链接层 (link layer),负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标记网络上的设备 ,所以有时候也叫 MAC 层。

第二层叫 网际层 或者 网络互连层 (internet layer),IP 协议就处在这一层。因为 IP 协议定义了 IP 地址 的概念,所以就可以在 链接层 的基础上,用 IP 地址取代 MAC 地址 ,把许许多多的局域网、广域网连接成一个虚拟的巨大网络,在这个网络里找设备时只要把 IP 地址再「翻译」成 MAC 地址就可以了。

第三层叫 传输层(transport layer),这个层次协议的职责是保证数据在 IP 地址标记的两点之间可靠地传输,是 TCP 协议工作的层次,另外还有它的一个小伙伴 UDP。

TCP 是一个有状态的协议,需要先与对方建立连接然后才能发送数据 ,而且保证数据不丢失不重复。而 UDP 则比较简单,它无状态,不用事先建立连接就可以任意发送数据,但不保证数据一定会发到对方。两个协议的另一个重要区别在于数据的形式。TCP 的数据是连续的字节流,有先后顺序,而 UDP 则是分散的小数据包,是顺序发,乱序收。

关于 TCP 和 UDP 可以展开讨论的话题还有很多,比如最经典的「三次握手」和「四次挥手」,一时半会很难说完,好在与 HTTP 的关系不是太大,以后遇到了再详细讲解。

协议栈的第四层叫 应用层 (application layer),由于下面的三层把基础打得非常好,所以在这一层就百花齐放了,有各种面向具体应用的协议。例如 Telnet、SSH、FTP、SMTP 等等,当然还有我们的 HTTP。

  • MAC 层的传输单位是帧(frame)
  • IP 层的传输单位是包(packet)
  • TCP 层的传输单位是段(segment)
  • HTTP 的传输单位则是消息或报文(message)

OSI 网络分层模型

第二个网络分层模型:OSI ,全称是 开放式系统互联通信参考模型 (Open System Interconnection Reference Model)。

OSI 网络分层模型

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

不过国际标准组织心里也很清楚,TCP/IP 等协议已经在许多网络上实际运行,再推翻重来是不可能的。所以,OSI 分层模型在发布的时候就明确地表明是一个「参考」,不是强制标准,意思就是说,「你们以后该干什么还干什么,我不管,但面子上还是要按照我说的来」。

两个分层模型的映射关系

现在我们有了两个网络分层模型:TCP/IP 和 OSI,新的问题又出现了,一个是四层模型,一个是七层模型,这两者应该如何互相映射或者说互相解释呢?

好在 OSI 在设计之初就参考了 TCP/IP 等多个协议,可以比较容易但不是很精确地实现对应关系。

两个分层模型的映射关系

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

TCP/IP 协议栈的工作方式

你可以把 HTTP 利用 TCP/IP 协议栈传输数据想象成一个发快递的过程。

TCP/IP 协议栈的工作方式

这次我们学习了 HTTP 所在的网络分层模型,它是工作中常用的交流语言,在这里简单小结一下今天的内容。

  1. TCP/IP 分为四层,核心是二层的 IP 和三层的 TCP,HTTP 在第四层;
  2. OSI 分为七层,基本对应 TCP/IP,TCP 在第四层,HTTP 在第七层;
  3. OSI 可以映射到 TCP/IP,但这期间一、五、六层消失了;
  4. 日常交流的时候我们通常使用 OSI 模型,用四层、七层等术语;
  5. HTTP 利用 TCP/IP 协议栈逐层打包再拆包,实现了数据传输,但下面的细节并不可见。

有一个辨别四层和七层比较好的(但不是绝对的)小窍门,两个凡是

  • 凡是由操作系统负责处理的就是四层或四层以下,
  • 否则,凡是需要由应用程序(也就是你自己写代码)负责处理的就是七层。

域名里有哪些门道

域名的形式

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

域名不仅能够代替 IP 地址,还有许多其他的用途。

在 Apache、Nginx 这样的 Web 服务器里,域名可以用来标识虚拟主机,决定由哪个虚拟主机来对外提供服务,比如在 Nginx 里就会使用 server_name 指令:

nginx
server {
    listen 80;
    server_name time.geekbang.org; # 主机名是 time.geekbang.org
}

域名的解析

就像 IP 地址必须转换成 MAC 地址才能访问主机一样,域名也必须要转换成 IP 地址,这个过程就是 域名解析

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

  1. 根域名服务器(Root DNS Server):管理顶级域名服务器,返回 comnetcn 等顶级域名服务器的 IP 地址;
  2. 顶级域名服务器(Top-level DNS Server):管理各自域名下的权威域名服务器,比如 com 顶级域名服务器可以返回 apple.com 域名服务器的 IP 地址;
  3. 权威域名服务器(Authoritative DNS Server):管理自己域名下主机的 IP 地址,比如 apple.com 权威域名服务器可以返回 www.apple.com 的 IP 地址。

DNS服务器

在这里根域名服务器是关键,它必须是众所周知的,否则下面的各级服务器就无从谈起了。目前全世界共有 13 组根域名服务器,又有数百台的镜像,保证一定能够被访问到。

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

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

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

这些「野生」服务器被称为「非权威域名服务器」,可以缓存之前的查询结果,如果已经有了记录,就无需再向根服务器发起查询,直接返回对应的 IP 地址

比较知名的 DNS 有 Google 的 8.8.8.8,Microsoft 的 4.2.2.1 ,还有 CloudFlare 的 1.1.1.1 等等

另外,操作系统里还有一个特殊的 主机映射 文件,通常是一个可编辑的文本,在 Linux 里是 /etc/hosts,在 Windows 里是 C:\WINDOWS\system32\drivers\etc\hosts ,如果操作系统在缓存里找不到 DNS 记录,就会找这个文件。

下面的这张图比较完整地表示了现在的 DNS 架构。

DNS 架构

域名的新玩法

可信的 DNS

  • 重定向、名字服务器、基于域名实现的负载均衡

不怀好意的 DNS

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

这次我们学习了与 HTTP 协议有重要关系的域名和 DNS,在这里简单小结一下今天的内容:

  1. 域名使用字符串来代替 IP 地址,方便用户记忆,本质上一个名字空间系统;
  2. DNS 就像是我们现实世界里的电话本、查号台,统管着互联网世界里的所有网站,是一个超级大管家;
  3. DNS 是一个树状的分布式查询系统,但为了提高查询效率,外围有多级的缓存;
  4. 使用 DNS 可以实现基于域名的负载均衡,既可以在内网,也可以在外网

自己动手,搭建 HTTP 实验环境

回顾:

HTTP 协议诞生于 30 年前,设计之初的目的是用来传输纯文本数据。但由于形式灵活,搭配 URI、HTML 等技术能够把互联网上的资源都联系起来,构成一个复杂的超文本系统,让人们自由地获取信息,所以得到了迅猛发展。

HTTP 有多个版本,目前应用的最广泛的是 HTTP/1.1,它几乎可以说是整个互联网的基石。但 HTTP/1.1 的性能难以满足如今的高流量网站,于是又出现了 HTTP/2 和 HTTP/3。不过这两个新版本的协议还没有完全推广开。在可预见的将来,HTTP/1.1 还会继续存在下去。

HTTP 翻译成中文是 超文本传输协议 ,是一个应用层的协议,通常基于 TCP/IP,能够在网络的任意两点之间传输文字、图片、音频、视频等数据。

HTTP 协议中的两个端点称为 请求方应答方 。请求方通常就是 Web 浏览器,也叫 user agent,应答方是 Web 服务器,存储着网络上的大部分静态或动态的资源。

在浏览器和服务器之间还有一些 中间人 的角色,如 CDN、网关、代理等,它们也同样遵守 HTTP 协议,可以帮助用户更快速、更安全地获取资源。

HTTP 协议不是一个孤立的协议,需要下层很多其他协议的配合。最基本的是 TCP/IP,实现寻址、路由和可靠的数据传输,还有 DNS 协议实现对互联网上主机的定位查找。

对 HTTP 更准确的称呼是 HTTP over TCP/IP ,而另一个 HTTP over SSL/TLS 就是增加了安全功能的 HTTPS

键入网址再按下回车,后面究竟发生了什么?

抓包分析

HTTP连接

使用域名访问 Web 服务器

多出了一个访问 hosts 文件的动作,也就是本机的 DNS 解析

DNS解析

真实的网络世界

第一个实验是最简单的场景,只有两个角色:浏览器和服务器,浏览器可以直接用 IP 地址找到服务器,两者直接建立 TCP 连接后发送 HTTP 报文通信。

第二个实验在浏览器和服务器之外增加了一个 DNS 的角色,浏览器不知道服务器的 IP 地址,所以必须要借助 DNS 的域名解析功能得到服务器的 IP 地址,然后才能与服务器通信。

真实的互联网世界要比这两个场景要复杂的多,我利用下面的这张图来做一个详细的说明。

真实的网络世界

  1. HTTP 协议基于底层的 TCP/IP 协议,所以必须要用 IP 地址建立连接;
  2. 如果不知道 IP 地址,就要用 DNS 协议去解析得到 IP 地址,否则就会连接失败;
  3. 建立 TCP 连接后会顺序收发数据,请求方和应答方都必须依据 HTTP 规范构建和解析报文;
  4. 为了减少响应时间,整个过程中的每一个环节都会有缓存,能够实现「短路」操作;
  5. 虽然现实中的 HTTP 传输过程非常复杂,但理论上仍然可以简化成实验里的「两点」模型

HTTP 报文是什么样子的?

HTTP 协议基本工作流程,也就是 请求 - 应答一发一收 的模式

那么 HTTP 协议的核心部分是什么呢?

答案就是它 传输的报文内容

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

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

原始抓包的数据

在这个浏览器发出的请求报文里,第一行 GET / HTTP/1.1 就是请求行,而后面的 Host、Connection 等等都属于 header,报文的最后是一个空白行结束,没有 body。

请求行

了解了 HTTP 报文的基本结构后,我们来看看请求报文里的起始行也就是 请求行(request line),它简要地描述了 客户端想要如何操作服务器端的资源

请求行由三部分构成:

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

例如

markdown
GET / HTTP/1.1

在这个请求行里,GET 是请求方法,/ 是请求目标,HTTP/1.1 是版本号,把这三部分连起来,意思就是「服务器你好,我想获取网站根目录下的默认文件,我用的协议版本号是 1.1,请不要用 1.0 或者 2.0 回复我。」

状态行

看完了请求行,我们再看响应报文里的起始行,在这里它不叫 响应行,而是叫 状态行(status line),意思是 服务器响应的状态

比起请求行来说,状态行要简单一些,同样也是由三部分构成:

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

例如:

markdown
HTTP/1.1 200 OK

意思就是:「浏览器你好,我已经处理完了你的请求,这个报文使用的协议版本号是 1.1,状态码是 200,一切 OK。」

常用头字段

HTTP 协议规定了非常多的头部字段,实现各种各样的功能,但基本上可以分为四大类:

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

Host

它属于 请求字段,只能出现在请求头里,它同时也是唯一一个 HTTP/1.1 规范里要求 必须出现 的字段

Host 字段告诉服务器这个请求应该由哪个主机来处理,当一台计算机上托管了多个虚拟主机的时候,服务器端就需要用 Host 字段来选择,有点像是一个简单的 路由重定向

User-Agent

User-Agent 是请求字段,只出现在请求头里。它使用一个字符串来描述发起 HTTP 请求的客户端,服务器可以依据它来返回最合适此浏览器显示的页面

Date

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

Server

Server 字段是 响应字段,只能出现在响应头里。它告诉客户端当前正在提供 Web 服务的软件名称和版本号

Content-Length

实体字段里要说的一个是 Content-Length ,它表示报文里 body 的长度,也就是请求头或响应头空行后面数据的长度

小结

  1. HTTP 报文结构就像是「大头儿子」,由「起始行 + 头部 + 空行 + 实体」组成,简单地说就是「header+body」;
  2. HTTP 报文可以没有 body,但必须要有 header,而且 header 后也必须要有空行,形象地说就是大头必须要带着脖子;
  3. 请求头由「请求行 + 头部字段」构成,响应头由「状态行 + 头部字段」构成;
  4. 请求行有三部分:请求方法,请求目标和版本号;
  5. 状态行也有三部分:版本号,状态码和原因字符串;
  6. 头部字段是 key-value 的形式,用 : 分隔,不区分大小写,顺序任意,除了规定的标准头,也可以任意添加自定义字段,实现功能扩展;
  7. HTTP/1.1 里唯一要求必须提供的头字段是 Host,它必须出现在请求头里,标记虚拟主机名

应该如何理解请求方法?

目前 HTTP/1.1 规定了八种方法,单词 都必须是大写的形式 ,我先简单地列把它们列出来,后面再详细讲解。

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

GET/HEAD

GET 方法应该是 HTTP 协议里最知名的请求方法了,它的含义是请求 从服务器获取资源 ,这个资源既可以是静态的文本、页面、图片、视频,也可以是由 PHP、Java 动态生成的页面或者其他格式的数据。

HEAD 方法与 GET 方法类似,也是请求从服务器获取资源,服务器的处理机制也是一样的,但服务器不会返回请求的实体数据,只会传回响应头,也就是资源的 元信息

POST/PUT

POST 也是一个经常用到的请求方法,使用频率应该是仅次于 GET,应用的场景也非常多,只要向服务器发送数据,用的大多数都是 POST。

PUT 的作用与 POST 类似,也可以向服务器提交数据,但与 POST 存在微妙的不同,通常 POST 表示的是「新建(create)」的含义,而 PUT 则是「修改(update)」的含义。

DELETE

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

CONNECT

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

OPTIONS

方法要求服务器列出可对资源实行的操作方法,在响应头的 Allow 字段里返回 。它的功能很有限,用处也不大,有的服务器(例如 Nginx)干脆就没有实现对它的支持

TRACE

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

你能写出正确的网址吗?

URI 的格式

下面的这张图显示了 URI 最常用的形式,由 scheme、host:port、path 和 query 四个部分组成,但有的部分可以视情况省略。

URI 的格式

URI 的基本组成

URI 第一个组成部分叫 scheme ,翻译成中文叫 方案名 或者 协议名 ,表示 资源应该使用哪种协议 来访问。

最常见的当然就是 http 了,表示使用 HTTP 协议。另外还有 https ,表示使用经过加密、安全的 HTTPS 协议。此外还有其他不是很常见的 scheme,例如 ftp、ldap、file、news 等。

小结

  1. URI 是用来唯一标记服务器上资源的一个字符串,通常也称为 URL;
  2. URI 通常由 scheme、host:port、path 和 query 四个部分组成,有的可以省略;
  3. scheme 叫方案名或者协议名,表示资源应该使用哪种协议来访问;
  4. host:port 表示资源所在的主机名和端口号;
  5. path 标记资源所在的位置;
  6. query 表示对资源附加的额外要求;
  7. 在 URI 里对 @&/ 等特殊字符和汉字必须要做编码,否则服务器收到 HTTP 报文后会无法正确处理

响应状态码该怎么用?

状态码

RFC 标准把状态码分成了五类 ,用数字的第一位表示分类,而 0~99 不用,这样状态码的实际可用范围就大大缩小了,由 000~999 变成了 100~599。

这五类的具体含义是:

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

目前 RFC 标准里总共有 41 个状态码

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 里的数据不是资源的全部,而是其中的一部分。

3××

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

301 Moved Permanently

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

302 Found

与 301 类似,曾经的描述短语是 Moved Temporarily ,俗称 临时重定向 ,意思是请求的资源还在,但需要暂时用另一个 URI 来访问。

301 和 302 都会在响应头里使用字段 Location 指明后续要跳转的 URI,最终的效果很相似,浏览器都会重定向到新的 URI。两者的根本区别在于语义,一个是 永久 ,一个是 临时 ,所以在场景、用法上差距很大。

比如,你的网站升级到了 HTTPS,原来的 HTTP 不打算用了,这就是永久的,所以要配置 301 跳转,把所有的 HTTP 流量都切换到 HTTPS

304 Not Modified

它用于 If-Modified-Since 等条件请求,表示资源未修改,用于缓存控制。它不具有通常的跳转含义,但可以理解成 重定向已到缓存的文件(即缓存重定向)

4××

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

400 Bad Request

是一个通用的错误码,表示请求报文有错误,但具体是数据格式错误、缺少请求头还是 URI 超长它没有明确说,只是一个笼统的错误,客户端看到 400 只会是一头雾水、不知所措。所以,在开发 Web 应用时应当尽量避免给客户端返回 400,而是要用其他更有明确含义的状态码。

403 Forbidden

实际上不是客户端的请求出错,而是表示服务器禁止访问资源。原因可能多种多样,例如信息敏感、法律禁止等,如果服务器友好一点,可以在 body 里详细说明拒绝请求的原因,不过现实中通常都是直接给一个闭门羹。

404 Not Found

可能是我们最常看见也是最不愿意看到的一个状态码,它的原意是资源在本服务器上未找到,所以无法提供给客户端。但现在已经被「用滥了」,只要服务器不高兴就可以给出个 404,而我们也无从得知后面到底是真的未找到,还是有什么别的原因,某种程度上它比 403 还要令人讨厌

4×× 里剩下的一些代码较明确地说明了错误的原因

  • 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

表示客户端请求的功能还不支持,这个错误码比 500 要温和一些,和即将开业,敬请期待的意思差不多,不过具体什么时候开业就不好说了。

502 Bad Gateway

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

503 Service Unavailable

表示服务器当前很忙,暂时无法响应服务,我们上网时有时候遇到的「网络服务正忙,请稍后重试」的提示信息就是状态码 503。

503 是一个「临时」的状态,很可能过几秒钟后服务器就不那么忙了,可以继续提供服务,所以 503 响应报文里通常还会有一个 Retry-After 字段,指示客户端可以在多久以后再次尝试发送请求

HTTP 有哪些特点?

  1. HTTP 是灵活可扩展的,可以任意添加头字段实现任意功能;
  2. HTTP 是可靠传输协议,基于 TCP/IP 协议“尽量”保证数据的送达;
  3. HTTP 是应用层协议,比 FTP、SSH 等更通用功能更多,能够传输任意数据;
  4. HTTP 使用了请求 - 应答模式,客户端主动发起请求,服务器被动回复请求;
  5. HTTP 本质上是无状态的,每个请求都是互相独立、毫无关联的,协议不要求客户端或服务器记录请求相关的信息。

HTTP 有哪些优点?又有哪些缺点?

不过在正式开讲之前我还要提醒你一下,今天的讨论范围仅限于 HTTP/1.1 ,所说的优点和缺点也仅针对 HTTP/1.1。实际上,专栏后续要讲的 HTTPS 和 HTTP/2 都是对 HTTP/1.1 优点的发挥和缺点的完善。

  1. HTTP 最大的优点是简单、灵活和易于扩展;
  2. HTTP 拥有成熟的软硬件环境,应用的非常广泛,是互联网的基础设施;
  3. HTTP 是无状态的,可以轻松实现集群化,扩展性能,但有时也需要用 Cookie 技术来实现“有状态”;
  4. HTTP 是明文传输,数据完全肉眼可见,能够方便地研究分析,但也容易被窃听;
  5. HTTP 是不安全的,无法验证通信双方的身份,也不能判断报文是否被窜改;
  6. HTTP 的性能不算差,但不完全适应现在的互联网,还有很大的提升空间。

海纳百川:HTTP 的实体数据

我会用连续的 8 讲的篇幅来详细解析 HTTP 协议里的各种头字段,包括定义、功能、使用方式、注意事项等等。学完了这些课程,你就可以完全掌握 HTTP 协议

在前面的基础篇里我们了解了 HTTP 报文的结构,知道一个 HTTP 报文是由 header+body 组成的。但那时我们主要研究的是 header,没有涉及到 body。所以,进阶篇的第一讲就从 HTTP 的 body 谈起。

数据类型与编码

幸运的是,早在 HTTP 协议诞生之前就已经有了针对这种问题的解决方案,不过它是用在电子邮件系统里的,让电子邮件可以发送 ASCII 码以外的任意数据,方案的名字叫做 多用途互联网邮件扩展(Multipurpose Internet Mail Extensions),简称为 MIME。

MIME 是一个很大的标准规范,但 HTTP 只是顺手牵羊取了其中的一部分,用来标记 body 的数据类型 ,这就是我们平常总能听到的 MIME type

MIME 把数据分成了 八大类 ,每个大类下再细分出多个子类,形式是 type/subtype 的字符串,巧得很,刚好也符合了 HTTP 明文的特点,所以能够很容易地纳入 HTTP 头字段里。

这里简单列举一下在 HTTP 里经常遇到的几个类别:

  1. text:即文本格式的可读数据,我们最熟悉的应该就是 text/html 了,表示超文本文档,此外还有纯文本 text/plain、样式表 text/css 等。
  2. image:即图像文件,有 image/gifimage/jpegimage/png 等。
  3. audio/video:音频和视频数据,例如 audio/mpegvideo/mp4 等。
  4. application:数据格式不固定,可能是文本也可能是二进制,必须由上层应用程序来解释。常见的有 application/jsonapplication/javascriptapplication/pdf 等,另外,如果实在是不知道数据是什么类型,像刚才说的黑盒,就会是 application/octet-stream即不透明的二进制数据

但仅有 MIME type 还不够,因为 HTTP 在传输时为了节约带宽,有时候还会 压缩数据 ,为了不要让浏览器继续猜,还需要有一个 Encoding type ,告诉数据是用的什么编码格式,这样对方才能正确解压缩,还原出原始的数据。

比起 MIME type 来说,Encoding type 就少了很多,常用的只有下面三种:

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

数据类型使用的头字段

有了 MIME type 和 Encoding type,无论是浏览器还是服务器就都可以轻松识别出 body 的类型,也就能够正确处理数据了。

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

协议交流

Accept 字段标记的是 客户端可理解的 MIME type ,可以用 , 做分隔符列出多个类型,让服务器有更多的选择余地,例如下面的这个头:

http
Accept: text/html,application/xml,image/webp,image/png

这就是告诉服务器:我能够看懂 HTML、XML 的文本,还有 webp 和 png 的图片,请给我这四类格式的数据。

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

http
Content-Type: text/html
Content-Type: image/png

这样浏览器看到报文里的类型是 text/html 就知道是 HTML 文件,会调用排版引擎渲染出页面,看到 image/png 就知道是一个 PNG 文件,就会在页面上显示出图像。

Accept-Encoding 字段标记的是 客户端支持的压缩格式 ,例如上面说的 gzip、deflate 等,同样也可以用 , 列出多个,服务器可以选择其中一种来压缩数据,实际使用的压缩格式放在响应头字段 Content-Encoding 里。

http
Accept-Encoding: gzip, deflate, br
Content-Encoding: gzip

不过这两个字段是可以省略的,如果请求报文里没有 Accept-Encoding 字段,就表示客户端不支持压缩数据;如果响应报文里没有 Content-Encoding 字段,就表示响应数据没有被压缩。

语言类型与编码

MIME type 和 Encoding type 解决了计算机理解 body 数据的问题,但互联网遍布全球,不同国家不同地区的人使用了很多不同的语言,虽然都是 text/html ,但如何让浏览器显示出每个人都可理解可阅读的语言文字呢?

这实际上就是 国际化 的问题。HTTP 采用了与数据类型相似的解决方案,又引入了两个概念:语言类型与字符集。

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

举几个例子:en 表示任意的英语,en-US 表示美式英语,en-GB 表示英式英语,而 zh-CN 就表示我们最常使用的汉语。

关于自然语言的计算机处理还有一个更麻烦的东西叫做 字符集

在计算机发展的早期,各个国家和地区的人们「各自为政」,发明了许多字符编码方式来处理文字,比如英语世界用的 ASCII、汉语世界用的 GBK、BIG5,日语世界用的 Shift_JIS 等。同样的一段文字,用一种编码显示正常,换另一种编码后可能就会变得一团糟。

所以后来就出现了 Unicode 和 UTF-8,把世界上所有的语言都容纳在一种编码方案里,UTF-8 编码 的 Unicode 也成为了互联网上的标准字符集

语言类型使用的头字段

同样的,HTTP 协议也使用 Accept 请求头字段和 Content 实体头字段,用于客户端和服务器就语言与编码进行 内容协商

Accept-Language 字段标记了 客户端可理解的自然语言 ,也允许用 , 做分隔符列出多个类型,例如:

http
Accept-Language: zh-CN, zh, en

这个请求头会告诉服务器:最好给我 zh-CN 的汉语文字,如果没有就用其他的汉语方言,如果还没有就给英文。

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

http
Content-Language: zh-CN

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

例如,浏览器请求 GBK 或 UTF-8 的字符集,然后服务器返回的是 UTF-8 编码,就是下面这样:

http
Accept-Charset: gbk, utf-8
Content-Type: text/html; charset=utf-8

内容协商的质量值

在 HTTP 协议里用 Accept、Accept-Encoding、Accept-Language 等请求头字段进行内容协商的时候,还可以用一种特殊的 q 参数表示权重来设定优先级,这里的 qquality factor的意思。

权重的最大值是 1,最小值是 0.01,默认值是 1,如果值是 0 就表示拒绝。具体的形式是在数据类型或语言代码后面加一个 ; ,然后是 q=value

这里要提醒的是 ; 的用法,在大多数编程语言里 ; 的断句语气要强于 , ,而在 HTTP 的内容协商里却恰好反了过来,; 的意义是小于 , 的。

例如下面的 Accept 字段:

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

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

内容协商的结果

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

http
Vary: Accept-Encoding,User-Agent,Accept

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

Vary 字段可以认为是响应报文的一个特殊的 版本标记 。每当 Accept 等请求头变化时,Vary 也会随着响应报文一起变化。也就是说,同一个 URI 可能会有多个不同的「版本」,主要用在传输链路中间的代理服务器实现缓存服务

小结

数据类型和语言类型

  1. 数据类型表示实体数据的内容是什么,使用的是 MIME type,相关的头字段是 Accept 和 Content-Type;
  2. 数据编码表示实体数据的压缩方式,相关的头字段是 Accept-Encoding 和 Content-Encoding;
  3. 语言类型表示实体数据的自然语言,相关的头字段是 Accept-Language 和 Content-Language;
  4. 字符集表示实体数据的编码方式,相关的头字段是 Accept-Charset 和 Content-Type;
  5. 客户端需要在请求头里使用 Accept 等头字段与服务器进行内容协商,要求服务器返回最合适的数据;
  6. Accept 等头字段可以用 , 顺序列出多个可能的选项,还可以用 ;q= 参数来精确指定权重。

把大象装进冰箱:HTTP 传输大文件的方法

上次我们谈到了 HTTP 报文里的 body,知道了 HTTP 可以传输很多种类的数据,不仅是文本,也能传输图片、音频和视频。

早期互联网上传输的基本上都是只有几 K 大小的文本和小图片,现在的情况则大有不同。网页里包含的信息实在是太多了,随随便便一个主页 HTML 就有可能上百 K,高质量的图片都以 M 论,更不要说那些电影、电视剧了,几 G、几十 G 都有可能。

相比之下,100M 的光纤固网或者 4G 移动网络在这些大文件的压力下都变成了 「小水管」,无论是上传还是下载,都会把网络传输链路挤的「满满当当」。

所以,如何在有限的带宽下高效快捷地传输这些大文件就成了一个重要的课题 。这就好比是已经打开了冰箱门(建立连接),该怎么把大象(文件)塞进去再关上门(完成传输)呢?

数据压缩

还记得上一讲中说到的 数据类型与编码 吗?如果你还有印象的话,肯定能够想到一个最基本的解决方案,那就是 数据压缩 ,把大象变成小猪佩奇,再放进冰箱。

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

如果压缩率能有 50%,也就是说 100K 的数据能够压缩成 50K 的大小,那么就相当于在带宽不变的情况下网速提升了一倍,加速的效果是非常明显的。

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

不过数据压缩在处理文本的时候效果还是很好的,所以各大网站的服务器都会使用这个手段作为保底。例如,在 Nginx 里就会使用 gzip on 指令,启用对 text/html 的压缩

分块传输

在数据压缩之外,还能有什么办法来解决大文件的问题呢?

压缩是把大文件整体变小,我们可以反过来思考,如果大文件整体不能变小,那就把它 拆开 ,分解成多个小块,把这些小块分批发给浏览器,浏览器收到后再组装复原。

这样浏览器和服务器都不用在内存里保存文件的全部,每次只收发一小部分,网络也不会被大文件长时间占用,内存、带宽等资源也就节省下来了。

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

这就好比是用魔法把大象变成乐高积木,拆散了逐个装进冰箱,到达目的地后再施法拼起来满血复活。

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

Transfer-Encoding: chunkedContent-Length 这两个字段是 互斥的 ,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked),这一点你一定要记住。

下面我们来看一下分块传输的编码规则,其实也很简单,同样采用了明文的方式,很类似响应头。

  1. 每个分块包含两个部分,长度头和数据块;
  2. 长度头是以 CRLF(回车换行,即 \r\n )结尾的一行明文,用 16 进制数字表示长度;
  3. 数据块紧跟在长度头后,最后也用 CRLF 结尾,但数据不包含 CRLF;
  4. 最后用一个长度为 0 的块表示结束,即 0\r\n\r\n

听起来好像有点难懂,看一下图就好理解了:

25e7b09cf8cb4eaebba42b4598192410.25e7b09c

范围请求

有了分块传输编码,服务器就可以轻松地收发大文件了,但对于上 G 的超大文件,还有一些问题需要考虑。

比如,你在看当下正热播的某穿越剧,想跳过片头,直接看正片,或者有段剧情很无聊,想拖动进度条快进几分钟,这实际上是想获取一个大文件其中的片段数据 ,而分块传输并没有这个能力。

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 字段后,需要做四件事。

  1. 第一,它必须检查范围是否合法

    比如文件只有 100 个字节,但请求 200-300 ,这就是范围越界了。服务器就会返回状态码 416,意思是「你的范围请求有误,我无法处理,请再检查一下」。

  2. 第二,如果范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码 206 Partial Content ,和 200 的意思差不多,但表示 body 只是原数据的一部分。

  3. 第三,服务器要添加一个响应头字段 Content-Range

    告诉片段的实际偏移量和资源的总大小,格式是 bytes x-y/length ,与 Range 头区别在没有 =,范围后多了总长度。例如,对于 0-10 的范围请求,值就是 bytes 0-10/100

  4. 最后剩下的就是发送数据了,直接把片段用 TCP 发给客户端,一个范围请求就算是处理完了。

多段数据

刚才说的范围请求一次只获取一个片段,其实它还支持在 Range 头里使用多个 x-y,一次性获取多个片段数据。

这种情况需要使用一种特殊的 MIME 类型:multipart/byteranges,表示报文的 body 是由多段字节序列组成的,并且还要用一个参数 boundary=xxx 给出段之间的分隔标记。

多段数据的格式与分块传输也比较类似,但它需要用分隔标记 boundary 来区分不同的片段

小结

今天我们学习了 HTTP 传输大文件相关的知识,在这里做一下简单小结:

  1. 压缩 HTML 等文本文件是传输大文件最基本的方法;
  2. 分块传输可以流式收发数据,节约内存和带宽,使用响应头字段 Transfer-Encoding: chunked 来表示,分块的格式是 16 进制长度头 + 数据块;
  3. 范围请求可以只获取部分数据,即 分块请求,实现视频拖拽或者断点续传,使用请求头字段 Range 和响应头字段 Content-Range ,响应状态码必须是 206;
  4. 也可以一次请求多个范围,这时候响应报文的数据类型是 multipart/byteranges ,body 里的多个部分会用 boundary 字符串分隔。

排队也要讲效率:HTTP 的连接管理

HTTP 的性能问题,用了六个字来概括:不算差,不够好 。同时,我也谈到了 队头阻塞

短连接

HTTP 协议最初(0.9/1.0)是个非常简单的协议,通信过程也采用了简单的 请求 - 应答 方式。

它底层的数据传输基于 TCP/IP,每次发送请求前需要先与服务器建立连接,收到响应报文后会立即关闭连接。

因为客户端与服务器的整个连接过程很短暂,不会与服务器保持长时间的连接状态,所以就被称为 短连接 (short-lived connections)。早期的 HTTP 协议也被称为是 无连接 的协议。

短连接的缺点相当严重,因为在 TCP 协议里,建立连接和关闭连接都是非常“昂贵”的操作。TCP 建立连接要有 三次握手,发送 3 个数据包,需要 1 个 RTT;关闭连接是 四次挥手,4 个数据包需要 2 个 RTT(一个来回就是 1 RTT)。

而 HTTP 的一次简单请求 - 响应通常只需要 4 个包,如果不算服务器内部的处理时间,最多是 2 个 RTT。这么算下来,浪费的时间就是3÷5=60% ,有三分之二的时间被浪费掉了,传输效率低得惊人。

54315ed9ac37fbc6547258040f00a80c.54315ed9

单纯地从理论上讲,TCP 协议你可能还不太好理解,我就拿打卡考勤机来做个形象的比喻吧。

假设你的公司买了一台打卡机,放在前台,因为这台机器比较贵,所以专门做了一个保护罩盖着它,公司要求每次上下班打卡时都要先打开盖子,打卡后再盖上盖子。

可是偏偏这个盖子非常牢固,打开关闭要费很大力气,打卡可能只要 1 秒钟,而开关盖子却需要四五秒钟,大部分时间都浪费在了毫无意义的开关盖子操作上了

可想而知,平常还好说,一到上下班的点在打卡机前就会排起长队,每个人都要重复“开盖 - 打卡 - 关盖”的三个步骤,你说着急不着急。

在这个比喻里,打卡机就相当于服务器,盖子的开关就是 TCP 的连接与关闭,而每个打卡的人就是 HTTP 请求,很显然,短连接的缺点严重制约了服务器的服务能力,导致它无法处理更多的请求。

长连接

针对短连接暴露出的缺点,HTTP 协议就提出了 长连接 的通信方式,也叫 持久连接 (persistent connections)、连接保活(keep alive)、连接复用(connection reuse)。

其实解决办法也很简单,用的就是 成本均摊 的思路,既然 TCP 的连接和关闭非常耗时间,那么就把这个时间成本由原来的一个请求 - 应答均摊到多个请求 - 应答上。

这样虽然不能改善 TCP 的连接效率,但基于 分母效应 ,每个请求 - 应答的无效时间就会降低不少,整体传输效率也就提高了。

57b3d80234a1f1b8c538a376aa01d3b4.57b3d802

在短连接里发送了三次 HTTP 请求 - 应答,每次都会浪费 60% 的 RTT 时间。而在长连接的情况下,同样发送三次请求,因为只在第一次时建立连接,在最后一次时关闭连接,所以浪费率就是 3÷9≈33% ,降低了差不多一半的时间损耗。显然,如果在这个长连接上发送的请求越多,分母就越大,利用率也就越高。

继续用刚才的打卡机的比喻,公司也觉得这种反复「开盖 - 打卡 - 关盖」的操作太反人类了,于是颁布了新规定,早上打开盖子后就不用关上了,可以自由打卡,到下班后再关上盖子。

这样打卡的效率(即服务能力)就大幅度提升了,原来一次打卡需要五六秒钟,现在只要一秒就可以了,上下班时排长队的景象一去不返,大家都开心

连接相关的头字段

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

当然,我们也可以在请求头里明确地要求使用长连接机制,使用的字段是 Connection ,值是 keep-alive

不过不管客户端是否显式要求长连接,如果服务器支持长连接,它总会在响应报文里放一个 Connection: keep-alive 字段,告诉客户端:我是支持长连接的,接下来就用这个 TCP 一直收发数据吧

队头阻塞

看完了短连接和长连接,接下来就要说到著名的 队头阻塞(Head-of-line blocking,也叫队首阻塞)了。

队头阻塞与短连接和长连接无关 ,而是由 HTTP 基本的 请求 - 应答 模型所导致的。

因为 HTTP 规定报文必须是 一发一收 ,这就形成了一个先进先出的 串行队列 。队列里的请求没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求被最优先处理。

如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本。

还是用打卡机做个比喻。

上班的时间点上,大家都在排队打卡,可这个时候偏偏最前面的那个人遇到了打卡机故障,怎么也不能打卡成功,急得满头大汗。等找人把打卡机修好,后面排队的所有人全迟到了。

性能优化

因为 请求 - 应答 模型不能变,所以队头阻塞问题在 HTTP/1.1 里无法解决,只能缓解,有什么办法呢?

公司里可以再多买几台打卡机放在前台,这样大家可以不用挤在一个队伍里,分散打卡,一个队伍偶尔阻塞也不要紧,可以改换到其他不阻塞的队伍。

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

所以,HTTP 协议建议客户端使用并发,但不能「滥用」并发。RFC2616 里明确限制每个客户端最多并发 2 个连接。不过实践证明这个数字实在是太小了,众多浏览器都「无视」标准,把这个上限提高到了 6~8。后来修订的 RFC7230 也就顺水推舟,取消了这个 2 的限制。

但并发连接所压榨出的性能也跟不上高速发展的互联网无止境的需求,还有什么别的办法吗?

公司发展的太快了,员工越来越多,上下班打卡成了迫在眉睫的大问题。前台空间有限,放不下更多的打卡机了,怎么办?那就多开几个打卡的地方,每个楼层、办公区的入口也放上三四台打卡机,把人进一步分流,不要都往前台挤。

这个就是 域名分片(domain sharding)技术,还是用数量来解决质量的思路。

HTTP 协议和浏览器不是限制并发连接数量吗?好,那我就多开几个域名,比如 shard1.chrono.com、shard2.chrono.com,而这些域名都指向同一台服务器 www.chrono.com ,这样实际长连接的数量就又上去了,真是美滋滋。不过实在是有点上有政策,下有对策的味道。

小结

  1. 早期的 HTTP 协议使用短连接,收到响应后就立即关闭连接,效率很低;
  2. HTTP/1.1 默认启用长连接,在一个连接上收发多个请求响应,提高了传输效率;
  3. 服务器会发送 Connection: keep-alive 字段表示启用了长连接;
  4. 报文头里如果有 Connection: close 就意味着长连接即将关闭;
  5. 过多的长连接会占用服务器资源,所以服务器会用一些策略有选择地关闭长连接;
  6. 队头阻塞问题会导致性能下降,可以用 并发连接域名分片 技术缓解。

四通八达:HTTP 的重定向和跳转

为了实现在互联网上构建超链接文档系统的设想,蒂姆·伯纳斯 - 李发明了万维网,使用 HTTP 协议传输 超文本 ,让全世界的人都能够自由地共享信息。

超文本 里含有 超链接 ,可以从一个超文本跳跃到另一个超文本,对线性结构的传统文档是一个根本性的变革。

能够使用超链接在网络上任意地跳转 也是万维网的一个关键特性。

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

重定向的过程

重定向是服务器发起的跳转,要求客户端改用新的 URI 重新发送请求,通常会自动进行,用户是无感知的;

重定向状态码

301 俗称 永久重定向(Moved Permanently)

302 俗称 临时重定向(Moved Temporarily),意思是原 URI 处于 临时维护 状态,新的 URI 是起顶包作用的临时工

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

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

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

小结

今天我们学习了 HTTP 里的重定向和跳转,简单小结一下这次的内容:

  1. 重定向是服务器发起的跳转,要求客户端改用新的 URI 重新发送请求,通常会自动进行,用户是无感知的;
  2. 301/302 是最常用的重定向状态码,分别是 永久重定向临时重定向
  3. 响应头字段 Location 指示了要跳转的 URI,可以用绝对或相对的形式;
  4. 重定向可以把一个 URI 指向另一个 URI,也可以把多个 URI 指向同一个 URI,用途很多;
  5. 使用重定向时需要当心性能损耗,还要避免出现循环跳转。

前面说到 HTTP 是 「无状态」的,这既是优点也是缺点。优点是服务器没有状态差异,可以很容易地组成集群 ,而缺点就是无法支持需要记录状态的事务操作。

好在 HTTP 协议是可扩展的,后来发明的 Cookie 技术,给 HTTP 增加了「记忆能力」

HTTP 的 Cookie 机制也是一样的道理,既然服务器记不住,那就在外部想办法记住 。相当于是服务器给每个客户端都贴上一张小纸条,上面写了一些只有服务器才能理解的数据,需要的时候客户端把这些信息发给服务器,服务器看到 Cookie,就能够认出对方是谁了

那么,Cookie 这张小纸条是怎么传递的呢?

这要用到两个字段:响应头字段 Set-Cookie 和请求头字段 Cookie

当用户通过浏览器第一次访问服务器的时候,服务器肯定是不知道他的身份的。所以,就要创建一个独特的身份标识数据,格式是 key=value,然后放进 Set-Cookie 字段里,随着响应报文一同发给浏览器。

浏览器收到响应报文,看到里面有 Set-Cookie,知道这是服务器给的身份标识,于是就保存起来,下次再请求的时候就自动把这个值放进 Cookie 字段里发给服务器。

因为第二次请求里面有了 Cookie 字段,服务器就知道这个用户不是新人,之前来过,就可以拿出 Cookie 里的值,识别出用户的身份,然后提供个性化的服务。

不过因为服务器的记忆能力实在是太差,一张小纸条经常不够用。所以,服务器有时会在响应头里添加多个 Set-Cookie,存储多个 key=value。但浏览器这边发送时不需要用多个 Cookie 字段,只要在一行里用 ; 隔开就行。

我画了一张图来描述这个过程,你看过就能理解了。

cookies

从这张图中我们也能够看到,Cookie 是由浏览器负责存储的,而不是操作系统。所以,它是浏览器绑定的,只能在本浏览器内生效。

如果你换个浏览器或者换台电脑,新的浏览器里没有服务器对应的 Cookie,就好像是脱掉了贴着纸条的衣服,健忘的服务器也就认不出来了,只能再走一遍 Set-Cookie 流程。

说到这里,你应该知道了,Cookie 就是服务器委托浏览器存储在客户端里的一些数据 ,而这些数据通常都会记录用户的关键识别信息。所以,就需要在 key=value 外再用一些手段来保护,防止外泄或窃取,这些手段就是 Cookie 的属性。

首先,我们应该 设置 Cookie 的生存周期 ,也就是它的有效期,让它只能在一段时间内可用,就像是食品的保鲜期,一旦超过这个期限浏览器就认为是 Cookie 失效,在存储里删除,也不会发送给服务器。

Cookie 的有效期可以使用 ExpiresMax-Age 两个属性来设置。

  • Expires 俗称 过期时间,用的是 绝对时间点 ,可以理解为 截止日期(deadline)。
  • Max-Age 用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间。

Expires 和 Max-Age 可以同时出现,两者的失效时间可以一致,也可以不一致,但浏览器会优先采用 Max-Age 计算失效期

设置 Cookie 的作用域让浏览器仅发送给特定的服务器和 URI ,避免被其他网站盗用。

作用域的设置比较简单,DomainPath 指定了 Cookie 所属的域名和路径,浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie。

最后要考虑的就是 Cookie 的安全性 了,尽量不要让服务器以外的人看到。

写过前端的同学一定知道,在 JS 脚本里可以用 document.cookie 来读写 Cookie 数据,这就带来了安全隐患,有可能会导致 跨站脚本(XSS) 攻击窃取数据。

  • HttpOnly

    属性 HttpOnly 会告诉浏览器,此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问 ,浏览器的 JS 引擎就会禁用 document.cookie 等一切相关的 API,脚本攻击也就无从谈起了。

  • SameSite

    可以防范 跨站请求伪造(XSRF)攻击 :设置成

    SameSite=Strict :可以严格限定 Cookie 不能随着跳转链接跨站发送

    SameSite=Lax:则略宽松一点,允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送。

    还有一个 None,不限制

  • Secure

    表示这个 Cookie 仅能用 HTTPS 协议加密传输 ,明文的 HTTP 协议会禁止发送。但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在。

小结

今天我们学习了 HTTP 里的 Cookie 知识。虽然现在已经出现了多种 Local Web Storage 技术,能够比 Cookie 存储更多的数据,但 Cookie 仍然是最通用、兼容性最强的客户端数据存储手段。

简单小结一下今天的内容:

  1. Cookie 是服务器委托浏览器存储的一些数据,让服务器有了记忆能力;
  2. 响应报文使用 Set-Cookie 字段发送 key=value 形式的 Cookie 值;
  3. 请求报文里用 Cookie 字段发送多个 Cookie 值;
  4. 为了保护 Cookie,还要给它设置有效期、作用域等属性,常用的有 Max-Age、Expires、Domain、HttpOnly 等;
  5. Cookie 最基本的用途是身份识别,实现有状态的会话事务。

还要提醒你一点,因为 Cookie 并不属于 HTTP 标准(RFC6265,而不是 RFC2616/7230),所以语法上与其他字段不太一致,使用的分隔符是 ; ,与 Accept 等字段的 , 不同,小心不要弄错了。

生鲜速递:HTTP 的缓存控制

缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器。

由于链路漫长,网络时延不可控,浏览器使用 HTTP 获取资源的成本较高。所以,非常有必要把来之不易的数据缓存起来,下次再请求的时候尽可能地 复用 。这样,就可以 避免多次请求 - 应答的通信成本,节约网络带宽 ,也可以加快响应速度。

基于 「请求 - 应答」模式的特点,可以大致分为 客户端缓存服务器端缓存 ,因为服务器端缓存经常与代理服务「混搭」在一起,所以今天我先讲客户端——也就是 **浏览器的缓存 **

服务器的缓存控制

为了更好地说明缓存的运行机制,下面我用「生鲜速递」作为比喻,看看缓存是如何工作的。

夏天到了,天气很热。你想吃西瓜消暑,于是打开冰箱,但很不巧,冰箱是空的。不过没事,现在物流很发达,给生鲜超市打个电话,不一会儿,就给你送来一个 8 斤的沙瓤大西瓜,上面还贴着标签:「保鲜期 5 天」。好了,你把它放进冰箱,想吃的时候随时拿出来。

在这个场景里:

  • 生鲜超市:就是 Web 服务器
  • 你:就是浏览器
  • 冰箱:就是浏览器内部的缓存

整个流程翻译成 HTTP 就是:

  1. 浏览器发现缓存无数据,于是发送请求,向服务器获取资源;
  2. 服务器响应请求,返回资源,同时标记资源的有效期;
  3. 浏览器缓存资源,等待下次重用。

缓存

Cache-Control 字段里的 max-age 和上一讲里 Cookie 有点像,都是标记资源的有效期。

但我必须提醒你注意,这里的 max-age 是 生存时间 (又叫 新鲜度、缓存寿命,类似 TTL,Time-To-Live),时间的计算起点是响应报文的创建时刻( 即 Date 字段,也就是离开服务器的时刻 ),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。

服务器标记资源有效期使用的头字段是 Cache-Control ,里面的值 max-age=30 就是资源的有效时间,相当于告诉浏览器,「这个页面只能缓存 30 秒,之后就算是过期,不能用。」

你可能要问了,让浏览器直接缓存数据就好了,为什么要加个有效期呢?

这是因为网络上的数据随时都在变化,不能保证它稍后的一段时间还是原来的样子。就像生鲜超市给你快递的西瓜,只有 5 天的保鲜期,过了这个期限最好还是别吃,不然可能会闹肚子。

max-age 是 HTTP 缓存控制最常用的属性,此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存:

  • no_store不允许缓存 ,用于某些变化非常频繁的数据,例如秒杀页面;
  • no_cache :它的字面含义容易与 no_store 搞混,实际的意思并不是不允许缓存,而是 可以缓存 ,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;
  • must-revalidate :又是一个和 no_cache 相似的词,它的意思是 如果缓存不过期就可以继续使用但过期了如果还想用就必须去服务器验证

听的有点糊涂吧。没关系,我拿生鲜速递来举例说明一下:

  • no_store:买来的西瓜不允许放进冰箱,要么立刻吃,要么立刻扔掉;
  • no_cache:可以放进冰箱,但吃之前必须问超市有没有更新鲜的,有就吃超市里的;
  • must-revalidate:可以放进冰箱,保鲜期内可以吃,过期了就要问超市让不让吃。

8a67535620ab9c7764560363f83982b2.8a675356

条件请求

浏览器用 Cache-Control 做缓存控制只能是刷新数据,不能很好地利用缓存数据,又因为缓存会失效,使用前还必须要去服务器验证是否是最新版。

那么该怎么做呢?

浏览器可以用两个连续的请求组成 验证动作

  1. 先是一个 HEAD,获取资源的修改时间等元信息,然后与缓存数据比较,如果没有改动就使用缓存,节省网络流量
  2. 否则就再发一个 GET 请求,获取最新的版本。

但这样的两个请求网络成本太高了(获取元信息),所以 HTTP 协议就定义了一系列 If 开头的 条件请求 字段,专门用来检查验证资源是否过期,把两个请求才能完成的工作合并在一个请求里做 。而且,验证的责任也交给服务器 (不获取元数据自己校验了),浏览器只需坐享其成。

条件请求一共有 5 个头字段:

  • if-Modified-Since:和 Last-modified 比较

    和 Last-modified 对比,是否已经修改了

  • If-None-Match :和 ETag 比较

    和 ETag 比较是否不匹配

  • If-Unmodified-Since

    和 Last-modified 对比,是否已未修改

  • If-Match

    和 ETag 比较是否匹配

  • If-Range

我们最常用的是 if-Modified-SinceIf-None-Match 这两个。需要第一次的响应报文预先提供 Last-modifiedETag ,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。

如果资源没有变,服务器就回应一个 304 Not Modified ,表示缓存依然有效,浏览器就可以更新一下有效期,然后放心大胆地使用缓存了。

b239d0804be630ce182e24ea9e4ab237.b239d080

Last-modified 很好理解,就是文件的最后修改时间。ETag 是什么呢?

ETag 是 实体标签(Entity Tag) 的缩写,是资源的一个唯一标识 ,主要是用来解决修改时间无法准确区分文件变化的问题。

有一个比较容易混淆的是 304 状态码和 200 状态码:

  • 200 (from memory cache):这个表示没有向服务器发起请求,使用的是浏览器本地的缓存数据
  • 304 Not Modified:这个是表示向服务器发起了请求,但是服务器响应该文件没有变化,没有传回数据内容,使用浏览器的缓存

小结

今天我们学习了 HTTP 的缓存控制和条件请求,用好它们可以减少响应时间、节约网络流量,一起小结一下今天的内容吧:

  1. 缓存是优化系统性能的重要手段,HTTP 传输的每一个环节中都可以有缓存;
  2. 服务器使用 Cache-Control 设置缓存策略,常用的是 max-age ,表示资源的有效期;
  3. 浏览器收到数据就会存入缓存,如果没过期就可以直接使用,过期就要去服务器验证是否仍然可用;
  4. 验证资源是否失效需要使用「条件请求」,常用的是 if-Modified-SinceIf-None-Match收到 304 就可以复用缓存里的资源
  5. 验证资源是否被修改的条件有两个:Last-modifiedETag ,需要服务器预先在响应报文里设置,搭配条件请求使用;
  6. 浏览器也可以发送 Cache-Control 字段,使用 max-age=0no_cache 刷新数据。

HTTP 缓存看上去很复杂,但基本原理说白了就是一句话:没有消息就是好消息,没有请求的请求,才是最快的请求

HTTP 的代理服务

在前面讲 HTTP 协议的时候,我们严格遵循了 HTTP 的 请求 - 应答 模型,协议中只有两个互相通信的角色,分别是 请求方 浏览器(客户端)和 应答方 服务器。

今天,我们要在这个模型里引入一个新的角色,那就是 HTTP 代理

代理服务

但其实 HTTP 协议里对它并没有什么特别的描述,它就是 在客户端和服务器原本的通信链路中插入的一个中间环节 ,也是一台服务器,但提供的是「代理服务」

之前你都是从超市里买东西,现在楼底下新开了一家 24 小时便利店,由超市直接供货,于是你就可以在便利店里买到原本必须去超市才能买到的商品。

今天我主要讲的是实际工作中最常见的 反向代理 ,它在传输链路中更靠近源服务器,为源服务器提供代理服务。

代理的作用

为什么要有代理呢?换句话说,代理能干什么、带来什么好处呢?

你也许听过这样一句至理名言:「计算机科学领域里的任何问题,都可以通过引入一个中间层来解决」(在这句话后面还可以再加上一句「如果一个中间层解决不了问题,那就再加一个中间层」)。TCP/IP 协议栈是这样,而代理也是这样。

由于代理处在 HTTP 通信过程的中间位置,相应地就对上屏蔽了真实客户端,对下屏蔽了真实服务器,简单的说就是 欺上瞒下 。在这个中间层的小天地里就可以做很多的事情,为 HTTP 协议增加更多的灵活性,实现客户端和服务器的双赢 。

代理最基本的一个功能是 负载均衡 。因为在面向客户端时屏蔽了源服务器,客户端看到的只是代理服务器,源服务器究竟有多少台、是哪些 IP 地址都不知道。于是代理服务器就可以掌握请求分发的“大权”,决定由后面的哪台服务器来响应请求。

反向代理

在负载均衡的同时,代理服务还可以执行更多的功能,比如:

  • 健康检查:使用 心跳 等机制监控后端服务器,发现有故障就及时 踢出 集群,保证服务高可用;
  • 安全防护:保护被代理的后端服务器,限制 IP 地址或流量,抵御网络攻击和过载;
  • 加密卸载:对外网使用 SSL/TLS 加密通信认证,而在安全的内网不加密,消除加解密成本;
  • 数据过滤:拦截上下行的数据,任意指定策略修改请求或者响应;
  • 内容缓存:暂存、复用服务器响应,这个与上一章的 HTTP 的缓存控制密切相关,我们稍后再说。

接着拿刚才的便利店来举例说明。

因为便利店和超市之间是专车配送,所以有了便利店,以后你买东西就更省事了,打电话给便利店让它去帮你取货,不用关心超市是否停业休息、是否人满为患,而且总能买到最新鲜的。

便利店同时也方便了超市,不用额外加大店面就可以增加客源和销量,货物集中装卸也节省了物流成本,由于便利店直接面对客户,所以也可以把恶意骚扰电话挡在外面

代理相关头字段

代理的好处很多,但因为它欺上瞒下的特点,隐藏了真实客户端和服务器 ,如果双方想要获得这些 丢失 的原始信息,该怎么办呢?

首先,代理服务器需要用字段 Via 标明代理的身份。

Via 字段只解决了 客户端和源服务器判断是否存在代理的问题,还不能知道对方的真实信息

但服务器的 IP 地址应该是保密的,关系到企业的内网安全,所以一般不会让客户端知道。不过反过来,通常服务器需要知道客户端的真实 IP 地址,方便做访问控制、用户画像、统计分析

可惜的是 HTTP 标准里并没有为此定义头字段 ,但已经出现了很多 事实上的标准 ,最常用的两个头字段是 X-Forwarded-ForX-Real-IP

  • X-Forwarded-For:链式存储

    字面意思是为 谁而转发 ,形式上和 Via 差不多,也是每经过一个代理节点就会在字段里追加一个信息,但 Via 追加的是代理主机名(或者域名),而 X-Forwarded-For 追加的是请求方的 IP 地址。所以,在字段里最左边的 IP 地址就客户端的地址。

  • X-Real-IP:只有客户端 IP 地址

    是另一种获取客户端真实 IP 的手段,它的作用很简单,就是记录客户端 IP 地址,没有中间的代理信息。

小结

  1. HTTP 代理就是客户端和服务器通信链路中的一个中间环节,为两端提供 代理服务
  2. 代理处于中间层,为 HTTP 处理增加了更多的灵活性,可以实现负载均衡、安全防护、数据过滤等功能;
  3. 代理服务器需要使用字段 Via 标记自己的身份,多个代理会形成一个列表;
  4. 如果想要知道客户端的真实 IP 地址,可以使用字段 X-Forwarded-ForX-Real-IP
  5. 专门的 代理协议 可以在不改动原始报文的情况下传递客户端的真实 IP。

冷链周转:HTTP 的缓存代理

缓存代理服务

我还是沿用「生鲜速递 + 便利店」的比喻,看看缓存代理是怎么回事。

便利店作为超市的代理,生意非常红火,顾客和超市双方都对现状非常满意。但时间一长,超市发现还有进一步提升的空间,因为每次便利店接到顾客的请求后都要专车跑一趟超市,还是挺麻烦的。

干脆这样吧,给便利店配发一个大冰柜。水果海鲜什么的都可以放在冰柜里,只要产品在保鲜期内,就允许顾客直接从冰柜提货。这样便利店就可以一次进货多次出货,省去了超市之间的运输成本。

5e8d10b5758685850aeed2a473a6cdc2.5e8d10b5

通过这个比喻,你可以看到:在没有缓存的时候,代理服务器每次都是直接转发客户端和服务器的报文,中间不会存储任何数据,只有最简单的中转功能。

加入了缓存后就不一样了。

代理服务收到源服务器发来的响应数据后需要做两件事:

  • 第一个当然是把报文转发给客户端
  • 而第二个就是把报文存入自己的 Cache 里。

下一次再有相同的请求,代理服务器就可以直接发送 304 或者缓存数据,不必再从源服务器那里获取。这样就降低了客户端的等待时间,同时节约了源服务器的网络带宽。

在 HTTP 的缓存体系中,缓存代理的身份十分特殊,它「既是客户端,又是服务器」,同时也「既不是客户端,又不是服务器」。

说它即是客户端又是服务器,是因为它 面向源服务器时是客户端在面向客户端时又是服务器 ,所以它即可以用客户端的缓存控制策略也可以用服务器端的缓存控制策略,也就是说它可以同时使用前面讲解的各种 Cache-Control 属性。

但缓存代理也「即不是客户端又不是服务器」,因为它只是一个数据的 中转站 ,并不是真正的数据消费者和生产者,所以还需要有一些新的 Cache-Control 属性来对它做特别的约束。

源服务器的缓存控制

首先,我们 要区分客户端上的缓存和代理上的缓存 ,可以使用两个新属性 privatepublic

  • private 表示缓存只能在客户端保存,是用户 私有 的,不能放在代理上与别人共享。
  • public 的意思就是缓存完全开放,谁都可以存,谁都可以用。

小结

  1. 计算机领域里最常用的性能优化手段是时空转换,也就是时间换空间或者空间换时间,HTTP 缓存属于后者;
  2. 缓存代理是增加了缓存功能的代理服务,缓存源服务器的数据,分发给下游的客户端;
  3. Cache-Control 字段也可以控制缓存代理,常用的有 privates-maxageno-transform 等,同样必须配合 Last-modifiedETag 等字段才能使用;
  4. 缓存代理有时候也会带来负面影响,缓存不良数据,需要及时刷新或删除。

TLS 又是什么?

曾经谈到过 HTTP 的一些缺点,其中的「无状态」在加入 Cookie 后得到了解决,而另两个缺点—— 明文不安全 仅凭 HTTP 自身是无力解决的,需要引入新的 HTTPS 协议

为什么要有 HTTPS?

简单的回答是 因为 HTTP 不安全

由于 HTTP 天生明文 的特点,整个传输过程完全透明,任何人都能够在链路中截获、修改或者伪造请求 / 响应报文,数据不具有可信性。

对于安全性要求不那么高的新闻、视频、搜索等网站来说,由于互联网上的恶意用户、恶意代理越来越多,也很容易遭到 流量劫持 的攻击,在页面里强行嵌入广告,或者分流用户,导致各种利益损失。

总的来说,今天的互联网已经不再是早期的田园牧歌时代,而是进入了黑暗森林状态。上网的时候必须步步为营、处处小心,否则就会被不知道埋伏在哪里的黑客所猎杀

什么是安全?

既然 HTTP 不安全,那什么样的通信过程才是安全的呢?

通常认为,如果通信过程具备了四个特性,就可以认为是 「安全」的,这四个特性是:机密性、完整性,身份认证和不可否认

  • 机密性(Secrecy/Confidentiality)

    是指对数据的「保密」,只能由可信的人访问,对其他人是不可见的秘密,简单来说就是不能让不相关的人看到不该看的东西。

    比如小明和小红私下聊天,但隔墙有耳,被小强在旁边的房间里全偷听到了,这就是没有机密性。我们之前一直用的 Wireshark ,实际上也是利用了 HTTP 的这个特点,捕获了传输过程中的所有数据。

  • 完整性(Integrity,也叫一致性)

    是指数据在传输过程中 没有被窜改 ,不多也不少,完完整整地保持着原状。

    机密性虽然可以让数据成为秘密 ,但不能防止黑客对数据的修改,黑客可以替换数据,调整数据的顺序,或者增加、删除部分数据,破坏通信过程。

    比如,小明给小红写了张纸条:明天公园见。小强把「公园」划掉,模仿小明的笔迹把这句话改成了「明天广场见」。小红收到后无法验证完整性,信以为真,第二天的约会就告吹了。

  • 身份认证(Authentication)

    是指确认对方的真实身份,也就是「证明你真的是你」,保证消息只能发送给可信的人。

    如果通信时另一方是假冒的网站,那么数据再保密也没有用,黑客完全可以使用冒充的身份套出各种信息,加密和没加密一样。

    比如,小明给小红写了封情书:我喜欢你,但不留心发给了小强。小强将错就错,假冒小红回复了一个「白日做梦」,小明不知道这其实是小强的话,误以为是小红的,后果可想而知。

  • 不可否认(Non-repudiation/Undeniable)

    也叫不可抵赖,意思是不能否认已经发生过的行为,不能「说话不算数、耍赖皮」

使用前三个特性,可以解决安全通信的大部分问题,但如果缺了不可否认,那通信的事务真实性就得不到保证,有可能出现老赖。

比如,小明借了小红一千元,没写借条,第二天矢口否认,小红也确实拿不出借钱的证据,只能认倒霉。另一种情况是小明借钱后还了小红,但没写收条,小红于是不承认小明还钱的事,说根本没还,要小明再掏出一千元。

所以,只有同时具备了机密性、完整性、身份认证、不可否认这四个特性,通信双方的利益才能有保障,才能算得上是真正的安全。

什么是 HTTPS

HTTPS 其实是一个「非常简单」的协议,RFC 文档很小,只有短短的 7 页,里面规定了 新的协议名「https」,默认端口号 443 ,至于其他的什么请求 - 应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用 HTTP,没有任何新的东西。

你可能要问了,既然没有新东西,HTTPS 凭什么就能做到机密性、完整性这些安全特性呢?

秘密就在于 HTTPS 名字里的 S ,它把 HTTP 下层的传输协议由 TCP/IP 换成了 SSL/TLS,由 HTTP over TCP/IP 变成了 HTTP over SSL/TLS ,让 HTTP 运行在了安全的 SSL/TLS 协议上,收发报文不再使用 Socket API,而是调用专门的安全接口。

注意:

上面表述有误,不是将 TCP/IP 换成了 SSL/TLS,另外下面的图是正确的,只是上面表述不对

传输还是使用的 TCP/IP 协议,只是 SSL/TLS 把 HTTP 协议再包装了一下

SSL/TLS协议

SSL/TLS

SSL 即安全套接层(Secure Sockets Layer),在 OSI 模型中处于第 5 层(会话层),由网景公司于 1994 年发明,有 v2 和 v3 两个版本,而 v1 因为有严重的缺陷从未公开过。

SSL 发展到 v3 时已经证明了它自身是一个非常好的安全通信协议,于是互联网工程组 IETF 在 1999 年把它改名为 TLS(传输层安全,Transport Layer Security),正式标准化,版本号从 1.0 重新算起,所以 TLS1.0 实际上就是 SSLv3.1。

到今天 TLS 已经发展出了三个版本,分别是 2006 年的 1.1、2008 年的 1.2 和去年(2018)的 1.3,每个新版本都紧跟密码学的发展和互联网的现状,持续强化安全和性能,已经成为了信息安全领域中的权威标准。

目前应用的最广泛的 TLS 是 1.2,而之前的协议(TLS1.1/1.0、SSLv3/v2)都已经被认为是不安全的,各大浏览器即将在 2020 年左右停止支持,所以接下来的讲解都针对的是 TLS1.2。

TLS 由记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术。

浏览器和服务器在使用 TLS 建立连接时需要选择一组恰当的加密算法来实现安全通信,这些算法的组合被称为 密码套件(cipher suite,也叫加密套件)

TLS 的密码套件命名非常规范,格式很固定。基本的形式是

markdown
密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法

比如刚才的密码套件 ECDHE-RSA-AES256-GCM-SHA384 的意思就是:

  • 握手时使用 ECDHE 算法进行密钥交换
  • 用 RSA 签名和身份认证,
  • 握手后的通信使用 AES 对称算法,密钥长度 256 位
  • 分组模式是 GCM
  • 摘要算法 SHA384 用于消息认证和产生随机数

小结

  1. 因为 HTTP 是明文传输,所以不安全,容易被黑客窃听或窜改;
  2. 通信安全必须同时具备机密性、完整性,身份认证和不可否认这四个特性;
  3. HTTPS 的语法、语义仍然是 HTTP,但把下层的协议由 TCP/IP 换成了 SSL/TLS;
  4. SSL/TLS 是信息安全领域中的权威标准,采用多种先进的加密技术保证通信安全;
  5. OpenSSL 是著名的开源密码学工具包,是 SSL/TLS 的具体实现

固若金汤的根本: 对称加密与非对称加密

我们初步学习了 HTTPS,知道 HTTPS 的安全性是由 TLS 来保证的。

你一定很好奇,它是怎么为 HTTP 增加了机密性、完整性,身份认证和不可否认等特性的呢?

先说说机密性。它是信息安全的基础,缺乏机密性 TLS 就会成为无水之源、无根之木。

实现机密性最常用的手段是 加密(encrypt),就是把消息用某种方式转换成谁也看不懂的乱码,只有掌握 特殊钥匙 的人才能再转换出原始文本。

这里的钥匙就叫做 密钥(key),加密前的消息叫 明文(plain text/clear text),加密后的乱码叫 密文(cipher text),使用密钥还原明文的过程叫 解密(decrypt),是加密的反操作,加密解密的操作过程就是 加密算法

按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密

对称加密

对称加密 很好理解,就是指加密和解密时使用的 密钥都是同一个 ,是 对称 的。只要保证了密钥的安全,那整个通信过程就可以说具有了机密性。

非对称加密

对称加密看上去好像完美地实现了机密性,但其中有一个很大的问题:如何把密钥安全地传递给对方 ,术语叫 密钥交换

因为在对称加密算法中只要持有密钥就可以解密。如果你和网站约定的密钥在传递途中被黑客窃取,那他就可以在之后随意解密收发的数据,通信过程也就没有机密性可言了。

所以,就出现了非对称加密(也叫公钥加密算法)。

它有两个密钥,一个叫 公钥(public key),一个叫 私钥(private key)。两个密钥是不同的(不对称) ,公钥可以公开给任何人使用,而私钥必须严格保密。

公钥和私钥有个特别的 单向 性,虽然都可以用来加密解密,但 公钥加密后只能用私钥解密 ,反过来,私钥加密后也只能用公钥解密

非对称加密可以解决 密钥交换 的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。

混合加密

看到这里,你是不是认为可以抛弃对称加密,只用非对称加密来实现机密性呢?

很遗憾,虽然非对称加密没有 密钥交换 的问题,但因为它们都是基于复杂的数学难题,运算速度很慢,即使是 ECC 也要比 AES 差上好几个数量级。如果仅用非对称加密,虽然保证了安全,但通信速度有如乌龟、蜗牛,实用性就变成了零。

RSA 的运算速度是非常慢的,2048 位的加解密大约是 15KB/S(微秒或毫秒级),而 AES128 则是 13MB/S(纳秒级),差了几百倍

那么,是不是能够 把对称加密和非对称加密结合起来 呢,两者互相取长补短,即能高效地加密解密,又能安全地密钥交换。

这就是现在 TLS 里使用的 混合加密 方式,其实说穿了也很简单:

  1. 在通信刚开始的时候使用非对称算法 ,比如 RSA、ECDHE,首先解决密钥交换的问题
  2. 然后用随机数产生对称算法使用的 会话密钥(session key),再用 公钥加密 。因为会话密钥很短,通常只有 16 字节或 32 字节,所以慢一点也无所谓。
  3. 对方拿到密文后用 私钥解密 ,取出会话密钥。这样,双方就实现了对称密钥的安全交换,后续就不再使用非对称加密,全都使用对称加密。

e41f87110aeea3e548d58cc35a478e85.e41f8711

小结

  1. 加密算法的核心思想是「把一个小秘密(密钥)转化为一个大秘密(密文消息)」,守住了小秘密,也就守住了大秘密;
  2. 对称加密只使用一个密钥,运算速度快,密钥必须保密,无法做到安全的密钥交换,常用的有 AES 和 ChaCha20;
  3. 非对称加密使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,解决了密钥交换问题但速度慢,常用的有 RSA 和 ECC;
  4. 把对称加密和非对称加密结合起来就得到了「又好又快」的混合加密,也就是 TLS 里使用的加密方式

信任始于握手: TLS 1.2 连接过程解析

HTTPS 建立连接

浏览器首先要从 URI 里提取出协议名和域名。因为协议名是 https ,所以浏览器就知道了端口号是默认的 443,它再用 DNS 解析域名,得到目标的 IP 地址,然后就可以使用 三次握手与网站建立 TCP 连接 了。

在 HTTP 协议里,建立连接后,浏览器会立即发送请求报文。但现在是 HTTPS 协议,它需要再用另外一个 「握手」过程,在 TCP 上建立安全连接,之后才是收发 HTTP 报文。

TLS 协议的组成

TLS 包含几个子协议,你也可以理解为它是由几个不同职责的模块组成,比较常用的有记录协议、警报协议、握手协议、变更密码规范协议等。

小结

今天我们学习了 HTTPS/TLS 的握手,内容比较多、比较难,不过记住下面四点就可以。

  1. HTTPS 协议会先与服务器执行 TCP 握手,然后执行 TLS 握手,才能建立安全连接;
  2. 握手的目标是安全地交换对称密钥,需要三个随机数,第三个随机数 Pre-Master 必须加密传输,绝对不能让黑客破解;
  3. Hello 消息交换随机数,Key Exchange 消息交换 Pre-Master
  4. Change Cipher Spec 之前传输的都是明文,之后都是对称密钥加密的密文。

更好更快的握手: TLS 1.3 特性解析

不过 TLS1.2 已经是 10 年前(2008 年)的老协议了,虽然历经考验,但毕竟岁月不饶人,在安全、性能等方面已经跟不上如今的互联网了。

于是经过四年、近 30 个草案的反复打磨,TLS1.3 终于在去年(2018 年)粉墨登场,再次确立了信息安全领域的新标准。

在抓包分析握手之前,我们先来快速浏览一下 TLS1.3 的三个主要改进目标:兼容安全与性能

今天我们一起学习了 TLS1.3 的新特性,用抓包研究了它的握手过程,不过 TLS1.3 里的内容很多,还有一些特性没有谈到,后面会继续讲。

  1. 为了兼容 1.1、1.2 等“老”协议,TLS1.3 会“伪装”成 TLS1.2,新特性在“扩展”里实现;
  2. 1.1、1.2 在实践中发现了很多安全隐患,所以 TLS1.3 大幅度删减了加密算法,只保留了 ECDHE、AES、ChaCha20、SHA-2 等极少数算法,强化了安全;
  3. TLS1.3 也简化了握手过程,完全握手只需要一个消息往返,提升了性能。

连接太慢该怎么办 HTTPS 的优化

通过前两讲的学习,你可以看到,HTTPS 连接大致上可以划分为两个部分,第一个是建立连接时的 非对称加密握手 ,第二个是握手后的 对称加密报文传输

小结

  1. 可以有多种硬件和软件手段减少网络耗时和计算耗时,让 HTTPS 变得和 HTTP 一样快,最可行的是软件优化;
  2. 应当尽量使用 ECDHE 椭圆曲线密码套件,节约带宽和计算量,还能实现 False Start
  3. 服务器端应当开启 OCSP Stapling 功能,避免客户端访问 CA 去验证证书;
  4. 会话复用的效果类似 Cache,前提是客户端必须之前成功建立连接,后面就可以用 Session IDSession Ticket 等凭据跳过密钥交换、证书验证等步骤,直接开始加密通信

我应该迁移到 HTTPS 吗?

今天我介绍了一些 HTTPS 迁移的技术要点,掌握了它们你就可以搭建出一个完整的 HTTPS 站点了。

但想要实现大型网站的 「全站 HTTPS」还是需要有很多的细枝末节的工作要做,比如使用 CSP(Content Security Policy)的各种指令和标签来配置安全策略,使用反向代理来集中「卸载」SSL,话题太大,以后有机会再细谈吧。

简单小结一下今天的内容:

  1. 从 HTTP 迁移到 HTTPS 是大势所趋,能做就应该尽早做;
  2. 升级 HTTPS 首先要申请数字证书,可以选择免费好用的 Let’s Encrypt;
  3. 配置 HTTPS 时需要注意选择恰当的 TLS 版本和密码套件,强化安全;
  4. 原有的 HTTP 站点可以保留作为过渡,使用 301 重定向到 HTTPS。

时代之风:HTTP/2 特性概览

在 HTTP 有哪些优点?又有哪些缺点? 中,我们看到 HTTP 有两个主要的缺点:安全不足和性能不高

刚结束的安全篇里的 HTTPS,通过引入 SSL/TLS 在安全上达到了「极致」,但在性能提升方面却是乏善可陈,只优化了握手加密的环节,对于整体的数据传输没有提出更好的改进方案,还只能依赖于「长连接」这种「落后」的技术

所以,在 HTTPS 逐渐成熟之后,HTTP 就向着性能方面开始发力,走出了另一条进化的道

在 HTTP 历史中你也看到了,「秦失其鹿,天下共逐之」,Google 率先发明了 SPDY 协议,并应用于自家的浏览器 Chrome,打响了 HTTP 性能优化的「第一枪」

随后互联网标准化组织 IETF 以 SPDY 为基础,综合其他多方的意见,终于推出了 HTTP/1 的继任者,也就是今天的主角 HTTP/2 ,在性能方面有了一个大的飞跃。

为什么不是 HTTP/2.0

你一定很想知道,为什么 HTTP/2 不像之前的 1.01.1 那样叫 2.0 呢?

这个也是很多初次接触 HTTP/2 的人问的最多的一个问题,对此 HTTP/2 工作组特别给出了解释。

他们认为以前的 1.01.1 造成了很多的混乱和误解,让人在实际的使用中难以区分差异,所以就决定 HTTP 协议不再使用小版本号(minor version),只使用大版本号(major version),从今往后 HTTP 协议不会出现 HTTP/2.0、2.1,只会有 HTTP/2HTTP/3 ……

这样就可以明确无误地辨别出协议版本的「跃进程度」,让协议在一段较长的时期内保持稳定,每当发布新版本的 HTTP 协议都会有本质的不同,绝不会有零敲碎打的小改良

兼容 HTTP/1

由于 HTTPS 已经在安全方面做的非常好了,所以 HTTP/2 的唯一目标就是改进性能。

但它不仅背负着众多的期待,同时还背负着 HTTP/1 庞大的历史包袱,所以协议的修改必须小心谨慎,兼容性是首要考虑的目标,否则就会破坏互联网上无数现有的资产,这方面 TLS 已经有了先例(为了兼容 TLS1.2 不得不进行伪装)。

那么,HTTP/2 是怎么做的呢?

因为必须要保持功能上的兼容,所以 HTTP/2 把 HTTP 分解成了 语义语法 两个部分,语义层不做改动,与 HTTP/1 完全一致(即 RFC7231)。比如请求方法、URI、状态码、头字段等概念都保留不变,这样就消除了再学习的成本,基于 HTTP 的上层应用也不需要做任何修改,可以无缝转换到 HTTP/2。

特别要说的是,与 HTTPS 不同,HTTP/2 没有在 URI 里引入新的协议名,仍然用 http 表示明文协议,用 https 表示加密协议

这是一个非常了不起的决定,可以让浏览器或者服务器去自动升级或降级协议,免去了选择的麻烦,让用户在上网的时候都意识不到协议的切换,实现平滑过渡。

在语义保持稳定之后,HTTP/2 在语法层做了天翻地覆的改造,完全变更了 HTTP 报文的传输格式

头部压缩

首先,HTTP/2 对报文的头部做了一个大手术。

通过进阶篇的学习你应该知道,HTTP/1 里可以用头字段 Content-Encoding 指定 Body 的编码方式,比如用 gzip 压缩来节约带宽,但报文的另一个组成部分—— Header 却被无视了,没有针对它的优化手段

由于报文 Header 一般会携带 User AgentCookieAcceptServer 等许多固定的头字段,多达几百字节甚至上千字节,但 Body 却经常只有几十字节(比如 GET 请求、204/301/304 响应),成了不折不扣的大头儿子。更要命的是,成千上万的请求响应报文里有很多字段值都是重复的,非常浪费,长尾效应导致大量带宽消耗在了这些冗余度极高的数据上。

所以,HTTP/2 把 头部压缩 作为性能改进的一个重点,优化的方式你也肯定能想到,还是 压缩

不过 HTTP/2 并没有使用传统的压缩算法,而是开发了专门的 HPACK 算法,在客户端和服务器两端建立「字典」,用索引号表示重复的字符串,还釆用哈夫曼编码来压缩整数和字符串,可以达到 50%~90% 的高压缩率

二进制格式

你可能已经很习惯于 HTTP/1 里纯文本形式的报文了,它的优点是 一目了然 ,用最简单的工具就可以开发调试,非常方便。

但 HTTP/2 在这方面没有妥协,决定改变延续了十多年的现状,不再使用肉眼可见的 ASCII 码,而是向下层的 TCP/IP 协议靠拢,全面采用二进制格式。

这样虽然对人不友好,但却大大方便了计算机的解析。原来使用纯文本的时候容易出现多义性,比如大小写、空白字符、回车换行、多字少字等等,程序在处理时必须用复杂的状态机,效率低,还麻烦。

而二进制里只有 0 和 1 ,可以严格规定字段大小、顺序、标志位等格式,对就是对,错就是错,解析起来没有歧义,实现简单,而且体积小、速度快,做到内部提效。

以二进制格式为基础,HTTP/2 就开始了大刀阔斧的改革。

它把 TCP 协议的部分特性挪到了应用层,把原来的 Header+Body 的消息打散为数个小片的 二进制「帧」(Frame),用 HEADERS 帧存放头数据、DATA 帧存放实体数据。

这种做法有点像是 Chunked 分块编码的方式(参见 把大象装进冰箱:HTTP 传输大文件的方法),也是化整为零的思路,但 HTTP/2 数据分帧后 Header+Body 的报文结构就完全消失了,协议看到的只是一个个的碎片

8fe2cbd57410299a1a36d7eb105ea896.8fe2cbd5

虚拟的「流」

消息的「碎片」到达目的地后应该怎么组装起来呢?

HTTP/2 为此定义了一个 (Stream)的概念,它是二进制帧的双向传输序列 ,同一个消息往返的帧会分配一个唯一的流 ID。你可以想象把它成是一个虚拟的「数据流」,在里面流动的是一串有先后顺序的数据帧,这些数据帧按照次序组装起来就是 HTTP/1 里的请求报文和响应报文。

因为流是虚拟的,实际上并不存在,所以 HTTP/2 就可以在一个 TCP 连接上用 同时发送多个「碎片化」的消息,这就是常说的 多路复用( Multiplexing)——多个往返通信都复用一个连接来处理。

在流的层面上看,消息是一些有序的帧序列,而在连接的层面上看,消息却是乱序收发的帧。多个请求 / 响应之间没有了顺序关系,不需要排队等待,也就不会再出现队头阻塞问题,降低了延迟,大幅度提高了连接的利用率。

流

为了更好地利用连接,加大吞吐量,HTTP/2 还添加了一些控制帧来管理虚拟的流,实现了优先级和流量控制,这些特性也和 TCP 协议非常相似。

HTTP/2 还在一定程度上改变了传统的请求 - 应答工作模式,服务器不再是完全被动地响应请求,也可以新建流主动向客户端发送消息。比如,在浏览器刚请求 HTML 的时候就提前把可能会用到的 JS、CSS 文件发给客户端,减少等待的延迟,这被称为 服务器推送(Server Push,也叫 Cache Push)

协议栈

面的这张图对比了 HTTP/1、HTTPS 和 HTTP/2 的协议栈,你可以清晰地看到,HTTP/2 是建立在 HPackStreamTLS1.2 基础之上的,比 HTTP/1、HTTPS 复杂了一些。

协议栈

小结

今天我简略介绍了 HTTP/2 的一些重要特性,比较偏重理论,下一次我会用 Wireshark 抓包,具体讲解 HTTP/2 的头部压缩、二进制帧和流等特性。

  1. HTTP 协议取消了小版本号,所以 HTTP/2 的正式名字不是 2.0;
  2. HTTP/2 在语义上兼容 HTTP/1,保留了请求方法、URI 等传统概念;
  3. HTTP/2 使用 HPACK 算法压缩头部信息,消除冗余数据节约带宽;
  4. HTTP/2 的消息不再是 Header+Body 的形式,而是分散为多个二进制帧;
  5. HTTP/2 使用虚拟的流传输消息,解决了困扰多年的队头阻塞问题,同时实现了多路复用,提高连接的利用率;
  6. HTTP/2 也增强了安全性,要求至少是 TLS1.2,而且禁用了很多不安全的密码套件

时代之风:HTTP/2 内核剖析

HTTP/2 的流有哪些特点呢?我给你简单列了一下:

  1. 流是可并发的,一个 HTTP/2 连接上可以同时发出多个流传输数据,也就是并发多请求,实现多路复用;
  2. 客户端和服务器都可以创建流,双方互不干扰;
  3. 流是双向的,一个流里面客户端和服务器都可以发送或接收数据帧,也就是一个请求 - 应答来回;
  4. 流之间没有固定关系,彼此独立,但流内部的帧是有严格顺序的;
  5. 流可以设置优先级,让服务器优先处理,比如先传 HTML/CSS,后传图片,优化用户体验;
  6. 流 ID 不能重用,只能顺序递增,客户端发起的 ID 是奇数,服务器端发起的 ID 是偶数;
  7. 在流上发送 RST_STREAM 帧可以随时终止流,取消接收或发送;
  8. 第 0 号流比较特殊,不能关闭,也不能发送数据帧,只能发送控制帧,用于流量控制

小结

HTTP/2 的内容实在是太多了,为了方便学习,我砍掉了一些特性,比如流的优先级、依赖关系、流量控制等。

但只要你掌握了今天的这些内容,以后再看 RFC 文档都不会有难度了。

  1. HTTP/2 必须先发送一个连接前言字符串,然后才能建立正式连接;
  2. HTTP/2 废除了起始行,统一使用头字段,在两端维护字段 Key-Value 的索引表,使用 HPACK 算法压缩头部;
  3. HTTP/2 把报文切分为多种类型的二进制帧,报头里最重要的字段是流标识符,标记帧属于哪个流;
  4. 流是 HTTP/2 虚拟的概念,是帧的双向传输序列,相当于 HTTP/1 里的一次请求 - 应答;
  5. 在一个 HTTP/2 连接上可以并发多个流,也就是多个“请求 - 响应”报文,这就是多路复用

未来之路:HTTP/3 展望

在前面的两讲里,我们一起学习了 HTTP/2,你也应该看到了 HTTP/2 做出的许多努力,比如头部压缩、二进制分帧、虚拟的流与多路复用,性能方面比 HTTP/1 有了很大的提升,基本上解决了队头阻塞这个老大难问题。

HTTP/2 的队头阻塞

等等,你可能要发出疑问了:为什么说是 基本上 ,而不是完全解决了呢?

这是因为 HTTP/2 虽然使用帧、流、多路复用,没有了队头阻塞,但这些手段都是在应用层里 ,而在下层,也就是 TCP 协议里,还是会发生队头阻塞。

这是怎么回事呢

让我们从协议栈的角度来仔细看一下。在 HTTP/2 把多个请求 - 响应分解成流,交给 TCP 后,TCP 会再拆成更小的包依次发送(其实在 TCP 里应该叫 segment,也就是段)。

在网络良好的情况下,包可以很快送达目的地。但如果网络质量比较差,像手机上网的时候,就有可能会丢包。而 TCP 为了保证可靠传输,有个特别的 丢包重传 机制,丢失的包必须要等待重新传输确认,其他的包即使已经收到了,也只能放在缓冲区里,上层的应用拿不出来,只能干着急。

我举个简单的例子:

客户端用 TCP 发送了三个包,但服务器所在的操作系统只收到了后两个包,第一个包丢了。那么内核里的 TCP 协议栈就只能把已经收到的包暂存起来,停下等着客户端重传那个丢失的包,这样就又出现了队头阻塞

由于这种队头阻塞是 TCP 协议固有的,所以 HTTP/2 即使设计出再多的花样也无法解决。

Google 在推 SPDY 的时候就已经意识到了这个问题,于是就又发明了一个新的 QUIC 协议,让 HTTP 跑在 QUIC 上而不是 TCP 上。

而这个 HTTP over QUIC 就是 HTTP 协议的下一个大版本,HTTP/3 。它在 HTTP/2 的基础上又实现了质的飞跃,真正完美地解决了队头阻塞问题。

不过 HTTP/3 目前还处于草案阶段,正式发布前可能会有变动,所以今天我尽量不谈那些不稳定的细节。

这里先贴一下 HTTP/3 的协议栈图,让你对它有个大概的了解

协议栈

QUIC 协议

从这张图里,你可以看到 HTTP/3 有一个关键的改变,那就是它把下层的 TCP 抽掉了,换成了 UDP。因为 UDP 是无序的,包之间没有依赖关系 ,所以就从根本上解决了队头阻塞。

你一定知道,UDP 是一个简单、不可靠的传输协议,只是对 IP 协议的一层很薄的包装,和 TCP 相比,它实际应用的较少。

不过正是因为它简单,不需要建连和断连,通信成本低,也就非常灵活、高效,可塑性很强。

所以,QUIC 就选定了 UDP,在它之上把 TCP 的那一套连接管理、拥塞窗口、流量控制等搬了过来,去其糟粕,取其精华,打造出了一个全新的可靠传输协议,可以认为是 新时代的 TCP

QUIC 最早是由 Google 发明的,被称为 gQUIC。而当前正在由 IETF 标准化的 QUIC 被称为 iQUIC。两者的差异非常大,甚至比当年的 SPDY 与 HTTP/2 的差异还要大。

gQUIC 混合了 UDP、TLS、HTTP,是一个应用层的协议。而 IETF 则对 gQUIC 做了清理,把应用部分分离出来,形成了 HTTP/3,原来的 UDP 部分下放到了传输层,所以 iQUIC 有时候也叫 QUIC-transport

接下来要说的 QUIC 都是指 iQUIC,要记住,它与早期的 gQUIC 不同,是一个传输层的协议,和 TCP 是平级的

QUIC 的特点

QUIC 基于 UDP,而 UDP 是无连接的,根本就不需要握手和挥手,所以天生就要比 TCP 快。

就像 TCP 在 IP 的基础上实现了可靠传输一样,QUIC 也基于 UDP 实现了可靠传输,保证数据一定能够抵达目的地。它还引入了类似 HTTP/2 的流和多路复用,单个流是有序的,可能会因为丢包而阻塞,但其他流不会受到影响。

为了防止网络上的中间设备(Middle Box)识别协议的细节,QUIC 全面采用加密通信,可以很好地抵御窜改和协议僵化(ossification)。

而且,因为 TLS1.3 已经在去年(2018)正式发布,所以 QUIC 就直接应用了 TLS1.3,顺便也就获得了 0-RTT、1-RTT 连接的好处。

但 QUIC 并不是建立在 TLS 之上,而是内部“包含”了 TLS。它使用自己的帧接管了 TLS 里的记录,握手消息、警报消息都不使用 TLS 记录,直接封装成 QUIC 的帧发送,省掉了一次开销

小结

HTTP/3 综合了我们之前讲的所有技术(HTTP/1、SSL/TLS、HTTP/2),包含知识点很多,比如队头阻塞、0-RTT 握手、虚拟的流、多路复用,算得上是集大成之作,需要多下些功夫好好体会。

  1. HTTP/3 基于 QUIC 协议,完全解决了队头阻塞问题,弱网环境下的表现会优于 HTTP/2;
  2. QUIC 是一个新的传输层协议,建立在 UDP 之上,实现了可靠传输;
  3. QUIC 内含了 TLS1.3,只能加密通信,支持 0-RTT 快速建连;
  4. QUIC 的连接使用不透明的连接 ID,不绑定在 IP 地址 + 端口上,支持连接迁移;
  5. QUIC 的流与 HTTP/2 的流很相似,但分为双向流和单向流;
  6. HTTP/3 没有指定默认端口号,需要用 HTTP/2 的扩展帧 Alt-Svc 来发现

我应该迁移到 HTTP/2 吗?

HTTP/2 的优点

首先要说的是,HTTP/2 最大的一个优点是 完全保持了与 HTTP/1 的兼容 ,在语义上没有任何变化,之前在 HTTP 上的所有投入都不会浪费

节约带宽的基本手段就是压缩,在 HTTP/1 里只能压缩 body,而 HTTP/2 则可以用 HPACK 算法压缩 header,这对高流量的网站非常有价值,有数据表明能节省大概 5%~10% 的流量,这是实实在在的真金白银。

与 HTTP/1 并发多个连接不同,HTTP/2 的多路复用特性要求对 一个域名(或者 IP)只用一个 TCP 连接 ,所有的数据都在这一个连接上传输,这样不仅节约了客户端、服务器和网络的资源,还可以把带宽跑满,让 TCP 充分吃饱。

这是为什么呢?

我们来看一下在 HTTP/1 里的长连接,虽然是双向通信,但任意一个时间点实际上还是单向的 :上行请求时下行空闲,下行响应时上行空闲,再加上 队头阻塞 ,实际的带宽打了个对折还不止。

而在 HTTP/2 里,多路复用则让 TCP 开足了马力,全速狂奔,多个请求响应并发,每时每刻上下行方向上都有流在传输数据,没有空闲的时候,带宽的利用率能够接近 100% 。所以,HTTP/2 只使用一个连接,就能抵得过 HTTP/1 里的五六个连接。

不过流也可能会有依赖关系,可能会存在等待导致的阻塞,这就是 延迟 ,所以 HTTP/2 的其他特性就派上了用场。

优先级 可以让客户端告诉服务器,哪个文件更重要,更需要优先传输,服务器就可以调高流的优先级,合理地分配有限的带宽资源,让高优先级的 HTML、图片更快地到达客户端,尽早加载显示。

服务器推送 也是降低延迟的有效手段,它不需要客户端预先请求,服务器直接就发给客户端,这就省去了客户端解析 HTML 再请求的时间

HTTP/2 的缺点

TCP 协议存在队头阻塞,所以 HTTP/2 在弱网或者移动网络下的性能表现会不如 HTTP/1

小结

今天我们讨论了是否应该迁移到 HTTP/2,还有应该如何迁移到 HTTP/2。

  1. HTTP/2 完全兼容 HTTP/1,是更安全的 HTTP、更快的 HTTPS,头部压缩、多路复用等技术可以充分利用带宽,降低延迟,从而大幅度提高上网体验;
  2. TCP 协议存在队头阻塞,所以 HTTP/2 在弱网或者移动网络下的性能表现会不如 HTTP/1;
  3. 迁移到 HTTP/2 肯定会有性能提升,但高流量网站效果会更显著;
  4. 如果已经升级到了 HTTPS,那么再升级到 HTTP/2 会很简单;
  5. TLS 协议提供 ALPN 扩展,让客户端和服务器协商使用的应用层协议,发现 HTTP/2 服务

Nginx:高性能的 Web 服务器

而 Nginx 作为轻量级的服务器,它的 CPU、内存占用都非常少,同样的资源配置下就能够为更多的用户提供服务,其奥秘在于它独特的工作模式。

3e94fbd78ed043e88c443f6416f99dc1.3e94fbd7

在 Nginx 之前,Web 服务器的工作模式大多是 Per-Process 或者 Per-Thread ,对每一个请求使用单独的进程或者线程处理。这就存在创建进程或线程的成本,还会有进程、线程 上下文切换 的额外开销。如果请求数量很多,CPU 就会在多个进程、线程之间切换时疲于奔命,平白地浪费了计算时间。

Nginx 则完全不同,一反惯例地没有使用多线程,而是使用了 进程池 + 单线程 的工作模式。

Nginx 在启动的时候会预先创建好固定数量的 worker 进程,在之后的运行过程中不会再 fork 出新进程,这就是进程池,而且可以自动把进程 「绑定」到独立的 CPU 上,这样就完全消除了进程创建和切换的成本,能够充分利用多核 CPU 的计算能力。

在进程池之上,还有一个 master 进程,专门用来管理进程池。它的作用有点像是 supervisor(一个用 Python 编写的进程管理工具),用来监控进程,自动恢复发生异常的 worker,保持进程池的稳定和服务能力。

不过 master 进程完全是 Nginx 自行用 C 语言实现的,这就摆脱了外部的依赖,简化了 Nginx 的部署和配置

I/O 多路复用

如果你用 Java、C 等语言写过程序,一定很熟悉 多线程 的概念,使用多线程能够很容易实现并发处理。

但多线程也有一些缺点,除了刚才说到的 「上下文切换」成本,还有编程模型复杂、数据竞争、同步等问题,写出正确、快速的多线程程序并不是一件容易的事情。

所以 Nginx 就选择了单线程的方式,带来的好处就是开发简单,没有互斥锁的成本,减少系统消耗。

那么,疑问也就产生了:为什么单线程的 Nginx,处理能力却能够超越其他多线程的服务器呢?

这要归功于 Nginx 利用了 Linux 内核里的一件神兵利器,I/O 多路复用接口 ,大名鼎鼎的 epoll。

小结

  1. Nginx 是一个高性能的 Web 服务器,它非常的轻量级,消耗的 CPU、内存很少;
  2. Nginx 采用 master/workers 进程池架构,不使用多线程,消除了进程、线程切换的成本;
  3. Nginx 基于 epoll 实现了 I/O 多路复用 ,不会阻塞,所以性能很高;
  4. Nginx 使用了职责链模式,多个模块分工合作,自由组合,以流水线的方式处理 HTTP 请求

OpenResty:更灵活的 Web 服务器

这就是我今天要说的 OpenResty,它是一个 更好更灵活的 Nginx

  1. Nginx 依赖于磁盘上的静态配置文件,修改后必须重启才能生效,缺乏灵活性;
  2. OpenResty 基于 Nginx,打包了很多有用的模块和库,是一个高性能的 Web 开发平台;
  3. OpenResty 的工作语言是 Lua,它小巧灵活,执行效率高,支持“代码热加载”;
  4. OpenResty 的核心编程范式是 同步非阻塞 ,使用协程,不需要异步回调函数;
  5. OpenResty 也使用阶段式处理的工作模式,但因为在阶段里执行的都是 Lua 代码,所以非常灵活,配合 Redis 等外部数据库能够实现各种动态配置

WAF:保护我们的网络服务

Web 服务遇到的威胁

黑客都有哪些手段来攻击 Web 服务呢?我给你大概列出几种常见的方式。

第一种叫 DDoS 攻击(distributed denial-of-service attack),有时候也叫「洪水攻击」

黑客会控制许多僵尸计算机,向目标服务器发起大量无效请求。因为服务器无法区分正常用户和黑客,只能照单全收,这样就挤占了正常用户所应有的资源。如果黑客的攻击强度很大,就会像洪水一样对网站的服务能力造成冲击,耗尽带宽、CPU 和内存,导致网站完全无法提供正常服务。

DDoS 攻击方式比较简单粗暴,虽然很有效,但不涉及 HTTP 协议内部的细节,技术含量比较低,不过下面要说的几种手段就不一样了。

SQL 注入(SQL injection)应该算是最著名的一种 代码注入 攻击了,它利用了服务器字符串拼接形成 SQL 语句的漏洞,构造出非正常的 SQL 语句,获取数据库内部的敏感信息。

另一种 HTTP 头注入 攻击的方式也是类似的原理,它在 HostUser-AgentX-Forwarded-For 等字段里加入了恶意数据或代码,服务端程序如果解析不当,就会执行预设的恶意代码。

在之前的让我知道你是谁:HTTP 的 Cookie 机制里,也说过一种利用 Cookie 的攻击手段,跨站脚本(XSS)攻击,它属于 JS 代码注入 ,利用 JavaScript 脚本获取未设防的 Cookie

网络应用防火墙

面对这么多的黑客攻击手段,我们应该怎么防御呢?

这就要用到 网络应用防火墙(Web Application Firewall)了,简称为 WAF

你可能对传统的防火墙比较熟悉。传统防火墙工作在三层或者四层,隔离了外网和内网,使用预设的规则,只允许某些特定 IP 地址和端口号的数据包通过,拒绝不符合条件的数据流入或流出内网,实质上是 一种网络数据过滤设备

WAF 也是一种防火墙,但它工作在七层,看到的不仅是 IP 地址和端口号,还能看到整个 HTTP 报文,所以就能够对报文内容做更深入细致的审核,使用更复杂的条件、规则来过滤数据。

WAF 都能干什么呢?

通常一款产品能够称为 WAF,要具备下面的一些功能:

  • IP 黑名单和白名单,拒绝黑名单上地址的访问,或者只允许白名单上的用户访问;
  • URI 黑名单和白名单,与 IP 黑白名单类似,允许或禁止对某些 URI 的访问;
  • 防护 DDoS 攻击,对特定的 IP 地址限连限速;
  • 过滤请求报文,防御“代码注入”攻击;
  • 过滤响应报文,防御敏感信息外泄;
  • 审计日志,记录所有检测到的入侵操作。

今天我们一起学习了“网络应用防火墙”,也就是 WAF,使用它可以加固 Web 服务。

  1. Web 服务通常都运行在公网上,容易受到 DDoS、代码注入等各种黑客攻击,影响正常的服务,所以必须要采取措施加以保护;
  2. WAF 是一种 HTTP 入侵检测和防御系统,工作在七层,为 Web 服务提供全面的防护;
  3. ModSecurity 是一个开源的、生产级的 WAF 产品,核心组成部分是 规则引擎规则集 ,两者的关系有点像杀毒引擎和病毒特征库;
  4. WAF 实质上是模式匹配与数据过滤,所以会消耗 CPU,增加一些计算成本,降低服务能力,使用时需要在安全与性能之间找到一个平衡点。

CDN:加速我们的网络服务

在正式开讲前,我们先来看看到现在为止 HTTP 手头都有了哪些「武器」。

协议方面,HTTPS 强化通信链路安全、HTTP/2 优化传输效率;应用方面,Nginx/OpenResty 提升网站服务能力,WAF 抵御网站入侵攻击,讲到这里,你是不是感觉还少了点什么?

没错,在应用领域,还缺一个在外部加速 HTTP 协议的服务,这个就是我们今天要说的 CDN(Content Delivery Network 或 Content Distribution Network),中文名叫 内容分发网络

套用一句广告词来形容 CDN 吧,我觉得非常恰当:我们不生产内容,我们只是内容的搬运工。

CDN,正是把数据传输这件看似简单的事情做大做强、做专做精,就像专门的快递公司一样,在互联网世界里实现了它的价值

小结

CDN 发展到现在已经有二十来年的历史了,早期的 CDN 功能比较简单,只能加速静态资源。随着这些年 Web 2.0、HTTPS、视频、直播等新技术、新业务的崛起,它也在不断进步,增加了很多的新功能,比如 SSL 加速、内容优化(数据压缩、图片格式转换、视频转码)、资源防盗链、WAF 安全防护等等。

现在,再说 CDN 是搬运工已经不太准确了,它更像是一个无微不至的网站保姆,让网站只安心生产优质的内容,其他的杂事都由它去代劳。

  1. 由于客观地理距离的存在,直连网站访问速度会很慢,所以就出现了 CDN;
  2. CDN 构建了全国、全球级别的专网,让用户就近访问专网里的边缘节点,降低了传输延迟,实现了网站加速;
  3. GSLB 是 CDN 的“大脑”,使用 DNS 负载均衡技术,智能调度边缘节点提供服务;
  4. 缓存系统是 CDN 的“心脏”,使用 HTTP 缓存代理技术,缓存命中就返回给用户,否则就要回源

WebSocket:沙盒里的 TCP

单从名字上看,Web 指的是 HTTP,Socket 是套接字调用,那么这两个连起来又是什么意思呢?

所谓望文生义,大概你也能猜出来,WebSocket 就是运行在 Web,也就是 HTTP 上的 Socket 通信规范,提供与 TCP Socket 类似的功能,使用它就可以像 TCP Socket 一样调用下层协议栈,任意地收发数据。

为什么要有 WebSocket

不过,已经有了被广泛应用的 HTTP 协议,为什么要再出一个 WebSocket 呢?它有哪些好处呢?

其实 WebSocket 与 HTTP/2 一样,都是为了解决 HTTP 某方面的缺陷而诞生的。HTTP/2 针对的是“队头阻塞”,而 WebSocket 针对的是“请求 - 应答”通信模式。

那么,请求 - 应答有什么不好的地方呢?

请求 - 应答是一种 半双工 的通信模式,虽然可以双向收发数据,但同一时刻只能一个方向上有动作,传输效率低。更关键的一点,它是一种“被动”通信模式,服务器只能“被动”响应客户端的请求,无法主动向客户端发送数据。

虽然后来的 HTTP/2、HTTP/3 新增了 Stream、Server Push 等特性,但“请求 - 应答”依然是主要的工作方式。这就导致 HTTP 难以应用在动态页面、即时消息、网络游戏等要求 实时通信 的领域。

在 WebSocket 出现之前,在浏览器环境里用 JavaScript 开发实时 Web 应用很麻烦。因为浏览器是一个受限的沙盒,不能用 TCP,只有 HTTP 协议可用,所以就出现了很多变通的技术,轮询(polling)就是比较常用的的一种。

简单地说,轮询就是不停地向服务器发送 HTTP 请求,问有没有数据,有数据的话服务器就用响应报文回应。如果轮询的频率比较高,那么就可以近似地实现“实时通信”的效果。

但轮询的缺点也很明显,反复发送无效查询请求耗费了大量的带宽和 CPU 资源,非常不经济。

所以,为了克服 HTTP 请求 - 应答模式的缺点,WebSocket 就应运而生了。它原来是 HTML5 的一部分,后来自立门户,形成了一个单独的标准,RFC 文档编号是 6455

小结

浏览器是一个沙盒环境,有很多的限制,不允许建立 TCP 连接收发数据,而有了 WebSocket,我们就可以在浏览器里与服务器直接建立 TCP 连接,获得更多的自由。

不过自由也是有代价的,WebSocket 虽然是在应用层,但使用方式却与 TCP Socket 差不多,过于原始,用户必须自己管理连接、缓存、状态,开发上比 HTTP 复杂的多,所以是否要在项目中引入 WebSocket 必须慎重考虑。

  1. HTTP 的请求 - 应答模式不适合开发实时通信应用,效率低,难以实现动态页面,所以出现了 WebSocket;
  2. WebSocket 是一个全双工的通信协议,相当于对 TCP 做了一层薄薄的包装,让它运行在浏览器环境里;
  3. WebSocket 使用兼容 HTTP 的 URI 来发现服务,但定义了新的协议名 wswss ,端口号也沿用了 80 和 443;
  4. WebSocket 使用二进制帧,结构比较简单,特殊的地方是有个“掩码”操作,客户端发数据必须掩码,服务器则不用;
  5. WebSocket 利用 HTTP 协议实现连接握手,发送 GET 请求要求协议升级,握手过程中有个非常简单的认证机制,目的是防止误连接

总结篇: HTTP 性能优化面面观(上)

既然要做性能优化,那么,我们就需要知道:什么是性能?它都有哪些指标,又应该如何度量,进而采取哪些手段去优化?

性能其实是一个复杂的概念。不同的人、不同的应用场景都会对它有不同的定义。对于 HTTP 来说,它又是一个非常复杂的系统,里面有非常多的角色,所以很难用一两个简单的词就能把性能描述清楚。

还是从 HTTP 最基本的 请求 - 应答 模型来着手吧。在这个模型里有两个角色:客户端和服务器,还有中间的传输链路,考查性能就可以看这三个部分。

HTTP 服务器

我们先来看看服务器,它一般运行在 Linux 操作系统上,用 Apache、Nginx 等 Web 服务器软件对外提供服务,所以,性能的含义就是它的服务能力,也就是尽可能多、尽可能快地处理用户的请求。

衡量服务器性能的主要指标有三个:吞吐量(requests per second)、并发数(concurrency)和 响应时间(time per request)。

吞吐量就是我们常说的 RPS,每秒的请求次数,也有叫 TPS、QPS,它是服务器最基本的性能指标,RPS 越高就说明服务器的性能越好。

并发数反映的是服务器的负载能力,也就是服务器能够同时支持的客户端数量,当然也是越多越好,能够服务更多的用户。

响应时间反映的是服务器的处理能力,也就是快慢程度,响应时间越短,单位时间内服务器就能够给越多的用户提供服务,提高吞吐量和并发数。

HTTP 客户端

首先,我们必须谨记有一个不可逾越的障碍—— 光速,因为地理距离而导致的延迟是无法克服的,访问数千公里外的网站显然会有更大的延迟。

其次,第二个因素是 带宽,它又包括接入互联网时的电缆、WiFi、4G 和运营商内部网络、运营商之间网络的各种带宽,每一处都有可能成为数据传输的瓶颈,降低传输速度,增加延迟。

第三个因素是 DNS 查询 ,如果域名在本地没有缓存,就必须向 DNS 系统发起查询,引发一连串的网络通信成本,而在获取 IP 地址之前客户端只能等待,无法访问网站,

第四个因素是 TCP 握手 ,你应该对它比较熟悉了吧,必须要经过 SYN、SYN/ACK、ACK 三个包之后才能建立连接,它带来的延迟由光速和带宽共同决定

小结

  1. 性能优化是一个复杂的概念,在 HTTP 里可以分解为服务器性能优化、客户端性能优化和传输链路优化;
  2. 服务器有三个主要的性能指标:吞吐量、并发数和响应时间,此外还需要考虑资源利用率;
  3. 客户端的基本性能指标是延迟,影响因素有地理距离、带宽、DNS 查询、TCP 握手等;
  4. 从服务器到客户端的传输链路可以分为三个部分,我们能够优化的是前两个部分,也就是第一公里和中间一公里;
  5. 有很多工具可以测量这些指标,服务器端有 ab、top、sar 等,客户端可以使用测试网站,浏览器的开发者工具

总结篇: HTTP 性能优化面面观(下)

在整个 HTTP 系统里有三个可优化的环节,分别是 服务器客户端传输链路(第一公里和中间一公里)。但因为我们是无法完全控制客户端的,所以实际上的优化工作通常是在服务器端。这里又可以细分为后端和前端,后端是指网站的后台服务,而前端就是 HTML、CSS、图片等展现在客户端的代码和数据。

知道了大致的方向,HTTP 性能优化具体应该怎么做呢?

总的来说,任何计算机系统的优化都可以分成这么几类:硬件软件、内部外部、花钱不花钱。

投资购买现成的硬件 最简单的优化方式,比如换上更强的 CPU、更快的网卡、更大的带宽、更多的服务器,效果也会立竿见影,直接提升网站的服务能力,也就实现了 HTTP 优化。

另外,花钱购买外部的软件或者服务 也是一种行之有效的优化方式,最物有所值的应该算是 CDN 了。CDN 专注于网络内容交付,帮助网站解决中间一公里的问题,还有很多其他非常专业的优化功能。把网站交给 CDN 运营,就好像是让网站坐上了喷气飞机,能够直达用户,几乎不需要费什么力气就能够达成很好的优化效果。

不过这些花钱的手段实在是太没有技术含量了,属于懒人(无贬义)的做法,所以我就不再细说,接下来重点就讲讲在网站内部、不花钱的软件优化。

我把这方面的 HTTP 性能优化概括为三个关键词:开源节流缓存

开源

首先,我们应该选用高性能的 Web 服务器,最佳选择当然就是 Nginx/OpenResty 了,尽量不要选择基于 Java、Python、Ruby 的其他服务器,它们用来做后面的业务逻辑服务器更好。利用 Nginx 强大的反向代理能力实现动静分离,动态页面交给 Tomcat、Django、Rails,图片、样式表等静态资源交给 Nginx。

下面给出一个简短的 Nginx 配置示例,启用了长连接等优化参数,实现了动静分离:

nginx
server {
  listen 80 deferred reuseport backlog=4096 fastopen=1024;


  keepalive_timeout  60;
  keepalive_requests 10000;

  location ~* \.(png)$ {
    root /var/images/png/;
  }

  location ~* \.(php)$ {
    proxy_pass http://php_back_end;
  }
}

节流

节流是指减少客户端和服务器之间收发的数据量,在有限的带宽里传输更多的内容。

节流最基本的做法就是使用 HTTP 协议内置的“数据压缩”编码,不仅可以选择标准的 gzip,还可以积极尝试新的压缩算法 br,它有更好的压缩效果。

缓存

在中间一公里,缓存更是性能优化的重要手段,CDN 的网络加速功能就是建立在缓存的基础之上的,可以这么说,如果没有缓存,那就没有 CDN。

利用好缓存功能的关键是理解它的工作原理,为每个资源都添加 ETag 和 Last-modified 字段,再用 Cache-Control、Expires 设置好缓存控制属性。

其中最基本的是 max-age 有效期,标记资源可缓存的时间。对于图片、CSS 等静态资源可以设置较长的时间,比如一天或者一个月,对于动态资源,除非是实时性非常高,也可以设置一个较短的时间,比如 1 秒或者 5 秒。

这样一旦资源到达客户端,就会被缓存起来,在有效期内都不会再向服务器发送请求,也就是:没有请求的请求,才是最快的请求

HTTP/2

通过飞翔篇的学习,你已经知道了 HTTP/2 的很多优点,它消除了应用层的队头阻塞,拥有头部压缩、二进制帧、多路复用、流量控制、服务器推送等许多新特性,大幅度提升了 HTTP 的传输效率。

实际上这些特性也是在开源和节流这两点上做文章,但因为这些都已经内置在了协议内,所以只要换上 HTTP/2,网站就能够立刻获得显著的性能提升。

不过你要注意,一些在 HTTP/1 里的优化手段到了 HTTP/2 里会有“反效果”。

对于 HTTP/2 来说,一个域名使用一个 TCP 连接才能够获得最佳性能,如果开多个域名,就会浪费带宽和服务器资源,也会降低 HTTP/2 的效率,所以 域名收缩 在 HTTP/2 里是必须要做的。

资源合并 在 HTTP/1 里减少了多次请求的成本,但在 HTTP/2 里因为有头部压缩和多路复用,传输小文件的成本很低,所以合并就失去了意义。而且资源合并还有一个缺点,就是降低了缓存的可用性,只要一个小文件更新,整个缓存就完全失效,必须重新下载。

所以在现在的大带宽和 CDN 应用场景下,应当尽量少用资源合并(JS、CSS 图片合并,数据内嵌),让资源的粒度尽可能地小,才能更好地发挥缓存的作用

小结

  1. 花钱购买硬件、软件或者服务可以直接提升网站的服务能力,其中最有价值的是 CDN;
  2. 不花钱也可以优化 HTTP,三个关键词是开源节流和缓存;
  3. 后端应该选用高性能的 Web 服务器,开启长连接,提升 TCP 的传输效率;
  4. 前端应该启用 gzip、br 压缩,减小文本、图片的体积,尽量少传不必要的头字段;
  5. 缓存是无论何时都不能忘记的性能优化利器,应该总使用 Etag 或 Last-modified 字段标记资源;
  6. 升级到 HTTP/2 能够直接获得许多方面的性能提升,但要留意一些 HTTP/1 的反模式