1 浏览器的工作原理
1.1 多进程的浏览器(原地址)
1.1.1 进程和线程的区别
① 进程是CPU资源分配的最小单位(是能拥有资源和独立运行的最小单位);
② 不同进程之间也可以通信,不过代价较大;
③ 线程是CPU调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程);
④ 现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多;
⑤ 同一进程下的各个线程之间共享进程的内存空间(包括代码段、数据集、堆等)。
1.1.2 浏览器的多进程
① Browser进程:浏览器的主进程(负责协调、主控),只有一个。作用:
<1> 负责浏览器界面显示,与用户交互。如前进,后退等;
<2> 负责各个页面的管理,创建和销毁其他进程;
<3> 将Renderer进程得到的内存中的Bitmap(位图),绘制到用户界面上;
<4> 网络资源的管理,下载等。
② 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建;
③ GPU进程:最多一个,用于一些图形操作、3D绘制等;
④ Renderer进程(浏览器内核,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为页面渲染,脚本执行,事件处理等。
1.1.3 Renderer进程的多线程
浏览器最重要的部分就是Renderer进程,也叫做浏览器内核,一般的浏览器内核会包括以下一些常驻线程:
① GUI渲染线程:
<1> 负责渲染浏览器界面,解析HTML,CSS,构建DOM树,布局和绘制等;
<2> 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行;
<3> 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎线程执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎线程空闲时立即被执行。
② JS引擎线程:
<1> 也称为JS内核,负责处理Javascript脚本程序,在第④章我们已经知道JS引擎是单线程的;
<2> JS引擎线程负责解析Javascript脚本,运行代码;
<3> JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序;
<4> 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
③ 事件触发线程:
<1> 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解为,JS引擎自己都忙不过来,需要浏览器另开线程协助);
<2> 接受浏览器里面的操作事件响应,如在监听到鼠标、键盘、AJAX异步请求等事件的时候,会将对应任务添加到事件线程中;
<3> 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理;
<4> 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)。
④ 定时触发器线程:
<1> 传说中的setInterval()
(在参数指定的时间后将待执行方法放到执行队列中, 如果队列中没有其他方法等待,则会立即执行setTimeout()
指定的方法)与setTimeout()
(定时触发器线程每间隔指定的时间将指定方法放入到执行队列中, 当函数执行时,如果发现同一个定时器已经有多个在等待执行的任务,只会执行1次。后面的会被忽略掉)所使用线程;
<2> 浏览器定时计数器并不是由JavaScript引擎计数的(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确);
<3> 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行);
<4> 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
⑤ 异步http请求线程:
XMLHttpReques在连接后,发送请求是由浏览器新开一个线程请求,当请求的状态变更时,如果先前已设置回调,这异步线程就产生状态变更事件放到JavaScript引擎的处理队列中等待处理,当任务被处理时,JavaScript引擎始终是单线程运行回调函数,具体点即还是单线程运行onreadystatechange事件里面所设置好的函数。
注意:
不同的浏览器内核对网页的解析过程不同,因此同一网页在不同的内核的浏览器里的渲染(显示)效果也可能不同,现今的四大内核:WebKit、Blink、Trident和Gecko;Chrome浏览器现在使用的就是Blink内核,严格来说应该是Blink内核再内嵌一个V8的Javascript引擎。
1.1.4 Renderer进程和Browser进程、GPU进程的通信过程
① Browser进程收到用户请求,首先需要获取页面内容(譬如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程;
② Renderer进程的Renderer接口收到消息,简单解释后,交给渲染线程;
③ GUI渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
④ 当然可能会有JS线程操作DOM(这样可能会造成回流并重绘)
⑤ 最后Render进程将结果传递给Browser进程去呈现;
1.1.5 解决JS阻塞页面加载
因为GUI渲染线程与JS引擎线程是互斥的,所以JS引擎如果执行时间过长就会阻塞页面,解决方法:使用HTML5提供的专用线程Web Workers,当创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM),然后JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)。
1.1.6 WebWorker与SharedWorker的区别
① HTML5还有个新特性Shared Worker,叫做共享线程,多个页面可以共用一个SharedWorker后台线程,并且可通过该后台线程共享数据,但必须保证这些页面都是同源的(相同的协议,主机和端口号);
② 创建SharedWorker线程的方法与前面创建Worker线程的方法类似,只是构造器略有区别,代码如下:
var worker=new SharedWorker(url, [name]);
该方法第一个参数用于指定后台线程文件的URL地址,该脚本文件中定义了在后台线程中所要执行的处理,第二个参数为可选参数,用于指定Worker的名称,当用户创建多个SharedWorker对象时,脚本程序将根据创建SharedWorker对象时使用的url参数值与name参数值来决定是否创建不同的线程;
③ Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中每个相同的JavaScript只存在一个管理SharedWorker的进程,不管它被创建多少次;
④ SharedWorker和WebWorker的区别在于:SharedWorker由独立的进程管理,WebWorker只是属于render进程下的一个线程。
1.2 浏览器运行流程
1 域名解析;
2 发起TCP三次握手建立连接;
3 使用HTTP协议或者HTTPS协议向服务端请求页面;
4 把请求回来的HTML代码经过渲染引擎解析,先构成一棵DOM树;
5 流式计算DOM树上的CSS属性,得到一棵包含样式信息的DOM树;
6 流式计算每个元素的位置和大小;
7 最后根据这些样式信息和大小信息,为每个元素在内存中渲染它的图形,并且把它绘制到界面上。
1.2.1 域名解析
① 浏览器会首先检查自身缓存,浏览器会缓存DNS记录一段时间;
② 如果浏览器缓存里没有找到需要的记录,浏览器会做一个操作系统调用(windows里是get host name),查询系统DNS缓存中的域名表,有则直接使用;
③ 系统缓存中还是没有,则检查hosts文件中的映射表;
④ 本地实在找不到,则向DNS域名服务器发起请求查询;
1 主机先向其本地域名服务器(每一个ISP(网络服务提供商)都会有一个自己的本地域名服务器)进行递归查询,如果缓存中没有,继续下一步;
2 本地域名服务器则发起迭代DNS请求,首先向根域服务器发起请求查询;
3 假如本次请求的是www.baidu.com,根域服务器发现这是一个com的顶级域名,就把顶级域名服务器dns.com地址返回给本地域名服务器;
4 本地域名服务器再向顶级域名服务器dns.com地址请求www.baidu.com,此时本地服务器返回了权限域名服务器dns.baidu.com的IP地址;
5 本地域名服务器向权限域名服务器dns.baidu.com进行查询请求;
6 权限域名服务器dns.baidu.com告诉本地域名服务器,所查询的主机的IP地址,然后本地域名服务器再把查询结果告诉操作系统内核,内核再返回给浏览器;
⑤ 如果连DNS服务器也没解析成功,操作系统就会查找NetBIOS名称缓存(如果这台计算机曾经与对方通信过,则对方计算机的NetBIOS名称和IP地址会被存储到这台计算机的NetBIOS名称缓存中);
⑥ 如果第⑤步也没有成功,那会查询WINS服务器(是NETBIOS名称和IP地址对应的服务器);
⑦ 如果第⑥步也没有查询成功,那么操作系统就要进行广播查找;
⑧ 如果第⑦步也没有成功,那么操作系统就读取LMHOSTS文件(和HOSTS文件同一个目录下,写法也一样),如果这一步还没有解析成功,那么就宣告这次解析失败,那就无法跟目标计算机进行通信;只要这八步中有一步可以解析成功,那就可以成功和目标计算机进行通信。
1.2.2 TCP三次握手
① 第一次握手:客户端向服务器发送SYN报文,并发送客户端初始序列号Seq=X;等待服务器确认;
② 第二次握手:服务器接收客户端的SYN报文,然后向客户端返回一个包SYN+ACK响应报文,并发送初始序列号Seq=Y;
③ 第三次握手:客户端接受SYN+ACK报文,并向服务器发送一个ACK确认报文,至此连接建立。
1.2.3 使用HTTP协议或者HTTPS协议向服务端请求页面 ★
① 建立了TCP连接后,浏览器就会向服务器发送请求命令,例如:
GET/ HTTP/1.1
<!-- 请求方法/请求的路径+协议/版本 -->
<!--
除了请求行,一般还会带有请求头(Requset Header),会包含许多有关的客户端环境和请求正文的有用信息,
例如,请求头可以声明浏览器所用的语言:如Accept-Language:zh-cn,
还有请求正文的长度等
-->
<!--
然后接下来的是请求正文,
请求头和请求正文必须有一个空行,这个行非常重要,它表示请求头已经结束,接下来的是请求正文。请求正文中可以包含客户提交的查询字符串信息:
如username=jinqiao&password=1234
-->
② 浏览器向服务器发出请求后,服务器会回送应答:
HTTP/1.1 301 Moved Permanently
<!-- 状态行 -->
<!-- 协议/版本 状态码 状态文本 -->
<!-- 下面是响应头(Response Header)包含许多信息,例如服务器类型、日期时间、内容类型和长度等 -->
Date: Fri, 25 Jan 2019 13:28:12 GMT
Content-Type: text/html
Content-Length: 182
Connection: keep-alive
<!--
一般情况下,一旦Web服务器向浏览器发送了请求数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了这行代码Connection:keep-alive,
TCP连接就会在发送后仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求,
保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽
-->
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>openresty</center>
</body>
</html>
<!-- 上面是服务器返回的响应正文——HTML代码 -->
1.2.3.1 请求方法
- GET
- POST
- HEAD
- PUT
- DELETE
- CONNECT
- OPTIONS
- TRACE
① 浏览器通过地址栏访问页面都是GET方法。表单提交产生POST方法;
② HEAD则是跟GET类似,只返回请求头,多数由JavaScript发起;
③ PUT和DELETE分别表示添加资源和删除资源,但是实际上这只是语义上的一种约定,并没有强约束;
④ CONNECT现在多用于HTTPS和WebSocket;
⑤ OPTIONS和TRACE一般用于调试,多数线上服务都不支持。
1.2.3.2 状态码和状态文本
- 1xx:临时回应,表示客户端请继续(由于HTTP/1.0协议中没有定义任何1xx状态码,所以除非在某些试验条件下,服务器禁止向此类客户端发送 1xx 响应);
- 2xx:请求成功;
200:请求成功。 - 3xx:表示请求的目标有变化,希望客户端进一步处理;
301&302:永久性与临时性跳转;
304:跟客户端缓存没有更新。(客户端本地已经有缓存的版本,并且在Request中告诉了服务端,当服务端通过时间或者tag,发现没有更新的时候,就会返回一个不含body的304状态) - 4xx:客户端请求错误;
403:服务器拒绝访问,权限不够;
404:表示请求的页面不存在;
418:It’s a teapot. 这是一个彩蛋,来自ietf的一个愚人节玩笑。 - 5xx:服务端请求错误。
500:服务端错误;
503:服务端暂时性错误,可以一会再试。
1.2.3.3 常见的请求头和响应头
1.2.3.4 常见的请求正文格式
- application/json
- application/x-www-form-urlencoded
- multipart/form-data
- text/xml
1.2.3.5 HTTP 2与HTTP 1.1的不同点
- 新的二进制格式(Binary Format),HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮;
- 多路复用(MultiPlexing),即则使用同一个TCP连接来传输多个HTTP请求,避免了TCP连接建立时的三次握手开销,和初建TCP连接时传输窗口小的问题;
- header压缩,HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。
- 服务端推送(server push),服务端推送能够在客户端发送第一个请求到服务端时,提前把一部分内容推送给客户端,放入缓存当中,这可以避免客户端请求顺序带来的并行度不高,从而导致的性能问题。
1.2.3.6 HTTPS与HTTP的一些区别
- HTTPS协议需要到CA申请证书,一般免费证书很少,需要交费;
- HTTP协议运行在TCP之上,所有传输的内容都是明文,HTTPS运W行在SSL/TLS之上,SSL/TLS运行在TCP之上,所有传输的内容都经过加密的;
- HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443;
- HTTPS可以有效的防止运营商劫持(什么是运营商劫持),解决了防劫持的一个大问题。
1.2.4 创建DOM树 ★
DOM(Document Object Model 文档对象模型)是为HTML和XML提供的API,可以把html、xml文档变成一种具有树形结构的object,我们如果需要修改这个object,可以通过在JS代码中调用DOM API。
浏览器在接收HTML代码(响应正文)后,将代码交予渲染引擎解析:
① 调用状态机(HTML官方文档)将每一个字符解析成词(指编译原理的术语token,表示最小的有意义的单元);
② 用语法分析器接收token,一边接收,一边构建DOM树,用栈进行保存和输出,栈顶是HTML代码(已转成tokens)最后的根节点,stack[0]
就是DOM树的产出。
tokens->DOM树的构建过程:
① 不同的HTML节点对应了不同的Node的子类,解析时将节点转换为Node类的实例;
② 栈顶元素就是当前节点(正在解析着的token词);
③ 解析时遇到属性,就添加到当前节点中;
④ 解析时遇到文本节点,如果当前节点是文本节点,则跟文本节点合并,否则入栈成为当前节点的子节点;
⑤ 解析时遇到注释节点,作为当前节点的子节点;
⑥ 解析时遇到tag start(开始标签)就入栈一个节点,当前节点就是这个节点的父节点;
⑦ 解析时遇到tag end(结束标签)就出栈一个节点(还可以检查是否匹配)。
全部出栈后,DOM树构建完毕。