01.网络与资源请求

网络与资源请求

网页加载过程

之前我们我们提到,Tab以外的大部分工作由浏览器进程Browser Process负责,针对工作的不同,Browser Process划分出不同的工作线程:

  • UI thread:控制浏览器上的按钮及输入框;
  • Network thread:处理网络请求,从网上获取数据;
  • Storage thread:控制文件等的访问;

Browser Process 子线程

处理输入

当用户开始在导航栏上面输入内容的时候,UI线程(UI thread)做的第一件事就是询问“你输入的字符串是一些搜索的关键词(search query)还是一个URL地址呢”。因为对于Chrome浏览器来说,导航栏的输入既可能是一个可以直接请求的域名还可能是用户想在搜索引擎(例如Google)里面搜索的关键词信息,所以当用户在导航栏输入信息的时候UI线程要进行一系列的解析来判定是将用户输入发送给搜索引擎还是直接请求你输入的站点资源。

Browser Process 输入响应

开始导航

回车按下后,UI thread将关键词搜索对应的URL或输入的URL交给网络线程Network thread,此时UI线程使Tab前的图标展示为加载中状态,然后网络进程进行一系列诸如DNS寻址,建立TLS连接等操作进行资源请求,如果收到服务器的301重定向响应,它就会告知UI线程进行重定向然后它会再次发起一个新的网络请求。

开始导航

URL解析浏览器会首先判断你输入的是否为有效的URL,还是属于需要传输给搜索引擎的默认搜索关键字。并且浏览器还会检查自带的 “ 预加载HSTS(HTTP严格传输安全)” 列表,这个列表里包含了那些请求浏览器只使用HTTPS进行连接的网站。如果网站在这个列表里,浏览器会使用HTTPS而不是HTTP协议,否则,最初的请求会使用HTTP协议发送。接下来,浏览器会检查你的输入字符是否含有特殊非ASCII关键字,如果含有特殊的字符会进行UTF8编码,部分特殊的网站会要求进行GBK编码。

网络协议分层

DNS解析在URL解析完成之后,浏览器会根据本地的hosts文件或者向本机/网关设置的DNS服务器发起域名解析请求。DNS的解析过程中,浏览器会首先检查本机是否有域名缓存,如果没有的话会向DNS服务器发起请求,如果子DNS服务器不存在该记录则会递归向父层级的DNS发起请求。我之前在进行iOS开发的时候还碰到IPV6的问题,即苹果要求iOS应用能够在IPV6环境下正常运行,那么这个时候DNS服务器发现如果你请求的是IPV6的地址,对于仅有IPV4地址的服务器其会提供一个NAT64的功能,即保证客户端虽然为IPV6地址,也能和IPV4的服务器正常通信。

读取响应

Network thread接收到服务器的响应后,开始解析HTTP响应报文,然后根据响应头中的Content-Type字段来确定响应主体的媒体类型(MIME Type

响应的头部有 Content-Type 信息,而响应的主体有真实的数据

如果媒体类型是一个HTML文件,则将响应数据交给渲染进程(renderer process)来进行下一步的工作,如果是zip文件或者其它文件,会把相关数据传输给下载管理器。

网络线程在询问响应的数据是不是来自安全源的 HTML 文件

与此同时,浏览器会进行Safe Browsing安全检查,如果域名或者请求内容匹配到已知的恶意站点,network thread会展示一个警告页。除此之外,网络线程还会做CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至渲染进程。

寻找一个渲染进程(renderer process)

各种检查完毕以后,Network thread确信浏览器可以导航到请求网页,Network thread会通知UI thread数据已经准备好,UI thread会查找到一个renderer process进行网页的渲染。

查找渲染进程

浏览器为了对查找渲染进程这一步骤进行优化,考虑到网络请求获取响应需要时间,所以在第二步开始,浏览器已经预先查找和启动了一个渲染进程,如果中间步骤一切顺利,当network thread接收到数据时,渲染进程已经准备好了,但是如果遇到重定向,这个准备好的渲染进程也许就不可用了,这个时候会重新启动一个渲染进程。

提交(commit)导航

到了这一步,数据和渲染进程都准备好了,Browser Process会向Renderer Process发送IPC消息来确认导航,此时,浏览器进程将准备好的数据发送给渲染进程,渲染进程接收到数据之后,又发送IPC消息给浏览器进程,告诉浏览器进程导航已经提交了,页面开始加载。

提交导航

这个时候导航栏会更新,安全指示符更新(地址前面的小锁,访问历史列表(history tab)更新,即可以通过前进后退来切换该页面。当导航提交完成后,渲染进程开始加载资源及渲染页面(详细内容下文介绍,当页面渲染完成后(页面及内部的iframe都触发了onload事件,会向浏览器进程发送IPC消息,告知浏览器进程,这个时候UI thread会停止展示tab中的加载中图标。

当导航提交完成后,渲染进程开始着手加载资源以及渲染页面。我会在后面的文章中讲述渲染进程渲染页面的具体细节。一旦渲染进程“完成”(finished)渲染,它会通过IPC告知浏览器进程(注意这发生在页面上所有帧(frames)的onload事件都已经被触发了而且对应的处理函数已经执行完成了的时候,然后UI线程就会停止导航栏上旋转的圈圈。

渲染进程通过 IPC 告诉浏览器进程页面已经加载完成了

切换站点

一个最简单的导航情景已经描述完了!可是如果这时用户在导航栏上输入一个不一样的URL会发生什么呢?如果是这样,浏览器进程会重新执行一遍之前的那几个步骤来完成新站点的导航。不过在浏览器进程做这些事情之前,它需要让当前的渲染页面做一些收尾工作,具体就是询问一下当前的渲染进程需不需要处理一下beforeunload事件。

beforeunload可以在用户重新导航或者关闭当前tab时给用户展示一个“你确定要离开当前页面吗”的二次确认弹框。浏览器进程之所以要在重新导航的时候和当前渲染进程确认的原因是,当前页面发生的一切(包括页面的JavaScript执行)是不受它控制而是受渲染进程控制,所以它也不知道里面的具体情况。

不过,不要随便给页面添加beforeunload事件监听,你定义的监听函数会在页面被重新导航的时候执行,因此这会增加重导航的时延。beforeunload事件监听函数只有在十分必要的时候才能被添加,例如用户在页面上输入了数据,并且这些数据会随着页面消失而消失。

浏览器进程通过IPC告诉渲染进程它将要离开当前页面导航到新的页面了

如果重新导航是在页面内被发起的呢?例如用户点击了页面的一个链接或者客户端的JavaScript代码执行了诸如 window.location = "newsite.com" 的代码。这种情况下,渲染进程会自己先检查一个它有没有注册beforeunload事件的监听函数,如果有的话就执行,执行完后发生的事情就和之前的情况没什么区别了,唯一的不同就是这次的导航请求是由渲染进程给浏览器进程发起的。如果是重新导航到不同站点(different site)的话,会有另外一个渲染进程被启动来完成这次重导航,而当前的渲染进程会继续处理现在页面的一些收尾工作,例如unload事件的监听函数执行。

浏览器进程告诉新的渲染进程去渲染新的页面并且告诉当前的渲染进程进行收尾工作

Service Worker与预加载

因为Service worker可以用来写网站的网络代理(network proxy,所以开发者可以对网络请求有更多的控制权,例如决定哪些数据缓存在本地以及哪些数据需要从网络上面重新获取等等。如果开发者在service worker里设置了当前的页面内容从缓存里面获取,当前页面的渲染就不需要重新发送网络请求了,这就大大加快了整个导航的过程。

service worker在注册的时候,它的作用范围(scope)会被记录下来。在导航开始的时候,网络线程会根据请求的域名在已经注册的service worker作用范围里面寻找有没有对应的service worker。如果有命中该URLservice workerUI线程就会为这个service worker启动一个渲染进程(renderer process)来执行它的代码。Service worker既可能使用之前缓存的数据也可能发起新的网络请求。

网络线程会在收到导航任务后寻找有没有对应的 Service Worker

UI线程会启动一个渲染进程来运行找到的Service Worker代码,代码具体是由渲染进程里面的工作线程(worker thread)执行。

跨线程交互

在上面的例子中,你应该可以感受到如果启动的service worker最后还是决定发送网络请求的话,浏览器进程和渲染进程这一来一回的通信包括service worker启动的时间其实增加了页面导航的时延。导航预加载就是一种通过在service worker启动的时候并行加载对应资源的方式来加快整个导航过程效率的技术。预加载资源的请求头会有一些特殊的标志来让服务器决定是发送全新的内容给客户端还是只发送更新了的数据给客户端。

UI 线程在启动一个渲染进程去运行 service worker 代码的同时会并行发送网络请求