HTTP 简史
HTTP,第九章
超文本传输协议(HTTP)是互联网上最普遍、应用最广泛的应用层协议之一:它是客户端与服务器之间的通用语言,支撑着现代 Web。从最初简单的单个关键字加文档路径,到如今已成为不仅是浏览器,而是几乎所有联网软硬件应用的首选协议。
在本章中,我们将简要回顾 HTTP 协议的演进历程。关于 HTTP 语义的完整讨论超出了本书的范围,但理解 HTTP 的关键设计变更及其背后的动机,将为我们讨论 HTTP 性能提供必要的背景知识,特别是在 HTTP/2 和 HTTP/3 带来的诸多改进的背景下。
HTTP 0.9:单行协议
Tim Berners-Lee 最初的 HTTP 提案本着简洁至上的原则设计,旨在帮助推广他的另一个初生想法:万维网。这一策略显然奏效了:有志于协议设计者们,请注意了。
1991 年,Berners-Lee 阐述了新协议的动机,并列出了几个高级设计目标:文件传输功能、请求超文本档案索引搜索的能力、格式协商,以及将客户端重定向到另一台服务器的能力。为了在实践中验证这一理论,构建了一个简单的原型,实现了所提议功能的一小部分子集:
- 客户端请求是单个 ASCII 字符串。
- 客户端请求以回车换行符(CRLF)终止。
- 服务器响应是 ASCII 字符流。
- 服务器响应是超文本标记语言(HTML)。
- 文档传输完成后连接终止。
然而,即便是这些描述听起来也比实际情况复杂得多。这些规则所实现的是一个极其简单、对 Telnet 友好的协议,至今仍有一些 Web 服务器支持:
$> telnet google.com 80
Connected to 74.125.xxx.xxx
GET /about/
(超文本响应)
(connection closed)
请求由单行组成:GET 方法和所请求文档的路径。响应是单个超文本文档——没有头部或其他任何元数据,只有 HTML。它真的不能再简单了。此外,由于上述交互是预期协议的一个子集,它非正式地获得了 HTTP 0.9 的标签。剩下的,正如他们所说,就是历史了。
从 1991 年这些 humble 的开端开始,HTTP 获得了自己的生命,并在随后的几年里迅速发展。让我们快速回顾一下 HTTP 0.9 的特性:
- 客户端-服务器、请求-响应协议。
- ASCII 协议,运行在 TCP/IP 链路上。
- 设计用于传输超文本文档(HTML)。
- 每次请求后服务器与客户端之间的连接都会关闭。
流行的 Web 服务器,如 Apache 和 Nginx,仍然支持 HTTP 0.9 协议——部分原因是它实在没什么复杂的!如果你好奇,打开一个 Telnet 会话,尝试通过 HTTP 0.9 访问 google.com 或你喜欢的任何网站,观察这个早期协议的行为和局限性。
HTTP/1.0:快速成长与信息性 RFC
1991 年到 1995 年是 HTML 规范、被称为”Web 浏览器”的新型软件,以及面向消费者的公共互联网基础设施快速协同进化的时期。
完美风暴:1990 年代初期的互联网繁荣
在 Tim Berners-Lee 最初的浏览器原型基础上,美国国家超级计算应用中心(NCSA)的一个团队决定实现他们自己的版本。由此,第一款流行的浏览器诞生了:NCSA Mosaic。NCSA 团队的一名程序员 Marc Andreessen 与 Jim Clark 合作,于 1994 年 10 月创立了 Mosaic Communications 公司。该公司后来更名为 Netscape,并于 1994 年 12 月发布了 Netscape Navigator 1.0。到这个时候,已经很清楚万维网注定将远不止是一个学术好奇心。
事实上,同年第一届万维网会议在瑞士日内瓦召开,导致了万维网联盟(W3C)的成立,以帮助指导 HTML 的演进。同样,互联网工程任务组(IETF)内成立了一个并行的 HTTP 工作组(HTTP-WG),专注于改进 HTTP 协议。这两个组织至今仍在 Web 演进中发挥着重要作用。
最后,为了形成这场完美风暴,CompuServe、AOL 和 Prodigy 在 1994-1995 年的同一时间段开始向公众提供拨号上网服务。乘着这股快速采用的浪潮,Netscape 于 1995 年 8 月 9 日进行了极其成功的 IPO——互联网繁荣已经到来,每个人都想分一杯羹!
初生 Web 日益增长的功能需求列表及其在公共 Web 上的用例,迅速暴露了 HTTP 0.9 的许多根本局限性:我们需要一个不仅能提供超文本文档,还能提供关于请求和响应的丰富元数据、实现内容协商等的协议。反过来,初生的 Web 开发者社区通过一种特别的过程做出了回应:实现、部署,看看其他人是否采用。
在这个快速实验的时期,一套最佳实践和通用模式开始出现,1996 年 5 月,HTTP 工作组(HTTP-WG)发布了 RFC 1945,记录了野外众多 HTTP/1.0 实现的”通用用法”。请注意,这只是一个信息性 RFC:我们所知的 HTTP/1.0 并非正式规范或互联网标准!
话虽如此,一个 HTTP/1.0 请求示例应该看起来很熟悉:
$> telnet website.org 80
Connected to xxx.xxx.xxx.xxx
GET /rfc/rfc1945.txt HTTP/1.0
User-Agent: CERN-LineMode/2.15 libwww/2.17b3
Accept: */*
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 01 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 1 May 1996 12:45:26 GMT
Server: Apache 0.84
(纯文本响应)
(connection closed)
- 带有 HTTP 版本号的请求行,后跟请求头部
- 响应状态,后跟响应头部
上述交换并非 HTTP/1.0 功能的详尽列表,但它确实说明了一些关键的协议变更:
- 请求可能由多个换行符分隔的头部字段组成。
- 响应对象以响应状态行开头。
- 响应对象有自己的一组换行符分隔的头部字段。
- 响应对象不限于超文本。
- 每次请求后服务器与客户端之间的连接都会关闭。
请求和响应头部都保持 ASCII 编码,但响应对象本身可以是任何类型:HTML 文件、纯文本文件、图像或任何其他内容类型。因此,HTTP 的”超文本传输”部分在引入后不久就成为了一个误称。实际上,HTTP 迅速演变成了超媒体传输,但原名保留了下来。
除了媒体类型协商外,RFC 还记录了许多其他常用功能:内容编码、字符集支持、多部分类型、授权、缓存、代理行为、日期格式等。
今天,Web 上几乎每台服务器都能且仍会说 HTTP/1.0。只是,到现在你应该知道得更清楚了!每个请求都需要一个新的 TCP 连接,这给 HTTP/1.0 带来了显著的性能损失;参见三次握手,然后是慢启动。
HTTP/1.1:互联网标准
将 HTTP 转变为官方 IETF 互联网标准的工作与 HTTP/1.0 的文档工作并行进行,历时约四年:1995 年至 1999 年。事实上,第一个官方 HTTP/1.1 标准定义在 RFC 2068 中,于 1997 年 1 月正式发布,大约在 HTTP/1.0 发布六个月后。然后,两年半后的 1999 年 6 月,许多改进和更新被纳入标准,作为 RFC 2616 发布。
HTTP/1.1 标准解决了早期版本中发现的大量协议歧义,并引入了许多关键的性能优化:持久连接、分块编码传输、字节范围请求、额外的缓存机制、传输编码和请求管道。
有了这些功能,我们现在可以检查任何现代 HTTP 浏览器和客户端执行的典型 HTTP/1.1 会话:
$> telnet website.org 80
Connected to xxx.xxx.xxx.xxx
GET /index.html HTTP/1.1
Host: website.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4)... (snip)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: __qca=P0-800083390... (snip)
HTTP/1.1 200 OK
Server: nginx/1.0.11
Connection: keep-alive
Content-Type: text/html; charset=utf-8
Via: HTTP/1.1 GWA
Date: Wed, 25 Jul 2012 20:23:35 GMT
Expires: Wed, 25 Jul 2012 20:23:35 GMT
Cache-Control: max-age=0, no-cache
Transfer-Encoding: chunked
100
<!doctype html>
(snip)
100
(snip)
0
GET /favicon.ico HTTP/1.1
Host: www.website.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4)... (snip)
Accept: */*
Referer: http://website.org/
Connection: close
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: __qca=P0-800083390... (snip)
HTTP/1.1 200 OK
Server: nginx/1.0.11
Content-Type: image/x-icon
Content-Length: 3638
Connection: close
Last-Modified: Thu, 19 Jul 2012 17:51:44 GMT
Cache-Control: max-age=315360000
Accept-Ranges: bytes
Via: HTTP/1.1 GWA
Date: Sat, 21 Jul 2012 21:35:22 GMT
Expires: Thu, 31 Dec 2037 23:55:55 GMT
Etag: W/PSA-GAu26oXbDi
(图标数据)
(connection closed)
- HTML 文件请求,包含编码、字符集和 cookie 元数据
- 原始 HTML 请求的分块响应
- 以 ASCII 十六进制数表示的块中的八位字节数(256 字节)
- 分块流响应结束
- 在同一 TCP 连接上发出的图标文件请求
- 通知服务器连接将不被重用
- 图标响应,后跟连接关闭
唷,这里面有很多东西!第一个也是最明显的区别是我们有两个对象请求,一个用于 HTML 页面,一个用于图像,都通过单个连接传递。这就是持久连接的作用,它允许我们为同一主机的多个请求重用现有的 TCP 连接,从而提供更快的最终用户体验;参见 TCP 优化。
要终止持久连接,请注意第二个客户端请求通过 Connection 头部向服务器发送明确的 close 令牌。同样,服务器可以在响应传输完成后通知客户端关闭当前 TCP 连接的意图。技术上,任何一方都可以在没有此类信号的情况下随时终止 TCP 连接,但客户端和服务器应尽可能提供它,以便在双方实现更好的连接重用策略。
HTTP/1.1 改变了 HTTP 协议的语义,默认使用持久连接。意思是,除非另有说明(通过 Connection: close 头部),服务器应默认保持连接打开。
然而,这一功能也被回溯移植到 HTTP/1.0,并通过 Connection: Keep-Alive 头部启用。因此,如果你使用的是 HTTP/1.1,技术上你不需要 Connection: Keep-Alive 头部,但许多客户端仍然选择提供它。
此外,HTTP/1.1 协议还添加了内容、编码、字符集甚至语言协商、传输编码、缓存指令、客户端 cookie,以及可以在每个请求上协商的十几种其他功能。
我们不打算详述每个 HTTP/1.1 功能的语义。这是专著的主题,已经有很多优秀的著作。相反,前面的示例很好地说明了 HTTP 的快速进步和演进,以及每个客户端-服务器交换的复杂而精细的舞蹈。这里面有很多东西!
关于 HTTP 协议所有内部工作原理的良好参考,请查看 David Gourley 和 Brian Totty 的 O’Reilly 著作《HTTP 权威指南》。
HTTP/2:提升传输性能
自发布以来,RFC 2616 作为互联网空前增长的基础:数十亿台各种形状和大小的设备,从台式电脑到我们口袋里的微型 Web 设备,每天都在使用 HTTP 传递新闻、视频以及数百万其他我们都已依赖的 Web 应用。
最初作为检索超文本的简单单行协议,迅速演变成了通用的超媒体传输,十年后几乎可以用于任何你能想象的用例。能够说这种协议的服务器的普遍性和消费它的客户端的广泛可用性意味着许多应用现在完全基于 HTTP 设计和部署。
需要一个协议来控制你的咖啡壶?RFC 2324 为你提供了超文本咖啡壶控制协议(HTCPCP/1.0)——最初是 IETF 的愚人节玩笑,但在我们新的超连接世界中越来越不像是一个玩笑。
超文本传输协议(HTTP)是一种用于分布式、协作式超媒体信息系统的应用层协议。它是一种通用的、无状态的协议,可以通过扩展其请求方法、错误代码和头部用于许多超出超文本用途的任务,如域名服务器和分布式对象管理系统。HTTP 的一个特性是数据表示的类型和协商,允许系统独立于正在传输的数据而构建。
RFC 2616: HTTP/1.1, 1999 年 6 月
HTTP 协议的简单性是其最初采用和快速增长的原因。事实上,现在发现嵌入式设备——传感器、执行器和咖啡壶——使用 HTTP 作为其主要控制和数据协议并不罕见。但在其自身成功的重压下,随着我们继续将日常交互迁移到 Web——社交、电子邮件、新闻和视频,以及 increasingly 我们整个个人和工作空间——它也开始显示出压力的迹象。用户和 Web 开发者现在都要求 HTTP/1.1 提供近实时的响应性和协议性能,而这没有一些修改它根本无法满足。
为了应对这些新挑战,HTTP 必须继续演进,因此 HTTPbis 工作组于 2012 年初宣布了 HTTP/2 的新计划:
对于一种保留 HTTP 语义但没有 HTTP/1.x 消息分帧和语法遗留问题的协议,正在出现实现经验和兴趣,这些问题已被确定为阻碍性能并鼓励底层传输的误用。
工作组将制定一份规范,以有序的双向流形式表达 HTTP 当前语义的新形式。与 HTTP/1.x 一样,主要目标传输是 TCP,但应该可以使用其他传输。
HTTP/2 章程,2012 年 1 月
HTTP/2 的主要重点是提升传输性能,实现更低的延迟和更高的吞吐量。主版本号的增加听起来像是一大步,就性能而言确实如此,但重要的是要注意,高级协议语义均未受影响:所有 HTTP 头部、值和用例都保持不变。
任何现有网站或应用都可以通过 HTTP/2 交付而无需修改:你不需要修改应用标记来利用 HTTP/2。HTTP 服务器必须会说 HTTP/2,但对于大多数用户来说,这应该是一个透明的升级。如果工作组实现其目标,唯一的区别应该是我们的应用以更低的延迟和更好的网络链路利用率交付!
话虽如此,让我们不要操之过急。在我们了解新的 HTTP/2 协议功能之前,值得退一步审视我们对 HTTP/1.1 的现有部署和性能最佳实践。HTTP/2 工作组正在新规范上快速取得进展,但即使最终标准已经完成并准备就绪,我们在可预见的未来仍然必须支持旧的 HTTP/1.1 客户端——实际上,十年或更长时间。
HTTP/3:基于 QUIC 的下一代协议
HTTP/2 于 2015 年标准化后,Web 性能社区很快发现,虽然多路复用解决了应用层的队头阻塞问题,但 TCP 层面的队头阻塞仍然存在。在丢包率较高的网络环境中,TCP 的拥塞控制机制会阻塞所有流,即使它们属于不同的独立请求。这促使了新一轮的创新。
2012 年,Google 开始实验一种新的传输协议 QUIC(Quick UDP Internet Connections),旨在结合 UDP 的灵活性与 TCP 的可靠性,同时内置加密。经过多年的迭代和 IETF 的标准化工作,HTTP/3 于 2022 年 6 月正式标准化为 RFC 9114。
HTTP/3 代表了协议架构的根本性转变:
- 传输层革命:HTTP/3 不再使用 TCP,而是基于 QUIC 运行在 UDP 之上。这消除了 TCP 队头阻塞,允许独立流的传输互不影响。
- 内置加密:TLS 1.3 集成在 QUIC 中,不再像 HTTP/2 那样是可选层。这提供了更强的安全性和更快的连接建立(0-RTT 或 1-RTT)。
- 连接迁移:QUIC 使用连接 ID 而非 IP 地址+端口来标识连接,允许用户在 Wi-Fi 和蜂窝网络之间切换时保持连接不中断。
- 改进的拥塞控制:QUIC 在用户空间实现,允许更快的迭代和更智能的拥塞控制算法。
截至 2024 年,HTTP/3 的采用正在加速。根据 HTTP Archive 的数据,约 26-28%的网站通过alt-svc头部宣告 HTTP/3 支持,而实际流量中 HTTP/2+(包括 HTTP/3)已占主导地位。主要 CDN 如 Cloudflare、Fastly 和 Akamai 已默认启用 HTTP/3,Chrome、Firefox、Safari 和 Edge 等主流浏览器均提供支持。
然而,HTTP/3 并非万能药。企业防火墙和某些旧版网络设备可能会阻止或降低 UDP 流量优先级,运营商级 NAT(CGNAT)在某些移动环境中仍可能造成问题。因此,现代浏览器实现了优雅的降级机制:当 QUIC 连接失败时,自动回退到 HTTP/2,用户甚至不会察觉。
与 HTTP/2 一样,HTTP/3 保持了与 HTTP/1.1 相同的高级语义——方法、头部、状态码都保持不变。这意味着现有 Web 应用无需修改即可受益,只要服务器和客户端支持新协议。这种向后兼容的演进策略,正是 HTTP 能够从简单单行协议发展为支撑现代互联网基础设施的关键所在。