Hypertext Transfer Protocol (HTTP) 🌐
“鱼总是最后看见水的” - 理解 HTTP 协议本质与应用
基本概念
网络分层模型
TCP/IP 网络分层模型
“分层”的概念,把复杂的网络通信划分出多个层次,再给每一个层次分配不同的职责,层次内只专心做自己的事情就好,用“分而治之”的思想把一个“大麻烦”拆分成了数个“小麻烦”,从而解决了网络通信的难题。
graph TB link_layer["link layer/MAC"] internet_layer["internet layer/IP"] transport_layer["transport Layer/TCP/UDP"] application_layer["application layer/HTTP"]
链接层(link layer)负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标记网络上的设备,所以有时候也叫 MAC 层
网际层或网络互连层(internet layer)在“链接层”的基础上,用 IP 地址取代 MAC 地址,把许许多多的局域网、广域网连接成一个虚拟的巨大网络,在这个网络里找设备时只要把 IP 地址再“翻译”成 MAC 地址就可以了
传输层(transport layer)的职责是保证数据在 IP 地址标记的两点之间“可靠”地传输,是 TCP (Transmission Control Protocol)协议工作的层次,另外还有 UDP(User Datagram Protocol)
- TCP 是一个有状态的协议,需要先与对方建立连接然后才能发送数据,而且保证数据不丢失不重复
- UDP 则比较简单,它无状态,不用事先建立连接就可以任意发送数据,但不保证数据一定会发到对方
- TCP 的数据是连续的“字节流”,有先后顺序,而 UDP 则是分散的小数据包,是顺序发,乱序收
应用层(application layer)“百花齐放”,有各种面向具体应用的协议,例如 Telnet、SSH、FTP、SMTP 等
MAC 层的传输单位是帧(frame),IP 层的传输单位是包(packet),TCP 层的传输单位是段(segment),HTTP 的传输单位则是消息或报文(message),但这些名词并没有什么本质的区分,可以统称为数据包
OSI 网络分层模型
OSI 全称是“开放式系统互联通信参考模型”(Open System Interconnection Reference Model)。TCP/IP 发明于 1970 年代,当时除了它还有很多其他的网络协议,整个网络世界比较混乱,后来国际标准组织(ISO)设计出了一个新的网络分层模型,想用这个新框架来统一既存的各种网络协议
graph LR L7["L7 Application Layer"] L6["L6 Presentation Layer"] L5["L5 Session Layer"] L4["L4 Transport Layer"] L3["L3 Network Layer"] L2["L2 Data Link Layer"] L1["L1 Physical Layer"]
- 第一层:物理层,网络的物理形式,例如电缆、光纤、网卡、集线器等等;
- 第二层:数据链路层,它基本相当于 TCP/IP 的 链接层;
- 第三层:网络层,相当于 TCP/IP 里的 网际层;
- 第四层:传输层,相当于 TCP/IP 里的 传输层;
- 第五层:会话层,维护网络中的连接状态,即保持会话和同步;
- 第六层:表示层,把数据转换为合适、可理解的语法和语义;
- 第七层:应用层,面向具体的应用传输数据。
OSI 分层模型在发布的时候就明确地表明是一个“参考”,不是强制标准
TCP/IP 是一个纯软件的栈,没有网络应有的最根基的电缆、网卡等物理设备的位置。而 OSI 则补足了这个缺失,在理论层面上描述网络更加完整,OSI 的分层模型在四层以上分的太细,而 TCP/IP 实际应用时的会话管理、编码转换、压缩等和具体应用经常联系的很紧密,很难分开
“四层负载均衡”:工作在传输层上,基于 TCP/IP 协议的特性,例如 IP 地址、端口号等实现对后端服务器的负载均衡
“七层负载均衡”:工作在应用层上,看到的是 HTTP 协议,解析 HTTP 报文里的 URI、主机名、资源类型等数据,再用适当的策略转发给后端服务器
凡是由操作系统负责处理的就是四层或四层以下,凡是需要由应用程序(也就是自己写代码)负责处理的就是七层
HTTP 是什么
HTTP 协议的发展过程
- HTTP 协议始于三十年前蒂姆·伯纳斯 - 李的一篇论文(1989 年);
- HTTP/0.9 是个简单的文本协议,只能获取文本资源;
- HTTP/1.0 确立了大部分现在使用的技术,但它不是正式标准(1993 年);
- HTTP/1.1 是目前互联网上使用最广泛的协议,功能也非常完善(1999 年);
- HTTP/2 基于 Google 的 SPDY 协议,注重性能改善,但还未普及(2015 年);
- HTTP/3 基于 Google 的 QUIC 协议,是将来的发展方向(2018 年)。
graph LR subgraph 协议 subgraph 传输 超文本 end end
Hypertext Transfer Protocol 超文本传输协议
- 协议
- 协议必须要有两个或多个参与者,也就是“协”
- 协议是对参与者的一种行为约定和规范,也就是“议”
- 传输
- HTTP 协议是一个“双向协议”,但允许中间有“中转”或者“接力”
- 超文本
- 文字、图片、音频和视频等的混合体,含有“超链接”
HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范。
在互联网上 HTTP 传输最多的可能就是 HTML(HyperText Markup Language),但要是论数据量,HTML 可能要往后排了,图片、音频、视频这些类型的资源显然更大。
HTTP 通常跑在 TCP/IP 协议栈之上,依靠 IP 协议实现寻址和路由、TCP 协议实现可靠数据传输、DNS 协议实现域名查找、SSL/TLS 协议实现安全通信。此外,还有一些协议依赖于 HTTP,例如 WebSocket、HTTPDNS 等。
HTTP 相关概念
Web 浏览器
- Web Browser 是检索、查看互联网上网页资源的应用程序,Web 指的是“World Wide Web”(万维网)
- 浏览器本质上是一个 HTTP 协议中的请求方,使用 HTTP 协议获取网络上的各种资源
- HTTP 协议里,浏览器的角色被称为“User Agent”即“用户代理 “,通常都简单地称之为“客户端”
Web 服务器
- 硬件含义:物理形式或“云”形式的机器,在大多数情况下它可能不是一台服务器,而是利用反向代理、负载均衡等技术组成的庞大集群
- 软件含义: 提供 Web 服务的应用程序,通常会运行在硬件含义的服务器上,利用强大的硬件能力响应海量的客户端 HTTP 请求,处理磁盘上的网页、图片等静态文件,或者把请求转发给后面的 Tomcat、Node.js 等业务应用,返回动态信息
CDN
- 内容分发网络(Content Delivery Network)应用 HTTP 协议里的缓存和代理技术,代替源站响应客户端的请求
- CDN 一项重要基础设施,除了基本的网络加速外,还提供负载均衡、安全防护、边缘计算、跨运营商网络等功能
爬虫
- “爬虫”(Crawler)是一种可以自动访问 Web 资源的应用程序
- 爬虫绝大多数是各大搜索引擎抓取网页存入庞大的数据库,再建立关键字索引
HTML/WebService/WAF
- HTML 描述了超文本页面,用各种“标签”定义文字、图片等资源和排版布局,最终由浏览器“渲染”出可视化页面
- 广义上的 HTML 通常是指 HTML、JavaScript、CSS 等前端技术的组合,能够实现比传统静态页面更丰富的动态页面
- Web Service 是一个基于 Web(HTTP)的服务架构技术,具有跨平台跨语言的优点
- WAF(Web Application Firewall) 是专门检测 HTTP 流量,是防护 Web 应用的安全技术,通常位于 Web 服务器之前,可以阻止如 SQL 注入、跨站脚本等攻击
TCP/IP
- TCP/IP 协议是一系列网络通信协议的统称,其中最核心的两个协议是 TCP 和 IP,其他的还有 UDP、ICMP、ARP 等等,共同构成了一个复杂但有层次的协议栈
- IP 协议是“Internet Protocol”的缩写,主要目的是解决寻址和路由问题,以及如何在两点间传送数据包
- TCP 协议是“Transmission Control Protocol”的缩写,意思是“传输控制协议”,它位于 IP 协议之上,基于 IP 协议提供可靠的、字节流形式的通信,是 HTTP 协议得以实现的基础
- “可靠”是指保证数据不丢失,“字节流”是指保证数据完整,在 TCP 协议的两端可以如同操作文件一样访问传输的数据,就像是读写在一个密闭的管道里“流动”的字节
- HTTP 协议就运行在了 TCP/IP 上,HTTP 也就可以更准确地称为“HTTP over TCP/IP”
DNS
- “域名系统”(Domain Name System)用有意义的名字来作为 IP 地址的等价替代
- “域名”(Domain Name)被设计成了一个有层次的结构
- 用 “.” 分隔成多个单词,级别从左到右逐级升高,最右边的被称为“顶级域名”,然后是“二级域名”,层级关系向左依次降低
- 最左边的是主机名,可以用来表明主机的用途,比如“www”表示提供万维网服务、“mail”表示提供邮件服务
- 在 Apache、Nginx 这样的 Web 服务器里,域名可以用来标识虚拟主机,决定由哪个虚拟主机来对外提供服务
- 域名本质上还是个名字空间系统
- 就像 IP 地址必须转换成 MAC 地址才能访问主机一样,域名也必须要转换成 IP 地址,这个过程就是“域名解析”
- 域名的其他用途
- 重定向:当主机有情况需要下线、迁移时,可以更改 DNS 记录,让域名指向其他的机器
- 内部使用:域名是一个名字空间,可以使用 bind9 等开源软件搭建一个在内部使用的 DNS,作为名字服务器
- 负载均衡:域名解析可以返回多个 IP 地址,所以一个域名可以对应多台主机
URI/URL
- 使用 URI(Uniform Resource Identifier),统一资源标识符能够唯一地标记互联网上资源
- URI 另一个更常用的表现形式是 URL(Uniform Resource Locator), 统一资源定位符(俗称的“网址”)
- URI 不完全等同于网址,它包含有 URL 和 URN(Uniform Resource Name) 两个部分,因为 URL 太普及,常常把这两者简单地视为相等
- 客户端看到的必须是完整的 URI,使用特定的协议去连接特定的主机,而服务器看到的只是报文请求行里被删除了协议名和主机名的 URI
sequenceDiagram participant Client as 客户端 participant Server as 服务器 Client->>+Server: 请求 (scheme://host:port/path?query) activate Server Server-->>-Client: 响应 deactivate Server
HTTPS
- HTTPS 全称是“HTTP over SSL/TLS”,SSL/TLS 它是一个负责加密通信的安全协议,建立在 TCP/IP 之上
- SSL 的全称是“Secure Socket Layer”,由网景公司发明,当发展到 3.0 时被标准化,改名为 TLS 即 “Transport Layer Security”
- 但由于历史的原因还是有很多人称之为 SSL/TLS,或者直接简称为 SSL,SSL 综合了对称加密、非对称加密、摘要算法、数字签名、数字证书等技术,能够在不安全的环境中为通信的双方创建出一个秘密的、安全的传输通道
代理
- 代理(Proxy)是 HTTP 协议中请求方和应答方中间的一个环节,作为“中转站”,既可以转发客户端的请求,也可以转发服务器的应答
- 代理有很多的种类,常见的有:
- 匿名代理:完全“隐匿”了被代理的机器,外界看到的只是代理服务器;
- 透明代理:顾名思义,它在传输过程中是“透明开放”的,外界既知道代理,也知道客户端;
- 正向代理:靠近客户端,代表客户端 向服务器发送请求;
- 反向代理:靠近服务器端,代表服务器 响应客户端的请求;
- CDN 实际上就是一种代理,它代替源站服务器响应客户端的请求,通常扮演着透明代理和反向代理的角色
- 由于代理在传输过程中插入了一个“中间层”,所以可以在这个环节做很多有意思的事情,比如:
- 负载均衡:把访问请求均匀分散到多台机器,实现访问集群化;
- 内容缓存:暂存上下行的数据,减轻后端的压力;
- 安全防护:隐匿 IP, 使用 WAF 等工具抵御网络攻击,保护被代理的机器;
- 数据处理:提供压缩、加密等额外的功能
HTTP 报文结构
HTTP 报文
HTTP 协议是一个“纯文本”的协议,可读性好,其请求报文和响应报文的结构基本相同,由三大部分组成:
- 起始行(start line):描述请求或响应的基本信息;
- 头部字段集合(header):使用 key-value 形式更详细地说明报文;
- 消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。
前两部分起始行和头部字段经常又合称为“请求头”或“响应头”,消息正文又称为“实体”,但与“header”对应,很多时候就直接称为“body”
请求行
请求行(request line),它简要地描述了客户端想要如何操作服务器端的资源,由三部分构成:
- 请求方法:是一个动词,如 GET/POST,表示对资源的操作;
- 请求目标:通常是一个 URI,标记了请求方法要操作的资源;
- 版本号:表示报文使用的 HTTP 协议版本。
1 | GET / |
状态行
响应报文里的起始行不叫“响应行”,而是叫“状态行”(status line),意思是服务器响应的状态,同样也是由三部分构成:
- 版本号:表示报文使用的 HTTP 协议版本;
- 状态码:一个三位数,用代码的形式表示处理的结果,比如 200 是成功,500 是服务器错误;
- 原因:是状态码的简短文字描述,但它只是为了兼容早期的文本客户端而存在,目前的大多数客户端都会忽略它。
1 | 200 OK |
头部字段
请求行或状态行再加上头部字段集合就构成了 HTTP 报文里完整的请求头或响应头,头部字段是 key-value 的形式,key 和 value 之间用“:”分隔,最后用 CRLF 换行表示字段结束。
HTTP 头字段非常灵活,不仅可以使用标准里的 Host、Connection 等已有头,也可以任意添加自定义头,用头字段需要注意下面几点:
- 字段名里不允许出现空格,可以使用连字符“-”,但不能使用下划线“_”。例如,“test-name”是合法的字段名,而“test name”、“test_name”是不正确的字段名;
- 字段名后面必须紧接着“:”,不能有空格,而“:”后的字段值前可以有多个空格;
- 字段的顺序是没有意义的,可以任意排列不影响语义;
- 字段原则上不能重复,除非这个字段本身的语义允许,例如 Set-Cookie。
常用头字段
HTTP 协议规定了非常多的头部字段,基本上可以分为四大类:
- 通用字段:在请求头和响应头里都可以出现;
- 请求字段:仅能出现在请求头里,进一步说明请求信息或者额外的附加条件;
- 响应字段:仅能出现在响应头里,补充说明响应报文的信息;
- 实体字段:它实际上属于通用字段,但专门描述 body 的额外信息。
常见字段:
- Host 字段,它属于请求字段,只能出现在请求头里,它同时也是唯一一个 HTTP/1.1 规范里要求必须出现的字段,Host 字段告诉服务器这个请求应该由哪个主机来处理,当一台计算机上托管了多个虚拟主机的时候,服务器端就需要用 Host 字段来选择,有点像是一个简单的“路由重定向”
- User-Agent 是请求字段,只出现在请求头里。它使用一个字符串来描述发起 HTTP 请求的客户端,服务器可以依据它来返回最合适此浏览器显示的页面。
- Date 字段是一个通用字段,但通常出现在响应头里,表示 HTTP 报文创建的时间,客户端可以使用这个时间再搭配其他字段决定缓存策略。
- Server 字段是响应字段,只能出现在响应头里。它告诉客户端当前正在提供 Web 服务的软件名称和版本号,Server 字段也不是必须要出现的,因为这会把服务器的一部分信息暴露给外界。
- Content-Length 是实体字段,它表示报文里 body 的长度,也就是请求头或响应头空行后面数据的长度
请求方法
请求方法的实际含义就是客户端发出了一个“动作指令”,要求服务器端对 URI 定位的资源执行这个动作,目前 HTTP/1.1 规定了八种方法,单词都必须是大写的形式:GET
、HEAD
、POST
、PUT
、DELETE
、CONNECT
、OPTIONS
、TRACE
GET/HEAD
GET 方法的含义是请求从服务器获取资源,这个资源既可以是静态的文本、页面、图片、视频,也可以是由 PHP、Java 动态生成的页面或者其他格式的数据
HEAD 方法可以看做是 GET 方法的一个“简化版”或者“轻量版”,但服务器不会返回请求的实体数据,只会传回响应头,也就是资源的“元信息”,比如,想要检查一个文件是否存在,只要发个 HEAD 请求就可以了,没有必要用 GET 把整个文件都取下来
POST/PUT
GET 和 HEAD 方法是从服务器获取数据,而 POST 和 PUT 方法则是相反操作,向 URI 指定的资源提交数据,数据就放在报文的 body 里,通常 POST 表示的是“新建”“create”的含义,而 PUT 则是“修改”“update”的含义
在实际应用中,PUT 用到的比较少,它与 POST 的语义、功能太过近似,有的服务器甚至就直接禁止使用 PUT 方法,只用 POST 方法上传数据
其他方法
- DELETE 方法指示服务器删除资源,因为这个动作危险性太大,所以通常服务器不会执行真正的删除操作,而是对资源做一个删除标记
- CONNECT 是一个比较特殊的方法,要求服务器为客户端和另一台远程服务器建立一条特殊的连接隧道,这时 Web 服务器在中间充当了代理的角色
- OPTIONS 方法要求服务器列出可对资源实行的操作方法,在响应头的 Allow 字段里返回,它的功能很有限,用处也不大,有的服务器(例如 Nginx)干脆就没有实现对它的支持
- TRACE 方法多用于对 HTTP 链路的测试或诊断,可以显示出请求 - 响应的传输路径。它的本意是好的,但存在漏洞,会泄漏网站的信息,所以 Web 服务器通常也是禁止使用
常用状态码
RFC 标准把状态码分成了五类:
- 1××:提示信息,表示目前是协议处理的中间状态,还需要后续的操作;
- 偶尔能够见到的是“101 Switching Protocols”,它的意思是客户端使用 Upgrade 头字段,要求在 HTTP 协议的基础上改成其他的协议继续通信,比如 WebSocket。而如果服务器也同意变更协议,就会发送状态码 101,但这之后的数据传输就不会再使用 HTTP 了
- 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××:重定向,资源位置发生变动,需要客户端重新发送请求;
- “301 Moved Permanently”俗称“永久重定向”,含义是此次请求的资源已经不存在了,需要改用改用新的 URI 再次访问。
- 与它类似的是“302 Found”,曾经的描述短语是“Moved Temporarily”,俗称“临时重定向”,意思是请求的资源还在,但需要暂时用另一个 URI 来访问。
- 301 和 302 都会在响应头里使用字段 Location 指明后续要跳转的 URI,最终的效果很相似,浏览器都会重定向到新的 URI
- “304 Not Modified” 用于 If-Modified-Since 等条件请求,表示资源未修改,用于缓存控制。它不具有通常的跳转含义,但可以理解成“重定向已到缓存的文件”(即“缓存重定向”)
- 4××:客户端错误,请求报文有误,服务器无法处理;
- “400 Bad Request”是一个通用的错误码,表示请求报文有错误,但具体是数据格式错误、缺少请求头还是 URI 超长它没有明确说,只是一个笼统的错误
- “403 Forbidden”实际上不是客户端的请求出错,而是表示服务器禁止访问资源。原因可能多种多样,例如信息敏感、法律禁止等,如果服务器友好一点,可以在 body 里详细说明拒绝请求的原因
- “404 Not Found”可能是我们最常看见也是最不愿意看到的一个状态码,它的原意是资源在本服务器上未找到,所以无法提供给客户端,但现在已经被“用滥了”,只要服务器“不高兴”就可以给出个 404
- 开发中常用的还有:
- 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××:服务器错误,服务器在处理请求时内部发生了错误。
- “500 Internal Server Error”与 400 类似,也是一个通用的错误码,服务器究竟发生了什么错误我们是不知道的。不过对于服务器来说这应该算是好事,通常不应该把服务器内部的详细信息,例如出错的函数调用栈告诉外界。虽然不利于调试,但能够防止黑客的窥探或者分析
- “501 Not Implemented”表示客户端请求的功能还不支持,这个错误码比 500 要“温和”一些,和“即将开业,敬请期待”的意思差不多,不过具体什么时候“开业”就不好说了。
- “502 Bad Gateway”通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的。
- “503 Service Unavailable”表示服务器当前很忙,暂时无法响应服务,我们上网时有时候遇到的“网络服务正忙,请稍后重试”的提示信息就是状态码 503。
- 503 是一个“临时”的状态,很可能过几秒钟后服务器就不那么忙了,可以继续提供服务,所以 503 响应报文里通常还会有一个“Retry-After”字段,指示客户端可以在多久以后再次尝试发送请求
HTTP 的特点
特点
- 灵活可扩展:HTTP 协议只规定了报文的基本格式,比如用空格分隔单词,用换行分隔字段,“header+body”等,各个组成部分都没有做严格的语法语义限制
- 可靠传输:HTTP 协议是基于 TCP/IP 的,而 TCP 本身是一个“可靠”的传输协议,所以 HTTP 自然也就继承了这个特性,能够在请求方和应答方之间“可靠”地传输数据,“可靠”只是向使用者提供了一个“承诺”,会在下层用多种手段“尽量”保证数据的完整送达
- 应用层协议:FTP 用于传输文件、SMTP 用于发送邮件、SSH 用于远程登录;HTTP 几乎可以传递一切东西,满足各种需求,称得上是一个“万能”的协议
- 请求-应答: HTTP 的请求-应答模式恰好契合了传统的 C/S(Client/Server)系统架构,也完全符合 RPC(Remote Procedure Call)的工作模式,可以把 HTTP 请求处理封装成远程函数调用,导致了 WebService、RESTful 和 gPRC 等的出现
- 无状态:“状态”其实就是客户端或者服务器里保存的一些数据或者标志,记录了通信过程中的一些变化信息,HTTP 可以通过“补丁”增加这个特性
优点/缺点
- HTTP 最大的优点是简单、灵活和易于扩展;
- HTTP 拥有成熟的软硬件环境,应用的非常广泛,是互联网的基础设施;
- HTTP 是无状态的,可以轻松实现集群化,扩展性能,但有时也需要用 Cookie 等技术来实现“有状态”;
- HTTP 是明文传输,数据完全肉眼可见,能够方便地研究分析,但也容易被窃听;
- HTTP 是不安全的,无法验证通信双方的身份,也不能判断报文是否被窜改;
- HTTP 1.1 的性能不算差,但不完全适应现在的互联网,还有很大的提升空间(HTTP/2 和 HTTP/3)。
HTTP 的实体数据
HTTP 协议作为应用层的协议,数据到达之后工作只能说是完成了一半,还必须要告诉上层应用这是什么数据才行,早在 HTTP 协议诞生之前,在电子邮件系统里让电子邮件可以发送 ASCII 码以外的任意数据,方案的名字叫做“多用途互联网邮件扩展”(Multipurpose Internet Mail Extensions),简称为 MIME
MIME 是一个很大的标准规范,HTTP 取了其中的一部分,用来标记 body 的数据类型(MIME type),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 二进制数据等
HTTP 在传输时为了节约带宽,有时候还会压缩数据,还需要有一个“Encoding type”,告诉数据是用的什么编码格式,这样对方才能正确解压缩,还原出原始的数据,常用的 Encoding type 只有下面三种:
- gzip:GNU zip 压缩格式,也是互联网上最流行的压缩格式;
- deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip
- br:一种专门为 HTTP 优化的新压缩算法(Brotli)
有了 MIME type 和 Encoding type,无论是浏览器还是服务器就都可以轻松识别出 body 的类型,也就能够正确处理数据了
Accept 字段标记的是客户端可理解的 MIME type,可以用“,”做分隔符列出多个类型,服务器会在响应报文里用头字段 Content-Type 告诉实体数据的真实类型:
1 | # 客户端 |
此外还有语言类型使用的头字段、内容协商的质量值等字段
graph LR A[Accept] --> B(Content-Type) C[Acept-Encoding] --> D(Content-Encoding) E[Acept-Language] --> F(Content-Language) G[Acept-Charset] --> B
HTTP 的应用
传输大文件
数据压缩:gzip 等压缩算法通常只对文本文件有较好的压缩率,而图片、音频视频等多媒体数据本身就已经是高度压缩的,再用 gzip 处理效果不好
分块传输
- “化整为零”的思路在 HTTP 协议里就是“chunked”分块传输编码,在响应报文里用头字段“Transfer-Encoding: chunked”来表示,意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送
- “Transfer-Encoding: chunked”和“Content-Length”这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked)
范围请求/多段数据
- HTTP 协议为了满足这样的需求,提出了“范围请求”(range requests)的概念,允许客户端在请求头里使用专用字段来表示只获取文件的一部分,相当于是客户端的“化整为零”
- 不仅看视频的拖拽进度需要范围请求,常用的下载工具里的多段下载、断点续传也是基于它实现的,要点是:
- 先发个 HEAD,看服务器是否支持范围请求,同时获取文件的大小;
- 开 N 个线程,每个线程使用 Range 字段划分出各自负责下载的片段,发请求传输数据;
- 下载意外中断也不怕,不必重头再来一遍,只要根据上次的下载记录,用 Range 请求剩下的那一部分就可以了。
这些方法不是互斥的,而是可以混合起来使用,例如压缩后再分块传输,或者分段后再分块
连接管理
短连接和长连接
HTTP 协议最初(0.9⁄1.0)是个非常简单的协议,通信过程也采用了简单的“请求 - 应答”方式,它底层的数据传输基于 TCP/IP,每次发送请求前需要先与服务器建立连接,收到响应报文后会立即关闭连接。因为客户端与服务器的整个连接过程很短暂,不会与服务器保持长时间的连接状态,所以就被称为“短连接”(short-lived connections)。早期的 HTTP 协议也被称为是“无连接”的协议。
- HTTP 协议就提出了“长连接”的通信方式,也叫“持久连接”(persistent connections)、“连接保活”(keep alive)、“连接复用”(connection reuse)
- 用的就是“成本均摊”的思路,既然 TCP 的连接和关闭非常耗时间,那么就把这个时间成本由原来的一个“请求 - 应答”均摊到多个“请求 - 应答”上
连接相关的头字段
由于长连接对性能的改善效果非常显著,所以在 HTTP/1.1 中的连接都会默认启用长连接。不需要用什么特殊的头字段指定,只要向服务器发送了第一次请求,后续的请求都会重复利用第一次打开的 TCP 连接,也就是长连接。
- 在请求头里明确地要求使用长连接机制,使用的字段是 Connection,值是“keep-alive”
- 如果服务器支持长连接,它总会在响应报文里放一个“Connection: keep-alive”字段
因为 TCP 连接长时间不关闭,服务器必须在内存里保存它的状态,这就占用了服务器的资源。如果有大量的空闲长连接只连不发,就会很快耗尽服务器的资源,导致服务器无法为真正有需要的用户提供服务。
客户端可以在请求头里加上“Connection: close”字段,告诉服务器:“这次通信后就关闭连接”。
服务器端通常不会主动关闭连接,但也可以使用一些策略。拿 Nginx 来举例,它有两种方式:
使用“keepalive_timeout”指令,设置长连接的超时时间,如果在一段时间内连接上没有任何数据收发就主动断开连接,避免空闲连接占用系统资源。
使用“keepalive_requests”指令,设置长连接上可发送的最大请求次数。比如设置成 1000,那么当 Nginx 在这个连接上处理了 1000 个请求后,也会主动断开连接。
队头阻塞
“队头阻塞”与短连接和长连接无关,而是由 HTTP 基本的“请求 - 应答”模型所导致的。因为“请求 - 应答”模型不能变,所以“队头阻塞”问题在 HTTP/1.1 里无法解决,只能缓解。
“并发连接”(concurrent connections),是同时对一个域名发起多个长连接,用数量来解决质量的问题
“域名分片”(domain sharding)技术还是用数量来解决质量的思路,多开几个域名都指向同一台服务器这样实际长连接的数量就又上去了
重定向和跳转
“Location”字段属于响应字段,必须出现在响应报文里。但只有配合 301⁄302 状态码才有意义,它 标记了服务器要求重定向的 URI
- 301 俗称“永久重定向”(Moved Permanently),意思是原 URI 已经“永久”性地不存在了,今后的所有请求都必须改用新的 URI。
- 浏览器看到 301,就知道原来的 URI“过时”了,就会做适当的优化。比如历史记录、更新书签,下次可能就会直接用新的 URI 访问,省去了再次跳转的成本。搜索引擎的爬虫看到 301,也会更新索引库,不再使用老的 URI。
- 302 俗称“临时重定向”(“Moved Temporarily”),意思是原 URI 处于“临时维护”状态,新的 URI 是起“顶包”作用的“临时工”。
- 浏览器或者爬虫看到 302,会认为原来的 URI 仍然有效,但暂时不可用,所以只会执行简单的跳转页面,不记录新的 URI,也不会有其他的多余动作,下次访问还是用原 URI。
重定向的应用场景
- 资源不可用时用另一个新的 URI 来代替
- 避免重复,让多个网址都跳转到一个 URI,增加访问入口的同时还不会增加额外的工作量
重定向的相关问题
- 性能损耗,定向的机制决定了一个跳转会有两次请求 - 应答,比正常的访问多了一次,站内重定向可以长连接复用,站外重定向就要开两个连接
- 循环跳转,HTTP 协议特别规定,浏览器必须具有检测“循环跳转”的能力,在发现这种情况时应当停止发送请求并给出错误提示
Cookie 机制
Cookie 机制需要用到响应头字段 Set-Cookie 和请求头字段 Cookie:
- 服务器有时会在响应头里添加多个 Set-Cookie,存储多个“key = value”
- 浏览器这边发送时不需要用多个 Cookie 字段,只要在一行里用“;”隔开就行
- Cookie 是由浏览器负责存储的,而不是操作系统
Cookie 的有效期可以使用 Expires 和 Max-Age 两个属性来设置:
- “Expires”俗称“过期时间”,用的是绝对时间点,可以理解为“截止日期”(deadline)
- “Max-Age”用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间
设置 Cookie 的作用域,让浏览器仅发送给特定的服务器和 URI,避免被其他网站盗用:
- “Domain”和“Path”指定了 Cookie 所属的域名和路径
- 浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie,现实中为了省事,通常 Path 就用一个“/”或者直接省略,表示域名下的任意路径都允许使用 Cookie,让服务器自己去挑
Cookie 的安全性:
- 属性“HttpOnly”会告诉浏览器,此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问,浏览器的 JS 引擎就会禁用 document.cookie 等一切相关的 API
- 属性“SameSite”可以防范“跨站请求伪造”(XSRF)攻击,设置成“SameSite = Strict”可以严格限定 Cookie 不能随着跳转链接跨站发送,而“SameSite = Lax”则略宽松一点,允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送
- “Secure”属性表示这个 Cookie 仅能用 HTTPS 协议加密传输,明文的 HTTP 协议会禁止发送,但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在
Cookie 的应用有身份识别、广告跟踪等,为了防止滥用 Cookie 搜集用户隐私,互联网组织相继提出了 DNT(Do Not Track)和 P3P(Platform for Privacy Preferences Project),但实际作用不大
因为 Cookie 并不属于 HTTP 标准(RFC6265,而不是 RFC2616/7230),所以语法上与其他字段不太一致,使用的分隔符是“;”,与 Accept 等字段的“,”不同
缓存控制
服务器的缓存控制
服务器标记资源有效期使用的头字段是“Cache-Control”,里面的值“max-age = 30”就是资源的有效时间,相当于告诉浏览器,“这个页面只能缓存 30 秒,之后就算是过期,不能用。”
“max-age”是 HTTP 缓存控制最常用的属性,此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存:
- no_store:不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面;
- no_cache:它的字面含义容易与 no_store 搞混,实际的意思并不是不允许缓存,而是可以缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;
- must-revalidate:又是一个和 no_cache 相似的词,它的意思是如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。
客户端的缓存控制
客户端也可以发“Cache-Control”,请求 - 应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略
- 当点“刷新”按钮的时候,浏览器会在请求头里加一个“Cache-Control: max-age = 0”,服务器看到 max-age = 0,也就会用一个最新生成的报文回应浏览器
- Ctrl+F5 的“强制刷新”是发了一个“Cache-Control: no-cache”,含义和“max-age = 0”基本一样,就看后台的服务器怎么理解,通常两者的效果是相同的
- 在“前进”“后退”“跳转”这些重定向动作中浏览器不会“夹带私货”,只用最基本的请求头,没有“Cache-Control”,所以就会检查缓存,直接利用之前的资源,不再进行网络通信
条件请求
条件请求一共有 5 个头字段,最常用的是“if-Modified-Since”和“If-None-Match”这两个
- 需要第一次的响应报文预先提供“Last-modified”和“ETag”,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的
- 如果资源没有变,服务器就回应一个“304 Not Modified”,表示缓存依然有效,浏览器就可以更新一下有效期
ETag 是“实体标签”(Entity Tag)的缩写,是资源的一个唯一标识,主要是用来解决修改时间无法准确区分文件变化的问题:
- 一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分。
- 一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽。
代理服务
graph LR A[浏览器] -- "请求" --> B(代理服务器) B -- "请求" --> C(源服务器) C -- "响应" --> B B -- "响应" --> A
代理的作用
“计算机科学领域里的任何问题,都可以通过引入一个中间层来解决”(在这句话后面还可以再加上一句“如果一个中间层解决不了问题,那就再加一个中间层”)
代理最基本的一个功能是负载均衡,在负载均衡的同时,代理服务还可以执行更多的功能,比如:
- 健康检查:使用“心跳”等机制监控后端服务器,发现有故障就及时“踢出”集群,保证服务高可用;
- 安全防护:保护被代理的后端服务器,限制 IP 地址或流量,抵御网络攻击和过载;
- 加密卸载:对外网使用 SSL/TLS 加密通信认证,而在安全的内网不加密,消除加解密成本;
- 数据过滤:拦截上下行的数据,任意指定策略修改请求或者响应;
- 内容缓存:暂存、复用服务器响应。
代理相关头字段
- Via 是一个通用字段,请求头或响应头里都可以出现。每当报文经过一个代理节点,代理服务器就会把自身的信息追加到字段的末尾,它只解决了客户端和源服务器判断是否存在代理的问题,还不能知道对方的真实信息
- 服务器的 IP 地址应该是保密的,关系到企业的内网安全,所以一般不会让客户端知道;通常服务器又需要知道客户端的真实 IP 地址,方便做访问控制、用户画像、统计分析
- HTTP 标准里并没有为此定义头字段,但已经出现了很多“事实上的标准”,最常用的两个头字段是“X-Forwarded-For”和“X-Real-IP”
- “X-Forwarded-For”的字面意思是“为谁而转发”,形式上和“Via”差不多,Via”追加的是代理主机名(或者域名),而“X-Forwarded-For”追加的是请求方的 IP 地址
- “X-Real-IP”是另一种获取客户端真实 IP 的手段,它的作用很简单,就是记录客户端 IP 地址,没有中间的代理信息,相当于是“X-Forwarded-For”的简化版
代理协议
因为通过“X-Forwarded-For”操作代理信息必须要解析 HTTP 报文头,这对于代理来说成本比较高,原本只需要简单地转发消息就好,而现在却必须要费力解析数据再修改数据,会降低代理的转发性能。所以就出现了一个专门的“代理协议”(The PROXY protocol),它由知名的代理软件 HAProxy 所定义,也是一个“事实标准”,被广泛采用(注意并不是 RFC)。
“代理协议”v1 是在 HTTP 报文前增加了一行 ASCII 码文本,相当于又多了一个头,这一行文本其实非常简单,开头必须是“PROXY”五个大写字母,然后是“TCP4”或者“TCP6”,表示客户端的 IP 地址类型,再后面是请求方地址、应答方地址、请求方端口号、应答方端口号,最后用一个回车换行(\r\n)结束。
服务器看到这样的报文,只要解析第一行就可以拿到客户端地址,不需要再去理会后面的 HTTP 数据,省了很多事情。
不过代理协议并不支持“X-Forwarded-For”的链式地址形式,所以拿到客户端地址后再如何处理就需要代理服务器与后端自行约定。
HTTPS
如果通信过程具备了四个特性,就可以认为是“安全”的,这四个特性是:机密性、完整性,身份认证和不可否认:
- 机密性(Secrecy/Confidentiality)是指对数据的“保密”,只能由可信的人访问,对其他人是不可见的“秘密”,简单来说就是不能让不相关的人看到不该看的东西。
- 完整性(Integrity,也叫一致性)是指数据在传输过程中没有被 窜改,不多也不少,“完完整整”地保持着原状。
- 身份认证(Authentication)是指确认对方的真实身份,也就是“证明你真的是你”,保证消息只能发送给可信的人。
- 不可否认(Non-repudiation/Undeniable),也叫不可抵赖,意思是不能否认已经发生过的行为,不能“说话不算数”“耍赖皮”
只有同时具备了机密性、完整性、身份认证、不可否认这四个特性,通信双方的利益才能有保障,才能算得上是真正的安全
HTTPS 名字里的“S”,是把 HTTP 下层的传输协议由 TCP/IP 换成了 SSL/TLS,由“HTTP over TCP/IP”变成了“HTTP over SSL/TLS”,让 HTTP 运行在了安全的 SSL/TLS 协议上
SSL/TLS
SSL 即安全套接层(Secure Sockets Layer),在 OSI 模型中处于第 5 层(会话层),由网景公司于 1994 年发明,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 年左右停止支持。
浏览器和服务器在使用 TLS 建立连接时需要选择 一组恰当的加密算法 来实现安全通信,这些算法的组合被称为“密码套件”(cipher suite,也叫 加密套件),TLS 的密码套件命名非常规范,基本的形式是“密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法”,如:
“ECDHE-RSA-AES256-GCM-SHA384” - “握手时使用 ECDHE 算法进行密钥交换,用 RSA 签名和身份认证,握手后的通信使用 AES 对称算法,密钥长度 256 位,分组模式是 GCM,摘要算法 SHA384 用于消息认证和产生随机数。”
OpenSSL 是一个著名的开源密码学程序库和工具包,几乎支持所有公开的加密算法和协议,已经成为了事实上的标准,许多应用软件都会使用它作为底层库来实现 TLS 功能,包括常用的 Web 服务器 Apache、Nginx 等。
对称与非对称加密
实现机密性最常用的手段是“加密”(encrypt),就是把消息用某种方式转换成谁也看不懂的乱码,只有掌握特殊“钥匙”的人才能再转换出原始文本。
这里的“钥匙”就叫做“密钥”(key),加密前的消息叫“明文”(plain text/clear text),加密后的乱码叫“密文”(cipher text),使用密钥还原明文的过程叫“解密”(decrypt),是加密的反操作,加密解密的操作过程就是“加密算法”。
按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密。
对称加密
“对称加密”就是指加密和解密时使用的密钥都是同一个,是“对称”的,只要保证了密钥的安全,那整个通信过程就可以说具有了机密性。
TLS 里有非常多的对称加密算法可供选择,比如 RC4、DES、3DES、AES、ChaCha20 等,但前三种算法都被认为是不安全的,通常都禁止使用,目前常用的只有 AES 和 ChaCha20。
- AES 的意思是“高级加密标准”(Advanced Encryption Standard),密钥长度可以是 128、192 或 256。它是 DES 算法的替代者,安全强度很高,性能也很好,而且有的硬件还会做特殊优化,所以非常流行,是应用最广泛的对称加密算法
- ChaCha20 是 Google 设计的另一种加密算法,密钥长度固定为 256 位,纯软件运行性能要超过 AES,曾经在移动客户端上比较流行,但 ARMv8 之后也加入了 AES 硬件优化,所以现在不再具有明显的优势,但仍然算得上是一个不错算法
非对称加密
对称加密看上去好像完美地实现了机密性,但其中有一个很大的问题:如何把密钥安全地传递给对方,术语叫“密钥交换”。
非对称加密(也叫公钥加密算法)有两个密钥,一个叫“公钥”(public key),一个叫“私钥”(private key)。两个密钥是不同的,“不对称”,公钥可以公开给任何人使用,而私钥必须严格保密。
公钥和私钥有个特别的“单向”性,虽然都可以用来加密解密,但公钥加密后只能用私钥解密,反过来,私钥加密后也只能用公钥解密。
非对称加密可以解决“密钥交换”的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。
非对称加密算法的设计要比对称算法难得多,在 TLS 里只有很少的几种,比如 DH、DSA、RSA、ECC 等
- RSA 可能是其中最著名的一个,几乎可以说是非对称加密的代名词,它的安全性基于“整数分解”的数学难题,使用两个超大素数的乘积作为生成密钥的材料,想要从公钥推算出私钥是非常困难的。
- ECC(Elliptic Curve Cryptography)是非对称加密里的“后起之秀”,它基于“椭圆曲线离散对数”的数学难题,使用特定的曲线方程和基点生成公钥和私钥,子算法 ECDHE 用于密钥交换,ECDSA 用于数字签名。
混合加密
然非对称加密没有“密钥交换”的问题,但因为它们都是基于复杂的数学难题,运算速度很慢,即使是 ECC 也要比 AES 差上好几个数量级。如果仅用非对称加密,虽然保证了安全,但通信速度有如乌龟、蜗牛,实用性就变成了零。
RSA 的运算速度是非常慢的,2048 位的加解密大约是 15KB/S(微秒或毫秒级),而 AES128 则是 13MB/S(纳秒级),差了几百倍。
TLS 里使用的混合加密方式,解决了对称加密算法的密钥交换问题,而且 安全和性能兼顾,完美地实现了 机密性。
数字签名与证书
仅有机密性,离安全还差的很远,黑客虽然拿不到会话密钥,无法破解密文,但可以通过窃听收集到足够多的密文,再尝试着修改、重组后发给网站,另外,黑客也可以伪造身份发布公钥。如果你拿到了假的公钥,混合加密就完全失效了。你以为自己是在和“某宝”通信,实际上网线的另一端却是黑客,银行卡号、密码等敏感信息就在“安全”的通信过程中被窃取了
摘要算法
实现完整性的手段主要是摘要算法(Digest Algorithm),也就是常说的散列函数、哈希函数(Hash Function)
- 可以把摘要算法近似地理解成一种特殊的压缩算法,它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”
- 也可以把摘要算法理解成特殊的“单向”加密算法,它只有算法,没有密钥,加密后的数据无法解密,不能从摘要逆推出原文
MD5(Message-Digest 5)和 SHA-1(Secure Hash Algorithm 1),是最常用的两个摘要算法,能够生成 16 字节和 20 字节长度的数字摘要,但这两个算法的安全强度比较低,不够安全,在 TLS 里已经被禁止使用了,目前 TLS 推荐使用的是 SHA-1 的后继者:SHA-2
SHA-2 实际上是一系列摘要算法的统称,总共有 6 种,常用的有 SHA224、SHA256、SHA384,分别能够生成 28 字节、32 字节、48 字节的摘要
完整性
摘要算法保证了“数字摘要”和原文是完全等价的。所以,我们只要在原文后附上它的摘要,就能够保证数据的完整性
不过摘要算法不具有机密性,如果明文传输,那么黑客可以修改消息后把摘要也一起改了,网站还是鉴别不出完整性,所以真正的完整性必须要建立在机密性之上,在混合加密系统里用会话密钥加密消息和摘要,这样黑客无法得知明文,也就没有办法动手脚了
数字签名
加密算法结合摘要算法,我们的通信过程可以说是比较安全了。但这里还有漏洞,就是通信的两个端点(endpoint)
使用 非对称加密+摘要算法,就能够实现“数字签名”,同时实现“身份认证”和“不可否认”
- 签名:使用私钥对数据(通常是数据的哈希值)进行加密的过程,这个过程产生一个数字签名,该签名是唯一的,并且只能由持有相应公钥的人来解密
- 验签:使用与签名私钥对应的公钥来解密签名,并验证其有效性
数字证书和 CA
CA(Certificate Authority,证书认证机构)像网络世界里的公安局、教育部、公证中心,具有极高的可信度,由它来给各个 公钥 签名,用自身的信誉来保证公钥无法伪造,是可信的,解决“公钥的信任”问题
CA 对公钥的签名认证也是有格式的,不是简单地把公钥绑定在持有者身份上就完事了,还要包含序列号、用途、颁发者、有效时间等等,把这些打成一个包再签名,完整地证明公钥关联的各种信息,形成“数字证书”(Certificate)
知名的 CA 全世界就那么几家,比如 DigiCert、VeriSign、Entrust、Let’s Encrypt 等,它们签发的证书分 DV、OV、EV 三种,区别在于可信程度。
小一点的 CA 可以让大 CA 签名认证,但链条的最后,也就是 Root CA,就只能自己证明自己了,这个就叫“自签名证书”(Self-Signed Certificate)或者“根证书”(Root Certificate)。你必须相信,否则整个证书信任链就走不下去了
操作系统和浏览器都内置了各大 CA 的根证书,上网的时候只要服务器发过来它的证书,就可以验证证书里的签名,顺着证书链(Certificate Chain)一层层地验证,直到找到根证书,就能够确定证书是可信的,从而里面的公钥也是可信的
TLS 1.2 连接过程
TLS 包含几个子协议,比较常用的有记录协议、警报协议、握手协议、变更密码规范协议等
- 记录协议(Record Protocol)规定了 TLS 收发数据的基本单位:记录(record)。它有点像是 TCP 里的 segment,所有的其他子协议都需要通过记录协议发出。但多个记录数据可以在一个 TCP 包里一次性发出,也并不需要像 TCP 那样返回 ACK
- 警报协议(Alert Protocol)的职责是向对方发出警报信息,有点像是 HTTP 协议里的状态码。比如,protocol_version 就是不支持旧版本,bad_certificate 就是证书有问题,收到警报后另一方可以选择继续,也可以立即终止连接
- 握手协议(Handshake Protocol)是 TLS 里最复杂的子协议,要比 TCP 的 SYN/ACK 复杂的多,浏览器和服务器会在握手过程中协商 TLS 版本号、随机数、密码套件等信息,然后交换证书和密钥参数,最终双方协商得到会话密钥,用于后续的混合加密系统
- 变更密码规范协议(Change Cipher Spec Protocol),是一个“通知”,告诉对方,后续的数据都将使用加密保护。那么反过来,在它之前,数据都是明文的
ECDHE 握手过程
- 在 TCP 建立连接之后,客户端会首先发一个“Client Hello”消息,也就是跟服务器“打招呼”。里面有客户端的版本号、支持的密码套件,还有一个 随机数(Client Random),用于后续生成会话密钥。
- 服务器收到“Client Hello”后,会返回一个“Server Hello”消息。把版本号对一下,也给出一个 随机数(Server Random),然后从客户端的列表里选一个作为本次通信使用的密码套件
- 服务器为了证明自己的身份,就把 证书 也发给了客户端(Server Certificate)
- 因为服务器选择了 ECDHE 算法,所以它会在证书后发送“Server Key Exchange”消息,里面是 椭圆曲线的公钥(Server Params),用来实现密钥交换算法,再加上自己的私钥签名认证
- 客户端和服务器通过明文共享了三个信息:Client Random、Server Random 和 Server Params
- 客户端按照密码套件的要求,也生成一个 椭圆曲线的公钥(Client Params),用“Client Key Exchange”消息发给服务器
- 客户端和服务器手里都拿到了密钥交换算法的两个参数(Client Params、Server Params),就用 ECDHE 算法一阵算,算出了一个新的东西,叫“Pre-Master”,其实也是一个随机数
- 现在客户端和服务器手里有了三个随机数:Client Random、Server Random 和 Pre-Master。用这三个作为原始材料,就可以生成用于加密会 话的主密钥,叫“Master Secret”。而黑客因为拿不到“Pre-Master”,所以也就得不到主密钥
- 有了主密钥和派生的会话密钥,握手就快结束了。客户端发一个“Change Cipher Spec”,然后再发一个“Finished”消息,把之前所有发送的数据做个摘要,再加密一下,让服务器做个验证
- 服务器也是同样的操作,发“Change Cipher Spec”和“Finished”消息,双方都验证加密解密 OK,握手正式结束,后面就收发被加密的 HTTP 请求和响应了
双向认证
上面说的是“单向认证”握手过程,只认证了服务器的身份,而没有认证客户端的身份。这是因为通常单向认证通过后已经建立了安全通信,用账号、密码等简单的手段就能够确认用户的真实身份。
但为了防止账号、密码被盗,有的时候(比如网上银行)还会使用 U 盾给用户颁发客户端证书,实现“双向认证”,这样会更加安全。
双向认证的流程也没有太多变化,只是在“Server Hello Done”之后,“Client Key Exchange”之前,客户端要发送“Client Certificate”消息,服务器收到后也把证书链走一遍,验证客户端的身份。
TLS 1.3 特性解析
TLS1.3 的三个主要改进目标:兼容、安全与性能
- 最大化兼容性
- 强化安全
- 提升性能
Other Points
HTTP 2
HTTP 有两个主要的缺点:安全不足和性能不高。
HTTPS,通过引入 SSL/TLS 在安全上达到了“极致”,但在性能提升方面却是乏善可陈,只优化了握手加密的环节,对于整体的数据传输没有提出更好的改进方案,还只能依赖于“长连接”这种“落后”的技术。
在 HTTPS 逐渐成熟之后,HTTP 就向着性能方面开始“发力”,走出了另一条进化的道路。
与 HTTPS 不同,HTTP/2 没有在 URI 里引入新的协议名,仍然用“http”表示明文协议,用“https”表示加密协议
- 兼容 HTTP/1
- 头部压缩
- 二进制格式
- 虚拟的“流”
- 强化安全
- 协议栈

HTTP 3

NGINX
Web 服务器就那么几款,目前市面上主流的只有两个:Apache 和 Nginx,两者合计占据了近 90% 的市场份额。Nginx,它是 Web 服务器的“后起之秀”,虽然比 Apache 小了 10 岁,但增长速度十分迅猛。
Nginx 应该读成“Engine X”,但“X”念起来太“拗口”,倾向于读做“Engine ks”,这也与 UNIX、Linux 的发音一致
进程池
Nginx 作为“轻量级”的服务器,它的 CPU、内存占用都非常少,同样的资源配置下就能够为更多的用户提供服务,其奥秘在于它独特的工作模式。
在 Nginx 之前,Web 服务器的工作模式大多是“Per-Process”或者“Per-Thread”,对每一个请求使用单独的进程或者线程处理。这就存在创建进程或线程的成本,还会有进程、线程“上下文切换”的额外开销。如果请求数量很多,CPU 就会在多个进程、线程之间切换时“疲于奔命”,平白地浪费了计算时间。
Nginx 使用了“进程池 + 单线程”的工作模式,在启动的时候会预先创建好固定数量的 worker 进程,在之后的运行过程中不会再 fork 出新进程,这就是进程池,而且可以自动把进程“绑定”到独立的 CPU 上,这样就完全消除了进程创建和切换的成本,能够充分利用多核 CPU 的计算能力。
在进程池之上,还有一个“master”进程,用来监控进程,自动恢复发生异常的 worker,保持进程池的稳定和服务能力。
I/O 多路复用
Nginx 就选择了单线程的方式,带来的好处就是开发简单,没有互斥锁的成本,减少系统消耗。单线程的 Nginx,处理能力却能够超越其他多线程的服务器要归功于 Nginx 利用了 Linux 内核里的一件“神兵利器”,I/O 多路复用接口,“大名鼎鼎”的 epoll。
Web 服务器从根本上来说是“I/O 密集型”而不是“CPU 密集型”,处理能力的关键在于网络收发而不是 CPU 计算(这里暂时不考虑 HTTPS 的加解密),而网络 I/O 会因为各式各样的原因不得不等待,比如数据还没到达、对端没有响应、缓冲区满发不出去等等。
- 对于一般的单线程来说 CPU 就会“停下来”,造成浪费
- 多线程的解决思路有点类似“并发连接”,虽然有的线程可能阻塞,但由于多个线程并行,总体上看阻塞的情况就不会太严重了。
- Nginx 里使用的 epoll,就好像是 HTTP/2 里的“多路复用”技术,它把多个 HTTP 请求处理打散成碎片,都“复用”到一个单线程里,不按照先来后到的顺序处理,而是只当连接上真正可读、可写的时候才处理,如果可能发生阻塞就立刻切换出去,处理其他的请求。
通过这种方式,Nginx 就完全消除了 I/O 阻塞,把 CPU 利用得“满满当当”,又因为网络收发并不会消耗太多 CPU 计算能力,也不需要切换进程、线程,所以整体的 CPU 负载是相当低的。
epoll 还有一个特点,大量的连接管理工作都是在操作系统内核里做的,这就减轻了应用程序的负担,所以 Nginx 可以为每个连接只分配很小的内存维护状态,即使有几万、几十万的并发连接也只会消耗几百 M 内存,而其他的 Web 服务器这个时候早就“Memory not enough”了。
多阶段处理
Nginx 在内部也采用的是“化整为零”的思路,把整个 Web 服务器分解成了多个“功能模块”,Nginx 的 HTTP 处理有四大类模块:
- handler 模块:直接处理 HTTP 请求;
- filter 模块:不直接处理请求,而是加工过滤响应报文;
- upstream 模块:实现反向代理功能,转发请求到其他服务器;
- balance 模块:实现反向代理时的负载均衡算法。
Nginx 里的 handler 模块和 filter 模块就是按照“职责链”模式设计和组织的,HTTP 请求报文就是“原材料”,各种模块就是工厂里的工人,走完模块构成的“流水线”,出来的就是处理完成的响应报文。
Nginx 的“流水线”,在 Nginx 里的术语叫“阶段式处理”(Phases),一共有 11 个阶段,每个阶段里又有许多各司其职的模块。
WAF
HTTPS 只是网络安全中很小的一部分,仅仅保证了“通信链路安全”,让第三方无法得知传输的内容。在通信链路的两端,也就是客户端和服务器,它是无法提供保护的。
Web 服务遇到的威胁
- DDoS(distributed denial-of-service attack),耗尽带宽、CPU 和内存,导致网站完全无法提供正常服务
- SQL 注入(SQL injection),利用了服务器字符串拼接形成 SQL 语句的漏洞,构造出非正常的 SQL 语句;HTTP 头注入攻击的方式也是类似的原理,服务端程序如果解析不当,就会执行预设的恶意代码
网络应用防火墙
传统“防火墙”工作在三层或者四层,隔离了外网和内网,使用预设的规则,只允许某些特定 IP 地址和端口号的数据包通过,拒绝不符合条件的数据流入或流出内网,实质上是一种网络数据过滤设备。
WAF (Web Application Firewall)也是一种“防火墙”,但它工作在七层,看到的不仅是 IP 地址和端口号,还能看到整个 HTTP 报文,所以就能够对报文内容做更深入细致的审核,使用更复杂的条件、规则来过滤数据。
WAF 就是一种“HTTP 入侵检测和防御系统”,通常一款产品能够称为 WAF,要具备下面的一些功能:
- IP 黑名单和白名单,拒绝黑名单上地址的访问,或者只允许白名单上的用户访问;
- URI 黑名单和白名单,与 IP 黑白名单类似,允许或禁止对某些 URI 的访问;
- 防护 DDoS 攻击,对特定的 IP 地址限连限速;
- 过滤请求报文,防御“代码注入”攻击;
- 过滤响应报文,防御敏感信息外泄;
- 审计日志,记录所有检测到的入侵操作。
WAF 就像是平时编写程序时必须要做的函数入口参数检查,拿到 HTTP 请求、响应报文,用字符串处理函数看看有没有关键字、敏感词,或者用正则表达式做一下模式匹配,命中了规则就执行对应的动作,比如返回 403/404。
网络安全领域必须时刻记得“木桶效应”(也叫“短板效应”),网站的整体安全不在于你加固的最强的那个方向,而是在于你可能都没有意识到的“短板”,使用 WAF 最好“不要重新发明轮子”,而是使用现有的、比较成熟的、经过实际考验的 WAF 产品。
CDN
光速是有限的,虽然每秒 30 万公里,但这只是真空中的上限,在实际的电缆、光缆中的速度会下降到原本的三分之二左右,也就是 20 万公里 / 秒,这样一来,地理位置的距离导致的传输延迟就会变得比较明显了。
此外,互联网从逻辑上看是一张大网,但实际上是由许多小网络组成的,网络内部的沟通很顺畅,但网络之间却只有很少的联通点。
还有,网络中还存在许多的路由器、网关,数据每经过一个节点,都要停顿一下,在二层、三层解析转发,这也会消耗一定的时间,带来延迟。把这些因素再放到全球来看,地理距离、运营商网络、路由转发的影响就会成倍增加。
什么是 CDN
CDN 的最核心原则是“就近访问”,如果用户能够在本地几十公里的距离之内获取到数据,那么时延就基本上变成 0 了,所以 CDN 投入了大笔资金,在全国、乃至全球的各个大枢纽城市都建立了机房,部署了大量拥有高存储高带宽的节点,构建了一个专用网络。
用户在上网的时候就不直接访问源站,而是访问离他“最近的”一个 CDN 节点,术语叫“边缘节点”(edge node),其实就是缓存了源站内容的代理服务器,这样一来就省去了“长途跋涉”的时间成本,实现了“网络加速”。
在 CDN 领域里,“内容”其实就是 HTTP 协议里的“资源”,比如超文本、图片、视频、应用程序安装包等等。很显然,只有静态资源才能够被缓存加速、就近访问,而动态资源只能由源站实时生成,即使缓存了也没有意义。
不过,如果动态资源指定了“Cache-Control”,允许缓存短暂的时间,那它在这段时间里也就变成了“静态资源”,可以被 CDN 缓存加速。
CDN 的负载均衡
全局负载均衡(Global Sever Load Balance)一般简称为 GSLB,它是 CDN 的“大脑”,主要的职责是当用户接入网络的时候在 CDN 专网中挑选出一个“最佳”节点提供服务,解决的是用户如何找到“最近的”边缘节点,对整个 CDN 网络进行“负载均衡”。
GSLB 最常见的实现方式是“DNS 负载均衡”,原来没有 CDN 的时候,权威 DNS 返回的是网站自己服务器的实际 IP 地址,浏览器收到 DNS 解析结果后直连网站。
加入 CDN 后,权威 DNS 返回的不是 IP 地址,而是一个 CNAME( Canonical Name ) 别名记录,指向的就是 CDN 的 GSLB,因为没拿到 IP 地址,于是本地 DNS 就会向 GSLB 再发起请求,这样就进入了 CDN 的全局负载均衡系统,开始“智能调度”,主要的依据有这么几个:
- 看用户的 IP 地址,查表得知地理位置,找相对最近的边缘节点;
- 看用户所在的运营商网络,找相同网络的边缘节点;
- 检查边缘节点的负载情况,找负载较轻的节点;
- 其他,比如节点的“健康状况”、服务能力、带宽、响应时间等。
GSLB 把这些因素综合起来,用一个复杂的算法,最后找出一台“最合适”的边缘节点,把这个节点的 IP 地址返回给用户,用户就可以“就近”访问 CDN 的缓存代理了。
CDN 的缓存代理
缓存系统是 CDN 的另一个关键组成部分,相当于 CDN 的“心脏”。如果缓存系统的服务能力不够,不能很好地满足用户的需求,那 GSLB 调度算法再优秀也没有用。
两个 CDN 的关键概念:“命中”和“回源”:
- “命中”就是指用户访问的资源恰好在缓存系统里,可以直接返回给用户
- “回源”则正相反,缓存里没有,必须用代理的方式回源站取。
相应地,也就有了两个衡量 CDN 服务质量的指标:“命中率”和“回源率”,好的 CDN 应该是命中率越高越好,回源率越低越好。现在的商业 CDN 命中率都在 90% 以上,相当于把源站的服务能力放大了 10 倍以上。
怎么样才能尽可能地提高命中率、降低回源率:
- 硬件方面:在存储系统上下功夫,硬件用高速 CPU、大内存、万兆网卡,再搭配 TB 级别的硬盘和快速的 SSD
- 软件方面:不断“求新求变”,各种新的存储软件,比如 Memcache、Redis、Ceph,尽可能地高效利用存储,存下更多的内容
- 缓存系统:划分出层次,分成一级缓存节点和二级缓存节点。一级缓存配置高一些,直连源站,二级缓存配置低一些,直连用户
- 高性能的缓存服务:国内的 CDN 厂商内部都是基于开源软件定制,最常用的是专门的缓存代理软件 Squid、Varnish 和 ATS(Apache Traffic Server),Nginx 和 OpenResty 作为 Web 服务器领域的“多面手”,凭借着强大的反向代理能力和模块化、易于扩展的优点,也在 CDN 里占据了不少的份额
WebSocket
TCP Socket 是一种功能接口,通过这些接口就可以使用 TCP/IP 协议栈在传输层收发数据,TCP 连接是全双工的。
WebSocket 就是运行在 Web,也就是 HTTP 上的 Socket 通信规范,提供与 TCP Socket 类似的功能,使用它可以像 TCP Socket 一样调用下层协议栈,任意地收发数据。更准确地说,WebSocket 是一种基于 TCP 的轻量级网络通信协议,在地位上是与 HTTP“平级”的。
WebSocket 与 HTTP/2 一样,都是为了解决 HTTP 某方面的缺陷而诞生的,HTTP/2 针对的是“队头阻塞”,而 WebSocket 针对的是“请求 - 应答”通信模式。
“请求 - 应答”是一种“半双工”的通信模式,虽然可以双向收发数据,但同一时刻只能一个方向上有动作,传输效率低。更关键的一点,它是一种“被动”通信模式,服务器只能“被动”响应客户端的请求,无法主动向客户端发送数据。
HTTP 难以应用在动态页面、即时消息、网络游戏等要求“实时通信”的领域
浏览器是一个“受限的沙盒”,不能用 TCP,只有 HTTP 协议可用,在 WebSocket 出现之前,在浏览器环境里用 JavaScript 开发实时 Web 应用很麻烦,就出现了很多“变通”的技术,“轮询”(polling)就是比较常用的的一种。
如果轮询的频率比较高,那么就可以近似地实现“实时通信”的效果。轮询的缺点也很明显,反复发送无效查询请求耗费了大量的带宽和 CPU 资源,非常不经济。
WebSocket 的特点
WebSocket 是一个真正“全双工”的通信协议,与 TCP 一样,客户端和服务器都可以随时向对方发送数据。服务器就可以变得更加“主动”了,一旦后台有新的数据,就可以立即“推送”给客户端,不需要客户端轮询,“实时通信”的效率也就提高了。
WebSocket 采用了二进制帧结构,语法、语义与 HTTP 完全不兼容,但因为它的主要运行环境是浏览器,为了便于推广和应用,在使用习惯上尽量向 HTTP 靠拢,这就是它名字里“Web”的含义。
服务发现方面,WebSocket 没有使用 TCP 的“IP 地址 + 端口号”,而是延用了 HTTP 的 URI 格式,但开头的协议名不是“http”,引入的是两个新的名字:“ws”和“wss”,分别表示明文和加密的 WebSocket 协议。
WebSocket 的默认端口也选择了 80 和 443,因为现在互联网上的防火墙屏蔽了绝大多数的端口,只对 HTTP 的 80、443 端口“放行”,所以 WebSocket 就可以“伪装”成 HTTP 协议,比较容易地“穿透”防火墙,与服务器建立连接。
虽然大多数情况下会在浏览器里调用 API 来使用 WebSocket,但它不是一个“调用接口的集合”,而是一个通信协议,把它理解成“TCP over Web”会更恰当一些。
WebSocket 的帧结构
WebSocket 的握手
总结
浏览器是一个“沙盒”环境,有很多的限制,不允许建立 TCP 连接收发数据,而有了 WebSocket,我们就可以在浏览器里与服务器直接建立“TCP 连接”,获得更多的自由。
不过自由也是有代价的,WebSocket 虽然是在应用层,但使用方式却与“TCP Socket”差不多,过于“原始”,用户必须自己管理连接、缓存、状态,开发上比 HTTP 复杂的多,所以是否要在项目中引入 WebSocket 必须慎重考虑。
- HTTP 的“请求 - 应答”模式不适合开发“实时通信”应用,效率低,难以实现动态页面,所以出现了 WebSocket;
- WebSocket 是一个“全双工”的通信协议,相当于对 TCP 做了一层“薄薄的包装”,让它运行在浏览器环境里;
- WebSocket 使用兼容 HTTP 的 URI 来发现服务,但定义了新的协议名“ws”和“wss”,端口号也沿用了 80 和 443;
- WebSocket 使用二进制帧,结构比较简单,特殊的地方是有个“掩码”操作,客户端发数据必须掩码,服务器则不用;
- WebSocket 利用 HTTP 协议实现连接握手,发送 GET 请求要求“协议升级”,握手过程中有个非常简单的认证机制,目的是防止误连接。