作为一名前端开发工程师,无论是在面试,还是在日常开发debug中,一定都会遇到一个常见的问题,当一个用户在浏览器输入网址并敲下回车键之后,直到页面完全渲染的这短时间里,到底发生了哪些事情?
DNS解析
DNS是一个把域名和ip地址相互映射的分布式数据库,这样的方式可以让用户能够以更加方便的方式去访问页面,当用户输入了想要访问的网址之后,浏览器便会对这个网址进行解析,以找到对应的服务IP地址。那么解析的过程及顺序是怎样的呢?
首先会找到浏览器缓存
,本地的hosts文件
和DNS缓存
。如果这些地方都没有找到对应的记录,则会去请求DNS服务器
。而DNS服务又分为三步,首先会走到本地DNS服务
,然后依次请求根域名
服务器和顶级域名
服务器,最终请求权威域名
服务器的地址,权威域名服务器会对网址进行解析并返回最终的IP地址给到本地DNS服务器,由它传递到浏览器。此外,本地DNS服务器不仅要把IP地址返回给用户浏览器,还会把这个对应关系保存在缓存中,以备下次别的用户查询时,就可以直接返回结果,提升访问效率。
也就是:
根据上面阐述的过程,我们可以得知域名的DNS解析也是一个性能上的开销,在完成域名解析之前,浏览器不能从服务器加载到任何东西。那么有什么方法可以去尽量减少或者避免这个开销呢?
- 尽可能的减少DNS请求次数
- 代码层面上去做DNS预解析
js
<link rel='dns-prefetch'href='baidu.com'>
- 充分利用好CDN进行负载均衡。每个域名在经过DNS解析后会把域名的解析权交给cname()指向的内容分发(CDN)专用的DNS服务器。CDN专用的DNS服务器把CDN的全局负载均衡设备的ip地址返回给用户。最终提高用户的访问速度。
建立TCP连接
在获取准确的IP地址之后,浏览器会先建立TCP连接。TCP是一种面向连接的,可靠的,基于字节流的传输层通信协议。在建立连接的时候,我们需要至少三次握手: 主要的握手过程:
-
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c),此时客户端处于 SYN_SENT 状态
-
第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,为了确认客户端的 SYN,将客户端的 ISN+1作为ACK的值,此时服务器处于 SYN_RCVD 的状态
-
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,值为服务器的ISN+1。此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接
为什么至少需要三次握手呢?
-
第一次握手确认了客户端的发送数据能力
-
第二次握手确认了服务端的接收数据能力
-
第三次握手确认了服务端的发送数据能力和客户端的接收数据能力。
发起HTTP/HTTPS的请求
在建立完TCP连接之后就可以通过 HTTP 进行数据传输了。HTTP本身是一个明文传输的超文本传输协议,在传输的途中容易遭受到中间人攻击。所以,我们大多数的网站都会使用HTTPS的传输服务,会在 TCP 与 HTTP 之间多添加一层协议做加密及认证的服务。这里还会涉及到一个TLS的握手过程,主要是利用了对称加密和非对称加密相结合的方式,来保障数据传输的安全性。
主要的加密原理:
- 用户在浏览器发起HTTPS请求,默认使用服务端的443端口进行连接;
- HTTPS需要使用一套CA数字证书 ,证书内会附带一个公钥Pub ,而与之对应的私钥Private保留在服务端不公开;
- 服务端收到请求,返回配置好的包含公钥Pub的证书给客户端;
- 客户端收到证书,校验合法性,主要包括是否在有效期内、证书的域名与请求的域名是否匹配,上一级证书是否有效(递归判断,直到判断到系统内置或浏览器配置好的根证书),如果不通过,则显示HTTPS警告信息,如果通过则继续;
- 客户端生成一个用于对称加密的随机Key ,并用证书内的公钥Pub进行加密,发送给服务端;
- 服务端收到随机Key 的密文,使用与公钥Pub 配对的私钥Private 进行解密,得到客户端真正想发送的随机Key;
- 服务端使用客户端发送过来的随机Key对要传输的HTTP数据进行对称加密,将密文返回客户端;
- 客户端使用随机Key对称解密密文,得到HTTP数据明文;
- 后续HTTPS请求使用之前交换好的随机Key进行对称加解密。
服务器响应数据
服务器收到了我们的请求之后,会根据相应的请求信息,把它的处理结果返回,也就是返回一个HTPP响应。
HTTP响应与HTTP请求相似,HTTP响应也由3个部分构成,分别是:
- 状态行
- 响应头(Response Header)
- 响应正文
状态行:
状态行由协议版本、数字形式的状态代码、及相应的状态描述,各元素之间以空格分隔。
格式:
HTTP-Version Status-Code Reason-Phrase CRLF
例如:HTTP/1.1 200 OK
协议版本: 是用http1.0还是其他版本
状态描述: 状态描述给出了关于状态代码的简短的文字描述。比如状态代码为200时的描述为 ok
状态码: 状态代码由三位数字组成,第一个数字定义了响应的类别,且有五种可能取值,如下:
1xx:信息性状态码,表示服务器已接收了客户端请求,客户端可继续发送请求。
- 100 Continue
- 101 Switching Protocols
- 2xx:成功状态码,表示服务器已成功接收到请求并进行处理。
200 OK 表示客户端请求成功
- 204 No Content 成功,但不返回任何实体的主体部分
- 206 Partial Content 成功执行了一个范围(Range)请求
3xx:重定向状态码,表示服务器要求客户端重定向。
- 301 Moved Permanently 永久性重定向,响应报文的Location首部应该有该资源的新URL
- 302 Found 临时性重定向,响应报文的Location首部给出的URL用来临时定位资源
- 303 See Other 请求的资源存在着另一个URI,客户端应使用GET方法定向获取请求的资源
- 304 Not Modified 服务器内容没有更新,可以直接读取浏览器缓存
- 307 Temporary Redirect 临时重定向。与302 Found含义一样。302禁止POST变换为GET,但实际使用时并不一定,307则更多浏览器可能会遵循这一标准,但也依赖于浏览器具体实现
4xx:客户端错误状态码,表示客户端的请求有非法内容。
- 400 Bad Request 表示客户端请求有语法错误,不能被服务器所理解
- 401 Unauthonzed 表示请求未经授权,该状态代码必须与 WWW-Authenticate 报头域一起使用
- 403 Forbidden 表示服务器收到请求,但是拒绝提供服务,通常会在响应正文中给出不提供服务的原因
- 404 Not Found 请求的资源不存在,例如,输入了错误的URL
5xx:服务器错误状态码,表示服务器未能正常处理客户端的请求而出现意外错误。
- 500 Internel Server Error 表示服务器发生不可预期的错误,导致无法完成客户端的请求
- 503 Service Unavailable 表示服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常
响应头部:
由关键字/值对组成,每行一对,关键字和值用英文冒号":"分隔,典型的响应头有:
location:服务器通过这个头告诉浏览器跳到哪里。
server:服务器通过这个头告诉浏览器服务器的型号。
content-encoding:服务器通过这个头告诉浏览器数据的压缩格式。
content-length:服务器通过这个头告诉浏览器回送数据的长度。
content-language:服务器通过这个头告诉浏览器语言环境。
content-type:服务器通过这个头告诉浏览器回送数据的类型。
refresh:服务器通过这个头告诉浏览器定时刷新。
content-disposition:服务器通过这个头告诉浏览器以下载方式打开数据。
transfer-encoding:服务器通过这个头告诉浏览器数据是以分块方式回送的
以下三个表示服务器通过这个头告诉浏览器不要缓存
expires:-1
cache-control:no-cache
pragma:no-cache
响应正文
包含着我们需要的一些具体信息,比如cookie,html,image,后端返回的请求数据等等。
加载静态资源并渲染页面
在浏览器没有完整接受全部HTML文档时,它就已经开始显示这个页面了,浏览器是如何把页面呈现在屏幕上的呢?不同浏览器可能解析的过程不太一样,这里我们只介绍webkit的渲染过程,下图对应的就是WebKit渲染的过程,这个过程包括:
解析html以构建dom树 -> 构建render树 -> 布局render树 -> 绘制render树
浏览器在解析html文件时,会"自上而下"加载,并在加载过程中进行解析渲染。在解析过程中,如果遇到请求外部资源时,如图片、外链的CSS、iconfont等,请求过程是异步的,并不会影响html文档进行加载。
解析过程中,浏览器首先会解析HTML文件构建DOM树,然后解析CSS文件构建渲染树,等到渲染树构建完成后,浏览器开始布局渲染树并将其绘制到屏幕上。这个过程比较复杂,涉及到两个概念: reflow(回流)和repain(重绘)。
CSS文件的下载和解析并不会阻塞DOM的解析,但是会阻塞Render树的渲染
DOM节点中的各个元素都是以盒模型的形式存在,这些都需要浏览器去计算其位置和大小等,这个过程称为relow;当盒模型的位置,大小以及其他属性,如颜色,字体,等确定下来之后,浏览器便开始绘制内容,这个过程称为repain。
页面在首次加载时必然会经历reflow和repain。reflow和repain过程是非常消耗性能的,尤其是在移动设备上,它会破坏用户体验,有时会造成页面卡顿。所以我们应该尽可能少的减少reflow和repain。
当文档加载过程中遇到js文件,html文档会挂起渲染(加载解析渲染同步)的线程,不仅要等待文档中js文件加载完毕,还要等待解析执行完毕,才可以恢复html文档的渲染线程。因为JS有可能会修改DOM,最为经典的document.write,这意味着,在JS执行完成前,后续所有资源的下载可能是没有必要的,这是js阻塞后续资源下载的根本原因。所以我明平时的代码中,js是放在html文档末尾的。
JS的解析是由浏览器中的JS解析引擎完成的,比如谷歌的是V8。JS是单线程运行,也就是说,在同一个时间内只能做一件事,所有的任务都需要排队,前一个任务结束,后一个任务才能开始。但是又存在某些任务比较耗时,如IO读写等,所以需要一种机制可以先执行排在后面的任务,这就是:同步任务(synchronous)和异步任务(asynchronous)。
JS的执行机制就可以看做是一个主线程加上一个任务队列(task queue) 。同步任务就是放在主线程上执行的任务,异步任务是放在任务队列中的任务。所有的同步任务在主线程上执行,形成一个执行栈;异步任务有了运行结果就会在任务队列中放置一个事件;脚本运行时先依次运行执行栈,然后会从任务队列里提取事件,运行任务队列中的任务,这个过程是不断重复的,所以又叫做事件循环(Event loop)。
断开TCP连接
现在的页面为了优化请求的耗时,默认都会开启持久连接(keep-alive),那么一个 TCP 连接确切关闭的时机,是这个 tab 标签页关闭的时候。这个关闭的过程就是四次挥手。关闭是一个全双工的过程,发包的顺序是不一定的。一般来说是客户端主动发起的关闭,过程如下图所示:
- 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
- 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
- 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
- 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
为什么交手只需要是三次,而挥手需要四次呢?
交手,是双端从关闭状态转向开启状态,这时候双端并不需要做什么准备。 而相反,结束的时候,双端都需要确认对方的数据已经全部传送完毕了,才能结束。 所以把释放连接的报文FIN和确认接收的报文ACK,分开发送。