当面试时被问对http了解多少的时候(三)—— http 协议进阶篇

428 阅读17分钟

前言

本篇文章为 http 系列文章中的第三部分,更多文章参考:juejin.cn/post/684490… 真的是一个既简单又复杂的东西,在学习的过程中发现每一个点都可以进行深入的探讨,本系列课程的目的是为了对 http 有个大纲式的学习,如果大家对里面的细节感兴趣的话可以去深入研究一下。后面有时间的话还会写一下 http 的安全篇 和 http 的面试篇,因为对很多前端童鞋来讲,短期内更需要的是能够在面试中游刃有余的回答面试官关于 http 的问题。

http 的特点

1. 简单、灵活、易于扩展

http 协议是一个 “灵活可扩展” 的传输协议。

http 协议最初诞生的时候比较简单,初次接触 HTTP 的人都会认为,HTTP 协议是很“简单”的,基本的报文格式就是“header+body”,头部信息也是简单的文本格式,用的也都是常见的英文单词,即使不去看 RFC 文档,只靠猜也能猜出个“八九不离十”。简单的特性降低了学习和使用的门槛,能够让更多的人学习和使用 http。其次,“简单”蕴含了进化和扩展的可能性,“把简单的系统变复杂”,要比“把复杂的系统变简单”容易得多。

在“简单”这个最基本的设计理念之下,随着互联网的发展,http 协议又多出了“灵活和易于扩展”的优点。在这个过程中,http 协议逐渐增加了请求方法、版本号、状态码、头字段等特性。而 body 也不再限于文本形式的 TXT 或 HTML,而是能够传输图片、音频视频等数据。

而那些 RFC 文档,实际上也可以理解为是对已有扩展的 “承认和标准化”,实现了 “从实践中来,到实践中去” 的良性循环。

也正是因为这个特点,http 才能在三十年的历史长河中 “屹立不倒”,始终保持着旺盛的生命力。

2. 可靠传输

http 协议是一个 “可靠” 的传输协议。

因为 http 协议是基于 TCP/IP 的,而 TCP 本身是一个可靠的传输协议,所以 http 自然也就继承了这个特性,能够在请求方和应答方之间 “可靠” 地传输数据。

不过我们必须正确地理解 “可靠” 的含义,http 并不能 100% 保证数据一定能够发送到另一端,在网络繁忙、连接质量差等情况下,也有可能发送失败。“可靠” 只是向使用者提供了一个 “承诺”,会 “尽量” 保证数据的完整送达。

3. 应用层协议

http 协议是一个应用层的协议。

在 TCP/IP 诞生后的几十年里,虽然出现了许多的应用层协议,但它们都仅关注很小的应用领域,局限在很少的应用场景。例如 FTP 只能传输文件、SSH 只能远程登录等,在通用的数据传输方面 “完全不能打”。

http 凭借着可携带任意头字段和实体数据的报文结构,以及连接控制、缓存代理等方便易用的特性,一出现就 “技压群雄”。

套用一个网上流行的段子,HTTP 完全可以用开玩笑的口吻说:“不要误会,我不是针对 FTP,我是说在座的应用层各位,都是垃圾。”

4. 应用广泛、环境成熟

http 协议的另一大优点是“应用广泛”,软硬件环境都非常成熟。

随着互联网特别是移动互联网的普及,HTTP 的触角已经延伸到了世界的每一个角落:从简单的 Web 页面到复杂的 JSON、XML 数据,从台式机上的浏览器到手机上的各种 APP,从看新闻、泡论坛到购物、理财、“吃鸡”,你很难找到一个没有使用 HTTP 的地方。

HTTP 广泛应用的背后还有许多硬件基础设施支持,各个互联网公司和传统行业公司都不遗余力地发展,购买服务器开办网站,建设数据中心、CDN 和高速光纤等,持续地优化上网体验,让 http 运行的越来越顺畅。

5. 请求 - 应答 模式

HTTP 协议使用的是请求 - 应答通信模式。

这个请求应答模式是 http 协议里最根本的通信模型,通俗来讲就是 “一发一收” “有来有回”。

6. 无状态

HTTP 协议是无状态的,这个特性是一把双刃剑

什么是无状态

“无状态” 形象地说就是 “没有记忆能力”。在整个协议里没有规定任何的状态,客户端和服务器处于一种物质的状态。建立连接前两者互不知情,每次收发的报文也都是互相独立的,没有任何的联系。

比如,浏览器发了一个请求,说“我是小明,请给我 A 文件。”,服务器收到报文后就会检查一下权限,看小明确实可以访问 A 文件,于是把文件发回给浏览器。接着浏览器还想要 B 文件,但服务器不会记录刚才的请求状态,不知道第二个请求和第一个请求是同一个浏览器发来的,所以浏览器必须还得重复一次自己的身份才行:“我是刚才的小明,请再给我 B 文件。”

无状态的优点

因为服务器没有“记忆能力”,所以就不需要额外的资源来记录状态信息,不仅实现上会简单一些,而且还能减轻服务器的负担,能够把更多的 CPU 和内存用来对外提供服务。

而且,“无状态”也表示服务器都是相同的,没有“状态”的差异,所以可以很容易地组成集群,让负载均衡把请求转发到任意一台服务器,不会因为状态不一致导致处理出错,轻松实现高并发高可用。

无状态的缺点

服务器没有“记忆能力”,所以无法支持需要连续多个步骤的“事务”操作。例如电商购物,首先要登录,然后添加购物车,再下单、结算、支付,这一系列操作都需要知道用户的身份才行,但“无状态”服务器是不知道这些请求是相互关联的,每次都得问一遍身份信息,不仅麻烦,而且还增加了不必要的数据传输量。

但不要忘了 HTTP 是“灵活可扩展”的,虽然标准里没有规定“状态”,但可以通过别的方式去实现,比如 cookie 等。

7. 明文传输

HTTP 协议里还有一把优缺点一体的“双刃剑”,就是明文传输。

“明文”意思就是协议里的报文(准确地说是 header 部分)不使用二进制数据,而是用简单可阅读的文本形式。

明文的优点

对比 TCP、UDP 这样的二进制协议,它的优点显而易见,不需要借助任何外部工具,用浏览器、Wireshark 或者 tcpdump 抓包后,直接用肉眼就可以很容易地查看或者修改,为我们的开发调试工作带来极大的便利。

明文的缺点 明文的缺点也是一样显而易见,HTTP 报文的所有信息都会暴露在“光天化日之下”,在漫长的传输链路的每一个环节上都毫无隐私可言,不怀好意的人只要侵入了这个链路里的某个设备,简单地“旁路”一下流量,就可以实现对通信的窥视。

HTTP 传输大文件的方法

我们知道 http 可以传输很多种类的数据,不仅是文本,也能传输图片、音频和视频。早期互联网上传输的基本上都是只有几 K 大小的文本和小图片,现在的情况则大有不同。网页里包含的信息实在是太多了,随随便便一个主页 HTML 就有可能上百 K,高质量的图片都以 M 论,更不要说那些电影、电视剧了,几 G、几十 G 都有可能。相比之下,100M 的光纤固网或者 4G 移动网络在这些大文件的压力下都变成了“小水管”,无论是上传还是下载,都会把网络传输链路挤的“满满当当”。

所以,如何在有限的带宽下高效快捷地传输这些大文件就成了一个重要的课题。这就好比是已经打开了冰箱门(建立连接),该怎么把大象(文件)塞进去再关上门(完成传输)呢?

今天我们就一起看看 HTTP 协议里有哪些手段能解决这个问题。

1. 数据压缩

通常浏览器在发送请求时都会带着“Accept-Encoding”头字段,里面是浏览器支持的压缩格式列表,例如 gzip 等,这样服务器就可以从中选择一种压缩算法,放进 “Content-Encoding” 响应头里,再把原数据压缩后发给浏览器。如果压缩率能有 50%,也就是说 100K 的数据能够压缩成 50K 的大小,那么就相当于在带宽不变的情况下网速提升了一倍,加速的效果是非常明显的。

2. 分块传输

数据压缩是把大文件整体变小,如果大文件整体不能变小,那就把它“拆开”,分解成多个小块,把这些小块分批发给浏览器,浏览器收到后再组装复原。

这种 “化整为零” 的思路在 http 协议里就是 “chunked” 分块传输编码,在响应报文里用头字段 “Transfer-Encoding: chunked” 来表示,意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。

下面我们来看一下分块传输的编码规则:

  1. 每个分块包含两个部分,长度头和数据块;
  2. 长度头是以 CRLF(回车换行,即\r\n)结尾的一行明文,用 16 进制数字表示长度;
  3. 数据块紧跟在长度头后,最后也用 CRLF 结尾,但数据不包含 CRLF;
  4. 最后用一个长度为 0 的块表示结束。

浏览器在收到分块传输的数据后会自动按照规则去掉分块编码,重新组装出内容。

3. 范围请求

有了分块传输编码,服务器就可以轻松地收发大文件了,但对于上 G 的超大文件,还有一些问题需要考虑。

比如,你在看当下正热播的某穿越剧,想跳过片头,直接看正片,或者有段剧情很无聊,想拖动进度条快进几分钟,这实际上是想获取一个大文件其中的片段数据,而分块传输并没有这个能力。

HTTP 协议为了满足这样的需求,提出了“范围请求”(range requests)的概念,允许客户端在请求头里使用专用字段来表示只获取文件的一部分。

请求头 Range 是 http 范围请求的专用字段,格式是 “bytes=x-y”,其中的 x 和 y 是以字节为单位的数据范围。

HTTP 的连接管理

短连接

HTTP 协议最初(0.9/1.0)是个非常简单的协议,通信过程也采用了简单的“请求 - 应答”方式。

它底层的数据传输基于 TCP/IP,每次发送请求前需要先与服务器建立连接,收到响应报文后会立即关闭连接。

因为客户端与服务器的整个连接过程很短暂,不会与服务器保持长时间的连接状态,所以就被称为“短连接”(short-lived connections)。早期的 HTTP 协议也被称为是“无连接”的协议。

“短连接” 的缺点相当严重,因为在 TCP 协议里,建立连接和关闭连接都是非常“昂贵”的操作。TCP 建立连接要有“三次握手”,关闭连接是“四次挥手”,传输效率非常低。

长连接

针对短连接暴露出的缺点,HTTP 协议就提出了“长连接”的通信方式,也叫“持久连接”(persistent connections)、“连接保活”(keep alive)、“连接复用”(connection reuse)。

其实解决办法也很简单,用的就是“成本均摊”的思路,既然 TCP 的连接和关闭非常耗时间,那么就把这个时间成本由原来的一个“请求 - 应答”均摊到多个“请求 - 应答”上。

HTTP/1.1 默认启用长连接,在一个连接上收发多个请求响应,提高了传输效率。过多的长连接会占用服务器资源,所以服务器会用一些策略有选择地关闭长连接。

下图是一个短连接与长连接的对比示意图:

连接相关的头字段

由于长连接对性能的改善效果非常显著,所以在 HTTP/1.1 中的连接都会默认启用长连接。不需要用什么特殊的头字段指定,只要向服务器发送了第一次请求,后续的请求都会重复利用第一次打开的 TCP 连接,也就是长连接,在这个连接上收发数据。

当然,我们也可以在请求头里明确地要求使用长连接机制,使用的字段是 Connection,值是“keep-alive”。

不过不管客户端是否显式要求长连接,如果服务器支持长连接,它总会在响应报文里放一个“Connection: keep-alive”字段,告诉客户端:“我是支持长连接的,接下来就用这个 TCP 一直收发数据吧”。

长连接的缺点

如果 TCP 连接长时间不关闭,服务器必须在内存里保存它的状态,这就占用了服务器的资源。如果有大量的空闲长连接只连不发,就会很快耗尽服务器的资源,导致服务器无法为真正有需要的用户提供服务。

所以,长连接也需要在恰当的时间关闭,不能永远保持与服务器的连接,这在客户端或者服务器都可以做到。

在客户端,可以在请求头里加上“Connection: close”字段,告诉服务器:“这次通信后就关闭连接”。服务器看到这个字段,就知道客户端要主动关闭连接,于是在响应报文里也加上这个字段,发送之后就关闭 TCP 连接。此外,还可以通过设置长连接的超时时间、长连接上可发送的最大请求次数等来关闭长连接。

队头阻塞 (Head-of-line blocking)

“队头阻塞” 是由 http 基本的 “请求 - 应答” 模型所导致的。

因为 http 规定报文必须是 “一发一收”,这就形成了一个先进先出的 “串行” 队列。队列里的请求没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求被优先处理。如果队首的请求因为处理的太慢耽误了时间,那么队列后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本。

用打卡机做个比喻。我们公司是指纹打卡,上班的时间点上,大家都在排队打卡,可这个时候偏偏最前面的那个人打卡机无法识别他的指纹,怎么也不能打卡成功,别人又不能插队,这样就会导致后面的人也会迟到。

性能优化

因为 “请求 - 应答” 模型不能变,所以 “队头阻塞” 问题在 http/1.1 里无法解决。“队头阻塞”问题会导致性能下降,可以用 “并发连接” 和 “域名分片” 技术缓解。

  • 并发连接

公司可以多买几台打卡机放到前台,这样大家可以不用挤在一个队伍里,分散打卡,一个队伍偶尔堵塞也不要紧,后面的人可以到别的打卡机打卡。

这在 http 里就是 “并发连接”,也就是对一个域名发起多个长连接,用数量解决质量的问题。

http 协议建议客户端使用并发,但不能 “滥用”并发。RFC 规定客户端最多并发 6-8 个连接。

  • 域名分片

另一个用数量来解决质量的思路是 “域名分片”技术。

http 协议和浏览器不是限制并发连接数量嘛,那我就多开几个域名,比如 nginx1.com、nginx2.com、nginx3.com,而这些域名都指向同一台服务器 nginx.com,这样实际长连接的数量就上去了。

http 缓存 (Cache)

缓存是计算机领域里的一个重要的概念,是优化系统性能的利器。

由于链路漫长,网络时延不可控,浏览器使用 http 获取资源的成本较高。所以,非常有必要把 “来之不易” 的数据缓存起来,下次再请求的时候尽可能的复用,这样就可以避免多次请求应答的通信成本,节约网络带宽,加快响应速度。

实际上,http 传输的每一个环节基本上都会有缓存,非常复杂。

基于“请求 - 应答”模式的特点,可以大致分为客户端缓存和服务器端缓存。

服务器的缓存控制

最初客户端向服务器发起请求的流程如下:

  1. 浏览器发现缓存无数据,于是发送请求,向服务端获取资源;
  2. 服务器响应请求,返回资源,同时标记资源的有效期;
  3. 浏览器缓存资源,等待下次重用。

下图是一个具体的请求 - 应答过程:

  • 资源有限期

服务器标记资源有效期使用的头字段是 “Cache-Control”, 里面的值 “max-age=30” 就是资源的有效时间,相当于告诉浏览器,“这个页面只能缓存30秒,之后就算是过期,过期之后不能再使用”。

  • 生存时间 这里的 max-age 是 “生存时间”,时间的计算起点是响应报文的创建时刻(即 Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,包含了在链路传输过程中所消耗的时间。

  • 缓存控制其他属性 “max-age”是 HTTP 缓存控制最常用的属性,此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存:

  1. no-store: 不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面;
  2. no-cache: 可以缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;
  3. must-revalidate:如果缓存不过期就可以继续使用,但如果过期了如果还想用就必须去服务器验证。

下图是服务器的缓存策略流程图,也就是 “Cache-Control” 的用法:

客户端的缓存控制

其实不止服务器可以发“Cache-Control”头,浏览器也可以发“Cache-Control”,也就是说请求 - 应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。