这一道题真的是前端面试必考的一道题了。就连我就面了一次试,就遇到了这个问题。也看了很多人的文章,但是还是先要自己总结一下,更简洁,不需要太多的其他内容。
解析url
URI的一般形式
- scheme: 协议名,方案名。表示资源应该使用哪种协议来访问。例如http, https, ftp、ldap、file、news等。
- authority: 表示资源所在的主机名,通常的形式是“host:port”,即主机名加端口号。有时端口号会被省略。HTTP 的默认端口号是 80,HTTPS 的默认端口号是 443。
- path:标记资源所在位置。必须以
/
开头。 - query: 表示对资源附加的额外要求。
注意file协议之后,允许省略主机名,默认是主机localhost。但对于 HTTP 或 HTTPS 这样的网络通信协议,主机名是绝对不能省略的。会导致浏览器无法找到服务器。
客户端和服务器看到的 URI 是不一样的。客户端看到的必须是完整的 URI,使用特定的协议去连接特定的主机,而服务器看到的只是报文请求行里被删除了协议名和主机名的 URI。
URI的完整形式
- User:passwd@ : 身份信息,但是不推荐这样使用,泄露重要信息 。
- #fragment :片段标识符。标识URI所定位的资源内部的一个锚点,浏览器可以跳转到它指示的位置。服务器看不到#fragment。
url编码
如果url中出现了非ASCII码以外的字符或者url中保留字符,那么它将被编码。
把字符(unicode)编码成utf-8,utf-8是用1-4个字节表示的,所以每个字节转换成16进制并在前面用百分号(%)连接,最后并把每个字节转换的结果连接起来。但是在浏览器输入框中并不会显示转以后的字符。
DNS域名解析
为什么需要域名解析呢?
在 TCP/IP 协议中使用 IP 地址来标识计算机。
- 根 DNS 服务器 :返回顶级域 DNS 服务器的 IP 地址
- 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址
- 权威 DNS 服务器 :返回相应主机的 IP 地址
DNS是树状的分布式查询系统。
- 首先查找浏览器中时候缓存的有该域名对应的ip地址。
- 再在本地的hosts文件中查找。
C:\WINDOWS\system32\drivers\etc\hosts
- 再去本地DNS服务器(即你家的网络服务商isp)
以上过程都没有缓存,那么它将请求根DNS服务器。
- 查询根DNS服务器,根据一级域名返回对应的顶级域名服务器的ip地址
- 查询顶级域名服务器,根据二级域名返回对应的权威域名服务器的ip地址
- 根据返回的权威服务器ip地址,然后去请求,即可查询到该域名对应的ip地址。
- 然后将其缓存在本地DNS服务器中,再将其返回给客户端。
并且这个过程也可以做负载均衡。根据服务器的压力,我们来决定返回那个服务器的ip地址。因为一个域名可以被映射出多个ip地址。
还有一个迭代解析的过程
本地DNS服务器不是自己向其他DNS服务器进行查询,而是把能解析该域名的其他DNS服务器的IP地址返回给客户端DNS程序,客户端DNS程序再继续向这些DNS服务器进行查询,直到得到查询结果为止。
TCP的三次握手
主要的目的就是保证收发双方都有接收和发送数据的能力。
在发送http请求之前,我们需要先建立一个可信的“传输通道”。这里的传输通道并不是真实存在的,他只是双方建立一种数据结构来保存双方的交互状态。
把上图的几个状态记住就行了。
-
确认 ACK :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
-
同步 SYN :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。
-
终止 FIN :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接。
-
序号 seq :用于对字节流进行编号。因为应用层的传输的数据报会被分割,为了保证字节流有序。
-
确认号 ack:期望收到的下一个报文段的序号。
为什需要三次握手
《计算机网络》第四版中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。”
书中的例子是这样的,“已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。
假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。
采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。”。主要目的防止server端一直等待,浪费资源。
发送HTTP请求
SSL / TLS 握手
如果发送的是https请求,那么就需要先建立TLS连接。
ECDHE握手
- 在 TCP 建立连接之后,浏览器会首先发一个“Client Hello”消息。里面有客户端的版本号、支持的密码套件,还有一个随机数(Client Random),用于后续生成会话密钥。
- 服务器收到“Client Hello”后,会返回一个“Server Hello”消息。把版本号对一下,也给出一个随机数(Server Random),然后从客户端的列表里选一个作为本次通信使用的密码套件。
- 发送数字证书给客户端(Server Certificate)。
- 发送数字证书后,服务器发送“Server Key Exchange”消息,里面是椭圆曲线的公钥(Server Params),用来实现密钥交换算法,再加上自己的私钥签名认证。
- 之后服务器发送“Server Hello Done”消息,表示消息发送完毕。
- 接下来浏览器验证数据证书的有效性。
- 验证通过后,客户端按照密码套件的要求,也生成一个椭圆曲线的公钥(Client Params),用“Client Key Exchange”消息发给服务器。
- 客户端和服务器手里都拿到了密钥交换算法的两个参数(Client Params、Server Params),使用ECDHE算法算出“Pre-Master”,其实也是一个随机数。
- 然后双方拿着 client random, server random, pre-master 生成一个主密钥。用于会话加密的。
- 客户端发一个“Change Cipher Spec”,然后再发一个“Finished”消息,把之前所有发送的数据做个摘要,再加密一下,让服务器做个验证。
- 服务器也是同样的操作,发“Change Cipher Spec”和“Finished”消息,双方都验证加密解密 OK,握手正式结束。
RAS握手
大体的流程没有变,只是“Pre-Master”不再需要用算法生成,而是客户端直接生成随机数,然后用服务器的公钥加密,通过“Client Key Exchange”消息发给服务器。服务器再用私钥解密,这样双方也实现了共享三个随机数,就可以生成主密钥。
RSA和ECDHE算法实现握手的区别
服务器不需要通过Server key Exchange消息发送生成的公钥,pre-master是客户端直接通过RAS算法的公钥加密后生成的。
如何验证证书的有效性
浏览器需要验证证书的有效期、证书是否被 CA 吊销、证书是否是合法的 CA 机构颁发的。
验证证书的有效期。只需要查看当前时间是否在证书的有效期内就行。
验证数字证书是否被吊销了。通常有两种方式,一种是下载吊销证书列表 -CRL (Certificate Revocation Lists),第二种是在线验证方式 -OCSP (Online Certificate Status Protocol) 。
验证证书的合法性
- 首先浏览器读取证书中相关的明文信息,采用 CA 签名时相同的 Hash 函数来计算并得到信息摘要 A;
- 然后再利用对应 CA 的公钥解密签名数据,得到信息摘要 B;
- 对比信息摘要 A 和信息摘要 B,如果一致,则可以确认证书是合法的。
上面我们提到了利用CA的公钥来解密签名,那浏览器是如何获取到CA公钥的呢?
通常,当你部署 HTTP 服务器的时候,除了部署当前的数字证书之外,还需要部署 CA 机构的数字证书,CA 机构的数字证书包括了 CA 的公钥,以及 CA 机构的一些基础信息。
有了CA的公钥,那我们怎么知道这个共要是安全的呢?不是恶意的CA机构颁发的呢?
这里就是无限套娃的验证了。直到查找到根CA证书(目前通过 WebTrust 认证的根 CA 有 Comodo、geotrust、rapidssl、symantec、thawte、digicert 等),然后比对操作系统中是否内置的有这个根CA,如果有,浏览器就会认为使用者的证书是合法的。
浏览器缓存
如果缓存生效,那么将不会向服务器请求数据,直接使用缓存,降低服务器的压力。
强缓存
- expires: GMT格式时间
- cache-control
no-store, no-cache, must-revalidate的区别:
协商缓存
- etag / if-none-match
- modified-since / if-modified-since 协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。
通过如果使用缓存,将返回304 Not Modified。 它用于 If-Modified-Since 等条件请求,表示资源未修改,用于缓存控制。它不具有通常的跳转含义,但可以理解成“重定向已到缓存的文件”(即“缓存重定向”)。
处理响应请求,服务器渲染HTML文件
按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。
- 开始每个子阶段都有其输入的内容;
- 然后每个子阶段有其处理过程;
- 最终每个子阶段会生成输出内容。
构建 DOM 树
为什么要构建 DOM 树呢?这是因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。
DOM 和 HTML 内容几乎是一样的,但是和 HTML 不同的是,DOM 是保存在内存中树状结构,可以通过 JavaScript 来查询或修改其内容。
样式计算(Recalculate Style)
把 CSS 转换为浏览器能够理解的结构
和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。可以通过document.styleSheets查看结构。
转换样式表中的属性值,使其标准化
现有的 CSS 文本转化为浏览器可以理解的结构了,那么接下来就要对其进行属性值的标准化操作。
CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。
计算出 DOM 树中每个节点的具体样式.
样式计算阶段的目的是为了计算出 DOM 节点中每个元素的具体样式,在计算过程中需要遵守 CSS 的继承和层叠两个规则。这个阶段最终输出的内容是每个 DOM 节点的样式,并被保存在 ComputedStyle 的结构内。
这就涉及到 CSS 的继承规则和层叠规则了。
- 首先是 CSS 继承。CSS 继承就是每个 DOM 节点都包含有父节点的样式。
- css层叠。层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在 CSS 处于核心地位,CSS 的全称“层叠样式表”正是强调了这一点。
布局阶段
现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。
创建布局树
你可能注意到了 DOM 树还含有很多不可见的元素,比如 head 标签,还有使用了 display:none 属性的元素。所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树。
布局计算
现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了。
在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容和输出内容区分开来。
分层
现在我们有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?不可以。
因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。 通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
元素应该满足什么样的条件,才会创建一个新的图层呢?
- 第一点,拥有层叠上下文属性的元素会被提升为单独的一层。
页面是个二维平面,但是层叠上下文能够让 HTML 元素具有三维概念,这些 HTML 元素按照自身属性的优先级分布在垂直于这个二维平面的 z 轴上。即z-index。
- 第二点,需要剪裁(clip)的地方也会被创建为图层。剪切这个是广义上的。比如多余的文字来控制怎么展示。即overflow属性。
图层绘制
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制。
渲染引擎实现图层的绘制会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。
栅格化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。
当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。
有可能一个图层很大,一个屏幕展示不下,那么在未看见的部分如果也渲染了,那么就产生很大的开销。基于这个原因。合成线程会将图层划分为图块。 这些图块的大小通常是 256x256 或者 512x512。
然后合成线程会按照视口附近的图块来优先生成位图, 实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
合成和显示
光栅化就是按照绘制列表中的指令生成图片。每一个图层都对应一张图片,合成线程有了这些图片之后,会将这些图片合成为“一张”图片,并最终将生成的图片发送到后缓冲区。这就是一个大致的分层、合成流程。
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。
需要重点关注的是,合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。这就是为什么经常主线程卡住了,但是 CSS 动画依然能执行的原因。
总结
- 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
- 创建布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,并将其提交到合成线程。
- 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
- 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
- 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。
断开连接,TCP的四次挥手
为什么建立连接是三次握手,而关闭连接却是四次挥手呢
这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,自己也未必全部数据都发送给对方了,所以自己可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,自己的ACK和FIN一般都会分开发送。
为什么客户端发送ACK之后不直接关闭,而是要等一阵子才关闭
客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。如果不等待,客户端直接跑路,当服务端还有很多数据包要给客户端发,且还在路上的时候,若客户端的端口此时刚好被新的应用占用,那么就接收到了无用数据包,造成数据包混乱。
为什么TIME_WAIT状态需要经过2MSL(最大报文生存时间)才能返回到CLOSE状态
理论上,四个报文都发送完毕,就可以直接进入CLOSE状态了,但是可能网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
- 为了确保最后一个确认报文能够到达,未到达,则重发。
- 为了使服务器发送的数据都在网络上消失,确保下一个连接不会出现旧连接的报文。