JavaScript 工作原理之十二-网络层探秘及如何提高其性能和安全性
原文请查阅这里,略有改动,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland。
本系列持续更新中,Github 地址请查阅这里。
这是 JavaScript 工作原理的第十二章。
正如在之前关于渲染引擎的文章中所讲的那样,我们相信好的和伟大的 JavaScript 开发者之间的差别在于后者不仅仅只是理解了语言的具体细节还了解其内部构造和运行环境。
网络简史
49 年前,ARPAnet 诞生了。它是早期的报文分组交换网络及第一个实现 TCP/IP 协议套件的网络。该网络连通了加利福亚大堂和斯坦福研究所。20 年后,Tim Berners-Lee 分发了一个后来为人所熟知的万维网的 『Mesh』草案。在 49 年的时间里,网络走过了一段漫长的旅程,从仅仅只是是两台电脑间交换数据报文到至少 7500 万台服务器,38 亿人使用互联网以及 13 亿个网站。
本文将试着分析现代浏览器使用哪些技术来自动提升应用性能(有些你甚至不了解),然后着重介绍浏览器网络层。最后,提供一些让浏览器提升网络应用程序性能的技巧。
概述
现代浏览器专门为快速,高效和安全数据传输的网络应用/网站而设计开发的。拥有数以百计的组件运行于各个不同的层级,从进程管理和安全沙箱到 GPU 管线,音频和视频及其它更多等等,网络浏览器更类似于一个操作系统而不仅仅只是一个软件。
浏览器的整体性能是由一些大型的组件所决定的,这些组件包括:解析,布局,样式计算,JavaScript 和 WebAssembly 执行,渲染,当然还有网络堆栈。
一般情况下,工程师们会把网络堆栈看成是一个性能瓶颈。经常会发生这样的情况因为从网络抓取所有的资源会堵塞渲染剩下的步骤。为了更加高效的网络层,它需要不仅仅只是扮演套接字管理员的角色。在我们看来获取资源是一个非常简单的机制,但是实际上它集成自身的优化准则,接口和服务的一整套平台。
网页开发者不需要担心单独的 TCP 或者 UDP 数据包,请求格式化,缓存以及其它正在发生的一切。浏览器会处理这些复杂的玩意,这样就可以专注开发自己的程序。但是,知道其内部的原理可以帮助开发者开发出更加高效和安全的程序。
本质上,当用户开始和浏览器发生交互所产生的情况如下:
- 用户在浏览器地址栏中输入 URL 地址。
- 在网络上查找指定 URL 的资源,浏览器开始检查本地和应用程序缓存并试着使用本地副本来响应资源的请求。
- 当缓存不可用,浏览器使用 URL 中的域名然后根据域名从 DNS 处获取服务器的 IP 地址。如果有域名缓存,将不需要进行 DNS 查询。
- 浏览器创建一个 HTTP 报文表明其请求远程服务器的某个网页。
- 报文被传输到 TCP 层,该层会在 HTTP 报文头部添加其自身的信息。该信息是保持创建的会话的必要信息。
- 然后在 IP 层处理报文,该层的主要职责即找出从用户发送报文到远程服务器的路径。在 HTTP 报文头部添加该路径信息。
- 传输报文到远程服务器。
- 一旦接收到报文,以类似的方式返回响应数据。
W3C Navigation Timing specification 提供了浏览器接口及浏览器中每个请求背后的可视化计时和性能数据。让我们浏览下这些组件,因为每个组件在获取最佳用户体验方面扮演了重要的角色。
整个网络请求过程是相当复杂的并且有许多的层次结构,每一层都有可能成为性能瓶颈。这就是为什么浏览器使用各种技术努力提升其性能,以便把整个网络通信的性能损耗降至最低。
套接字管理
看些新技术吧:
- 源-由应用程序协议,域名和端口号的三个部分组成(比如 https, www.example.com, 443)
- 套接字池-属于同源的一组套接字(所有的主流浏览器都限制套接字池最多只能有 6 个套接字)
JavaScript 和 WebAssembly 禁止开发者操作单独的网络套接字的生命周期,这样是相当的明智的。这样不仅仅可以让你头发少掉点而且可以让浏览器自动优化大量的性能,这些性能包括套接字重用,请求优化和延迟绑定,协议协商,强制连接限制及其它的优化措施。
实际上,现代浏览器更一步地将请求管理周期从套接字管理中剥离了出来。用套接字池来组织套接字,以源来分组套接字,每个套接字池强制限制其连接数和安全约束。排队,优先化等待的请求,然后和套接字池中的单个套接字绑定。如果不是服务器主动关闭这些连接,多个请求可以自动重用相同的套接字。
由于创建一个新的 TCP 连接会带来额外的性能开销,重用连接会为其自带来极大的性能提升。默认情况下,当发起请求的时候,浏览器使用所谓的 『keepalive』机制以节省创建到服务器的新连接所耗费的时间。创建一个新的 TCP 连接的平均时间为:
- 本地请求-23 毫秒
- Transcontinental 请求-120 毫秒
- Intercontinental 请求-225 毫秒
这样的架构衍生出了一些其它的优化方法。请求可以依据优先级来以不同的顺序执行。浏览器可以优化所有套接字间的带宽分配或者它可以创建套接字以等待预期的请求。
如上所述,这些都是浏览器所控制而不用开发者进行干预。但这并不意味着我们无所事事了。选择正确的数据传输所用的网络通信模式,类型和频率,正确的协议类型以及正确的服务器堆栈隧道/优化对于提升整个程序的性能有着至关重要的作用。
一些浏览器甚至更进一步。例如,当你使用 Chrome 的时候,当用户使用的时候它会进行自我学习从而变得更加快速。它基于访问过的网页和典型的浏览器模式来进行学习,这样就可以预期可能的用户行为且在用户进行任意操作之前进行操作。最简单的例子即当用户悬停在某个链接上的时候预渲染页面。如果你想学习更多关于 Chrome 优化技术的文章,可以查看 High-Performance Browser Networking 这本书的 https://www.igvita.com/posa/h... 章节。
网络安全和沙箱
允许浏览器操作单独的套接字有另一个非常重要的目的即:浏览器就可以针对不被信任的程序资源强制实施一套一致的安全和政策约束措施。例如,浏览器禁止通过 API 直接访问原始网络套接字,因为这样会导致任意可疑的程序随意连接任意主机。浏览器也强制连接数限制以保护服务器免受由于客户端访问而耗尽其资源。
浏览器格式化所有流出的请求以强制格式正确和一致的协议语义来保护服务器。同样地,浏览器会自动解码响应内容以保护用户免受可疑服务器的攻击。
TSL 协商
Transport Layer Security (TLS) 是一个为计算机网络提供通信安全的加密协议。它广泛应用于大量应用程序,其中之一即浏览网页。网站可以使用 TLS 来保证服务器和网页浏览器之间的所有通信安全。
整个 TLS 握手过程包含以下几个步骤:
- 客户端向服务器发送 『Client hello』 信息,附带着客户端随机值和支持的密码组合。
- 服务器返回给客户端 『Server hello』信息,附带着服务器随机值。
- 服务器返回给客户端认证证书及或许要求客户端返回一个类似的证书。服务器返回『Server hello done』信息。
- 如果服务器要求客户端发送一个证书,客户端进行发送。
- 客户端创建一个随机的 Pre-Master 密钥然后使用服务器证书中的公钥来进行加密,向服务器发送加密过的 Pre-Master 密钥。
- 服务器收到 Pre-Master 密钥。服务器和客户端各自生成基于 Pre-Master 密钥的主密钥和会话密钥。
- 客户端给服务器发送一个 『Change cipher spec』的通知,表明客户端将会开始使用新的会话密钥来哈希和加密消息。客户端也发送了一个 『Client finished』的消息。
- 服务器接收到『Change cipher spec』的通知然后使用会话钥匙来切换其记录层安全状态为对称加密状态。服务器返回客户端一个 『Server finished』消息。
- 客户端和服务器现在可以通过建立的安全通道来交换程序数据。所有客户端和服务器之间发送的信息都会使用会话密钥进行加密。
每当发生任何验证失败的时候,用户会收到警告。比如服务器使用自签名的证书。
同源策略
当两个页面的协议,端口(如果有指定)以及主机名都是一样的则称为同源。
以下为一些可能包含跨域的资源示例:
<script src=”…”></script>
里面的 JavaScript 代码。语法错误的错误信息仅适用于同源脚本。<link rel=”stylesheet” href=”…”>
的 CSS。由于 CSS 的松散语法规则,跨域 CSS 要求正确的 Content-Type 头。各个浏览器的限制不同。<img>
图片<video>
和<audio>
媒体文件<object>
,<embed>
和<applet>
插件- @font-face 字体。一些浏览器允许跨域字体,其它则要求同源字体。
- <frame> 和 <iframe> 相关的一切内容。网站可以使用 X-Frame-Options 头来防止此种跨域交互。
以上的列表还远远不够;该列表旨在强调工作中的『最小特权』原则。浏览器只为应用程序代码暴露出其所必需的接口和资源:应用提供数据和 URL 地址,然后浏览器格式化请求及处理每条连接的整个生命周期。
需要注意的是并没有一个简单的 『同源策略』概念。
相反,有一系列相关的机制来强制限制浏览器的 DOM 访问,cookie 和 会话状态管理,网络连接和其它组件。
资源和客户端状态缓存
最好和最快的请求即不创建请求。在分派一个请求前,浏览器自动检查其资源缓存,进行必要的验证检查然后当指匹配指定的条件时返回一份本地资源拷贝。如果缓存中没有可用的本地资源,则发起网络请求然后把响应内容自动放置于缓存中以备之后的访问(如果这是被允许的)。
- 浏览器自动为每个资源求值缓存指令。
- 当条件允许时,浏览器自动重新恢复过期资源
- 浏览器自动处理缓存和资源回收的大小
管理一个高效和优化的资源缓存是非常困难的。谢天谢地,浏览器为我们处理了整个复杂的玩意,而我们所需要做的即保证服务器返回恰当的缓存指令;想了解更多可以看 客户端资源缓存 文章。你为网页上的所有资源添加 Cache-Control,ETag,和 Last-Modified 的响应头信息。
最后,一个经常被忽略但至关重要的浏览器功能即其提供了验证,会话和 cookie 管理。浏览器为每个源维护单独的『cookie jars』,通过提供必要的程序和服务器接口来读写新的 cookie,会话和认证数据,以及自动挂载和处理适当的 HTTP 头来为我们自动处理整个过程。
例子:
一个简单但明了的用来展示浏览器的延迟会话状态管理的方便性的例子即:多个选项卡或者浏览器窗口可以共享一个认证会话,反之亦然;一个选项卡中的登出操作可以使所有其它打开窗口的会话失效。
应用程序接口和协议
了解了网络服务之后,最终要讲到应用程序接口和协议。众所周知,更底层的结构提供了一组广泛的重要服务:套接字和连接管理,请求和响应处理,各种安全策略,缓存及其它更多的强制措施。每当初始化一个 HTTP 请求或者 XMLHttpRequest,一个持久的服务推事件或者 WebSocket 会话抑或打开一个 WebRTC 连接,我们就是在和部分或者所有这些底层服务进行交互。
没有单一的最佳协议或者接口。每个复杂的程序都会基于不同的要求混合使用不同的传输协议:和浏览器缓存的交互,协议开销,消息延迟,可靠性,数据传输类型以及其它。一些协议拥有低数据传输延迟的特性(比如服务器推事件,WebSocket),但是可能不符合其它重要的场合,比如利用浏览器缓存或者支持任意情况下的二进制数据传输的能力。
一些可以用来提升程序性能和安全的小技巧
- 一直在请求中使用 『Connection: Keep-Alive』头信息。浏览器默认在请求头中添加 『Connection: Keep-Alive』。保证服务器也使用同样的机制。
- 使用合适的 Cache-Control,Etag 和 Last-Modified 头信息以便节省浏览器的下载时间。
- 花些时间调整和优化服务器。这是奥秘所在!注意这一过程是否针对每个程序和所传输的数据。
- 一直使用 TLS!特别是如果程序中包含有任意类型的认证。
- 研究浏览器所提供的安全策略并且在程序中强制实施。
扩展
本系列持续更新中,Github 地址请查阅这里。
相关推荐
写过小程序的应该知道,微信的request不封装基本上不能用,写的显的太冗长,而且是回调式的,回调地狱什么的就不说了,可读性差。下面是我的封装代码,顺便支持一下promise。加了登录锁后的代码如下: