11

概览

日期:2018-4-26
目标:了解从输入URL到页面加载完成的过程中都发生了什么事情
总用时:一天
完成情况:达成

基本过程

为什么会想要了解从输入URL到页面加载完成的过程中都发生了什么事情这个问题呢,因为课程参考资料的Web 建站技术中HTML、HTML5、XHTML、CSS、SQL、JavaScript、PHP、ASP.NET、Web Services 是什么中最高票答案中给出了下图所示的网站访问基本过程,张秋怡学姐的解答也十分易懂:

clipboard.png

再者这个问题可谓是常见的面试题之一,而这张图中只是给出了非常基本的一个前后端交互的过程,由于自己有基础,所以列出的相关概念也都基本理解了,于是就花些时间扩展一下

跟我一起来学起来

  • 我们在打开浏览器,然后在输入URL的时候有没有发现浏览器会给你一些你似曾相识且与你输入的内容相匹配的网址呢?

    clipboard.png

    其实我们在浏览器中输入URL的时候,浏览器就会开始智能的匹配可能URL,浏览器会从历史记录,书签等地方,找到你已经输入的字符串可能对应的URL,然后给出智能提示

  • 在输好URL后我们会按下Enter键,浏览器会发起请求,如果URL是域名而不是IP地址,将进行域名解析,所谓域名解析是指什么呢?

    IP地址是网络上标识站点的数字地址,为了方便记忆,采用域名来代替IP地址标识站点地址,域名解析就是域名到IP地址的转换过程。

    域名解析按下面的步骤进行(部分内容涉及到计算机网络知识):

    • 我们本地硬盘下有一个hosts(windows下路径为C:\Windows\System32\drivers\etc)文件,作用是将一些常用的网址域名与其对应的IP地址建立一个关联“数据库”。一般来说,系统会首先自动从hosts文件中寻找对应的IP地址,如果有的话就直接使用hosts文件里面的IP地址,然后直接进行端口确认
    • 如果上一步没有找到,浏览器将调用解析程序,并成为DNS服务器的一个客户,把待解析的域名放在DNS请求报文中,以UDP用户数据报的方式发给本地DNS服务器
    • 如果本地DNS服务器查找到相应的域名的IP地址,就把对应的IP地址放在回答报文中返回
    • 如果上一步没有找到,即本地DNS服务器不知道被查询域名的IP地址,由于主机向本地DNS服务器的查询是递归查询,所以此时,本地DNS服务器就会以DNS客户的身份向其他DNS服务器继续发出查询请求报文。本地DNS服务器向根DNS服务器的查询是迭代查询,当找到相应域名的IP地址后,就会把这个结果返回给最初发起查询请求的浏览器

      递归查询:在该模式下DNS服务器接收到客户机请求,必须返回一个准确的查询结果给客户机。如果该DNS服务器本地没有存储被查询的DNS信息,那么该服务器会(替客户机)询问其他服务器,并将返回的查询结果再返回给客户机。
      迭代查询:在该模式下DNS服务器接收到客户机请求,如果该DNS服务器本地没有存储被查询的DNS信息,DNS服务器会向客户机提供其他能够解析查询请求的DNS服务器地址,让客户机再向这台DNS服务器提交请求,依次循环直到返回查询的结果为止。
    • 经过上面的步骤后,浏览器已经获得输入域名的IP地址,可以进行下一步了。
  • 浏览器得到IP地址后,还要确认一下端口,默认端口是80端口,一个服务器可能会提供不同的服务,这些服务通过端口来区分,可以指定端口号
  • 浏览器得到IP地址并确认端口后,会向目标服务器发起HTTP请求,HTTP请求是通过TCP连接来发送的(如果是HTTPS则需要先建立SSL连接,再是TCP连接,下面的讨论基于HTTP),具体如下

    • 浏览器会生成目标服务器的HTTP请求报文,请求报文一般包含请求方法、请求URI、协议版本、请求首部字段等内容,HTTP请求准备好后,HTTP请求报文从应用层传到传输层后会被分割为报文段,并会发起一条到达目标服务器的TCP连接,开始TCP三次握手,过程如图所示:

      clipboard.png

      通俗的可以理解为:

      A主动向B打电话:嗨,能听到吗(SYN=1,seq=x),然后A就开始等待B的回答(SYN-SENT状态),此时A不知道B能不能听到
      B听到A的话之后,可以确认它能听到A,但是它还要确认一下A能不能听到他自己的声音,于是B说:我能听到你的声音(ACK=1,ack=x+1),你能听到我的声音吗(SYN=1,seq=y),然后B开始等待A的恢复(SYN-RECD状态)
      A听到B的话之后,A可以确认两件事,一是B能听到它说话,二是它也能听到B说话,A已经可以随时说话和倾听了(ESTABLISHED状态)。但是此时的B还在等待中,并不知道A能不能听到,所以此时A需要再回复B说:我可以听到你的声音(ACK=1,ack=y+1),开始愉快的聊天吧~(seq=x+1),B听到这句话后便也可以随时说话和倾听了(ESTABLISHED状态)
      之后两个人就可以balabalabala....
    • HTTP请求的请求报文是直接附在第三次握手的消息中
    • 穿插补充小知识,为什么是三次握手,而不是两次四次?

      有一种观点是三次握手是基于TCP协议的可靠性(Reliability)要求,这是确认双发都能进行收发的最小次数,两次确认不了,四次多余。但是并没有完全意义上的可靠,不论握手多少次都只能表明握手的时候是可靠的,不能保证后面数据传输时一直可靠,因为信道是不可靠的,当然三次握手至少可以表明它曾经可靠,这是两次握手无法完成的,而四次甚至更多次握手仅仅是提高“它曾经可靠”这个结论的可信程度。所以这个握手也只是确保可靠的一个基本需要,TCP协议的可靠性(注意区分完整性integrity)更多的是由校验和、定时器超时重传、确认机制

      在《计算机网络》一书中也有讲过这个问题,给出的解释是:三次握手是为了防止失效的连接请求报文段被服务端接收,从而产生错误。具体例子如下所述:
      client发出的一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个的连接请求。于是就向client发出确认报文段,同意建立连接。
      假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。但是由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。而server却以为新的连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。
      采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接

  • 连接建立之后,开始进行数据传输,虽然浏览器知道目标服务器的IP和端口,但是数据总不可能飞过去吧?HTTP请求报文段会从传输层传到网络层,在网络层被封装成IP数据包,网络层规定了通过怎样的路径(所谓的传输路线)到达目标服务器,并把数据包传送给对方。
  • 网络层封装好的IP数据包会进一步传到下一层 --- 数据链路层,然后会再次被封装到MAC数据帧结构中,由于IP地址间的通信依赖于MAC地址(网卡所属的固定地址),所以MAC数据帧结构中会有经过ARP协议解析后的MAC地址(不一定是目标服务器的MAC地址,因为实际上通信的双方在同一局域网(LAN)内的情况是很少的,一般都会经过路由中转)。
  • 数据链路层的MAC数据帧再向下传,便会到达物理层,这里要注意物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流,而不是指具体的传输媒体。 物理层需要确保原始的数据可在各种物理媒体上传输,它规定了传输媒体的机械特性、电气特性、功能特性、过程特性:

    clipboard.png

    常见的传输媒体有双绞线、电缆、光缆、无线信道等,物理层的任务就是要让数据在这些传输媒体上都能能进行传输

  • 通过MAC地址匹配,数据通过传输媒体到达目标服务器的物理层,物理层接收数据比特流然后向上传送到服务器的数据链路层,在数据链路层MAC数据帧将进行封装的逆操作,还原成IP数据包之后向上传送到网络层,网络层也进行封装的逆操作还原成HTTP请求报文段(分割后的一小段一小段的),然后这些报文段向上传到传输层,在传输层按原来的序号重新组装成完整的HTTP请求报文,再向上传到应用层,应用层的HTTP协议便会开始对请求进行处理
  • 这个处理可能是直接返回静态的资源,也可能经过PHPJAVA等语言进行处理等,等处理完成后,会返回一个HTTP响应,它生成一个HTTP响应报文,与HTTP请求报文结构类似,然后这个响应报文会“走过”请求报文来时的路到达浏览器
  • 浏览器接收HTTP响应,然后有可能释放TCP连接,也有可能重新使用这个TCP连接发送新的请求(持久连接),此处了解一下TCP连接的释放,不同于TCP连接建立的三次握手,TCP连接的释放是四次挥手,客户端和服务器端都可以发起关闭请求,也存在两者同时发起关闭请求的情况,图中为客户端A主动发起关闭请求:

    clipboard.png

    同样通俗的解释一波:

    A对B要传的文件已经传完了,于是他对B说:我要传的文件已经传完了,我要准备下线了(seq=u,FIN=1)。然后A就等待B的回复(FIN-WAIT-1状态)
    B看到A的消息后,回复A说:知道了,但是我还有文件给你(ACK=1,ack=u+1,seq=v)。B进入等他文件传完的状态(CLOSE-WAIT状态)。
    A收到B的回复之后,下线不了了,于是继续等待着B的文件传完(FIN-WAIT-2状态)
    几分钟后,B的文件传完了,此时他对A说:我的文件传完了,我也要下线了(seq=w,FIN=1,ACK=1,ack=u+1),然后B等待A的回复来确认真的可以下线了(LAST-ACK状态)
    A收到B的回复后,便对A说:好的,那你下线吧(ACK=1,seq=u+1,ack=w+1)。此时A会等待一段时间(2MSL,TIME-WAIT状态),B收到后就直接下线了(CLOSE状态),然后2MSL时间到了之后,A也下线(CLOSE状态)
    • 为什么服务器B在接到A的断开请求时不立即同意断开?
      当服务器B收到断开连接的请求时,服务器可能仍然有数据未发送完毕,所以服务器先发送确认信号,等所有数据发送完毕后再同意断开
    • 为什么是四次挥手,而不是像建立连接一样的三次
      因为TCP连接是全双工模式,服务器B收到A的断开请求时,仅仅表明A没有东西传给服务器B了,但此时服务器B可能向A的传输还没结束,所以服务器B要先给A一个确认收到A的断开请求的ACK报文,然后继续向A把信息传完,等传完之后服务器B再向A发送断开请求的报文段,等A收到并回复ACK报文后再释放连接。
      也就是说对于A来说他要发送请求给B并等待B确认,对于B来说也要发送请求给A并等待A确认,两者都经过这两个过程才能完全释放TCP连接,而非单方面的释放。
      建立连接只需要建立,没有数据的影响,而释放连接还要考虑数据是否传输完,所以建立连接的时候B确认收到A的建立请求与B发送建立请求这一步可以合成一步成为TCP建立连接的第二次握手,而释放连接时却必须分开。
    • 最后一次握手后A为什么要等2MSL
      首先解释一下MSLMSL是指最长报文段寿命,RFC793建议为两分钟,但实际上可据实际情况而定,也就是说一个报文段最久可存在的时间是MSL

      1. 这是为了保证A发送的最后一个ACK报文能够到达服务器B,如果这个ACK报文丢失了,服务器B没有收到,B会超时重传第三次握手的FIN+ACK报文给A,这个时候处于等待的A就可以收到这个重传的FIN+ACK报文,并再次发送ACK报文给服务器B,并且重新启动2MSL计时器,最终结果是A和B都正常进入CLOSE状态。如果A发完ACK报文后就直接释放了A-->B的连接,那么A就收不到B重传的FIN+ACK报文,也不能重新发送ACK`报文,那么B就无法按正常步骤释放B-->A的连接
      2. 防止“已失效的连接请求报文”出现在下一个新的连接中,因为一个报文段的寿命是MSL,所以A在发送完最后一个ACK报文段之后,再经过时间2MSL,本连接持续的时间内所产生的所有报文段都将在网络中消失,这样这些旧的报文段便不会出现在下一个新的连接中
  • 浏览器之后会检查HTTP的响应状态,主要通过响应码来判断

    1xx: 表示通知信息的,比如请求收到了或正在处理
    2xx:表示成功,操作被成功接收并处理
    3xx:表示重定向,一般完成请求还必须采取进一步的行动
    4xx:表示客户端的差错
    5xx:表示服务器的差错
  • 如果响应可缓存,浏览器将把响应存入缓存
  • 浏览器根据HTTP报头信息解码响应,决定如何处理这些响应,并展现响应,以响应为一个HTML为例
  • 浏览器开始自上而下,自左而右的加载HTML文档,最开始会遇到<!DOCTYPE>声明,然后根据<!DOCTYPE>声明浏览器就知道该用哪种规范来解析这个文档
  • 再继续边加载边解析,边生成DOM树,加载过程中遇到外部CSS文件,浏览器便会另外发出一个请求,来获取CSS文件(过程和上面说的一样),获取CSS后会生成CSS Rule树。DOM树和CSS Rule树生成Render树,页面可以开始边加载边渲染了

    • 渲染树和DOM树的关系:那些不可见的DOM元素(如<head>…</head>display=none的元素)不会被插入渲染树中;还有像一些节点是绝对定位或浮动,这些节点会在文本流之外,因此他们会在渲染树和DOM树的不同位置,渲染树标识出真实的位置,并用一个占位结构标识出他们原来的位置,而DOM树上是他们原来的位置
    • 渲染包含"布局"(layout)和"绘制"(paint)这两个步骤,所谓"布局"是指给出每个DOM节点在浏览器窗口中的准确位置,"绘制"是指遍历Render树将布局好的DOM节点绘制在屏幕上。

      clipboard.png

  • 浏览器继续加载渲染,如果遇到<script>标签,浏览器会立即执行(暂不考虑deferasync属性),此时会出现页面阻塞,不仅要等待文档中JS文件下载加载完毕,还要等待JS解析执行完毕,才可以恢复HTML文档的加载解析。

    • 这是浏览器为了防止出现JS修改DOM树,需要重新构建DOM树的情况,DOM树改变浏览器需要回过头来重新渲染这部分代码,所以浏览器希望通过阻塞其他内容的下载和呈现,来避免出现更多的不必要的Reflow(称为回流或者重排)
    • 如果<script>放在的<head>中,则<body>标签无法被加载,那么页面自然就无法渲染了,因此这将导致在该JS代码完全执行完之前,页面都是一片空白,用户体验非常不好,一般我看到长时间的空白页面,我都非常想直接关闭它。因此会推荐将所有<script>标签尽可能放到<body>标签的底部,以尽量减少对整个页面下载的影响,此时虽然还会存在一个脚本阻塞另一个脚本的问题,但是用户体验比上面的好很多,因为用户看到了大部分内容,而不是空白
    • defer属性相当于告诉浏览器立即下载,延迟执行。它使得加载后续文档元素的过程将和JS文件的加载并行进行(异步),但是JS文件的执行要在整个页面解析完成之后,DOMContentLoaded事件触发之前完成,执行顺序为出现的先后顺序。(高程中指出现实中不一定会按照顺序执行,也不一定会在DOMContentLoaded事件触发之前完成,因此最好只包含一个延迟脚本,这可能是与浏览器的实现有关,具体什么情况下会出现我还不知道???)
    • async属性相当于告诉浏览器立即下载执行,并且页面的加载渲染不需要等待该脚本加载和执行,它们两者会异步进行。标记为async的脚本不会按照它们出现的先后顺序执行,而是谁先下载完了谁就先执行,它们一定会在页面的load事件触发之前执行,但可能会在DOMContentLoaded事件触发之前或之后执行。基于前面所说的一点原因,异步脚本最好不要修改DOM,如果由多个异步脚本,它们之间最好没有依赖关系
  • 浏览器继续加载渲染,如果遇到图片资源,浏览器也会另外发出一个请求,来获取图片资源,这是异步请求,所以不会等到图片下载完,而是继续渲染后面的HTML文档。
  • 等到服务器返回图片文件,如果先前并没有为这个图片设定宽高,那么由于图片占用了一定面积,影响了后面段落的排布,浏览器会进行Reflow
  • 然后然后终于和</html>碰面了,此次的页面加载渲染过程完成,浏览器也是很累了,然后会立即触发DOMContentLoaded事件,该事件是在形成完整的DOM树之后就会触发,而不会理会图像、JS文件、CSS文件或其他资源是否已经下载完毕
  • 当页面完全加载后,也就是所有图像、JS文件、CSS文件等外部资源都加载完成后会触发load事件
  • 用户在页面上进行交互时,可能会导致页面进行RepaintReflow

    • Repaint:如果只是改变了某个元素的背景颜色,文字颜色等,不影响元素周围或内部布局的属性,将只会引起浏览器的Repaint,重绘某一部分
    • Reflow:如果某个部分发生了的变化影响了布局,那浏览器就需要倒回去重新渲染,每次Reflow必然会导致Repaint

尾声

本来只是想了解了解,结果一入深似海,看似简单的操作背后藏着数不清的小动作,文中也只是涉及了一部分,还有很多相关的过程没有涉及到,但是能力有限,还是慢慢来,暂时就先告一段落,文中如有错误还请指正哦~

参考


Cshine
169 声望5 粉丝

前端魔法修炼ing