浏览器输入网址到页面展示所经历的过程

最后更新于 2019-12-22

这其实是一个比较久远的面试题,原本针对于这个问题,参考了网上各种博文,查阅了许多资料,然后洋洋洒洒的整理出了好多步骤,自以为大致了解了整个过程,直到看到了 What happens when… 这篇文章以后,我决定把原文内容全部删除掉,直接搬运这篇文章的内容,然后从新的整理成自己比较好理解的方式放到这里,方便自己以后没事来复习复习,内容有所调整,主要是方便自己理解,如果想了解更为详细的流程可以参考原文

问题是这样的,当你在浏览器中输入 baidu.com 并且按下回车之后发生了什么?这个问题乍一眼看上去可能比较简单,发生什么?不就是在浏览器当中显现出对应的页面吗,虽然看上去是这样的,但是其实背后发生的事情却是非常之多的,大致的梳理一下分为以下这些,当然其中抛开了一些比如键盘输入,解析输入值等硬件处理的过程,我们在本章当中主要看的是数据传递过程和浏览器渲染相关部分的内容

  • DNS 查询
  • ARP 过程
  • 使用套接字
  • HTTP 协议
  • HTTP 服务器请求处理
  • 浏览器
  • HTML 解析
  • CSS 解析
  • 页面渲染
  • GPU 渲染
  • 后期渲染与用户引发的处理

DNS 查询

  • 首先浏览器会搜索自身的 DNS 缓存,看看自身的缓存中有没有 baidu.com 这个域名已经缓存的地址,这个缓存的时间大概只有一分钟,如果使用的是 Chrome 浏览器的话可以通过 chrome://net-internals/#dns 来查看浏览过的网站的 DNS 缓存有没有失效
  • 如果浏览器没有找到缓存,或者说这个缓存已经失效,则会检查域名是否在本地 Hosts 里,如果找到,则它会停止搜索,然后解析也会到此结束
  • 如果在 Hosts 文件内也没有找到对应的配置项,那么浏览器就会发起一个 DNS 的系统调用,就会向本地主控 DNS 服务器(也就是你的宽带运营商提供,通常是本地路由器或者 ISP 的缓存 DNS 服务器)发送一条 DNS 查询请求
  • 接下来查询本地 DNS 服务器
  • 如果 DNS 服务器和我们的主机在同一个子网内,系统会按照下面的 ARP 过程对 DNS 服务器进行 ARP 查询
  • 如果 DNS 服务器和我们的主机在不同的子网,系统会按照下面的 ARP 过程对默认网关进行查询

ARP 过程

要想发送 ARP(地址解析协议)广播,我们需要有一个目标 IP 地址,同时还需要知道用于发送 ARP 广播的接口的 MAC 地址,所以首先查询 ARP 缓存,如果缓存命中,我们返回结果,即目标 IPMAC,如果缓存没有命中,则

  • 查看路由表,看看目标 IP 地址是不是在本地路由表中的某个子网内,是的话,使用跟那个子网相连的接口,否则使用与默认网关相连的接口
  • 查询选择的网络接口的 MAC 地址
  • 我们发送一个二层( OSI 模型 中的数据链路层)ARP 请求
1
2
3
4
5
// ARP Request:
Sender MAC: interface:mac:address:here
Sender IP: interface.ip.goes.here
Target MAC: FF:FF:FF:FF:FF:FF (Broadcast)
Target IP: target.ip.goes.here

根据连接主机和路由器的硬件类型不同,可以分为几种情况,但是我们这里只看最简单的一种,也就是和路由器是直接连接的方式(其他方式可以参考原文),此时路由器会返回一个 ARP Reply

1
2
3
4
5
// ARP Reply:
Sender MAC: target:mac:address:here
Sender IP: target.ip.goes.here
Target MAC: interface:mac:address:here
Target IP: interface.ip.goes.here

现在我们有了 DNS 服务器或者默认网关的 IP 地址,我们可以继续 DNS 请求了

  • 使用 53 端口向 DNS 服务器发送 UDP 请求包,如果响应包太大,会使用 TCP 协议
  • 如果 ISP DNS 服务器没有找到结果,它会发送一个递归查询请求,一层一层向高层 DNS 服务器做查询,直到查询到起始授权机构,如果找到会把结果返回

它流程大致是下面这样的

  • 根域(拿到 com 域)
  • comDNS 服务器(拿到 baidu.com
  • baidu.comDNS 服务器(域名的注册商提供,万网,新网等)
  • 结果发送给运营商的 DNS 服务器(就拿到了 baidu.com 这个域名对应的 IP 地址)
  • 结果返回操作系统内核,同时缓存起来(当然,这个缓存可能会失效,有时间长短 )
  • 内核从服务器上拿到这个 IP 地址,就把这个结果返回给浏览器
  • 最终浏览器拿到了 www.baidu.com 对应的 IP 地址

使用套接字

当浏览器得到了目标服务器的 IP 地址,以及 URL 中给出来端口号(HTTP 协议默认端口号是 80HTTPS 默认端口号是 443),它会调用系统库函数 socket ,请求一个 TCP 流套接字,对应的参数是 AF_INET/AF_INET6SOCK_STREAM

  • 这个请求首先被交给传输层,在传输层请求被封装成 TCP segment,目标端口会被加入头部,源端口会在系统内核的动态端口范围内选取
  • TCP segment 被送往网络层,网络层会在其中再加入一个 IP 头部,里面包含了目标服务器的 IP 地址以及本机的 IP 地址,把它封装成一个 IP packet
  • 这个 TCP packet 接下来会进入链路层,链路层会在封包中加入 frame 头部,里面包含了本地内置网卡的 MAC 地址以及网关(本地路由器)的 MAC 地址,像前面说的一样,如果内核不知道网关的 MAC 地址,它必须进行 ARP 广播来查询其地址

到了现在,TCP 封包已经准备好了,可以使用下面的方式进行传输

这里我们就不探讨传输方式的差异,以比较常见的光纤或以太网为例,在这种情况下信号一直是数字的,会被直接传到下一个 网络节点 进行处理

最终封包会到达管理本地子网的路由器,在那里出发,它会继续经过自治区域的边界路由器,其他自治区域,最终到达目标服务器,一路上经过的这些路由器会从 IP 数据报头部里提取出目标地址,并将封包正确地路由到下一个目的地,IP 数据报头部 time to liveTTL)域的值每经过一个路由器就减 1,如果封包的 TTL 变为 0,或者路由器由于网络拥堵等原因封包队列满了,那么这个包会被路由器丢弃

上面的发送和接受过程在 TCP 连接期间会发生很多次(也就是经典的三次握手与四次挥手的流程)

  • 客户端选择一个初始序列号(ISN),将设置了 SYN 位的封包发送给服务器端,表明自己要建立连接并设置了初始序列号
  • 服务器端接收到 SYN 包,如果它可以建立连接
    • 服务器端选择它自己的初始序列号
    • 服务器端设置 SYN 位,表明自己选择了一个初始序列号
    • 服务器端把(客户端 ISN + 1)复制到 ACK 域,并且设置 ACK 位,表明自己接收到了客户端的第一个封包
  • 客户端通过发送下面一个封包来确认这次连接
    • 自己的序列号 + 1
    • 接收端 ACK + 1
    • 设置 ACK
  • 数据通过下面的方式传输
    • 当一方发送了 NBytes 的数据之后,将自己的 SEQ 序列号也增加 N
    • 另一方确认接收到这个数据包(或者一系列数据包)之后,它发送一个 ACK 包,ACK 的值设置为接收到的数据包的最后一个序列号
  • 关闭连接时
    • 要关闭连接的一方发送一个 FIN
    • 另一方确认这个 FIN 包,并且发送自己的 FIN
    • 要关闭的一方使用 ACK 包来确认接收到了 FIN

HTTP 协议

如果浏览器使用 HTTP 协议而不支持 SPDY 协议,它会向服务器发送这样的一个请求

1
2
3
4
GET / HTTP/1.1
Host: baidu.com
Connection: close
[其他头部]

其他头部当中包含了一系列的由冒号分割开的键值对,它们的格式符合 HTTP 协议标准,它们之间由一个换行符分割开来,HTTP/1.1 定义了关闭连接的选项 close,发送者使用这个选项指示这次连接在响应结束之后会断开,例如

1
Connection: close

不支持持久连接的 HTTP/1.1 应用必须在每条消息中都包含 close 选项,在发送完这些请求和头部之后,浏览器发送一个换行符,表示要发送的内容已经结束了,服务器端返回一个响应码,指示这次请求的状态,响应的形式是这样的

1
2
200 OK
[响应头部]

然后是一个换行,接下来有效载荷(payload),也就是 www.baidu.comHTML 内容,服务器下面可能会关闭连接,如果客户端请求保持连接的话,服务器端会保持连接打开,以供之后的请求重用

如果浏览器发送的 HTTP 头部包含了足够多的信息(例如包含了 Etag 头部),以至于服务器可以判断出,浏览器缓存的文件版本自从上次获取之后没有再更改过,服务器可能会返回这样的响应

1
2
304 Not Modified
[响应头部]

这个响应没有有效载荷,浏览器会从自己的缓存中取出想要的内容,在解析完 HTML 之后,浏览器和客户端会重复上面的过程,直到 HTML 页面引入的所有资源(ImagesCSSfavicon.ico 等等)全部都获取完毕,如果 HTML 引入了 www.baidu.com 域名之外的资源,浏览器会回到上面解析域名那一步,按照下面的步骤往下一步一步执行,请求中的 Host 头部会变成另外的域名

HTTP 服务器请求处理

HTTPDHTTP Daemon)在服务器端处理请求/响应,最常见的 HTTPDLinux 上常用的 Apachenginx,以及 Windows 上的 IIS

  • HTTPD 接收请求
  • 服务器把请求拆分为以下几个参数
    • HTTP 请求方法(GETPOSTHEADPUTDELETECONNECTOPTIONS 或者 TRACE)直接在地址栏中输入 URL 这种情况下,使用的是 GET 方法
    • 域名 baidu.com
    • 请求的路径或页面为 /(因为我们没有请求 baidu.com 下的指定的页面,因此 / 是默认的路径)
  • 服务器验证 baidu.com 接受 GET 方法
  • 服务器验证该用户可以使用 GET 方法(根据 IP 地址,身份信息等)
  • 服务器根据请求信息获取相应的响应内容,这种情况下由于访问路径是 /,所以会访问首页文件
  • 服务器会使用指定的处理程序分析处理这个文件

浏览器

当服务器提供了资源之后(HTMLCSSJavaScriptImages 等),浏览器会执行下面的操作

  • 解析,HTMLCSSJavaScript
  • 渲染,会依次执行以下流程
1
构建 DOM 树 ==> CSSOM ==> Render Tree ==> 渲染 ==> 布局 ==> 绘制

HTML 解析

浏览器渲染引擎从网络层取得请求的文档,一般情况下文档会分成 8kb 大小的分块传输,HTML 解析器的主要工作是对 HTML 文档进行解析,生成解析树

解析树是以 DOM 元素以及属性为节点的树,DOM 是文档对象模型的缩写,它是 HTML 文档的对象表示,同时也是 HTML 元素面向外部(如 JavaScript)的接口,树的根部是 Document 对象,整个 DOMHTML 文档几乎是一对一的关系

由于 HTML 不能使用常见的自顶向下或自底向上方法来进行分析,所以浏览器创造了专门用于解析 HTML 的解析器,解析算法在 HTML5 标准规范中有详细介绍,算法主要包含了两个阶段,标记化(tokenization)和树的构建

解析结束之后浏览器开始加载网页的外部资源(CSSImagesJavaScript 文件等),此时浏览器把文档标记为可交互的(interactive),浏览器开始解析处于异步(deferred)模式的脚本,也就是那些需要在文档解析完毕之后再执行的脚本,之后文档的状态会变为 complete,浏览器会触发 load 事件

CSS 解析

  • 根据 CSS词法和句法 分析 CSS 文件和 <style> 标签包含的内容以及 style 属性的值
  • 每个 CSS 文件都被解析成一个样式表对象(StyleSheet object),这个对象里包含了带有选择器的 CSS 规则,和对应 CSS 语法的对象
  • CSS 解析器可能是自顶向下的,也可能是使用解析器生成器生成的自底向上的解析器

页面渲染

  • 通过遍历 DOM 节点树创建一个渲染树(DOM Tree + CSSOM,这里是泛指),并计算每个节点的各个 CSS 样式值,然后构建每个节点的坐标
  • 当存在元素使用 floated,位置有 absolutelyrelatively 属性的时候,会有更多复杂的计算,详见 CSS 2CSS Current Work

GPU 渲染

  • 在渲染过程中,图形处理层可能使用通用用途的 CPU,也可能使用图形处理器 GPU
  • 当使用 GPU 用于图形渲染时,图形驱动软件会把任务分成多个部分,这样可以充分利用 GPU 强大的并行计算能力,用于在渲染过程中进行大量的浮点计算

后期渲染与用户引发的处理

渲染结束后,浏览器根据某些时间机制运行 JavaScript 代码或与用户交互,类似 FlashJava 的插件也会运行,尽管我们请求的主页(/)里面可能没有,但是这些脚本可以触发网络请求,也可能改变网页的内容和布局,产生又一轮渲染与绘制

如果全部完成以后,至此就把一个完整的页面呈现给了用户

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×