浏览器请求资源的过程发生了什么

当我们在浏览器地址栏输入像’www.google.com’这样的网址时,按下Enter键后一段时间后就能看到网页呈现在我们面前,其实中间发生了很多事情,大致的流程是这样的:

  • DNS域名解析
  • 建立TCP连接(三次握手)
  • 发起HTTP请求
  • 获取响应结果
  • 浏览器解析HTML,获取其他静态资源
  • 浏览器页面渲染
  • 断开链接(四次挥手)

DNS域名解析

DNS解析过程其实就是查询域名与IP映射的过程,在网络上,IP才是计算机的唯一(通讯)标识,之所以会出现域名,是因为它对于用户来说方便记忆并且有比IP更高的可用性。既然底层的通信是通过ip来进行的,那么我们输入url后,势必就需要一个中间人来帮我们进行域名ip之间的转换,这样我们才可能进行后续的步骤,这个中间人就是DNS解析。

域名解析过程

在分析解析过程之前,我们需要域名的组成方式:

组成部分 说明 例子
根域 位于域名的末尾,用句号(.)表示,表明最高级别的层次结构 .
顶级域 用来指示某个国家/地区或组织使用的名称的类型名称 .com
二级域 个人或组织在Internet上使用的注册名称 google.com
子域 已注册的二级域名的派生域名,即网站名 www.google.com

域名服务器分类

这里有几种不同的域名服务器分类:

  • 根域名服务器:管理顶级域名,会告诉本地服务器顶级域名服务器的ip
  • 顶级域名服务器:管理二级域名,与根服务器类似
  • 权限域名服务器:负责一个区的域名服务器
  • 本地域名服务器:主机发出的dns请求会先被发送到本地域名服务器

查询方式

1 主机向本地域名服务器的查询一般都是递归式查询。递归查询:主机向本地服务器询问要查询域名对应的ip时,如果本地服务器不知道,便代替用户向根域名服务器发起请求。

2 本地域名服务器向根域名服务器的查询是迭代查询。迭代查询:当根服务器收到本地服务器的请求时,要么给出所要查询的ip地址,要么告诉本地服务器向下一级的域名服务器去查询。本地域名服务器再向顶级域名服务器发起请求,结果类似,查询不到的情况下继续向权限服务器发起请求,最终将结果返回给主机

综上所述:域名的解析过程类似下面:

.->.com->google.com->www.google.com

DNS负载均衡

现实生活中,淘宝双十一每分钟的请求量是无比巨大的,如果用户请求的都是同一台服务器的话,那么这对服务器的性能要求是非常之高的。但实际上,用户并不关心获取数据的来源,他们只关注更好的服务和更快更流畅的体验,这时我们就可以根据每台机器的负载量,该机器离用户地理位置的距离来给用户分配一个合适的服务器IP,这就叫做DNS负载均衡,又称DNS重定向,开发中经常涉及到的CDN就是利用了这个原理。

反向代理的原理和DNS负载均衡很像,都是为了解决负载均衡。

建立TCP连接

由于HTTP是一个无状态要求可靠传输的协议,所以我们需要建立TCP而不是UDP链接。
TCP在建立链接的时候,需要经过三次握手:

  • client先发送一个(SYN=1,seq=client_seq_num)标志的数据包给接收方
  • server接收后,回传一个(SYN=1,ack=client_seq+1,seq=server_seq_num)标志的数据包进行确认
  • client再回传一个(SYN=0,ack=server_seq+1)标志的数据包来表示握手成功

TCP对于由于各种问题而丢失的数据包会进行重传,这让用户能够接收到完整且正确的信息,这也是选用TCP而不是UDP的原因!

浏览器发送http请求

三次握手建立tcp连接完毕后,就是给服务器发送http请求了。
http请求报文包括三个部分:

  • 起始行
  • 首部
  • 主体

起始行

起始行中包括了请求方式,资源路径,http协议版本三部分
EXP: GET index.html HTTP/1.1

常见的方法有:GET,POST,PUT,DELETE

首部

首部在请求报文中又称请求报头,里面包含客户端自身的信息和向服务器发送的附加信息。

常见的请求报头有: Accept, Accept-Charset, Accept-Encoding, Accept-Language, Content-Type, Authorization, Cookie, User-Agent等。
Accept用于指定客户端用于接受哪些类型的信息,Accept-Encoding与Accept类似,它用于指定接受的编码方式。Connection设置为Keep-alive用于告诉客户端本次HTTP请求结束之后并不需要关闭TCP连接,这样可以使下次HTTP请求使用相同的TCP通道,节省TCP连接建立的时间。

主体

主体在请求报文中又称请求正文,对于GET方法来说他是空的,对于POST,PUT等方法来说,里面是要向服务器发送的数据,请求包头中有些字段与它有关,例如当主体中的数据格式为json时,这是要设置content-type为application/json。

返回响应报文

与请求报文类似,响应报文也由三部分组成:

  • 状态码
  • 响应报头
  • 响应正文

状态码

状态码都是3位的数字,百位上的数字代表了响应的类别,有5中可能的取值:

  • 1**:信息性状态码
  • 2**:成功状态码
    • 200:OK 请求正常处理
    • 204:No Content请求处理成功,但没有资源可返回
    • 206:Partial Content对资源的某一部分的请求
  • 3**:重定向状态码
    -301:Moved Permanently 永久重定向
    -302:Found 临时性重定向
    -304:Not Modified 缓存中读取
  • 4**:客户端错误状态码
    • 400:Bad Request 请求报文中存在语法错误
    • 401:Unauthorized需要有通过Http认证的认证信息
    • 403:Forbidden访问被拒绝
    • 404:Not Found无法找到请求资源
  • 5**:服务器错误状态码
    • 500:Internal Server Error 服务器端在执行时发生错误
    • 503:Service Unavailable 服务器处于超负载或者正在进行停机维护

响应报头

常见字段:server,connection

响应正文

请求返回的文本信息(资源),如html,css,js,图片等文件就存放在里面。

浏览器解析HTML,获取其他静态资源

浏览器对于页面的解析时至上而下的,通过解析html来构建DOM树,当解析到<link>标签或@import时,就会请求服务器获取css文件,在下载的同时浏览器还是会继续向下解析的,但当下载js文件和执行它时,解析器便会停止手头的工作,等待js的操作完成后再向下解析,这便是js的阻塞问题,也是为什么<link>标签可以放在<head>中,而引入的js文件最好放在</body>前面的原因,这样可以避免js阻塞了html的解析而导致页面短时间内无法呈现在用户面前的尴尬情况。

html5中提供了defer和async来实现js外联的无阻塞加载

<link>和@imoprt的区别:

  • link是XHTML标签,除了加载CSS外,还可以定义RSS等其他事务;@import属于CSS范畴,只能加载CSS
  • link引用CSS时,在页面载入时同时加载;@import需要页面网页完全载入以后加载
  • link是XHTML标签,无兼容问题;@import是在CSS2.1提出的,低版本的浏览器不支持
  • ink支持使用Javascript控制DOM去改变样式;而@import不支持

浏览器渲染页面

渲染树(render树)

前面已经说过,解析html的时候会生成DOM树,而解析css则会生成CSSOM树,前者描述内容,后者描述应用与内容的样式规则。

DOM树和CSSOM结合在一起会构成一棵渲染树,渲染树既包含了页面上所有的可视DOM节点,又包含了CSSOM中每个节点的样式信息。

渲染树的构建步骤:

  • 从DOM树的根节点开始,遍历所有的可视节点,不可视节点有:

    • 脚本标签,元数据标签
    • 应用display:none的元素
  • 对于可视节点,从CSSOM中找到对应的样式规则,附加在节点上

  • 输出可视节点以及每个节点计算出来的样式

布局

通过渲染树,浏览器已经能知道可视内容的样式信息了,但是真正要渲染时,我们还需要获取节点的位置和尺寸,这是布局阶段要做的工作,也成为“回流”(reflow).

布局阶段的输出结果成为“盒模型”(box model),盒模型精确表达了窗口中元素的位置和大小,所有相对的度量单位都会被转化为屏幕上的绝对像素位置。

当以上步骤都完成后,浏览器就能把节点绘制成屏幕上每个真实的像素点了,此阶段为“绘制”或者“重绘”(resterizing)

其实从这里也能引出一个概念了:

回流必定导致重绘,重绘不一定导致回流^_^!

页面渲染的过程中至少会发生一次reflow和repaint,reflow的开销相对与repaint要高得多。一般来说如果一个元素的尺寸发生了改变,会对后面的已渲染的页面造成影响,那么就需要重新计算布局,即回流,而如果只是改变了外观的话,那么只需要进行重绘即可。

举个通用的例子来说明一下重绘与回流:

  1. 用户输入网址(假设是个html页面,并且是第一次访问),浏览器向服务器发出请求,服务器返回html文件;
  2. 浏览器开始载入html代码,发现<head>标签内有一个<link>标签引用外部CSS文件;
  3. 浏览器又发出CSS文件的请求,服务器返回这个CSS文件;
  4. 浏览器继续载入html中<body>部分的代码,并且CSS文件已经拿到手了,可以开始渲染页面了;
  5. 浏览器在代码中发现一个<img>标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续渲染后面的代码;
  6. 服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码;
  7. 浏览器发现了一个包含一行JavaScript代码的<script>标签,赶快运行它;
  8. javascript脚本执行了这条语句,它命令浏览器隐藏掉代码中的某个<div> (style.display=”none”)。杯具啊,突然就少了这么一个元素,浏览器不得不重新渲染这部分代码;
  9. 终于等到了</html>的到来,浏览器泪流满面……
  10. 等等,还没完,用户点了一下界面中的“换肤”按钮,Javascript让浏览器换了一下<link>标签的CSS路径;
  11. 浏览器召集了在座的各位<div><span><ul><li>们,“大伙儿收拾收拾行李,咱得重新来过……”,浏览器向服务器请求了新的CSS文件,重新渲染页面

对于重绘回流的优化以及js阻塞的解决会在另一篇博客提及。

断开链接(四次挥手)

当浏览器获取到了所有想要的资源并且用户没有发起新的请求之前,为了不让tcp空耗着,我们会选择断开这个tcp链接,断开的主动方可以时服务器也可以是客户端。

假设有client发起终端请求,则过程是这样的:

  • client端发起FIN报文,告诉server端如果数据没发送完,可以不急着关闭socket,继续发送
  • server端收到FIN报文后,回传一个确认包并让client等待(因为自己要确认是否已经发送完数据了),client进入FIN_WAIT状态
  • server确认自己已经发送完所有数据了,可以真正关闭链接了,向client发送FIN
  • client收到FIN后回传一个ACK并进入TIME_WAIT状态
  • server接收到ACK后关闭链接
  • client在2MSL(报文最大生存时间,4分钟)后如果没有收到server发过来的FIN包,证明server端已经关闭成功,那么自己也可以关闭了

需要四次挥手的原因:

当一方主动发起FIN请求是,可能另一方的数据还没发送完,故此只能返回一个ACK让他先等待,只有当自己确认已经发送完所有的数据后才会发起一个FIN来并当接收到一个回传的ACK时结束这个连接,俗称四次挥手。