正如标题所说,这道经典面试题就是“从你在搜索框输入网址敲下回车,到你看到页面这个过程发生了什么?”,虽然此题已是人尽皆知,但是能将此题完整的回答上来并且各个细节都梳理清楚的话,还是能难到很多人的,于是就有了这篇文章。
计算机网络部分
该部分主要从用户在地址栏输入网址到网页资源传输到本地浏览器的过程
DNS查询
DNS,域名系统,是一个由分层的DNS服务器实现的分布式数据库,一个使得主机能够查询到分布式数据库的应用层协议。
由于互联网域名采用层次树状的命名方法,每一个域名都是一个唯一的具有层级结构的名字。域名的结构由标点序列组成,各标号之间用点隔开,类似于:...三级域名.二级域名.顶级域名
,各级域名由上一级的域名管理机构管理,最高级的顶级域名则由ICANN管理。
前面提到域名系统是一个分布式,层次的数据库,体现在由三类服务器构成:
- 根DNS服务器:最高层次的DNS服务器,提供顶级域DNS服务器的IP地址。
- 顶级域DNS服务器:顶级域(com,org,net)和国家的顶级域(uk,fr)都有顶级域DNS服务器,提供权威DNS服务器的IP地址
- 权威DNS服务器:因特网上所有公共可访问的主机的每个组织机构都必须提供公共可访问的DNS记录(名字->IP的映射),而这些记录就存放在权威DNS服务器当中
- 本地DNS服务器:不在DNS服务器的层次结构中,但是相当重要,每个ISP都有一个本地服务器,当主机与ISP连接时,该ISP提供一台主机的IP地址,该主机具有一台或多台本地DNS服务器的IP地址。
知道了这些之后,让我们看看当浏览器访问https://yzh2002.cn
时,如何解析其IP地址的。
- 浏览器会查看系统DNS解析程序缓存
ipconfig/displaydns
即可查看。- 网上还看到过浏览器缓存的说法,不过尚未得到证实,暂时搁置。
- 如果DNS解析程序缓存中未找到,则查看系统缓存
hosts
文件中硬编码的DNS记录。 - 还未查询到则进入路由器缓存中检查
- 还未查询到则进入ISP DNS服务器(本地DNS服务器)中进行查询
之后的查询层次服务器的方式又可分为迭代查询和递归查询:
所谓迭代查询,即本地DNS服务器向根域名服务器查询顶级域名服务器的IP地址,返回之后,本地DNS服务器再去向顶级域名服务器查询权威域名服务器的IP,就这样每次都由本地DNS服务器查询最终返回结果。
所谓递归查询,即本地DNS服务器向根域名服务器发送查询报文,之后由根域名服务器向顶级域名服务器发送查询报文,最后在权威DNS服务器查询到之后再层层返回的查询方式。
但是实际情况下,除了前3步的客户端缓存之外,DNS服务器也会缓存每次经其查询到的DNS记录,也因此,除了少数的DNS查询外,根服务器基本被绕过。
补充:通常我们在购买了服务器和域名后,要在服务器厂商那里添加域名解析,其实就是一条A类型的DNS记录,A类型提供标准的主机名到IP地址的映射,除此之外还有NS,CNAME等类型,感兴趣可自行查阅。
建立TCP连接
当浏览器查询到域名对应的IP地址时,就要开始发出HTTP请求获取网页资源文件了。
七层网络协议,每一层都依赖于下一层提供的服务,而HTTP作为最顶层的应用层协议,依赖于传输层协议的服务,再往下的IP协议就不再是本文关心的内容了,传输层有TCP/UDP两大协议,不同的应用层服务会选择不同的传输层协议,而HTTP服务一般选择TCP协议。
传输层协议为运行在不同主机上的应用进程之间提供了逻辑通信,传输层协议提供最低限度的服务就是:进程到进程之间的数据交付 和 差错检查
除此之外,TCP还提供了以下服务:可靠数据传输(通过流量控制,序号,确认和定时器,TCP确保正确的,按序的将数据从发送进程交付给接收进程) 和 拥塞控制
TCP报文结构简述
计算机网络最复杂的部分之一,下面主要讲一下建立TCP连接的过程。先来看一下TCP报文结构:
TCP首部一般是20字节,包含源端口号和目的端口号(各2字节) , 4字节的序号(seq
)和4字节的确认号(ack
),4bit的首部长度,可选与边长的选项字段,2字节的因特网校验和和2字节的紧急数据指针, 2字节的接收窗口字段 (用于流量控制,该字段用于指示接收方愿意接收的字节数量)以及 6比特的标志字段 :
- ack:确认序号有效(不要和确认号混淆)
- fin:释放一个连接
- psh:接收方应该尽快将这个报文交给应用层
- rst:重置连接
- syn:发起一个连接
- urg:紧急指针有效
这里重点说明一下TCP报文首部的序号和确认号,TCP把数据看成一个无结构的,有序的字节流。每个报文段的序号都是 该报文段首字节的字节流编号 ,确认号要比序号难处理一些,因为TCP是全双工的,主机填充进报文段的确认号是其期望从目标主机接收到的下一个字节的序号。例如主机A收到了主机B 0-535和900-1000的报文段,没有收到536-899的报文段,于是主机A发送给主机B的报文段的确认号为536(TCP只确认该流中第一个丢失的字节,这种机制也被称为累积确认)
TCP三次握手
三次握手的本质是为了确认通信双方收发数据的能力。
常见问题:
- 为什么TCP连接时是三次?两次不可以吗?
如果是两次握手的话, 其实也能进行通信,但是会存在安全问题 ,当服务器收到SYN报文时,服务器会为本次连接 分配TCP缓存和变量,然后服务器发送一个SYNACK报文进行响应,如果两次握手的话,那么此时就认为双方建立了连接,如果SYNACK数据报丢失,那么客户端超时未收到确认报文就会重新发送,服务器会认为这是一次新的TCP连接,从而再次为其分配TCP缓存和变量,然后发送SYNACK数据报。
而如果采用三次握手,SYNACK报文丢失之后,服务端再次收到客户端的SYN报文,会知晓这是因为这是报文丢失的缘故(根据确认序号可以识别),也就不会造成上述问题。SYN泛洪攻击的原理也基本就是这样。
- 为什么TCP连接要协商一个随机的初始序列号(ISN)?
TCP四次挥手
常见问题:
- 为什么连接需要三次,但是关闭却需要四次?
因为客户端发送FIN报文表示其无数据发送了,但是服务端不一定没有数据发送了,所以服务端收到FIN之后先返回一个ack报文表示收到了fin报文,但是服务端必须等待自身无数据发送之后才能发送FIN报文与客户端断开连接。
- 为什么客户端发送第四次挥手的确认报文之后要等待2MSL的时间才能释放TCP连接?
同样是为了考虑丢包问题,如果第四次挥手的报文丢失,服务端没有收到确认报文就会重发,这样如果2MSL(最长报文段寿命)时间内没有收到报文,就可以肯定服务端收到了确认报文。
HTTP(s)协议
TLS协议
由于传输层协议不提供数据的加密的能⼒,即以明⽂传输会导致报⽂在传输链路中被拦截修改,为了解决这⼀问题,引⼊了SSL(security socket layer),https就是基于ssl的http协议,保证了在传输链路不易被修改(因为内容加密)(且就算修改了端系统中ssl也提供了识别的能⼒)
TLS协商是在建立TCP连接的基础上,通过客户端与服务端通信来获取主密钥,如何获取主密钥呢?来看看下面的一系列问题:
- 加密⽅式?
直接对称加密肯定不⾏(密钥怎么传输给浏览器呢?这个过程容易被劫持)⾮对称加密呢?(公钥被劫持,浏览器到服务器的数据只有私钥解密是安全的,但是服务器到浏览器的数据却由于公钥暴漏而⽆法保证安全)
- 两次⾮对称加密? 即服务器有公钥A,密钥B,传输给浏览器之后浏览器⽣成公钥C,密钥D,浏览器到服务器的数据⽤公钥A加密,需要密钥B解密,服务器到浏览器的⽤公钥C加密,需要密钥D解密,就算拦截到公钥也⽆法查看。
但是性能太差了,每次传输都要频繁的加密和解密,怎么改善?
- 两次⾮对称+⼀次对称加密? 即两次⾮对称传递对称加密公钥,这样以后就⽤对称加密即可。
但这样⼜产⽣了新的问题:中间人攻击
当服务器第⼀次给浏览器传递公钥A时被拦截,中间⼈将其⽤公钥E替换,浏览器拿到E,将对称密钥X使用公钥E加密后传递 给服务器被拦截,中间人可以解密得到对称密钥X。
- 这个问题的关键在于:⽆法确定收到的公钥是否正确。如何保证呢?
就像数学证明的源头⼀定是⼀个公理⼀样,如何确保公钥的合理性,是借助CA机构,⽹站使⽤HTTPS之前,需要向CA机构申领⼀份数字证书,其中包含整数持有者信息和公钥信息等。
- 那证书如何防⽌修改呢?
对证书⽣成⼀份数字签名。CA机构有⾮对称加密的公钥和私钥,CA机构对证书明文数据T进⾏hash运算,然后进⾏私钥加密,得 到数字签名S。明⽂和数字签名共同组成了数字证书,然后颁发给⽹站。
- 浏览器如何验证其真假呢?
拿到证书,得到明⽂T,签名S,那CA机构的公钥对S解密得到S',⽤证书中指明的hash算法对明⽂hash,然后⽐较即可判定。
- 补充:⼀个服务器需要与多个浏览器进⾏数据传输?每次都要经历密钥的传输过程吗?
不会,服务器会为每个浏览器的密钥⽣成⼀个sessionid,浏览器发送的请求会携带sessionid,故不会 每次都重新传输密钥。
TLS协商就是双方确定一个对称加密密钥,然后以后的消息都使用该密钥加密传输。
HTTP请求
关于HTTP请求,这里存在诸多问题,例如跨域请求问题,状态验证问题(cookie/token?),HTTP缓存问题等等,这里就不再过多介绍了,抽时间单独写篇文章总结一下....
通过HTTP请求获得网站页面文件等资源,第一个数据报一般是14KB,后面的数据包为上一个的两倍,直到遇到拥塞或者达到阈值。相关内容属于TCP拥塞控制的内容,感兴趣可自行查阅。
页面解析
当浏览器收到数据的第一块时,就开始解析收到的信息,
解析
是浏览器将HTML等文件转换为DOM
和CSSOM
的步骤,再通过渲染器把两者在屏幕上绘制成页面,在渲染到屏幕之前,HTML,CSS,JavaScript必须被解析完成。
构建DOM树
浏览器是如何将HTML转换为DOM对象的,可以参考How browsers work,也可以参考HTML spec,本篇博客不细讲这些内容,只说大概过程。
第一步是处理HTML标记并构造DOM树,HTML解析涉及到tokenization
和树的构造,DOM树描述了文档的内容,DOM结点的数量越多,构建DOM树所需的时间就越长。当解析器发现非阻塞资源时(例如图片),浏览器会请求这些资源并且继续解析,遇到CSS文件也可以继续进行解析,但是当遇到<script>
标签,特别是没有async
和defer
属性的,会阻塞渲染并停止HTML的解析。
- defer:告诉浏览器不要等待脚本。相反,浏览器将继续处理 HTML,构建 DOM。脚本会“在后台”下载,然后等 DOM 构建完成后,脚本才会执行。
- async:async 脚本会在后台加载,并在加载就绪时运行。DOM 和其他脚本不会等待它们,它们也不会等待其它的东西。async 脚本就是一个会在加载完成时执行的完全独立的脚本。(因此async应该避免操作DOM引起回流和重绘??)
为什么会阻塞呢?
浏览器是一个多进程模型,其中网页应用运行在渲染进程上,渲染进程又是多线程的,包括:GUI渲染线程,JavaScript引擎线程,事件触发线程,定时触发器线程和异步HTTP请求线程。其中GUI渲染进程负责解析HTML,CSS等,JavaScript引擎线程则是解释js脚本,那么js的解析执行为什么会阻塞HTML的解析呢?
原因在于JavaScript执行的过程中很可能会操作DOM,发生回流和重绘(相关概念后续介绍),因此渲染线程和JavaScript引擎线程是互斥的。故在解析HTML过程中,如果遇到 script 标签,渲染线程会暂停渲染过程,将控制权交给 JS 引擎。内联的js代码会直接执行,如果是js外部文件,则要下载该js文件,下载完成之后再执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染线程,继续 DOM 的解析。同样获取CSS不会阻塞HTML的解析或下载,但是会阻塞JavaScript的运行,因为JavaScript经常用于查询元素的CSS属性。
但是虽然渲染引擎被JavaScript脚本阻塞了,但是还是会将已经构建好的DOM元素渲染到屏幕上,减少白屏时间(这也是为什么把script
标签放在body
标签的底部),如果JavaScript执行之间过长,那么就算页面要更新,GUI线程也会被挂载在队列中,从而导致用户感到页面卡顿,为了解决因CPU密集运算而导致的卡顿,H5引入了web worker的概念,感兴趣的自行查阅。
预加载扫描器
Speculative parsing
浏览器构建 DOM 树时,这个过程占用了渲染进程的主线程。当这种情况发生时,预加载扫描仪将解析可用的内容并请求高优先级资源,如 CSS、JavaScript 和 web 字体。正是因为有了预加载扫描器,使得我们不必等到解析器找到外部资源的引用来请求它,他将在后台检索资源,以便在主HTML解析器到达请求的资源时,他已经在运行或者已经被下载,预加载扫描器提供的优化减少了阻塞。
构建CSSOM树
第二步是处理CSS并构建CSSOM树。DOM和CSSOM是两棵树,具有独立的数据结构。具体过程不再介绍,但是需要知道的是,CSS的解析和JavaScript的解析也是互斥的(上文也提到过....)
页面渲染
渲染步骤包括:样式,布局,绘制,合成(某些情况下)
Style
第三步是将 DOM 和 CSSOM 组合成一个 Render 树,计算样式树或渲染树从 DOM 树的根开始构建,遍历每个可见节点。需要注意,<head>
和它的子节点以及任何具有display:none
样式的结点不会在render树上。(具有visibility:true
的结点会在render树上)
Layout
第四步是在渲染树上运行布局以计算每个节点的几何体。布局是确定呈现树中所有节点的宽度、高度和位置,以及确定页面上每个对象的大小和位置的过程。回流是对页面的任何部分或整个文档的任何后续大小和位置的确定。
第一次确定节点的大小和位置称为布局。随后对节点大小和位置的重新计算称为回流。在我们的示例中,假设初始布局发生在返回图像之前。由于我们没有声明图像的大小,因此一旦知道图像大小,就会有回流。简单来讲,回流就是重新布局。
绘制
最后一步是将各个节点绘制到屏幕上,为了确保平滑滚动和动画,占据主线程的所有内容,包括计算样式,以及回流和绘制,必须让浏览器在 16.67 毫秒内完成。为了确保重绘的速度比初始绘制的速度更快,屏幕上的绘图通常被分解成数层。如果发生这种情况,则需要进行合成。
图层??
在DOM树中,每个结点都会对应一个渲染对象,当他们的渲染对象处于相同的坐标空间时(z轴空间),就会形成一个RenderLayers,渲染层将保证页面元素以正常的顺序堆叠,这时就会出现层合成,类似于PhotoShop的图层模型。一般来说浏览器会为以下元素创建新的渲染层:
- 根元素 document
- 有明确定位属性的元素
- opacity<1
- CSS filter,mask属性
- CSS transform属性
- ....
GraphicsLayer 其实是一个负责生成最终准备呈现的内容图形的层模型,它拥有一个图形上下文(GraphicsContext),GraphicsContext 会负责输出该层的位图。存储在共享内存中的位图将作为纹理上传到 GPU,最后由 GPU 将多个位图进行合成,然后绘制到屏幕上,此时,我们的页面也就展现到了屏幕上。
满足某些特殊条件的渲染层,会被浏览器自动提升为合成层。合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 的父层共用一个。