HTTP 协议发展史(一)

Tim Berners-Lee 在发明万维网(即 World Wide Web)时,创造了三项核心技术:

  • HTTP(Hypertext Transfer Protocol)
  • URL(Uniform Resource Locator)
  • HTML(Hypertext Markup Language)

其中 HTTP 代表「超文本传输协议」,很多人并不理解这里面「超」的含义,甚至会误解,认为超文本就是超越文本资源(例如图片、视频等)的意思。其实不然,所谓的超文本文档是指文档中包含指向其他文档的链接。

HTTP 是比较上层的协议,基于可靠的网络连接(例如 TCP/IP),它不关心如何建立网络连接这种底层细节,从本质上讲,HTTP 就是一个请求-响应协议:

截至目前,HTTP 协议总共有五个版本,分别是:

  • HTTP/0.9
  • HTTP/1.0
  • HTTP/1.1
  • HTTP/2
  • HTTP/3

本文先为大家介绍前面三个协议,也是应用最广泛的三个协议,第二篇再为大家介绍剩下两个。

简单的 HTTP/0.9

在 HTTP 的第一个版本,即 HTTP/0.9 中,是不支持传输图片、视频等数据格式的,只能传输超文本,而且客户端发送请求的语法非常简单,例如:

GET /index.html

服务器只能响应 HTML 文本,如下所示:

<html>
  Welcome to juejin
</html>

HTTP/0.9 协议只支持 GET 方法,现在我们常用的 POST、PUT 等方法是 HTTP/1.0 才加进去的,在 0.9 版本的年代,GET 其实已经能够满足日常需求了,不过优秀的设计者们高瞻远瞩,考虑到未来可扩展性,于是才有了后序版本的迭代,催生了现在繁荣的 Web 生态。

除此之外,HTTP/0.9 中是不支持请求头和响应头的,自然也没有状态码的概念。请求成功或失败只能在 HTML 文本中透出。服务器响应完之后,会立即关闭连接。

接下来,我们使用 telnet 命令来进行上机实验,在终端中输入 telnet 命令:

$ telnet www.taobao.com 80
Trying 36.156.170.6...
Connected to www.taobao.com.danuoyi.tbcache.com.
Escape character is '^]'.

telnet 命令接受两个参数:

  • host:主机名
  • port:端口号

上面的命令就是尝试与淘宝服务器建立连接,连接建立成功后,可以输入 HTTP 命令了,我们继续输入 GET /

$ telnet www.taobao.com 80
Trying 36.156.170.6...
Connected to www.taobao.com.danuoyi.tbcache.com.
Escape character is '^]'.
GET /

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=GB18030"/>
    <title>Index</title>
</head>
<body><script type="text/javascript">
(function (d) {
var t=d.createElement("script");t.type="text/javascript";t.async=true;t.id="tb-beacon-aplus";
t.setAttribute("exparams","category=&userid=&aplus");
t.src=("https:"==d.location.protocol?"https://s":"http://a")+".tbcdn.cn/s/aplus_v2.js";
d.getElementsByTagName("head")[0].appendChild(t);
})(document);
</script>

<script>window.location.href="http://www.taobao.com"</script>
</body>
</html>
Connection closed by foreign host.

可以看到,淘宝响应了一段 HTML 文本,然后关闭了连接。如果我们用同样的命令连接百度的服务器,会得到不一样的结果:

$ telnet www.baidu.com 80
Trying 36.152.44.96...
Connected to www.a.shifen.com.
Escape character is '^]'.
GET /
HTTP/1.1 400 Bad Request

Connection closed by foreign host.

因为百度已经不支持 HTTP/0.9 协议了,直接响应了 HTTP/1.1 协议,状态码是 400,随后关闭连接。

繁荣的 HTTP/1.0

HTTP/0.9 是 1991 年发布的,到了 1995 年 9 月,网络上就有了 19705 个主机名,然后仅仅过了一个月就增至 31568 个,大量的网站如雨后春笋一般涌现,HTTP/0.9 协议的简单性是其得到蓬勃发展的重要原因,然而其局限性也已经到了不容忽视的程度了,于是 HTTP 工作组紧锣密鼓的开始研究 HTTP/1.0,在 1996 年 5 月正式发布,对 HTTP/0.9 进行了扩展,增加了一些重要的功能:

  • 除了 GET 之外,新增了 HEAD 和 POST 方法
  • 增加了可选的版本号字段,默认为 HTTP/0.9
  • 增加了 HTTP 首部,可以与请求和响应一起发送
  • 增加了响应状态码和描述

可以看到,这些功能正是今天被大家所熟知的,请求从原先的单行:

GET /index.html

变成了多行(请求行+首部):

GET /index.html HTTP/1.0
Header1: Value1
Header2: Value2

响应也增加了状态行(status line)

HTTP/1.0 200 OK
Content-Type: text/html

<html>
  Welcome to juejin
</html>

可以看到,HTTP/1.0 的发布,让 HTTP 协议的语法得到了极大的扩展,虽然时隔近 30 年过去了,大部分的网站现在依然在按照 HTTP/1.0 的语法标准来通信。例如:正是因为增加了 POST 方法,可以让客户端携带消息体给服务器;

POST / HTTP/1.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 27

field1=value1&field2=value2

正是因为增加了首部,让 HTTP 可以处理各种格式的文件,而不再是简单的 HTML 文本了,Content-Type 首部字段会告诉客户端具体的文件类型:

HTTP/1.0 200 OK
Content-Type: text/gif

<Encoded data of logo.gif>

这些重大的变化是今天 HTTP 繁荣的基石,为了加深理解,笔者用 Node.js 写了一个非常基础的 HTTP 服务器:

const http = require('http')
const consumers = require('stream/consumers')

const app = http.createServer(async (req, res) => {
  console.log('method: ', req.method)
  console.log('url: ', req.url)
  console.log('httpVersion: ', req.httpVersion)
  console.log('headers: ', req.headers)
  const body = await consumers.text(req)
  console.log('body: ', body)
  res.end('<html>Welcome to juejin</html>\n')
})

app.listen(8080)

上面的程序监听本地 8080 端口,并打印 HTTP 请求的方法名、版本号、路径和首部等信息。我们仍然使用 telnet 命令来进行测试:

$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.0
User-Agent: MyAwesomeBrowser

HTTP/1.1 200 OK
Connection: close

<html>Welcome to juejin</html>
Connection closed by foreign host.

当连续按两次回车之后,Server 做出了响应(按照 HTTP/1.1 格式),此时服务端的输出结果为:

method:  GET
url:  /
httpVersion:  1.0
headers:  { 'user-agent': 'MyAwesomeBrowser' }
body:

可以看到,程序正确地获取到了 HTTP 协议的方法、路径、版本号和首部信息,接下来对 POST 请求进行测试,下面试图携带一个 application/x-www-form-urlencoded 格式的数据:

$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
POST / HTTP/1.0
Content-Type: application/x-www-form-urlencoded

但是当输入两次回车准备填入消息体的时候,发现服务器直接响应了

HTTP/1.1 200 OK
Connection: close

这是因为在 Node.js 中,请求的时候,需要通过 Content-Length 首部告诉服务器消息体的长度,这样才能在流当中提取到正确的数据:

$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
POST / HTTP/1.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 27

field1=value1&field2=value2
HTTP/1.1 200 OK
Connection: close

在补充 Content-Length 之后,输入两次回车之后,服务器并没有断开连接,而是尝试从流当中提取数据,最终拿到了 body 里面的值:

method:  POST
url:  /
httpVersion:  1.0
headers:  {
  'content-type': 'application/x-www-form-urlencoded',
  'content-length': '27'
}
body:  field1=value1&field2=value2

仓促的 HTTP/1.1

在 HTTP/1.0 规范发布后仅仅 9 个月,HTTP/1.1 规范就发布了,这是因为网络爆炸式增长,需要增加新的功能,并以文档的形式记录这些约定。

不像从 HTTP/0.9 升级到 HTTP/1.0 那样,有很多显著的变化,HTTP/1.1 与 HTTP/1.0 相比,基本结构并没有改动,而是新增了 PUT、OPTIONS、CONNECT、TRACE、PATCH、DELETE 等方法,进一步扩展了 HTTP 的表达能力,另外还多了一些额外的约定,例如:

  • 强制 Host 首部
  • 默认持久连接

因为这两个问题是当时迫在眉睫的事情。HTTP 协议在设计之初,一个服务器只能托管一个网站,随着虚拟主机技术的普及,一个服务器可以托管很多网站,而 HTTP 协议里面只包含了请求路径,并没有主机或域名信息,好的解决办法是把协议里面的请求路径从相对路径改成绝对路径,然而这会导致新的协议不向前兼容,很多正在运行的服务器和客户端都无法运行,因此通过强制增加 Host 首部来明确告诉服务器主机名,例如:

POST / HTTP/1.1
Host: foo.example

在 HTTP/1.0 中,客户端发送请求,服务器相应资源之后就断开连接了,如果一个页面需要多个资源,只能重新打开连接,从而导致不必要的延迟。HTTP/1.1 版本中新增了 Connection 首部,可以指定值为 Keep-Alive,要求服务器保持连接,以支持发送更多的请求。

GET / HTTP/1.1
Connection: Keep-Alive

在 HTTP/1.1 中,这个行为是默认的,即使没有显式指定 Keep-Alive 的值,服务器也会默认保持连接,用上面写的 Node.js 服务器可以非常清晰地看出来:

# HTTP/1.0 默认关闭连接
$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.0

HTTP/1.1 200 OK
Date: Wed, 28 Jun 2023 09:53:32 GMT
Connection: close

<html>Welcome to juejin</html>
Connection closed by foreign host.

# HTTP/1.1 默认保持连接
$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.1

HTTP/1.1 200 OK
Date: Wed, 28 Jun 2023 09:53:40 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 31

<html>Welcome to juejin</html>
Connection closed by foreign host.

Node.js 默认 Keep-Alive 的时间是 5 秒,如果在 5 秒之内,没有新的请求过来,会关闭连接。这个值可以在创建服务器的时候通过 keepAliveTimeout 字段进行设置。

除了上面的变化之外,HTTP/1.1 还增加了更好的缓存方法,例如 Cache-Control 首部比之前的 Expires 首部功能更强大。这些能力的增强进一步加速了 Web 生态的蓬勃发展,然而随着网站需要加载的资源越来越多,如何进一步提升加载速度是亟需攻克的难题。HTTP/1.1 的版本中,为了解决这个问题,提出了管道化的概念,其思想非常简单:既然可以通过 Keep-Alive 保持连接,就能复用一个传输通道,此时把串行的 HTTP 请求变成并行的岂不是可以提升加载速度了?例如在 HTTP/1.0 中一个带两张图片的网页,总共需要发送三个请求:

而两张图片其实是可以同时请求的,这样就能够缩短一半时间了:

听起来非常有道理,然而直到今天大部分浏览器并不支持这个功能,因为它有个限制:必须按照请求的顺序返回响应,这就会遇到队头(HOL)阻塞问题,假如获取图像1的时间很长,即使图像2已经准备完毕,也不能发送给客户端,所以从本质上讲,并没有真正解决问题。不过,这个技术难点,在 HTTP/2 当中被攻克了,那 HTTP/2 协议又带来了哪些新的特性呢?后续文章将会揭晓。

全部评论

相关推荐

04-08 10:36
已编辑
华南理工大学 C++
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务