From 3c44d788684f820ae98b512633b5d5c4194b1c82 Mon Sep 17 00:00:00 2001 From: Omooo <869759698@qq.com> Date: Tue, 4 Jun 2019 22:53:41 +0800 Subject: [PATCH] add HTTP/1.1 --- blogs/computer_network/HTTP:1.1.md | 155 ++++++++++++++++++ .../针对浏览器的优化建议.md | 62 +++++++ 2 files changed, 217 insertions(+) create mode 100644 blogs/computer_network/HTTP:1.1.md create mode 100755 blogs/computer_network/针对浏览器的优化建议.md diff --git a/blogs/computer_network/HTTP:1.1.md b/blogs/computer_network/HTTP:1.1.md new file mode 100644 index 0000000..3a5fff3 --- /dev/null +++ b/blogs/computer_network/HTTP:1.1.md @@ -0,0 +1,155 @@ +--- +HTTP/1.1 +--- + +#### 目录 + +1. 前言 +2. 持久连接 +3. HTTP 管道 +4. 使用多个 TCP 连接 +5. 域名分区 +6. 度量和控制协议开销 +7. 连接和拼合 +8. 嵌入资源 + +#### 前言 + +HTTP/1.1 引入了大量增强性能的重要特性,其中一些大家比较熟知的有: + +- 持久化连接以支持连接复用 +- 分块传输编码以支持流式响应 +- 请求管道以支持并行请求处理 +- 字节服务以支持基于范围的资源请求 +- 改进的更好的缓存机制 + +但是有些 HTTP/1.1 特性,比如请求管道,由于缺乏支持而流产,而其他协议限制,比如队首响应阻塞,则导致了更多问题。为此,Web 开发社区,创造和推行了很多自造的优化手段:域名分区、连接文件、拼合图标、嵌入代码,等等,不下十种。它们都是针对当前 HTTP/1.1 协议的局限性而采用的权宜之计。 + +#### 持久连接 + +HTTP/1.1 的一个主要改进就是引入了持久 HTTP 连接。 + +每个 TCP 连接开始都有三次握手,要经历一次客户端与服务器间完整的往返。此后,会因为 HTTP 请求和响应的两次通信而至少引发另一次往返,最后,还要加上服务器处理时间,才能得到每次请求的总时间。 + +服务器处理时间无法预测,因为这个时间因资源和后端硬件而异。不过,这里的重点其实是由一个新 TCP 连接发送的 HTTP 请求所花的总时间,最少等于两次网络往返的时间:一次用于握手,一次用于请求和响应。这是所有非持久连接 HTTP 会话都要付出的固定时间成本。 + +实际上,这时候最简单的优化就是重用底层连接!添加对 HTTP 持久连接的支持,就可以避免第二次 TCP 连接时的三次握手、消除另一次 TCP 慢启动的往返,节约整整一次网络延迟。 + +HTTP/1.1 默认启用持久连接,如果只能使用 HTTP/1.0,则可以明确使用 Connection: Keep-Alive 首部声明使用持久连接。 + +#### HTTP 管道 + +持久 HTTP 可以让我们重用已有的连接来完成多次应用请求,但多次请求必须严格满足先进先出(FIFO)的队列顺序:发起请求,等待响应完成,再发送客户端队列中的下一个请求。HTTP 管道是一个很小但对上述工作流却非常重要的一次优化,管道可以让我们把 FIFO 队列从客户端(请求队列)迁移到服务器(响应队列)。 + +服务器处理完第一次请求之后,会发生一次完整的往返:先是响应回传,接着是第二次请求。在此期间,服务器空闲。如果服务器能在处理完第一次请求后,立即开始处理第二次请求呢?甚至,如果服务器可以并行或在多线程上或者多个工作进程,同时处理两个请求呢? + +通过尽早分派请求,不被每次响应阻塞,可以再次消除额外的网络往返。这样,就从非持久连接状态下的每个请求两次往返,变成了整个请求队列只需要两次网络往返。 + +但是,HTTP/1.x 也有它的局限性,HTTP/1.x 只能严格串行的返回响应。特别是,HTTP/1.x 不允许一个连接上的多个响应数据交错到达(多路复用),因而一个响应必须完全返回后,下一个响应才会开始传输。 + +比如,服务器并行处理两个请求,一个是 HTML 请求,一个是 CSS 请求。服务器处理 HTML 请求比 CSS 请求耗时,即使 CSS 资源先准备就绪,服务器也会先发送 HTML 响应,然后再交互 CSS,这种情况通常被称为队首阻塞,并经常导致次优化交互:不能充分利用网络连接,造成服务器缓冲开销,最终导致无法预测的客户端延迟。假如第一个请求无限期挂起,或者要花很长时间才能处理完,怎么办呢?在 HTTP/1.1 中,所有后续的请求都将被阻塞,等待它完成。 + +实际中,由于不可能实现多路复用,HTTP 管道会导致 HTTP 服务器、代理和客户端出现很多微妙的、不见文档记载的问题: + +- 一个慢响应就会阻塞所有后续请求 +- 并行处理请求时,服务器必须缓冲管道中的响应,从而占用服务器资源,如果有个响应非常大,则很容易形成服务器的受攻击面 +- 响应失败可能终止 TCP 连接,从而强迫客户端重新发送对所有后续资源的请求,导致重复处理 +- 由于可能存在中间代理,因此检测管道兼容性,确保可靠性很重要 +- 如果中间代理不支持管道,那它可能会中断连接,也可能会把所有请求串行起来 + +由于存在这些以及其他类似的问题,而 HTTP/1.1 标准中也未对此作出说明,HTTP 管道技术的应用非常有限,虽然其优点毋庸置疑。今天,一些支持管道的浏览器,通常都将其作为一个高级配置选项,但大多数浏览器都会禁用它。换句话说,如果浏览器是 Web 应用的主要交互工具,那还是很难指望通过 HTTP 管道来提升性能。 + +#### 使用多个 TCP 连接 + +由于 HTTP/1.x 不支持多路复用,浏览器可以不假思索的在客户端排队所有 HTTP 请求,然后通过一个持久连接,一个接一个的发送这些请求。然鹅,这种方式在实践中太慢。实际上,浏览器开发商没有别的办法,只能允许我们并行打开多个 TCP 会话。多少个?现实中,大多数现代浏览器,包括桌面和移动浏览器,都支持每个主机打开六个连接。 + +进一步讨论之前,有必要先想一想同时打开多个 TCP 连接意味着什么。当然,有正面的也有负面的。下面我们以每个主机打开最多六个独立连接为例: + +- 客户端可以并行分派最多六个请求 +- 服务端可以并行处理最多六个请求 +- 第一次往返可以发送的累积分组数量(TCP cwnd)增长为原来的六倍 + +在没有管道的情况下,最大的请求数与打开的连接数相同。相应的,TCP 拥塞窗口也要乘以打开的连接数量,从而允许客户端绕开由 TCP 慢启动规定的分组限制。这好像是一个方便的解决方案,我们再看看这样做的代价: + +- 更多的套接字会占用客户端、服务器以及代理的资源,包括内存缓冲区和 CPU 时钟周期 +- 并行 TCP 流之间竞争共享的带宽 +- 由于处理多个套接字,实现复杂性更高 +- 即使并行 TCP 流,应用的并行能力也受限制 + +实践中,CPU 和内存占用并非微不足道,由此会导致客户端和服务端的资源占用量上升、运维成本提高。类似的,由于客户端实现的复杂性提高,开发成本也会提高。最后,说到应用的并行性,这种方式提供的好处还是非常有限的。这不是一个长期的方案,了解这些之后,可以说今天之所以使用它,主要有三个原因: + +1. 作为绕过应用协议(HTTP)限制的一个权宜之计 +2. 作为绕过 TCP 中低起始阻塞窗口的一个权宜之计 +3. 作为让客户端绕过不能使用 TCP 窗口缩放的一个权宜之计 + +#### 域名分区 + +根据 HTTP Archive 的统计,目前平均每个页面都包含 90 多个独立的资源,如果这些资源都来自同一个主机,那么仍然会导致明显的排队等待。既然每个主机最多 6 个 TCP 流,那么何必把自己只限制在一个主机上呢?我们不必只通过一个主机(例如 www.example.com)提供所有资源,而是可以手工将所有资源分散到多个子域名:{shard1.shardn}.example.com。由于主机名称不一样了,就可以突破浏览器的连接限制,实现更高的并行能力,域名分区使用的越多,并行能力就越强。 + +当然,天下没有免费的午餐,域名分区也不例外:每个新主机名都要求有一次额外的 DNS 查询,每多一个套接字就会多消耗两端的一些资源,而更糟糕的是,站点作者必须手工分离这些资源,并分别把它们托管到多个主机上。 + +实践中,域名分区经常会被滥用,导致几十个 TCP 流都得不到充分利用,其中很多永远也避免不了 TCP 慢启动,最坏的情况下还会降低性能。此外,如果使用的是 HTTPS,那么由于 TLS 握手导致额外的网络往返,会使上述代价更高。 + +域名分区是一种合理但又不完美的优化手段,所以实践中一定先从最小分区数目开始,然后逐个增加分区并度量分区后对应用的影响。 + +#### 度量和控制协议开销 + +HTTP/1.0 增加了请求和响应首部,以便双方能够交换有关请求和响应的元信息。最终,HTTP/1.1 把这种格式变成了标准:客户端和服务端都可以轻松扩展首部,而且始终以纯文本形式发送,以保证与之前的 HTTP 保持兼容。 + +今天,每个浏览器发起的 HTTP 请求,都会携带额外 500-800 字节的 HTTP 元数据:用户代理字符串、很少改变的接收和传输首部、缓存指令,等等。有时候,500-800 字节都说少了,因为没有包含最大的一块:HTTP Cookie。现代应用经常通过 Cookie 进行会话管理,记录个性选项或者完成分析。综合到一起,所有这些未经压缩的 HTTP 元数据经常会给每个 HTTP 请求增加几千字节的协议开销。 + +#### 连接和拼合 + +最快的请求是不用请求,减少请求次数总是最好的优化手段。可是,如果你无论如何也无法减少请求,那么对 HTTP/1.x 而言,可以考虑把多个资源捆绑打包到一块,通过一次网络请求获取: + +- 连接 + + 把多个 JavaScript 或 CSS 文件组合为一个文件。 + +- 拼合 + + 把多张图片组合为一个更大的复合的图片。 + +对 JavaScript 和 CSS 来说,只要保持一定的顺序,就可以做到把多个文件连接起来而不影响代码的行为和执行。类似的,多张图片可以组合为一个 "图片精灵",然后使用 CSS 选择这张大图中的适当部分,显示在浏览器中。这两种技术都具备两方面的优点: + +- 减少协议开销 + + 通过把文件组合成一个资源,可以消除与文件相关的协议开销。如前所述,每个文件很容易招致 KB 级未压缩数据的开销。 + +- 应用层管道 + + 说到传输的字节,这两种技术的效果都好像是启用了 HTTP 管道:来自多个响应的数据前后相继的连接在一起,消除了额外的网络延迟。实际上,就是把管道提高了一层,置入了应用中。 + +连接和拼合技术都属于以内容为中心的应用层优化,它们通过减少网络往返开销,可以获得明显的性能提升。可是,实现这些技术也要求额外的处理、部署和编码,因而也会给应用带来额外的复杂性。此外,把多个资源打包到一块,也可能给缓存带来负担,影响页面的执行速度。 + +要理解为什么这些技术会伤害性能,可以考虑一种并不少见的情况:一个包含十来个 JavaScript 和 CSS 文件的应用,在产品状态下把所有文件合并为一个 CSS 文件和一个 JavaScript 文件。 + +- 相同类型的资源都位于一个 URL 下面 +- 资源包中可能包含当前页面不需要的内容 +- 对资源包中任何文件的更新,都要求重新下载整个资源包,导致较高的字节开销 +- JavaScript 和 CSS 只有在传输完成后才能被解析和执行,因而会拖慢应用的执行速度 + +实践中,大多数 Web 应用都不是只有一个页面,而是由多个视图构成,每个视图都有自己的资源,同时资源之间还有部分重叠:公用的 CSS、JavaScript 和图片。实际上,把所有资源都组合到一个文件经常会导致处理和加载不必要的字节。虽然可以把它看成一种预获取,但代价则是降低了初始启动的速度。 + +对很多应用来说,更新资源带来的问题更大,更新图片精灵或组合 JavaScript 文件中的某一处,可能就会导致重新传输几百 KB 数据。由于牺牲了模块化和缓存粒度,假如打包资源变动频率过高,特别是在资源包过大的情况下,很快就会得不偿失。 + +内存占用也会成为问题,对图片精灵来说,浏览器必须分析整个图片,即便实际上只显示其中的一小块,也要始终把整个图片都保存在内存中。浏览器是不会把不显示的部分从内存中剔除掉的! + +最后,为什么执行速度还会受影响呢?我们知道,浏览器是以递增方式处理 HTML 的,而对于 JavaScript 和 CSS 的解析及执行,则要等到整个文件下载完毕。JavaScript 和 CSS 处理器都不允许递增式执行。 + +总之,连接和拼合是在 HTTP/1.x 协议限制的现实下可行的应用层优化。使用得当的话,这两种技术都可以带来明显的性能提升,代价则是增加应用的复杂度,以及导致缓存、更新、执行速度、甚至渲染页面的问题。应用这两种优化时,要注意度量结果。 + +#### 嵌入资源 + +嵌入资源是另一种非常流行的优化方法,把资源嵌入文档可以减少请求的次数。比如,JavaScrpt 和 CSS 代码,通过适当的 script 和 style 块可以直接放在页面中,而图片甚至音频或 PDF 文件,都可以通过数据 URI 的方式嵌入页面中。 + +数据 URI 适合特别小的,理想情况下,最好是只用一次的资源。以嵌入方式放到页面中的资源,应该算是页面的一部分,不能被浏览器、CDN 或其他缓存代理作为单独的资源缓存。换句话说,如果在多个页面中都嵌入同样的资源,那么这个资源将会随着每个页面的加载而被加载,从而增大每个页面的总体大小。另外,如果嵌入资源被更新,那么所有以前出现过它的页面都将被宣告无效,而由客户端重新从服务器获取。 + +最后,虽然 CSS 和 JavaScript 等基于文本的资源很容易直接嵌入页面,也不会带来多余的开销,但非文本性资源则必须通过 base64 编码,而这会导致开销明显增大,编码后的资源大小比原大小增大 33%。 + +实践中,常见的一个经验规则是只考虑嵌入 1~2 KB 以下的资源,因为高于这个标准的资源经常会导致比它自身更高的 HTTP 开销。然而,如果嵌入的资源频繁更新,又会导致宿主文档的无效缓存率升高。如果你的应用要使用很小的、个别的文件,在考虑是否嵌入时,可以参照以下建议: + +1. 如果文件很小,而且只有个别页面使用,可以考虑嵌入 +2. 如果文件很小,但需要在多个页面中重用,应该考虑集中打包 +3. 如果小文件经常更新,就不要嵌入了 +4. 通过减少 HTTP cookie 的大小将协议开销最小化 \ No newline at end of file diff --git a/blogs/computer_network/针对浏览器的优化建议.md b/blogs/computer_network/针对浏览器的优化建议.md new file mode 100755 index 0000000..30a4f10 --- /dev/null +++ b/blogs/computer_network/针对浏览器的优化建议.md @@ -0,0 +1,62 @@ +--- +针对浏览器的优化建议 +--- + +#### 前言 + +浏览器可远远不止一个网络套接字管理器那么简单,性能可以说是每个浏览器开发商的核心卖点,既然性能如此重要,那浏览器越来越聪明也就毫不奇怪了。预解析可能的 DNS 查询、预连接可能的目标、预取得和优先取得重要资源,这些都是浏览器变聪明的标志。 + +#### 优化建议 + +可行的优化手段会因浏览器而异,但从核心优化策略来说,可以宽泛的分为两类: + +* 基于文档的优化 + + 熟悉网络协议,了解文档、CSS 和 JavaScript 解析管道,发现和优先安排关键网络资源,尽早分配请求并取得页面,使其尽快达到可交互的状态。主要方法是优先获取资源、提前解析等。 + +* 推测性优化 + + 浏览器可以学习用户的导航模式,执行推测性优化,尝试预测用户的下一次操作,然后,预先解析 DNS、预先连接可能的目标。 + +好消息是,所有的这些优化都是由浏览器替我们自动完成的,经常可以节省几百 ms 的网络延迟。既然如此,那理解这些优化背后的原理就至关重要了,这样才能利用浏览器的这些特性,提升应用性能。大多数浏览器都利用了如下四种技术: + +1. 资源预取和排定优先次序 + + 文档、CSS 和 JavaScript 解析器可以与网络协议层沟通,声明各种资源的优先级;初始渲染必需的阻塞资源具有最高优先级,而低优先级的请求可能会被临时保存在队列中。 + +2. DNS 预解析 + + 对可能的域名进行提前解析,避免将来 HTTP 请求时的 DNS 延迟。预解析可以通过学习导航历史、用户的鼠标悬停,或其他页面信号来触发。 + +3. TCP 预连接 + + DNS 解析之后,浏览器可以根据预测的 HTTP 请求,推测性的打开 TCP 连接。如果猜对的话,则可以节省一次完整的往返(TCP 握手)时间。 + +4. 页面预渲染 + + 某些浏览器可以让我们提升下一个可能的目标,从而在隐藏的标签页中预先渲染整个页面。这样,当用户真的触发导航时,就能立即切换过来。 + +从外部看,现代浏览器的网络协议实现以简单的资源获取机制的面目示人,而从内部来说,它又极为复杂精密,为了解如何优化性能,非常值得深入钻研。那么,在探寻的过程中,我们怎么利用浏览器的这些机制呢?首先,要密切关注每个页面的结构和交互: + +* CSS 和 JavaScript 等重要资源应该尽早在文档中出现 +* 应该尽早交互 CSS,从而解除渲染阻塞并让 JavaScript 执行 +* 非关键性 JavaScript 应该推迟,以避免阻塞 DOM 和 CSSOM 构建 +* HTML 文档由解析器递增解析,从而保证文档可以间歇性发送,以求得最佳性能 + +除了优化页面结构,还可以在文档中嵌入提示,以触发浏览器为我们采用其他优化机制: + +```html + + + + +``` + +从上到下,依次是: + +1. 预解析特定的域名 +2. 预取得页面后面要用到的关键性资源 +3. 预取得将来导航要用的资源 +4. 根据对用户下一个目标的预测,预渲染特定页面 + +这里的每一个提示都会触发一个推测性优化机制。浏览器虽然不能保证落实,但可以利用这些提示优化加载策略。可惜的是,并非所有浏览器都支持这些提示,不过,如果它们不支持,也只会把提示当做空操作,有益无害。因此,一定要尽早可能利用这些字段。 \ No newline at end of file