输入URL后发生了什么

参考:李兵 浏览器工作原理与实践

​ 神三元博客

浏览器检查

浏览器做初步的格式化检查,构建请求行信息,构建好之后,浏览器准备发起网络请求

GET /index.html HTTP1.1

查找缓存

在发起真正的网络请求之前,浏览器会先在浏览器缓存中查找是否存在请求的资源文件,如果浏览器发现请求的资源在浏览器缓存中存有副本,则拦截请求,浏览器判断这些请求参数,击中强缓存就直接返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载,否则就把请求参数加到请求头,准备在网络请求时中传给服务器,去判断协商缓存

准备IP地址和端口

在了解准备IP地址和端口之前,先看看HTTP和TCP的关系,因为浏览器使用HTTP协议作为应用层协议用来封装请求的文本信息;并使用TCP/IP 作传输层协议将它发到网络上,所以在 HTTP 工作开始之前,浏览器需要通过 TCP 与服务器建立连接。也就是说HTTP 的内容是通过 TCP 的传输数据阶段来实现的


DNS解析(递归一个服务器跑一个来回,就需要1次RRT)

02

  1. 首先查找本地的hosts缓存文件是否有这个url地址的IP映射

  2. 如果本地hosts缓存文件没有,浏览器向本地域名服务器发起请求(本地dns服务器由运营商提供)

  3. 如果本地域名服务器查找本地缓存后,如果有对应的IP映射会直接返回对应映射

  4. 如果没有本地域名服务器没有对应的IP映射,本地域名服务器会向根域名服务器发出请求

    img

    根域名服务器,只保存其下一级域名服务器,下一级服务器主要有com、net,、org、mil、或者国家域名比如cn。

    Com服务器用于管理域名后缀为“.com”的域名,比如google.com, cisco.com。其它的域名解析以此类推。

  5. 根DNS服务器收到请求后会判断这个域名(.com)是哪个顶级域名服务器来授权管理,并会返回一个负责该顶级域名服务器的一个IP,本地DNS服务器收到IP信息后,将会联系负责.com域的这台顶级域名服务器。

  6. 如果这个顶级域名服务器也没有对应的IP映射,则顶级域名服务器(.com)会提供负责管理这个域名的下一级DNS域名服务器(google.com)的IP地址给本地DNS服务器,本地DNS服务器收到IP信息后,将会联系负责的这台DNS域名服务器。

  7. DNS域名服务器收到请求后,查找自己的映射表,如果有对应的映射,将返回对应的IP地址给本地DNS服务器。

  8. 本地DNS服务器对应的IP地址后,本地dns服务器对域名的解析服务器进行请求,从而得到一个ip地址和域名的对应关系,并存储在缓存文件中。

  9. 如果经过DNS递归查询之后仍找不到对应的IP映射,则报错,表示无法查询到所需的IP地址

img

递归:客户端只发一次请求,要求对方给出最终结果。

迭代:客户端发出一次请求,对方如果没有授权回答,它就会返回一个能解答这个查询的其它名称服务器列表,

​ 客户端会再向返回的列表中发出请求,直到找到最终负责所查域名的名称服务器,从它得到最终结果。

获取端口号

拿到 IP 之后,接下来就需要获取端口号了。通常情况下,如果 URL 没有特别指明端口号,那么 HTTP 协议默认是 80 端口。(HTTPS协议默认端口为443,FTP默认端口为21,Telnet默认端口为23)

等待TCP队列

当我们获得网络请求第一步——TCP连接的条件(IP地址和端口号),还需要等待浏览器的TCP队列为空闲状态才能建立TCP链接

建立TCP连接

TCP提供一种可靠的传输,这个过程涉及到三次握手,四次挥手,下面我们详细看看 TCP提供一种面向连接的,可靠的字节流服务。 其首部的数据格式如下

在这里插入图片描述

字段分析

  • 源端口:源端口和IP地址的作用是标识报文的返回地址。
  • 目的端口:端口指明接收方计算机上的应用程序接口。

TCP报头中的源端口号目的端口号同IP数据报中的源IP目的IP唯一确定一条TCP连接。

  • 序号:是TCP可靠传输的关键部分。序号是该报文段发送的数据组的第一个字节的序号。在TCP传送的流中,每一个字节都有一个序号。比如一个报文段的序号为300,报文段数据部分共有100字节,则下一个报文段的序号为400。所以序号确保了TCP传输的有序性
  • 确认号:即ack,指明下一个期待收到的字节序号,表明该序号之前的所有数据已经正确无误的收到。例:A向B发送数据报文段,B需给A发送一个收到确认报文段来告知A已收到其发来的数据报文段。在B给A发送的确认报文段中,确认号为501,即表明序号1-500的字节已成功收到,接下来期望收到从A发来的序号为501的字节。
  • 首部长度/数据偏移:占4位,它指出TCP报文的数据距离TCP报文段的起始处有多远。由于首部可能含有可选项内容,因此TCP报头的长度是不确定的,报头不包含任何任选字段则长度为20字节,4位首部长度字段所能表示的最大值为1111,转化为10进制为15,15*32/8=60,故报头最大长度为60字节。首部长度也叫数据偏移,是因为首部长度实际上指示了数据区在报文段中的起始偏移值。
  • 保留:占6位,保留今后使用,但目前应都位0。
  • 控制位:URG ACK PSH RST SYN FIN,共6个,每一个标志位表示一个控制功能。
    • 紧急URG:当URG=1,表明紧急指针字段有效。告诉系统此报文段中有紧急数据
    • 确认ACK:仅当ACK=1时,确认号字段才有效。TCP规定,在连接建立后所有报文的传输都必须把ACK置1。
    • 推送PSH:当两个应用进程进行交互式通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应,这时候就将PSH=1。
    • 复位RST:当RST=1,表明TCP连接中出现严重差错,必须释放连接,然后再重新建立连接。
    • 同步SYN:在连接建立时用来同步序号。当SYN=1,ACK=0,表明是连接请求报文,若同意连接,则响应报文中应该使SYN=1,ACK=1。
    • 终止FIN:用来释放连接。当FIN=1,表明此报文的发送方的数据已经发送完毕,并且要求释放。
  • 窗口:滑动窗口大小,用来告知发送端接受端的缓存大小,以此控制发送端发送数据的速率,从而达到流量控制。窗口大小时一个16bit字段,因而窗口大小最大为65535。
  • 校验和:奇偶校验,此校验和是对整个的 TCP 报文段,包括 TCP 头部和 TCP 数据,以 16 位字进行计算所得。由发送端计算和存储,并由接收端进行验证。
  • 紧急指针:只有当 URG 标志置 1 时紧急指针才有效。紧急指针是一个正的偏移量,和顺序号字段中的值相加表示紧急数据最后一个字节的序号。 TCP 的紧急方式是发送端向另一端发送紧急数据的一种方式。
  • 选项和填充:最常见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size),每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志为1的那个段)中指明这个选项,它表示本端所能接受的最大报文段的长度。选项长度不一定是32位的整数倍,所以要加填充位,即在这个字段中加入额外的零,以保证TCP头是32的整数倍。
  • 数据部分: TCP 报文段中的数据部分是可选的。在一个连接建立和一个连接终止时,双方交换的报文段仅有 TCP 首部。如果一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据。在处理超时的许多情况中,也会发送不带任何数据的报文段。

需要注意的是:

(A)不要将确认序号ack与标志位中的ACK搞混了。 (B)确认方ack=发起方Seq+1,两端配对。

三次握手

第一次握手

客户端发送syn包(Seq=x)到服务器,并进入SYN_SEND状态,等待服务器确认;

第二次握手

服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(Seq=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手

客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。

三次握手

为什么会采用三次握手,若采用二次握手可以吗? 四次呢?

三次握手的目的是确认双方发送接收的能力

为什么两次握手不行?

根本原因: 无法确认客户端的接收能力。

分析如下:如果是两次,你现在发了 SYN 报文想握手,但是这个包滞留在了当前的网络中迟迟没有到达,TCP 以为这是丢了包,于是重传,两次握手建立好了连接。

看似没有问题,但是连接关闭后,如果这个滞留在网路中的包到达了服务端呢?这时候由于是两次握手,服务端只要接收到然后发送相应的数据包,就默认建立连接,但是现在客户端已经断开了。

看到问题的吧,这就带来了连接资源的浪费。

可以是四次握手吗?

当然可以,100 次都可以。但为了解决问题,三次就足够了,再多用处就不大了。

大家应该知道通信中著名的蓝军红军约定, 这个例子说明, 通信不可能100%可靠, 而上面的三次握手已经做好了通信的准备工作, 再增加握手, 并不能显著提高可靠性, 而且也没有必要。

发送HTTP请求

一旦建立了 TCP 连接,浏览器就可以和服务器进行通信了。而 HTTP 中的数据正是在这个通信过程中传输的。

首先浏览器会向服务器发送请求行它包括了请求方法、请求 URI(Uniform Resource Identifier)和 HTTP 版本协议

URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。 URL是uniform resource locator,统一资源定位器,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何定位到这个资源(协议)。

最常用的请求方法是GETPOST,如果要通过POST方法发送信息给服务器,那么浏览器还要准备数据给服务器,这里准备的数据是通过请求体来发送。

在浏览器发送请求行命令之后,还要以请求头形式发送其他一些信息,把浏览器的一些基础信息告诉服务器。比如包含了浏览器所使用的操作系统、浏览器内核等信息,以及当前请求的域名信息、浏览器端的 Cookie 信息,等等。

服务器处理HTTP请求

跟发送相同的,首先服务器会返回响应行,包括HTTP协议版本状态码

但并不是所有的请求都可以被服务器处理的,那么一些无法处理或者处理出错的信息,服务器会通过请求行的状态码来告诉浏览器它的处理结果。

而响应体就是我们后续浏览器进行渲染的HTML,CSS,JS文件

浏览器渲染

准备渲染进程

上述都是浏览器主进程网络进程干的活了,当接收到http响应头中的content-type类型(text/html)时就开始准备渲染进程了

默认情况下,Chrome 会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。但是,也有一些例外,在多个页面同属于一个根域名下,浏览器会让多个页面直接运行在同一个渲染进程中。

具体地讲,我们将“同一站点”定义为根域名(例如,baidu.com)加上协议(例如,https:// 或者 http://),还包含了该根域名下的所有子域名和不同的端口,比如下面这三个: https://baidu.com https://image.baidu.com https://baike.baidu.com 它们都是属于同一站点,因为它们的协议都是 HTTPS,而且根域名也都是baidu.com Chrome 的默认策略是,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫 process-per-site-instance。

比如我这打开了4个页面都是运行在同一个渲染进程下,进程ID同为14932

渲染流程

服务器HTTP请求返回的响应体的文件(HTML,CSS,JS)浏览器并读不懂,

1、构建DOM树

HTML文件需要将这些文件转换成浏览器能够理解的结构——DOM树

2、样式计算

根据CSS文件进行样式计算,转换成浏览器能够理解的结构——styleSheets(所谓的CSSOM),这一步还需要转换样式表中的属性值(标准化),比如一些rem,em需要根据对应的字体大小进行等值转换,比如一些颜色值,将会转换成RGB的格式

再将标准化后的styleSheets结合到DOM树上,这里涉及CSS的层叠规则继承规则

比如一份标准化后的样式表为

body { font-size: 20px }
p {color:blue;}
span  {display: none}
div {font-weight: bold;color:red}
div  p {color:green;}

结合到上面的DOM树上计算出来的每个节点的具体样式为

比如这个掘金,可以看到是由多个图层叠加在一起产生的界面

布局树和图层树之间的关系类似于

但是并不是每一个布局树上的节点都对应拥有图层树上的一层,如果对应节点没有在图层树上有对应的层级,则该节点归属于父节点的图层上

那布局树上的节点需要具备什么条件才能在图层树上拥有一个图层呢?

满足以下两个条件之一即可

第一点,拥有层叠上下文属性的元素会被提升为单独的一层

页面是个二维平面,但是层叠上下文能够让 HTML 元素具有三维概念,这些 HTML 元素按照自身属性的优先级分布在垂直于这个二维平面的 z 轴上。

明确定位属性的元素(position)、定义透明属性的元素(opacity)、使用 CSS 滤镜的元素等(filter),都拥有层叠上下文属性。

第二点,需要剪裁(clip)的地方也会被创建为图层(overflow)

<style>
      div {
            width: 200;
            height: 200;
            overflow:auto;
            background: gray;
        } 
</style>
<body>
    <div >
        <p> 所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p>
        <p> 从上图我们可以看到,document 层上有 A 和 B 层,而 B 层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p>
        <p> 图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p> 
    </div>
</body>

这里定义一个div为200*200像素,而div内的文字内容比较多,文字所显示的区域肯定会超出 200 * 200 的面积,这时候就产生了剪裁,渲染引擎会把裁剪文字内容的一部分用于显示在 div 区域

出现这种裁剪情况时,渲染引擎会把被裁剪部分独立创造一个图层,如果出现滚动条,滚动条也会被提升为单独的层。

4、产生绘制列表

渲染引擎实现图层的绘制,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,

绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。

5、栅格化(raster)操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。

如上图所示,当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程

通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。

在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。

基于这个原因,合成线程会将图层划分为图块(tile)

6、重绘重排

可以看到,重绘的时候由于没有改变元素的集合信息,所以在渲染过程中,会直接跳过布局阶段

重排(更新了元素的几何属性(宽高)),重绘(更新了元素的绘制属性(颜色))

重排一定会重绘,重绘不一定会重排

断开TCP链接

通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接。不过如果浏览器或者服务器在其头信息中加入了:

Connection:Keep-Alive 

那么 TCP 连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个 TCP 连接发送请求。保持 TCP 连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。比如,一个 Web 页面中内嵌的图片就都来自同一个 Web 站点,如果初始化了一个持久连接,你就可以复用该连接,以请求其他资源,而不需要重新再建立新的 TCP 连接。

如果要关掉,那就在浏览器或者服务器的头信息中加入:

Connection:close

四次挥手

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,假设客户端主动关闭,服务器被动关闭。

img

第一次挥手:主动关闭方(客户端)—>被动关闭方(服务器)

客户端:我不给你发了

客户端发送一个FIN = 1,用来关闭客户端到服务器的数据传送,也就是客户端告诉服务器:我已经不会再给你发数据了(当然,在FIN包之前发送出去的数据,如果没有收到对应的ACK确认报文,客户端依然会重发这些数据),但是,此时客户端还可以接受数据FIN=1,其序列号为seq=p(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

第二次挥手:服务器—>客户端

服务器:我知道啦,但是我有可能要给你发东西,你等一下

服务器收到FIN包后,发送一个ACK = 1给对方并且带上自己的序列号seq,ack为p+1(与SYN相同,一个FIN占用一个序号)。此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

第三次挥手:服务器—>客户端

服务器:我也不给你发了

服务器发送一个FIN,用来关闭服务器到客户端的数据传送,也就是告诉客户端,我的数据也发送完了,不会再给你发数据了。由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=q,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

第四次挥手:客户器—>服务端

客户端:我听着先,服务器你确定没话说了吧,我等你2MSL,等完我就关闭了

主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB(Transmission Control Block 传输控制块)后,才进入CLOSED状态。

同一个设备对应一个IP,每与一个应用程序进行数据传输,都需要一个不被占用的端口去进行通信,这难免会出现在同一时刻,设备可能会产生多种数据需要分发给不同的设备,为了确保数据能够正确分发,TCP用一种叫做TCB,也叫传输控制块的数据结构把发给不同设备的数据封装起来,TCB中包含了**发送方和接收方的socket信息(大概就是IP和端口)**和**数据的缓冲区**,从而保证同步通信能够不出错

服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

至此,完成四次挥手。

为什么客户端最后还要等待2MSL?

最长报文寿命MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。

如果不等待会怎样?

如果不等待,客户端直接跑路,当服务端还有很多数据包要给客户端发,且还在路上的时候,若客户端的端口此时刚好被新的应用占用,那么就接收到了无用数据包,造成数据包混乱。所以,最保险的做法是等服务器发来的数据包都死翘翘再启动新的应用。

那,照这样说一个 MSL 不就不够了吗,为什么要等待 2 MSL?

  • 1 个 MSL 确保四次挥手中主动关闭方最后的 ACK 报文最终能达到对端
  • 1 个 MSL 确保对端没有收到 ACK 重传的 FIN 报文可以到达
  • 如果直到2MSL之后,客户端都没有再次收到FIN,那么客户端推断ACK已经被成功接收,则结束TCP连接。

这就是等待 2MSL 的意义。

为什么建立连接是三次握手,关闭连接确是四次挥手呢?

因为服务端在接收到FIN, 往往不会立即返回FIN, 必须等到服务端所有的报文都发送完毕了,才能发FIN。因此先发一个ACK表示已经收到客户端的FIN,延迟一段时间才发FIN。这就造成了四次挥手。

如果是三次挥手会有什么问题?

等于说服务端将ACKFIN的发送合并为一次挥手,这个时候长时间的延迟可能会导致客户端误以为FIN没有到达客户端,从而让客户端不断的重发FIN