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 协议又带来了哪些新的特性呢?后续文章将会揭晓。

全部评论

相关推荐

后来123321:别着急,我学院本大二,投了1100份,两个面试,其中一个还是我去线下招聘会投的简历,有时候这东西也得看运气
点赞 评论 收藏
分享
面试官人很好,态度和蔼可亲,没答出来时也会引导你去思考。由于是晚上面的,导致我白天一天都有点紧张,面的时候状态也不是很好,正常可能面试官提问完应该思考几秒再答,而我就像抢答一样一口气把所有会的都说出来,这样就导致逻辑比较混乱,东一句西一句的。首先是自我介绍,先把会的技术大致讲一下,由于我八股背的多所以着重讲了一下,Java,go,jvm,MySQL,Redis,计网,操作系统这些,然后一小部分闲聊,然后先问了一下项目,面试官问我这个项目是否落实之类的,直接坦言说是写的练手的,包括之前也写过IM通讯,外卖之类的。然后面试官就把提问的重点放在了八股上。先问了Java:类加载器(答:3种+自定义类加载器、tomcat、原因+双亲委派+好处)JVM参数(答:xmx,xms,newsize这些,问我是如何设定的,我回答是把内存分一半给堆,再把堆分一半给新生代,这方面确实不太了解)然后问了一下并发相关的:线程池(答:线程池的7个参数(忘了线程工厂和阻塞时间了),3个重要参数,还有线程如何启用,为什么要设计最大线程数之类的,提到Java栈默认分配1MB运行时不可以更改)AQS(答:先讲clh是自旋锁+list,然后是AQS在这个基础上做的两个优化,然后举了一下reentrantlock根据state如何获取资源)CAS(答:使用三个字段,aba问题,然后将通常搭配自旋锁实现,面试官问通常会自旋多少次,这个不太了解,答的100,然后问100次大概多少秒,回答微秒级,然后面试官讲了一下怎么做资源可能没用完,意识到可能还需要进行阻塞操作)然后考虑一下Linux命令(top,ps,如何使用管道符过滤线程和使用Linux启动线程没答出来)然后问Redis:持久化机制(答:三种aof,rdb,混合,aof的三个参数刷盘策略,rdb以快照保存,使用bgsave会使用子线程来保存不会阻塞,而aof虽然会阻塞但是只在写完数据后追加一条命令,不会太影响,然后是他俩的优缺点,还有混合是怎么保存数据的)集群模式(答:三种,主从复制到缺点再到哨兵机制,正常使用三个哨兵互相监督,主节点挂了投票选主哨兵然后选主节点,然后额外讲一下脑裂的问题,主节点进行数据更新然后把命令写入aof来同步从节点,最后cluster集群,如何实现,使用16383个哈希槽(艹答成16384了),先根据哈希码取余,再根据节点数取余决定放在哪个节点上,然后问了一下我会怎么选集群模式,首先是cluster的问题,会让管道操作之类的失效,然后哨兵会导致整个集群结构变得复杂,使用小项目可能会考虑哨兵,大的考虑cluster,然后考了一下cluster如果一个节点挂了怎么办,根据节点数重新取余然后数据转移,面试官说这么转移比较慢,有没有别的办法,我隐约记得使用一个类似环形数组的方式,想不起来了)然后考了一下MySQL的b+树(这方面的知识点太多了,导致我什么都想讲逻辑就比较乱,讲了一下聚簇索引,树的叶子节点对应着一张页16KB,MySQL有一个区的概念,把这些页放在同一个区中,这样叶子节点的双向链表遍历时速度更快,然后b+树的扇出比较大(非常二,说成扇度之类的,面试官以为说的是扇区)这样层数就比较小,一行1kb数据的话3层可以放心2000w数据)其他的暂时想不起来了算法是lru,面试官问要不要提示,我说写个,然后写了10分钟左右,说大概写好了,但是面试官指出了2个小错误,第一个马上就改回来了,第二个一直没看出来(大脑这时候已经停止工作了)反问:问学习建议,说根据实际的项目进行深入,考虑应该怎么做,还问了一下组里面是做Java的吗?面试官说他是做go的,组里什么语言都有,语言影响不大,连忙补充了一句我对go的底层有深入源码的学习)结束。总体感觉答得不太好,没有太体现出深度,细节也不够全面。
下一个更好呗:佬,我投完云智一直没消息,多久约的一面啊
查看14道真题和解析
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务