Life Will Change Lyn
  1. 1 Life Will Change Lyn
  2. 2 Libertus Chen-U
  3. 3 Quiet Storm Lyn
  4. 4 Warcry mpi
  5. 5 Flower Of Life 发热巫女
  6. 6 Hypocrite Nush
  7. 7 One Last You Jen Bird
  8. 8 Last Surprise Lyn
  9. 9 The Night We Stood Lyn
  10. 10 かかってこいよ NakamuraEmi
  11. 11 Time Bomb Veela
2018-10-07 21:36:26

浏览器输入URL经历流程全览

这是一道很经典的面试题,可以说考察了前端知识的各个方面,其中每个点都有很多可以展开的地方。网络上的解析也不少,但大多数也都是不那么全面,干脆自己总结一版也顺便回顾一下相关知识。当然说是全览也有些标题党了,本意是尽可能的在前端知识领域涉猎范围内记录完整。

URL输入

首先是输入URL,在输入URL的过程中,特定浏览器可能会有一些性能优化手段,比如谷歌浏览器会根据首字母查询历史记录,提前建立TCP连接。

URL输入完成后,浏览器首先会对URL进行检查,如果URL不合法,浏览器会将这段输入传递给默认搜索引擎并附加一段特定字符告诉搜索引擎这次搜索的浏览器来源。如果URL合法则进入下一步。

预加载HSTS列表检查

谈到这个列表的作用之前,必须先介绍一下HSTS。HSTS是一种安全策略机制,全称是HTTP Strict-Transport-Security。这个机制是干什么用的呢?

首先要明白,当我们访问某个https网站时,通常是直接输入域名,不过浏览器依然正确使用https发起请求,这是因为首次请求仍然是http,在服务器进行了一次对用户完全透明的重定向。问题就来了,https虽然是安全的,但重定向这一步,仍然给了攻击者以中间人方式劫持的机会。那么解决这一问题的思路自然是如何避免第一步的重定向了,这就是HSTS的目的。

其核心机制为设置一个HTTP Response Header,告知浏览器接下来的某段时间内只能通过https访问。语法如下:

Strict-Transport-Security: <max-age=>[; includeSubDomains][; preload]

max-age设置过期时间,includeSubDomains设置子域名也开启该保护,preload就和预加载列表有关了。

这个预加载列表有什么用呢?其实仔细想想HSTS依然有可以被攻击的地方,那就是浏览器要得到这个响应头,你必然还是要进行一次HTTP请求。。HSTS的应对方法就是,干脆任何请求发出之前就在浏览器里完成HSTS的设置,就是在浏览器里内置一个预加载HSTS列表,只要在这个列表里的域名都只能使用https发起连接,至此就完全规避了被劫持的问题。

该列表检查完后,就会决定接下来是发起http还是https请求。

DNS解析

因为域名长度不固定,机器处理起来比较困难,所以需要域名系统DNS将域名转化为IP地址。理论上来说整个互联网只需要一台域名服务器,但肯定会由于过负荷无法正常工作,所以DNS被设计为层次树状的分布式结构,按层次可以将域名服务器从上到下划分为四类:

  • 根域名服务器
  • 顶级域名服务器
  • 权限域名服务器
  • 本地域名服务器

DNS解析流程如下:

  1. 检查域名是否在浏览器缓存中
  2. 不存在缓存的话检查域名是否在本地Hosts文件里
  3. 主机向本地DNS服务器发送DNS查询请求,查询方式为递归查询,即本地DNS服务器不知道被查询IP地址的话,那么该服务器则以用户身份向根服务器发出查询请求
  4. 本地域名服务器向根域名服务器查询为迭代查询,即根域名服务器收到请求后要么给出IP地址,要么给出下一步查询的顶级域名服务器IP地址,本地DNS服务器再继续查询。

整个DNS查询手段是基于UDP协议的,至此就获得了目标域名的IP地址,有了IP地址就可以通过Socket发送数据了。

Socket

当浏览器拿到目标IP以及端口后,它会调用系统的库函数socket请求一个TCP流的套接字,并经过如下几层的封装。

  • 首先被交给传输层,在该层,本地端口,目标端口窗口长度等信息会被加入到其中封装为TCP segment。
  • TCP segment被交给网络层,在该层,目标服务器IP地址以及本机IP地址会被加入其中,封装为TCP packet。
  • TCP packet被交给链路层,在该层,封包中会加入frame头部,包含本地内置网卡的MAC地址以及网关的MAC地址。

那么问题来了,之前通过DNS查询才能获取目标服务器的IP地址,网关的MAC地址如何获取呢?这就要依靠和IP协议配套使用的协议之一——地址解析协议ARP了。

ARP

每台主机都设置有一个ARP高速缓存,里面有本局域网上的各主机和路由器的IP地址到MAC地址的映射表。当主机要向局域网上的某台主机发送IP数据报时,就现在该缓存中查询有无该IP地址,有的话就取出其MAC地址封装到frame头部。如果没有找到的话,主机就自动运行ARP,按如下步骤查询出目标主机的MAC地址。

  1. ARP进程在本局域网上广播发送一个ARP请求分组
  2. 局域网上的所有主机上运行的ARP进程都收到该分组
  3. 收到该分组的主机IP地址与ARP请求查询的IP地址一致,那么该主机就收下这个请求并向源主机发送响应分组,并在该响应分组中写入自己的MAC地址。
  4. 源主机收到响应分组后,将其写入高速缓存。

至此就获取了目标MAC地址,需要注意的是如果目标IP和源主机不在同一局域网上的话,需要通过同一局域网上的路由器转发,但原理还是一样的,只是使用几次ARP的问题而已。

分组转发

现在已经有了封装完好的TCP数据包,接下来封包将就会从本地网关出发,经过调制解调器把数字信号转化为模拟信号使其适用于在各种物理线路上传播,在传输线路的另一端是另一个调制解调器,将该包转化为数字信号交由当前网络节点处理。

每个网络节点之间跳转的过程使用的是路由分组转发算法,概要如下:

  1. 从数据包中提取出目标主机IP,得出目的网络地址
  2. 若目的网络地址就是与此路由器相连的网络地址,则直接交付。否则进行下一步。
  3. 若路由表有目的地址为目标主机IP的特定路由,则把数据包传送给路由表所指明的下一跳路由。
  4. 若路由表有到达目的网络的路由,则把数据包传送给路由表所指明的下一跳路由。
  5. 若路由表有默认路由,则把数据包传送给路由表所指明的默认路由。
  6. 报告转发出错。

按照以上分组转发算法,请求最终就能到达目标服务器。

负载均衡

当然目标IP对应的目标服务器可能并不是真正的应用服务器,实际上有可能是一台负载均衡的机器。作用就是为了缓解单机服务器的压力,将请求合理的分布在多台部署相同应用的服务器上。负载均衡的实现方法也有很多,比较常见的分为如下两类:

IP负载均衡

其工作在TCP/IP协议栈上,原理在于前端服务器对数据包中的IP地址和端口进行修改,从而转发到后端合适的服务器上。

反向代理负载均衡

其工作在HTTP协议栈上,通常借助nginx,haproxy,apache等实现,由于其工作在更上层,所以相比IP负载均衡更做到更智能的功能。

建立连接

至此我们的第一个请求终于到达了目标服务器。但需要知道TCP是面向连接的协议,在真正开始发送数据之前,客户端和服务端必须建立起一个TCP连接,也就是有名的三次握手了。

TCP三次握手

  1. TCP首部同步位SYN置为1,同时选择一个初始序号seq=x。虽然SYN报文段不携带数据,但是要消耗一个序号。同时TCP客户端进程进入SYN-SENT状态。
  2. 服务器收到请求报文段后,向A发送确认报文段,确认报文段把SYN位和ACK位都置为1,确认号ack = x + 1,同时也为自己选择一个初始序号seq = y,同样不携带数据,消耗一个序号,TCP服务端进程进入SYN-RCVD状态。
  3. 客户端收到服务端的确认后,再次给服务端发送确认。ACK位置为1,确认号ack = y + 1,而自己的序号为x + 1。这个确认报文段就可以携带真正的数据了。A进入ESTABLISHED状态。
  4. 服务端收到客户端确认后同样进入ESTABLISHED状态。

如果使用协议是基于https的话,双方还需要进行一次TLS四次握手确保安全连接。

TLS四次握手

  1. 客户端首先向服务端发送Server Hello报文,消息中包含了客户端的TLS版本,以及可用的加密与压缩算法列表。
  2. 服务端向客户端返回一个Server Hello报文,包含了服务端的TLS版本,以及服务端最终选择使用的加密和压缩算法,并同时发送Certificate报文,包含服务器的公开证书,证书中包含了公钥,客户端会根据这个公钥非对称加密接下来的对称加密会话的密钥。
  3. 客户端根据自己信任的CA列表,验证服务器端发送的证书是否有效,如果有效,客户端会生成一串伪随机数使用之前获得的公钥加密,这就是预主秘钥,客户端会通过Client Key Exchange报文将其发送到服务端。
  4. 服务器端使用自己的私钥解密收到的预主秘钥,并用其生成自己的对称主秘钥。
  5. 客户端向服务器发送Change Cipher Spec报文,表示自己的对称主密钥也已生成,接下来的消息就可以用对称加密的形式传输了。
  6. 客户端向服务器发送Finished报文,并且经过对称加密,如果服务器能通过对称主密钥解密,则向客户端发送Finished报文。
  7. 至此完成TLS握手。

内容传输

连接建立后就可以在HTTP上愉快的传输真正的数据了。这部分当然是不会将原始内容直接放在实体主体中发送,通常会经过一些编码处理保证性能。

内容编码

这部分编码作用于内容上,常规使用上是使用gzip压缩,再让客户端解码减少传输尺寸。步骤如下:

  1. 服务器端生成响应报文,其中有原始的Content-Type和Content-Length。
  2. 内容编码服务器创建编码后报文,有同样的Content-Type但Content-Length会减少,并在报文中Content-Encoding首部,供客户端精确解码。
  3. 客户端接收到编码报文进行解码获取原始报文。

分块编码

前面提到的内容编码是对报文实体部分进行编码,而分块编码的目的是改变整个报文的结构以便更好的传输。主要是为了解决大文件和未知长度文件传输的问题,使用该种编码方式需要设置Transfer-Encoding响应首部为chunk,而不需要设置Content-Length,服务器可以不断向客户端发送动态产生的数据,在传输每个chunk之前,服务器会告诉客户端当前块大小,当传输长度为0的 chunk时就表明传输结束。

优点是比较明显的,一是可以解决大文件计算Content-Length值时导致内存不足的问题,二是让浏览器可以尽快开始渲染,缩短首屏时间。

断开连接

当完成一次HTTP请求后,服务器由于Connection首部的keep-alive默认开启,并不会立刻断开TCP连接,以便减少重新连接和慢启动的开销。服务端和客户端各自有一套断开连接的机制,服务端通常是检测一段时间内是否有新请求,客户端则是发送TCP探测包检测连接状况,如果达到断开连接的条件,则通过四次挥手断开TCP连接。

四次挥手

数据传输结束后,双方都可以主动断开连接,就以客户端为主动说明。

  1. 客户端主动关闭TCP连接,并将连接释放报文段首部终止控制位FIN置为1,序号seq = u,u为传送过的数据最后一个字节的序号加1,并进入FIN-WAIT-1状态等待服务端确认。
  2. 服务端收到报文后发出确认,确认号ack = u + 1,这个报文自己的序号是v,等于前面已传送过数据的最后一个字节序号加1,然后进入CLOSE-WAIT状态,至此客户端到服务端这个方向的连接就释放了,但服务端如果要发送数据,客户端仍然要接受,此时客户端进入FIN-WAIT-2状态,等待服务端的释放报文。
  3. 若服务端没有数据发送了,服务端则发出FIN=1的释放报文,同时带上确认号ack = u + 1,并假设此时服务端的序号seq = w,服务端进入LAST-ACK阶段,等待客户端的确认。
  4. 客户端收到释放报文后将ACK置为1,确认号ack = w + 1,自己的序号是 u + 1,进入到TIME-WAIT状态。
  5. 服务端收到确认后进入CLOSE状态,经过时间等待计时器设置的时间2MSL后,A进入到CLOSE状态。

至此网络请求部分就结束了,目前浏览器已经获取到了服务器返回的数据,接下来就进入浏览器解析和渲染内容的部分。

HTML解析

在谈到HTML如何解析之前,有必要先谈谈浏览器的架构——多线程多进程架构。大致可分为如下4类进程:

  • 浏览器进程:这个进程用来管理浏览器应用的各个部分,比如书签,地址栏,以及各种网络请求,文件管理等等。
  • 渲染器进程:每个选项卡都有自己独立的渲染进程,用来管理任何在选项卡中显示的内容。
  • GPU进程:独立于其他进程处理GPU任务。
  • 插件进程:多个插件进程用来管理站点内使用的插件内容。

更多细节的东西就不谈,现在要往下继续分析,需要重点关注浏览器进程和渲染器进程。

浏览器进程内部,又有UI线程,网络线程,存储线程等。当浏览器读取到服务器响应时,网络线程就会在必要时查看数据流的前几个字节,根据Content-Type字段了解到这是什么类型的数据,然后根据其类型决定下一步如何处理。

如果是zip文件或是其他文件,就表示这是一个下载请求,网络线程就会将其传递给下载管理器,如果是HTML类型的文件,就会将数据传递给渲染器进程,开始页面的解析工作了。

构建DOM

浏览器创建了自定义解析器来解析HTML,该解析器算法由如下两个阶段构成:

  • 标记化:这个阶段作用于词法分析,将输入内容解析为多个标记(起始标记,结束标记,属性名称和属性值),然后传递给树构造器。
  • 树构建:在创建自定义解析器的同时,也会创建Document对象,在该阶段以Document对象为根节点,结合传递过来的标记构建DOM树。这个构建阶段有几个关键生命周期如下图。

构建CSSOM

和HTML不同,CSS是上下文无关的语法,可以使用不同的解析器进行解析,Webkit使用Flex创建词法分析器,Bison创建解析器,将CSS文件解析成CSSOM树

特殊标签处理

由于JS文件和CSS文件在解析过程会影响到树构建,所以针对这两类标签,浏览器有特殊处理方式。

  • JS:由于JS可能会影响到DOM树结构,当遇到script标签时,会阻塞DOM树构建同时异步获取JS,但主线程不挂起,此时浏览器会使用轻量扫描器发现后续下载资源,JS下载完毕后,V8引擎编译JS并在主线程中执行。当然HTML5也可以使用defer或async属性自定义JS的加载时机防止阻塞。
  • CSS:CSS只会影响到CSSOM构建,但不影响DOM树结构,所以不会阻塞DOM树构建,但需要注意后续DOM树和CSSOM树结合构建Render树的部分会被阻塞。

DOM树解析结束后,浏览器就开始加载外部资源(CSS,图像等),浏览器就会将文档标注为可交互状态,并开始解析处于deferred模式的script,最终触发onload事件表示资源加载完成。

渲染

现在有了DOM树和CSSOM树,浏览器就可以结合两棵树渲染具体内容了。

渲染树构建

实际上,在DOM树和CSSOM树构建完成之前,渲染树就已经开始生成了,其是通过DOM树上的名为attachment的操作完成时,当Document被解析并添加DOM节点时,节点上的attach就会被调用,生成名为RenderObject的对象。

attach过程中,DOM查询CSS获取样式信息,并将信息存储在RenderStyle对象中,该对象可以通过RenderObject的style方法获取,DOM所对应的RenderObject就组成了Render Tree。

布局

现在根据渲染树我们已经知道了文档每个节点的样式和结构,但并不足以绘制出内容,因为每个节点还缺少postion和size信息,决定这些信息的过程就是布局(layout)了,所有的RenderObject都有一个layout方法。

布局是一个递归的过程,从根RenderObject开始递归调用所有RenderObject的layout方法,计算各自的布局信息。

为了避免所有更改都触发整体布局的更改,浏览器采用了名为"Dirty位系统"标记方式,如果某个RenderObject发生更改需要重新布局,就将其自身及子代标记为dirty,在下次布局时只通过增量方式对dirty标记的RenderObject重新布局。

出于性能方面的考量,增量布局的方式往往是异步进行的,但如果有影响所有RenderObject的样式进行了更改,例如字体,屏幕大小等,会同步更新全局的布局。

绘制

现在我们有了每个RenderObject的足够的信息,是否就能开始呈现页面了呢?还有RenderObject之间的相互关系没有考虑,比如两个元素相交时,谁上谁下呢?这一步就是绘制阶段考虑的事了。

同样的以递归的形式遍历RenderObject调用其paint方法,将处在同一z轴坐标系的RenderObject合并为一个RenderLayer,表示页面中哪些部分可以被成组绘制,可以想象为PS中的图层,这样就能做到元素间重叠信息的良好呈现了。

合成

好了,现在终于各种上下左右位置问题,元素样式问题,尺寸问题都解决了,是否终于可以开始呈现页面了呢?不还不行,不要忘了现在浏览器里经常有动画, canvas等等频繁变动的东西,如果根据层绘制,意味着每次变动都会重绘整个位图,性能开销肯定是不能接受了。

为了解决这个问题,浏览器又在RenderLayer 之上引入了GraphicsLayer和Graphics Context,某些具有动画的元素,transform属性的元素等等,他们会被单独提升为GraphicsLayer(也就是Composite Layer),其他元素则往上查询找到最近的那个GraphicsLayer并与其合并。每个GraphicsLayer都有一个Graphics Context ,会为自己的Layer开辟一段位图,GraphicsLayer就负责将自己的Render Layer绘制到位图里,这一步称为栅格化。最终将栅格化的分块发送到GPU完成最终的渲染。

合成这一步有专用的合成器线程,不涉及主线程的占用,这就是为什么CSS动画性能优秀的原因了,因为根本就不会重绘,只需要重新合成即可。

小结

至此整个流程就介绍完了,可以看到确实涉及到了不少知识点,但其实每个知识点由于篇幅所限和个人水平原因也没有更深入的写下去,每个点也都有很多深挖的地方,总之这篇文章也可以看做是一个索引,大概构建一个全局观,在合适的时机就可以针对其中的某点再做深入研究。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「JavaScript」「CSS」「HTML」「HTTP」 标签。