简单的 HTTP 开始变得不简单了

我经常听到或看到有人声称“HTTP 是一个简单的协议”。当然,这种说法主要来自那些对实际实现缺乏经验或了解的人。我认为,在我刚开始接触该协议时,自己也曾有过类似的想法。

在过去近三十年里,我一直致力于编写客户端 HTTP 代码,并自 2008 年左右以来参与了 IETF 制定的所有 HTTP 规范。因此,我认为自己有资格对这一主题进行更深入的探讨。HTTP 并非一个简单的协议。远非如此。即使我们假设人们在说“HTTP 简单”时指的是 HTTP/1。

HTTP/1看似简单,原因有几方面:它是可读的文本,最简单的用例并不复杂,且现有工具如curl和浏览器使HTTP易于操作。

HTTP的“理念”和“概念”或许仍可被视为简单甚至颇具巧思,但实际实现机制并非如此。

元素周期表

但确实,你可以通过telnet连接到HTTP/1服务器,手动输入GET /命令并看到响应。然而,我认为这不足以将整个协议称为简单。

我认为没有人会声称 HTTP/2 或 HTTP/3 是简单的。要正确实现第二版或第三版,你几乎必须同时实现第一版,因此从这个角度来看,它们在各自的规范中积累了复杂性并带来了相当多的额外挑战。

让我详细说明一些使我认为 HTTP/1 协议不简单的方面。

换行符

HTTP 不仅是基于文本的,也是基于行的;具体来说是协议的头部部分。一行可以任意长,因为规范中没有长度限制——但在实现中需要设置限制以防止 DoS 等攻击。服务器在拒绝它们之前,它们可以有多长?每行以回车符和换行符结束。但在某些情况下,仅换行符即可。

此外,头部不是 UTF-8 编码,而是八位字节,因此不能假设可以随意传输任何内容。

空格

基于文本的协议容易出现此类问题。字段之间可以包含一个或多个空格字符。其中部分是强制要求的,部分是可选的。在许多情况下,HTTP还会使用_令牌_,这些令牌可以是没有空格的字符序列,也可以是双引号(“)内的文本。在某些情况下,它们_始终_位于引号内。

主体结束

确定HTTP/1下载的“主体”(即协议术语中的“body”)结束方式并非唯一。事实上,甚至没有两种方式。至少有三种(Content-Length、分块编码和Connection: close)。其中两种需要HTTP客户端解析以文本格式提供的内容大小。这些多种多样的正文结束选项在过去几年中导致了无数与HTTP/1相关的安全问题。

解析数字

以文本形式提供的数字解析速度较慢且有时容易出错。需要特别注意避免整数溢出、处理空格、正负号前缀、前导零等情况。虽然对人类来说易于阅读,但对机器来说并不理想。

折叠头部

除了长度任意且行尾不明确的头部外,它们还可以以两种方式“折叠”。第一种:代理服务器可以将多个头部合并为一个以逗号分隔的头部——但某些头部(如Cookie)无法合并。第二种:服务器可以通过添加前导空格将头部作为前一个头部的“延续”发送。这种方式很少使用(且在最新规范版本中不推荐),但作为协议细节,实现时仍需关注,因为它确实被使用。

从未实现

HTTP/1.1 雄心勃勃地添加了当时尚未被广泛使用或部署到互联网上的功能。因此,尽管规范描述了例如 HTTP 管道化如何工作,但在实际环境中尝试使用它只会带来一系列问题,无异于自寻烦恼。

后续版本的 HTTP 添加了更好地满足管道功能未能满足的标准的特性:主要体现在 多路复用 方面。

100 状态码也属于类似情况:虽有规定,但实际使用极少。它会给新实现带来复杂性。本周关于 100 响应状态处理细节的讨论(https://lists.w3.org/Archives/Public/ietf-http-wg/2025JulSep/0088.html),距离该规范首次发布已过去二十八年,这或许说明了一些问题。

如此多的头部

HTTP/1 规范详细描述了大量头部及其功能,但这不足以支持当前正常的 HTTP 实现。这是因为像 cookie、认证、新的响应代码以及实现可能希望支持的其他功能,都超出了主规范的范围,并在额外的独立文档中描述。一些细节,如 NTLM,甚至不在 RFC 文档中。

因此,一个现代的HTTP/1客户端需要实现并支持一系列额外的功能和头部,才能在整个网络中正常工作。“HTTP/1.1”在至少40份独立的RFC文档中被提及。其中一些文档本身就相当复杂。

并非所有方法都相同

虽然理想情况下,无论使用哪种方法(有时称为动词),语法都应能够完全相同地工作,但现实并非如此。

例如,如果使用 GET 方法,我们确实可以像使用 POST 和 PUT 时那样在请求中发送请求主体,但由于过去从未明确规定过这一点,因此目前这种做法在网络上无法实现足够的互操作性,导致在相当一部分尝试中会导致失败。

这就是为什么现在正在研究一种新的 HTTP 方法,称为 QUERY,它基本上就是 GET + 请求正文应该有的样子。但这并不会简化协议。

并非所有标头都相同

由于多个标头是以有机方式创建、部署和演变的,因此代理不能盲目地将两个标头合并为一个,尽管通用规则允许这样做。因为有些头部明确不遵循这些规则,需要特殊处理。例如Cookie。

缺乏原则的浏览器

请记住,浏览器对协议的实现总是倾向于向用户显示内容并“猜测”意图,而非显示错误,因为如果它们严格遵守规则,用户可能会切换到不严格的浏览器。

这影响了世界其他地方如何处理 HTTP,因为用户开始期望浏览器能正常工作的内容也应在非浏览器及其 HTTP 实现中正常工作。

这使得解读和理解规范变得次要,相比之下,遵循主要浏览器在特定情况下做出的决定更为重要。它们甚至可能随时间改变立场,有时甚至与规范中的明确指导相矛盾。

规范的大小

1997年1月发布的首个HTTP/1.1 RFC 2068的纯文本版本长达52,165字——几乎是HTTP/1.0规范RFC1945(仅18,615字)的三倍。这清楚地表明,或许曾经简单的HTTP 1.0在1.1版本中已不再简单。

1999年6月,更新后的RFC 2616新增了数百行内容,总字数达到57,897字。比之前增加了近6,000字。

随后,IETF展开了大规模工作,在接下来的十五年里,原本单一的HTTP/1.1规范被拆分为_六_个独立文档。

RFC7230至RFC7235于2014年6月发布,总字数达90,358字。字数增长了56%,与一部中等篇幅的小说相当。

整个规范随后再次重新排列和重组,以更好地适应新的HTTP版本,最新更新于2022年6月发布。HTTP/1.1部分已被压缩为三个文档RFC 9110至RFC9112,总计95,740个单词。

为了论证起见,假设我们以每分钟阅读200个单词的速度阅读这些文档。这可能比平均阅读速度稍慢,但我想我们阅读标准规范的速度可能比阅读小说稍慢。假设其中10%的单词是无需阅读的冗余内容。

如果我们只阅读与HTTP/1.1相关的最新三份RFC文档,不间断地阅读,仍然需要超过七个小时。

必须淘汰?

在最近一场题为点击诱饵标题的会议演讲中,有人建议HTTP/1如此难以正确实现,我们应该停止使用它。

必要如此?

尽管如此,几乎没有其他互联网协议能在使用、采用和流行度方面与HTTP/1相抗衡。HTTP是互联网上的大佬。也许这种复杂性正是实现这一成功所必需的?

与仍在使用的其他流行协议(如DNS或SMTP)相比,我们可以看到类似的模式:它们最初都是简单的东西。几十年后:不再那么简单了。

或许这就是生活的常态?

结论

HTTP 并非一个简单的协议。

随着时间的推移,更多功能被添加到 HTTP 中,未来只会变得更加复杂——这适用于所有版本。

你也许感兴趣的:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注