浏览器工作原理:从URL输入到页面展现到底发生了什么

# 从输入 URL 到页面加载完成的过程中都发生了什么?
# 简单路径线
# 1. 键盘或触碰输入 URL 并回车确认
# 2. URL 解析/DNS 解析查找域名 IP 地址
# 3. 网络连接发起 HTTP 请求
# 4. HTTP 报文传输过程
# 5. 服务器接收数据
# 6. 服务器响应请求/MVC
# 7. 服务器返回数据
# 8. 客户端接收数据
# 9. 浏览器加载/渲染页面
# 10. 打印绘制输出
# 详细路径线
# 1.键盘或触碰输入 URL 并回车确认
实际上,在此之前有准备工作。服务器启动了操作系统,然后开启了 http 服务进程(daemon),可能是(Apache/Nginx/IIS/Lighttpd)中的一个。http 服务开始定位到服务器上的 www 文件夹(网站根目录),一般位于/var/www,然后启动一些附属模块,如 php 或使用 fastcgi 方式连接到 php 的 fpm 管理进程,接着向操作系统申请一个 tcp 连接并绑定在 80 端口,调用 accept 函数,开始默默监听来自地球任何一个地方的请求,随时准备做出响应。
然后,开始键盘或手机触屏输入 URL,通过某种机制传到 CPU,CPU 进行内部处理后,再传到操作系统内核,然后由操作系统 GUI 传到浏览器,再到浏览器内核。
在 GUI 将输入事件传递到浏览器从过程中。浏览器可能会做一些预处理,他会从历史记录,书签等地方,智能匹配所有有可能的 URL。对于 Chrome,他甚至会直接从缓存中把网页渲染处理,再或者在浏览器启动时预先查询 10 个你有可能访问的域名等
# 2.URL 解析/DNS 查询
完整的 URL 组成:协议、网络地址、资源路径、文件名、动态参数
DNS 解析域名的两种方式:递归查询和迭代查询 递归查询
浏览器缓存 -> 系统缓存(本地 hosts 文件) -> 上层路由器缓存 -> ISP DNS 缓存(负载均衡)-> 本地名称服务器 -> 权威名称服务器 -> 顶级名称服务器 -> 根名称服务器
根名称服务器是互联网域名解析系统 DNS 中最高级别的域名服务器,全球一共 13 组,每组都只有一个主根名称服务器采用同一个 IP(注意不是 13 个,前期是个现在是集群),好多用于负载均衡、备份等,全球有 386 台根物理服务器,被编号 A 到 M 共 13 个标号
迭代查询 
DNS 递归查询和迭代查询的区别
- 递归查询:以本地名称服务器为中心,是 DNS 客户端和服务器间的查询活动,查询的递交者一直在更替,其结果是直接告诉 DNS 客户端需查询的网站目标 IP 地址
- 迭代查询:以 DNS 客户端为中心,是各个服务器和服务器间的查询活动,查询的递交者一直没变化,其结果是间接告诉 DNS 客户端另一个 DNS 服务器的地址

# 3.应用层客户端发送 HTTP 请求
互联网内各网络设备间的通信都遵循 TCP/IP 协议,会通过分层顺序与对方进行通信。分层由高到低:应用层、传输层、网络层、数据链路层。
发送端从应用层往下走,接收端从数据链路层往上走,如图:

得到 IP 地址后,浏览器会开始构造一个 HTTP 请求,应用层客户端向服务端发送 HTTP 请求:请求报头(request header)和请求主体,请求头包括请求的方法,目标 url,遵循的协议,返回的信息是否缓存,以及客户端是否发送 cookie 等信息。因 HTTP 请求是纯文本格式,所以 TCP 的数据段中可以直接分析 HTTP 文本的。
# 4.传输层 TCP 传输报文
当应用层的 HTTP 请求准备好后,TCP 协议通过三次握手后,将已经分割成报文段的数据包传输

由于 TCP 的 Head-of-line blocking 问题:如客户端发 3 个 TCP 片段,结果 1 包传丢了,因 TCP 协议需保证顺序,所以其它包到达也只能等待,在 HTTP pipelining 下更严重, HTTP pipelining 可以让多个 HTTP 请求通过一个 TCP 发送。为解决此问题,Chrome 团队提出 QUIC 协议,它基于 UDP 实现的可靠传输,能减少很多来回(round trip)时间,还有前向纠错码等功能,可通过chrome://net-internals/#spdy (opens new window)页面来发现。
# 5.网络层 IP 协议查询 MAC 地址
IP 协议的作用是把 TCP 分割好的各种数据包封装到 IP 包里面传送个接收方,而确保传到和传对还需接收方的 MAC 地址(物理地址)。 一个网络设备的 IP 地址可以更换,但 MAC 地址一般是固定不变的。ARP 协议将 IP 地址解析成对应的 MAC 地址。当通信双方不在同一局域网,需多次中转,中转过程需通过下一个中转站的 MAC 地址搜索下一个中转目标。
# 6.数据到达数据链路层
在找到对方的 MAC 地址后,被封装好的 IP 包在被封装到数据链路层的数据帧结构中,将数据发送到数据链路层传输,再通过物理层的比特流送出去
# 7.服务器接收数据
接收端的服务器在链路层接收到数据包,再层层向上到应用层。
# 8.服务器响应请求并返回响应文件
服务接收到客户端发送的 HTTP 请求后,服务器上的的 http 监听进程会得到这个请求,然后一般情况下会启动一个新的子进程去处理这个请求,同时父进程继续监听。http 服务器首先会查看重写规则,然后如果请求的文件是真实存在,例如一些图片,或 html、css、js 等静态文件,则会直接把这个文件返回,如果是一个动态的请求,那么会根据 url 重写模块的规则,把这个请求重写到一个 rest 风格的 url 上,然后根据动态语言的脚本,来决定调用什么类型的动态文件脚本解释器来处理这个请求。
# 9.浏览器开始处理数据信息并渲染页面
浏览器会根据返回响应报文里的状态码,做判断。
- 200:请求成功,直接进入渲染流程
- 300:要去相应头里面找 location 域,进行跳转。这里跳转需开启一个跳转计数器,避免多个页面间形成循环跳,当跳转多次后,浏览器会报错
- 301:永久重定向,即请求的资源永久转移到新位置
- 400/500:浏览器会出一错误页面
当浏览得到一个正确的 200 响应之后,接下来面临的一个问题就是多国语言的编码解析了,响应头是一个 ascii 的标准字符集的文本,这个还好办,但是响应的正文本质上就是一个字节流,对于这一坨字节流,浏览器要怎么去处理呢?首先浏览器会去看响应头里面指定的 encoding 域,如果有了这个东西,那么就按照指定的 encoding 去解析字符,如果没有的话,那么浏览器会使用一些比较智能的方式,去猜测和判断这一坨字节流应该使用什么字符集去解码。相关的笔记可以看这里,字符集编码
接下来就是构建 dom 树了,在 html 语言嵌套正常而且规范的情况下,这种 xml 标记的语言是比较容易的能够构建出一棵 dom 树出来的,当然,对于互联网上大量的不规范的页面,不同的浏览器应该有自己不同的容错去处理。构建出来的 dom 本质上还是一棵抽象的逻辑树,构建 dom 树的过程中,如果遇到了由 script 标签包起来的 js 动态脚本代码,那么会把代码送到 js 引擎里面去跑,如果遇到了 style 标签包围起来的 css 代码,也会保存下来,用于稍后的渲染。如果遇到了 img 或 css 和 js 等引用外部文件的标签,那么浏览器会根据指定的 url 再次发起一个新的 http 请求,去把这个文件拉取回来,值得一提的是,对于同一个域名下的下载过程来说,浏览器一般允许的并发请求是有限的,通常控制在两个左右,所以如果有很多的图片的话,一般出于优化的目的,都会把这些图片使用一台静态文件的服务器来保存起来,负责响应,从而减少主服务器的压力。
dom 树构造好了之后,就是根据 dom 树和 css 样式表来构造 render 树了,这个才是真正的用于渲染到页面上的一个一个的矩形框的树,网页渲染是浏览器最复杂、最核心的功能,对于 render 树上每一个框,需要确定他的 x y 坐标,尺寸,边框,字体,形态,等等诸多方面的东西,render 树一旦构建完成,整个页面也就准备好了,可以上菜了。需要说明的是,下载页面,构建 dom 树,构建 render 树这三个步骤,实际上并不是严格的先后顺序的,为了加快速度,提高效率,让用户不要等那么久,现在一般都并行的往前推进的,现代的浏览器都是一边下载,下载到了一点数据就开始构建 dom 树,也一边开始构建 render 树,构建了一点就显示一点出来,这样用户看起来就不用等待那么久了