这个问题在面试中,基本属于必问的了,鉴于在面试中的时间局限,可能大家也就聊一聊dns查询,http请求,tcp三次握手四次挥手,解析 HTML构建DOM树,计算DOM树上的CSS,合成图片,绘制到屏幕上。这个问题是一个很开放的问题,涉及的内容,都能够让我水一篇文章了。
一、打开浏览器
在输入url之前,总得先打开浏览器,不然url往哪儿输呢,chrome会启动多个进程:主进程、渲染进程、网络进程、插件进程和GPU进程等。
进程和线程
首先呢,进程是CPU分配资源的最小单位,线程是CPU调度任务的最小单位。打个比方,工作中的各种部门,划分资源的时候肯定是给前端部门多少人给后端多少人,觉得自己资源不够可以再申请;那线程呢,就是部门内的员工了,各个员工都共享一套代码,但都是给部门内干活,前端当然不能去写后端的接口,那不串了么。
在启动chrome时,主要是有下面的几个进程。
-
主进程相当于行政部门,管理着地址栏、书签、后退和前进按钮界面,还有标签页的创建销毁等;
-
插件进程,谁用浏览器还没几个插件呢,比如Adblock,一个插件一个进程,用的时候才会创建;
-
GPU进程,就一个,以前是用来绘制3D的,不过现在网页和界面也都交给了GPU进行绘制;
-
网络进程,这是近两年才被独立出来的,以前是放在主进程内,主要负责网络资源的请求;
-
渲染进程,就是咱们前端关注的重点了。
首先,打开一个标签页,chrome就会为其创建一个进程,这个进程实际上就是一个渲染进程副本,在这个进程内,会有各种各样的线程,gui线程啦,网络请求啦,事件线程啦,js内核线程(v8引擎)。
此外,还有一个备用渲染进程是常驻的,不过这跟备胎还是有区别的,这个渲染进程能扶正,之后再去创建个备用的。
输入URL
URL的全称是Uniform Resource Locator,统一资源定位符。就拿https://www.example.com/index.html?uid=1#ch1
这个链接地址来说。
-
https://
是协议方案部分,其他的协议方案还有ftp:
或javascript:
; -
www.example.com
这个是经过DNS解析后转成ip地址,当然咱们也可以不用域名,直接用ip访问服务器,开发中常用的192.168.1.1
就是指向本机中的node服务器; -
/index.html
就是咱们要拿到的html文档; -
?uid=1
查询字符串,用来传参的; -
#ch1
这个叫片段标识符,用来标记已获取资源中子资源的位置。hashrouter的实现就基于此。
二、网络请求
打开了浏览器,输入了URL,找到了出口,这不就得去请求资源了么。
在请求资源之前呢,还有个问题得说清楚喽。
前文不是说有单独的一个网络进程了么,那咱们这新开的标签页,也就是渲染进程,里也有http请求的线程,那这个输入URL后回车的网络请求,到底是由网络进程还是渲染进程发起的呢?
答案是由标签页自身的渲染进程发起的。而网络进程,主要是处理浏览器本身的网络请求,比如谷歌账号,比如下载。
端口号
对网络请求有些了解的朋友知道,这个网络请求都是从一个网线/wifi里边出去的,一个计算机的网络流量可大了,又不止浏览器在用网络,还有什么微信啊网抑云啊都在请求,那计算机怎么分辨这个请求应该给到谁呢?
想一想,外卖是怎么从楼下送到家门口的?是不是通过门牌号。对了,计算机里边也有门牌号,当然计算机不叫门牌号,叫端口号,渲染进程需要进行网络通信时就会向计算机申请端口,这里说的是端口可不是pid,这两个是不一样的。
那还有一个问题,现在家里都有大大小小的各种网络设备,计算机是怎么知道要把数据包发给路由器,而不是别的设备呢。就是上边提到的外卖例子,送上门之前,怎么找到楼下?地址,是的,每个设备都有一个地址,叫MAC地址,这个地址是唯一的,首先计算机在传输数据之前要进行广播,找到网关的MAC地址,就像商城找人那样,那商城也不知道人在哪儿,只能广播,等人回应了才行。
这样,浏览器找到了路由出口,可以开始向外传输数据了。
DNS解析
可是咱输入的是URL,服务器的IP地址是一组纯数字呀,这怎么访问呢,为了解决这个问题,DNS服务就应运而生了,可以通过域名或IP地址互查。因此呢,在填了URL之后,还不能去请求网络资源,还得先经过以下步骤,以下步骤是可以中断的,只要在任一阶段找到了IP地址,就会停止。
- chrome自己的DNS缓存
- 操作系统的缓存
- 本机host
- 向计算机系统配置的首选DNS服务器发起域名解析请求,比如
114.114.114.114
就是咱们国内电信移动联通的通用域名解析DNS服务器 - 电信运营商发起迭代DNS解析请求,先找到根域DNS的IP地址后发起请求,根域里边并不储存域名和IP地址的映射关系,返回的是域名后缀域的IP地址,比如
example.com
,根域返回的就是com域
的IP地址。 - 运营商再次请求域名后缀域(
com域
)的DNS服务器,com域
也不返回实际的IP地址,而是返回example.com
这个域名的DNS地址,这个地址一般就是域名注册商提供的了,比如万网、新网。 - 域名注册商提供了IP地址后,返回给运营商,运营商再返回给操作系统,操作系统再给到浏览器。
- 如果都没有,操作系统会找到NetBios name Cache,这里存的主要是最近一段时间内和计算机成功通讯的IP地址。
- 查询WINS服务器
- 广播查找
- 读取LMHOSTS文件
经过DNS解析之后,就拿到了目标服务器的IP地址,就可以快乐地请求数据了。当然如果都没有找到IP地址,那就宣告解析失败了。
TCP连接
一个请求发出去,大概会经历这么几个步骤,网卡=>路由器=>交换机=>服务器,中间还会各种转发,物理层面的链路,本文的重点也不在此处,就不展开讲了。在进行HTTP请求之前,要先和服务器建立连接,这里用到的是TCP协议,除此之外还有UDP,这两个协议的主要区别是:UDP不需要建立连接,想发就发,会有不可靠性,除此之外还有单播多播广播,面向报文之类的特性。
TCP链接是一个全双工的连接,全双工的意思是数据可以同时在两个方向传输。TCP和服务器建立连接的过程,就是面试中常常会被问到的TCP三次握手。
- 第一次握手是客户端主动发起的,发送一个
SYN(syn=j)
的数据包给服务器,同时进入SYN_SENT
的状态,等待服务器回复。 - 服务器接收到这个请求之后,确认客户端的
SYN(ack=j+1)
,同时发送一个SYN(syc=x)
包,此时服务器进入SYN_RECV的状态,这是第二次握手。 - 最后一次握手则是客户端向服务器发送
ACK
包,进行状态确认,服务器和客户端都进入ESTABLISHED
的状态,TCP连接完成。
字段 | 含义(ACK等大写字母表示标志位,其值为1或0,ack等小写的单词表示序号) |
---|---|
URG | 紧急指针是否有效。为1,表示某一位需要被优先处理。 |
ACK | 确认号是否有效,一般置为1。 |
PSH | 提示接收端应用程序立即从TCP缓冲区把数据读走。 |
RST | 对方要求重新建立连接,复位。 |
SYN | 请求建立连接,并在其序列号的字段进行序列号的初始值设定。建立连接,设置为1。 |
FIN | 希望断开连接。 |
这里有一个问题,为什么需要三次握手?
这是因为网络环境的复杂,丢包是很常见的事情,服务端向客户端发送确认回执的时候,如果数据包丢失了,那么客户端将一直是SYN_SENT
的状态,而服务端将会认为连接已经建立并且向客户端发送数据,由于客户端认为连接还没有建立,就会忽略掉服务端发送来的数据,而服务端在发出数据超时后会不断进行重试,如此一来,就造成了死锁。
简单来说,就是服务端需要知道连接是否已经成功建立,而不是自己在那一厢情愿地发送数据包。
HTTP传输
连接建立后,就要开始HTTP协议传输了,HTTP协议在TCP通道中传输的完全是文本,请求网页,用的都是GET
请求。在初次连接的时候,会向服务器请求资源,如果有做CDN缓存的话,则会从CDN服务器中请求资源。
倘若我们之前已经访问过这个网页了,则会检查HTTP请求中的请求头设置,有一个Cache-Control
的设置,倘若命中缓存且expires
未过期,则从缓存中获取,此时是不需要从服务器获取资源,也不需要TCP连接的,此时HTTP的状态为200
,这属于强缓存。
而协商缓存是,请求还是要发送到服务器,由服务器来决定缓存是否可用,相关字段有Last-Modified/If-Modified-Since
和Etag/If-None-Match
,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后决定是否返回304
。
流水线过程
三、渲染页面
拿到了HTML文档之后,就要开始进行网页渲染了。前面所述的网络请求也是在当前标签页的渲染进程中完成的,之后,这个渲染进程会使用RCP通信和浏览器进程进行通信,通知浏览器页面进行相应的变更,比如加载图标的停止。
编译
从HTML字符流中读取文档,先利用状态机进行token
分词,同时构建DOM树,DOM树的栈顶一般是<html>
这个元素,DOM元素都添加到树下,遇到属性就添加到当前节点,如果是文本内容,则分析当前节点是否是文本节点,否则就添加为子节点;除此之外,浏览器还要构建CSS RenderObject树,这两者是流式处理的,依次拿到DOM树构建的元素,然后按照CSS选择器的优先级进行匹配覆盖,之后会确定每个元素在文档中的位置,也就是进行排版,生成一个render树。
渲染
图形的渲染,在Chrome当中,是交给skia
这个引擎来处理的,顺带一提,安卓和flutter的绘图引擎也是这个。skia
绘制出来的实际上是图片,每张图片就是一帧。因此,展现在我们面前的网页,实际上是不断重新绘制渲染的图片。当然不是全图绘制,而是分区域分块,将需要更新的部分重新绘制。
由于在JS当中,是可以操作DOM元素的,这就会产生一个重绘和回流的概念。回流是指render树需要进行重新构建,比如修改了元素的display:none
中的值,将元素显示出来,就会引发回流。而重绘,只是修改的了render tree中的值。回流必然会触发重绘,但无论是回流还是重绘,都会导致渲染引擎重绘合成位图。
同时,GUI和JS是互斥的,当JS引擎在执行的时候,GUI就会被挂起,GUI的更新会被保存在一个队列当中,等待JS空闲时立刻切换执行。这是为了保证在渲染引擎在获得的元素数据前后一致,避免JS和GUI同时操作一个元素。
任务队列
我们都知道,在整个页面被加载之后,就会触发window.onload
这个钩子。在这之后,就是JS的事件逻辑了。JS是单线程的,所有的任务都要排队,但JS引擎完全可以不管IO(网络请求、鼠标点击等),而是先处理其他的任务,等IO返回了结果之后,再处理挂起的任务。
三、结语
至此,这篇文章也就说完了,参考了很多的资料,其中李兵老师的《浏览器工作原理与实践》给了我很大的帮助,很值得一读。由于这个问题所涉及到的内容之多,核心的部分还是网络请求这块。至于后面提到的绘图渲染,那是计算机图形学的研究方向了。