最后更新于
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
缓存,如果缓存命中,我们返回结果,即目标 IP
为 MAC
,如果缓存没有命中,则
- 查看路由表,看看目标
IP
地址是不是在本地路由表中的某个子网内,是的话,使用跟那个子网相连的接口,否则使用与默认网关相连的接口 - 查询选择的网络接口的
MAC
地址 - 我们发送一个二层( OSI 模型 中的数据链路层)
ARP
请求
1 | // ARP Request: |
根据连接主机和路由器的硬件类型不同,可以分为几种情况,但是我们这里只看最简单的一种,也就是和路由器是直接连接的方式(其他方式可以参考原文),此时路由器会返回一个 ARP Reply
1 | // ARP Reply: |
现在我们有了 DNS
服务器或者默认网关的 IP
地址,我们可以继续 DNS
请求了
- 使用
53
端口向DNS
服务器发送UDP
请求包,如果响应包太大,会使用TCP
协议 - 如果
ISP DNS
服务器没有找到结果,它会发送一个递归查询请求,一层一层向高层DNS
服务器做查询,直到查询到起始授权机构,如果找到会把结果返回
它流程大致是下面这样的
- 根域(拿到
com
域) com
域DNS
服务器(拿到baidu.com
)baidu.com
的DNS
服务器(域名的注册商提供,万网,新网等)- 结果发送给运营商的
DNS
服务器(就拿到了baidu.com
这个域名对应的IP
地址) - 结果返回操作系统内核,同时缓存起来(当然,这个缓存可能会失效,有时间长短 )
- 内核从服务器上拿到这个
IP
地址,就把这个结果返回给浏览器 - 最终浏览器拿到了
www.baidu.com
对应的IP
地址
使用套接字
当浏览器得到了目标服务器的 IP
地址,以及 URL
中给出来端口号(HTTP
协议默认端口号是 80
, HTTPS
默认端口号是 443
),它会调用系统库函数 socket
,请求一个 TCP
流套接字,对应的参数是 AF_INET/AF_INET6
和 SOCK_STREAM
- 这个请求首先被交给传输层,在传输层请求被封装成
TCP segment
,目标端口会被加入头部,源端口会在系统内核的动态端口范围内选取 TCP segment
被送往网络层,网络层会在其中再加入一个IP
头部,里面包含了目标服务器的IP
地址以及本机的IP
地址,把它封装成一个IP packet
- 这个
TCP packet
接下来会进入链路层,链路层会在封包中加入frame
头部,里面包含了本地内置网卡的MAC
地址以及网关(本地路由器)的MAC
地址,像前面说的一样,如果内核不知道网关的MAC
地址,它必须进行ARP
广播来查询其地址
到了现在,TCP
封包已经准备好了,可以使用下面的方式进行传输
这里我们就不探讨传输方式的差异,以比较常见的光纤或以太网为例,在这种情况下信号一直是数字的,会被直接传到下一个 网络节点 进行处理
最终封包会到达管理本地子网的路由器,在那里出发,它会继续经过自治区域的边界路由器,其他自治区域,最终到达目标服务器,一路上经过的这些路由器会从 IP
数据报头部里提取出目标地址,并将封包正确地路由到下一个目的地,IP
数据报头部 time to live
(TTL
)域的值每经过一个路由器就减 1
,如果封包的 TTL
变为 0
,或者路由器由于网络拥堵等原因封包队列满了,那么这个包会被路由器丢弃
上面的发送和接受过程在 TCP
连接期间会发生很多次(也就是经典的三次握手与四次挥手的流程)
- 客户端选择一个初始序列号(
ISN
),将设置了SYN
位的封包发送给服务器端,表明自己要建立连接并设置了初始序列号 - 服务器端接收到
SYN
包,如果它可以建立连接- 服务器端选择它自己的初始序列号
- 服务器端设置
SYN
位,表明自己选择了一个初始序列号 - 服务器端把(客户端
ISN + 1
)复制到ACK
域,并且设置ACK
位,表明自己接收到了客户端的第一个封包
- 客户端通过发送下面一个封包来确认这次连接
- 自己的序列号
+ 1
- 接收端
ACK + 1
- 设置
ACK
位
- 自己的序列号
- 数据通过下面的方式传输
- 当一方发送了
N
个Bytes
的数据之后,将自己的SEQ
序列号也增加N
- 另一方确认接收到这个数据包(或者一系列数据包)之后,它发送一个
ACK
包,ACK
的值设置为接收到的数据包的最后一个序列号
- 当一方发送了
- 关闭连接时
- 要关闭连接的一方发送一个
FIN
包 - 另一方确认这个
FIN
包,并且发送自己的FIN
包 - 要关闭的一方使用
ACK
包来确认接收到了FIN
- 要关闭连接的一方发送一个
HTTP 协议
如果浏览器使用 HTTP
协议而不支持 SPDY
协议,它会向服务器发送这样的一个请求
1 | GET / HTTP/1.1 |
其他头部当中包含了一系列的由冒号分割开的键值对,它们的格式符合 HTTP
协议标准,它们之间由一个换行符分割开来,HTTP/1.1
定义了关闭连接的选项 close
,发送者使用这个选项指示这次连接在响应结束之后会断开,例如
1 | Connection: close |
不支持持久连接的 HTTP/1.1
应用必须在每条消息中都包含 close
选项,在发送完这些请求和头部之后,浏览器发送一个换行符,表示要发送的内容已经结束了,服务器端返回一个响应码,指示这次请求的状态,响应的形式是这样的
1 | 200 OK |
然后是一个换行,接下来有效载荷(payload
),也就是 www.baidu.com
的 HTML
内容,服务器下面可能会关闭连接,如果客户端请求保持连接的话,服务器端会保持连接打开,以供之后的请求重用
如果浏览器发送的 HTTP
头部包含了足够多的信息(例如包含了 Etag
头部),以至于服务器可以判断出,浏览器缓存的文件版本自从上次获取之后没有再更改过,服务器可能会返回这样的响应
1 | 304 Not Modified |
这个响应没有有效载荷,浏览器会从自己的缓存中取出想要的内容,在解析完 HTML
之后,浏览器和客户端会重复上面的过程,直到 HTML
页面引入的所有资源(Images
,CSS
,favicon.ico
等等)全部都获取完毕,如果 HTML
引入了 www.baidu.com
域名之外的资源,浏览器会回到上面解析域名那一步,按照下面的步骤往下一步一步执行,请求中的 Host
头部会变成另外的域名
HTTP 服务器请求处理
HTTPD
(HTTP Daemon
)在服务器端处理请求/响应,最常见的 HTTPD
有 Linux
上常用的 Apache
和 nginx
,以及 Windows
上的 IIS
HTTPD
接收请求- 服务器把请求拆分为以下几个参数
HTTP
请求方法(GET
,POST
,HEAD
,PUT
,DELETE
,CONNECT
,OPTIONS
或者TRACE
)直接在地址栏中输入URL
这种情况下,使用的是GET
方法- 域名
baidu.com
- 请求的路径或页面为
/
(因为我们没有请求baidu.com
下的指定的页面,因此/
是默认的路径)
- 服务器验证
baidu.com
接受GET
方法 - 服务器验证该用户可以使用
GET
方法(根据IP
地址,身份信息等) - 服务器根据请求信息获取相应的响应内容,这种情况下由于访问路径是
/
,所以会访问首页文件 - 服务器会使用指定的处理程序分析处理这个文件
浏览器
当服务器提供了资源之后(HTML
,CSS
,JavaScript
,Images
等),浏览器会执行下面的操作
- 解析,
HTML
,CSS
,JavaScript
- 渲染,会依次执行以下流程
1 | 构建 DOM 树 ==> CSSOM ==> Render Tree ==> 渲染 ==> 布局 ==> 绘制 |
HTML 解析
浏览器渲染引擎从网络层取得请求的文档,一般情况下文档会分成 8kb
大小的分块传输,HTML
解析器的主要工作是对 HTML
文档进行解析,生成解析树
解析树是以 DOM
元素以及属性为节点的树,DOM
是文档对象模型的缩写,它是 HTML
文档的对象表示,同时也是 HTML
元素面向外部(如 JavaScript
)的接口,树的根部是 Document
对象,整个 DOM
和 HTML
文档几乎是一对一的关系
由于 HTML
不能使用常见的自顶向下或自底向上方法来进行分析,所以浏览器创造了专门用于解析 HTML
的解析器,解析算法在 HTML5
标准规范中有详细介绍,算法主要包含了两个阶段,标记化(tokenization
)和树的构建
解析结束之后浏览器开始加载网页的外部资源(CSS
,Images
,JavaScript
文件等),此时浏览器把文档标记为可交互的(interactive
),浏览器开始解析处于异步(deferred
)模式的脚本,也就是那些需要在文档解析完毕之后再执行的脚本,之后文档的状态会变为 complete
,浏览器会触发 load
事件
CSS 解析
- 根据 CSS词法和句法 分析
CSS
文件和<style>
标签包含的内容以及style
属性的值 - 每个
CSS
文件都被解析成一个样式表对象(StyleSheet object
),这个对象里包含了带有选择器的CSS
规则,和对应CSS
语法的对象 CSS
解析器可能是自顶向下的,也可能是使用解析器生成器生成的自底向上的解析器
页面渲染
- 通过遍历
DOM
节点树创建一个渲染树(DOM Tree + CSSOM
,这里是泛指),并计算每个节点的各个CSS
样式值,然后构建每个节点的坐标 - 当存在元素使用
floated
,位置有absolutely
或relatively
属性的时候,会有更多复杂的计算,详见 CSS 2 和 CSS Current Work
GPU 渲染
- 在渲染过程中,图形处理层可能使用通用用途的
CPU
,也可能使用图形处理器GPU
- 当使用
GPU
用于图形渲染时,图形驱动软件会把任务分成多个部分,这样可以充分利用GPU
强大的并行计算能力,用于在渲染过程中进行大量的浮点计算
后期渲染与用户引发的处理
渲染结束后,浏览器根据某些时间机制运行 JavaScript
代码或与用户交互,类似 Flash
和 Java
的插件也会运行,尽管我们请求的主页(/
)里面可能没有,但是这些脚本可以触发网络请求,也可能改变网页的内容和布局,产生又一轮渲染与绘制
如果全部完成以后,至此就把一个完整的页面呈现给了用户