从输入 URL 到页面展示

本文从网络层以上讨论从浏览器输入 HTTPS 协议的 URL 到页面展现的全过程。由于不同浏览器之间也存在差异,这里以 Chrome 浏览器为例。

1. DNS查询

DNS 缓存有好几个环节,浏览器缓存,系统缓存,路由器缓存,ISP 缓存。

浏览器首先会查看自身是否已经有进行 DNS 缓存。Chrome 可以通过 chrome://net-internals/#dns 查看缓存的 DNS,浏览器的 DNS 缓存可以加快 DNS 解析速度,但缓存时间不会太长。

Chrome 的 DNS 缓存

如果浏览器没有相应的缓存,则查找系统缓存,浏览器会向系统发送一个查询请求,如果系统存在缓存或者设置了 host ,则返回相应的 ip 地址给浏览器。

如果系统没有缓存,那么它会发出一个 DNS 查询请求给路由器。

如果路由器有 DNS 缓存,他会提取出 IP 地址返回。否则,他会向本地域名服务器发出查询,从请求主机到本地域名服务器的请求一般是递归查询,而其他的查询一般是迭代查询。

DNS 请求主机到本地域名服务器的查询

DNS 域名服务器之间的查询

请求报文如下

DNS 请求报文

我们先简单分析下请求报文。

  1. DNS 使用 UDP 协议,端口号53。
  2. 在 DNS 报文的 Flags 中的 RD=1。表示它建议域名服务器以递归方式查询。
  3. Question section format 需要给出 QNAME, QTYPE, QCLASS。即查询的域名,查询的类型以及查询的类。

有关DNS报文的更多信息可以参考RFC1035

响应报文如下

DNS 响应报文

我们也简单分析下,如果想详细了解,可以查看上面的 RFC1035 标准。

  1. 与请求报文相比,对比 Flags 可以发现,QR=1 表示这是一个响应报文。RA=1 表示递归查询可用。
  2. ARecord 记录了DNS请求获得的一个或多个IP地址。一般还会得到 CNAME 记录和存活时间等信息。
  3. 这里对 Rcode 也稍加说明下,Rcode=0 表示成功,他还有好几种状态码,比如1表示 Format error,2表示 Server failure,3表示 Name error,4表示 Not lmplemented,5表示 Refused,具体信息可以参考 RFC1035

2. 三次握手建立连接

经过上述过程,此时浏览器得到了要访问的域名的 IP 地址。由于 DNS 查询需要一定时间,所以有些网站会使用 DNS Prefetching 进行 DNS 预解析,结合浏览器的 DNS 缓存,以加快网站速度。这里不做详细叙述。

首先贴出 TCP 三次握手的建立图。

三次握手

我们抓包依次进行分析。

  1. 第一个握手包

    第一个握手包

    从这个包我们可以得到以下信息:

    • Seq = 2988862522 这个是本报文段所发送的数据的第一个字节的序号

    • DataOffset = 32(字节) 指明 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远

    • SYN = 1 SYN=1 而 ACK=0 时表示这是一个连接请求报文。

    • Window = 8192(字节) 客户端滑动窗口8192字节,滑动窗口协议是设计来提高报文段的传输效率,这里不详细叙述,可以自己查资料。

    • Checksum = 0x5818 校验和,校验和字段校验的范围包括首部和数据这两个部分,在计算校验和时,要在 TCP 报文段的前面加上12字节的伪首部。接收方接收到此报文段后,加上这个伪首部来计算校验和。校验和是用来保证数据的无差错。

    • MaxSegmentSize = 1460(字节) 即 MSS,表示 TCP 报文段中数据字段的最大长度。注意它加上 TCP 首部才等于整个的 TCP 报文。有关 MSS 的信息自己查资料。

    比较重要的就是上述信息,客户端需要在第一次发包时要告知自身的一些信息。

  2. 第二个握手包

    第二个握手包

    在接收到客户端发来的握手包后,服务端进行应答,我们对这个包进行分析:

    • Seq = 1648068786
    • Ack = 2988862523 表示服务端期望收到对方的下一个报文段的第一个数据字节的序号。
    • ACK = 1 当 ACK=1 时确认号字段有效。
    • SYN = 1 ACK=1 且 SYN=1 表示这是一个同意建立连接的响应报文。
    • Window = 63343(字节) 服务端的滑动窗口为63343字节。
    • MaxSegmentSize = 1448(字节) 服务端的 TCP 报文中数据字段的最大长度为1448字节。

    在前两次握手时,双方交换一些信息如 Window 和 MSS,确定 seq 和 ack 起始号。

  3. 第三个握手包

    第三个握手包

    服务端接受请求后,客户端还需要在发送一个握手包。这个握手包明显信息更少了,同样我们查看一些关键值:

    • Seq = 2988862523 由于上一个握手包没有携带正文信息,不占用字节空间,所以 Seq 与上一个握手包的 Ack 值相同。
    • Ack = 1648068787 同上,双方建立通信之后是会进行数据交换,同样的,这个地方表示客户端期望收到对方的下一个报文段的第一个数据字节的序号。
    • ACK = 1 表示对上一个握手包的确认
    • Window = 260(字节) TCP 的发送窗口时会不断变化的,TCP 的流量控制和拥塞控制会根据情况动态地调整发送窗口上限值,从而控制发送数据的平均速率。

自此三次握手完成,连接建立,可以开始传输数据。

3. 建立HTTPS连接

以上完成握手,只能建立 HTTP 连接。但如果网站使用了 HTTPS 协议,那么还需要进行 SSL/TLS 握手。

在这之前,我们先介绍下 SSL/TLS。

SSL/TLS 是一种互联网安全加密技术。HTTP 报文是进行明文传输的,这意味着用户的 cookie 或者其提交的信息比如账号和密码都是在互联网上裸奔,如果这个报文被其他人抓取到,会带来很大的不安全性。使用 HTTPS 协议是非常有必要的,并且目前最新的 HTTP2 规范也仅支持 HTTPS。而 SSL/TLS 协议位于 TCP/IP 协议与各个应用层协议之间,为数据通信提供安全支持。他们可以分为记录协议和握手协议。

那么 SSL 和 TLS 又有什么区别呢?

简单的说,TLS 的建立在 SSL 3.0 协议规范之上的,是 SSL v3 的强化版,在整个协议格式上和 SSL 类似。TLS 增强了加密算法,并带来了更严格的警报,在安全性方面有很多改进。

关于 HTTP 与 HTTPS 和 SSL 与 TLS 的具体差别自行搜索。

SSL/TLS Protocol

关于 SSL/TLS 协议这里不做详解,此处主要讲述 TLS 握手的建立过程和 HTTPS 安全的原因。

我们来看看 TLS 握手的大致过程,这里不再详细叙述每个握手包. TLS 有两种密钥交换方式, 分别为 DH/ECDH 类密钥交换和
RSA 密钥交换。这里说的是 RSA 密钥交换。

TLS-handshake

  • Client Hello

    TLS-handshake-1

    可以看到 TCP 报文的 Push 为1,表示接收方尽快对该报文要尽快交付而不是积累到足够多的数据。

    这是一个 TLS 握手包,我们可以看到 HandShakeType 为 ClientHello ,对应 TLS 握手图的第一步。

    同时,该过程浏览器还发送了自己支持的一套加密规则。

  • Server Hello

服务端接受到上述报文后,会发送 TCP 报文进行确认。报文如下

TLS-handshake-2

服务端发出 ACK 包的同时,发送 TLS 报文,此时 HandShakeType 为 ServerHello,对应 TLS 握手图的第二步。

TLS-handshake-3

接着上述的报文,服务端从客户端发来的加密规则中选出一组加密算法和 HASH 算法,并把自己的身份信息以证书形式发给客户端。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。如图,发送公钥使用了两个报文,最后客户端发送 ACK 确认报文进行确认,注意这个报文和上面的 ACK 报文一样不占用空间,只是进行确认而已。

TLS-handshake-4

  • Client Key Exchange

    浏览器获得证书后,会首先验证证书的合法性,如果证书受信任,则浏览器栏里面会显示一个小绿锁。同时浏览器会生成一串随机数的密码,并用证书中提供的公钥进行加密,然后使用约定好的 HASH 算法计算握手信息,并使用未加密的随机数对消息进行加密。然后将消息重新发给服务端。这一步是加密握手包的开始。

  • Cipher Change Spec

    服务端使用自己的私钥对信息进行解密取出密码(密码被用公钥加密,只有用私钥才能加开,非对称加密),然后用该密码来解密握手消息,然后验证 HASH 是否与浏览器发来的一致。

    接着使用这个密码来加密一段握手信息,发给客户端即浏览器。

  • Finish

    客户端用密码对加密握手包进行解密(注意此时已经变为对称加密),同时计算 HASH 值,若 HASH 值一致,则握手过程结束。之后双方通信都使用之前浏览器生成的密码来进行加密。

至此, HTTPS 连接已成功建立。由于浏览器会对证书进行校验,由证书授权机构保证其有效性,可以保证证书不被篡改。同时浏览器方面生成的随机数密码,一开始只有浏览器知道,而这个密码用公钥加密之后,只有私钥能解开,公钥也无法解开,这是非对称加密。服务器接收到该数据包后解开得到该密码,双方就可以使用该密码通过对称加密传输数据了。

现给出 TCP 三次握手和 TLS 握手的列表信息。

handshake.png

这之后双方都是用 TLS 来传输数据,但不带数据的 ACK 确认包不进行加密,仍然使用 TCP 协议来传输。

需要明确的是,TLS 协议还是建立在 TCP 协议之上的,此处为便于说明将两者区分对待。即 TCP 协议是不进行加密的,TLS 是进行加密的 TCP 协议。

这个过程其实是先进行 HTTP 连接建立的三次握手,然后服务器进行 3xx 重定向,然后再进行 HTTPS 的三次握手。这样既增加了连接建立的开销,而且也可能在 HTTP 连接建立的过程被劫持。所以有出现了 HSTS。

HTTP 严格传输安全(HTTP Strict Transport Security)是一套由互联网工程任务组发布的互联网安全策略机制。网站可以选择使用 HSTS 策略,来让浏览器强制使用 HTTPS 与网站进行通信,以减少会话劫持风险。

我们有时候会看到出现证警告的提醒,使用了 HSTS 之后用户将不再允许忽略警告。但是用户首次访问某个网站是不受 HSTS 保护的,因为首次访问时浏览器还未收到 HSTS ,目前比较流行的方案是浏览器预置 HTST 域名列表,Google Chrome, Firefox, IE, Edge 都实现了这一方案,我们可以在服务器配置相关信息后去进行登记。HSTS 可以抵御 SSL 剥离攻击,它利用用户通过 HTTP 建立连接再重定向到 HTTPS 连接,阻止浏览器与服务器建立 HTTPS 连接,攻击者可以在用户访问 HTTP 页面时替换所有 HTTPS 链接为 HTTP,达到阻止 HTTPS 的目的。

4. 客户端请求网站信息

  1. 如果访问的是网站的主页,那么此时会发送一个请求给服务器,如下:

    request-package

    HTTP 里面附带了 URI, ProtocolVersion, Host, Connection 以及 Cookie 等很多信息。

    这里只讨论正常访问的情况。

  2. 服务端接收到该请求后,验证无误后,首先发送 ACK 报文进行确认。接着对该请求进行处理,如果是静态页面则直接返回。但大多数时候服务端都需要动态生成一个新的页面再返回该页面。这里讨论下 php 的处理。

    假设我们这时候访问的是index.php这个页面,服务器例如 Nginx 知道这个不是静态文件,它回去寻找 PHP 解析器去做处理,这依靠的就是 CGI 来实现。

    下面介绍几个概念,即 CGI, PHP-CGI, FastCGI, PHP-FPM。

    • CGI

      CGI 全称是“公共网关接口”(Common Gateway Interface),HTTP 服务器与服务器上其他程序进行通讯的一种协议。

    • PHP-CGI

      简单的说 PHP-CGI 就是HTTP服务器与 PHP 进行通讯的工具,当浏览器发出一个类似 /index.php 的请求时,HTTP 服务器请求 PHP-CGI 去处理这个请求,PHP-CGI 处理完成后再把处理结果发回给 HTTP 服务器,然后由 HTTP 服务器发给浏览器。

      PHP-CGI 在处理请求之前,需要解析php.ini文件,初始化执行环境,然后才开始处理请求。

    • FastCGI

      FastCGI 全称是“快速通用网关接口”(Fast Common Gateway Interface / FastCGI)是一种让交互程序与 Web 服务器通信的协议。它是 CGI 的增强版本。

      CGI 程序运行在独立的进程中,并对每一个请求建立一个进程,每个建立都要经过解析配置文件和初始化执行环境。这种方法易于实现,但效率很差,难以面对大量请求。

      FastCGI 使用持续的进程来处理一连串的请求。这些进程由 FastCGI 服务器管理,而不是 Web 服务器。它是一个常驻型的 CGI(master),只要激活后就不需要重新去解析 php.ini 和初始化,提高了效率。他在自身初始化后会启动多个 CGI 解析器(worker),当有请求过来时,他会交给其中一个 worker 去处理。同时,它还可以动态调整 worker 的数量。

    • PHP-FPM

      PHP-FPM 是一个 PHP FastCGI 管理器,从 PHP 5.3.3 开始已经集成了 PHP-FPM,在编译安装时可以通过 -enable-fpm 参数开启。

    php-fpm

    所以浏览器如果请求 /index.html 那么 Web 服务器会直接把这个静态页面返回,如果是请求 /index.php,则交由 PHP-FPM 进行处理,PHP-FPM 处理后再把结果返回给 Web 服务器,由 Web 服务器发给浏览器。

    我们来看看响应报文

    response-1

    可以看到除了对浏览器请求的 ACK 确认包之外,服务端发回了10个包。第一个报文里面有StatusCode, Etag, ContentEncoding 等信息。这里面涉及到不少网站优化的技巧,我们将在下一篇中再讨论。这里大概了解下就可以了。

    接下来的9个报文都是跟着第1个报文一起过来的。最后一个报文设置了 PUSH 标志,因为报文已经传输完毕,所以它请求浏览器客户端尽快交付。

    接着客户端会发送 ACK 确认报文,之所以这里客户端对最后一个报文进行了确认,是因为 TCP Delayed Ack 的原因,它是为了避免多次发送 ACK 确认报文而设置的延时,一般为 40ms,也就是说如果接收到一个报文 40ms 后没有接收到第二个报文,他就会对这个报文进行确认。

    服务器发送完全部数据后,最后再发送一个报文,即图中的 #191 报文,表示我已经发完。客户端相应的发出 ACK 确认报文。主页请求结束。

5. 浏览器渲染

浏览器接收到 index.html 后,开始浏览器渲染,这里讨论 webkit 内核,其他内核不讨论。主要分为四步进行。如下图:

html-parse

浏览器渲染不是本文的重点,所以只做简单说明。

  • Parsing HTML to construct the DOM tree

    数据会交给 HTMLDocumentParser,然后 HTMLDocumentParser 将文本字符的解析交给 HTMLDocumentTokenizer 来负责,HTMLDocumentTokenizer 解析出一个一个的标签,然后 HTMLDocumentParser 将标签交给 HTMLTreeBuilder 来构建 DOM 树。

    有些节点需要加载其他资源,比如加载外链 CSS 和图片等。会调用资源加载器进行异步加载,不会阻碍当前 DOM 树的构造。但是如果是非 defer 非 async 的 script 标签,则需要停止当前 DOM 树的构造,直到脚本被加载并被 JavaScript 执行后才继续 DOM 树的构造。

    因此我们有时候会要求 <script> 标签放到最后面,目的是为了不影响前面 DOM 树的构建。

    对于 css 文件,会解析 css 生成 CSS Rule Tree。

    对于 JavaScript` 文件,主要是通过 DOM API 和 CSSOM API 来操作 DOM TREE 和 CSS Rule Tree,从而改变渲染的效果。

  • Render tree construction

    接着浏览器会开始构建渲染树(Rendering Tree)。Renderint Tree 只包括渲染页面需要的节点。

    CSS Rule Tree 会匹配的把 CSS Rule 附加到相应的 Rendering Tree 的元素上面。

    render-tree-construction

  • Layout of the render tree

    到目前为止,我们已经计算了哪些节点是可见的,以及它们的计算样式,但还没有计算它们在设备视窗中的准确位置与尺寸。这就是布局阶段做的事情。

    渲染树的构建、布局与绘制所需的时间取决于文档大小、应用的样式,当然,还有运行文档的设备:文档越大,浏览器要完成的工作就越多;样式越复杂,绘制需要的时间就越长(例如,绘制单色成本较低,而计算、呈现阴影的成本就非常高了)。

  • Painting the render tree

    一旦布局完成,浏览器便会发出 Paint Setup 与 Paint 事件,将渲染树转化为屏幕上的实际像素。

大概可以用下图来表示整个过程。

webkitflow


参考资料