笔记:阅读浏览器工作原理(一)

783 阅读15分钟

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

阅读浏览器工作原理(一)

浏览器组成结构

  1. 用户界面

    除了显示请求页面的主窗口以外,其他部分都属于用户界面,包括地址栏啥的。

  2. 浏览器引擎

    在用户界面和呈现引擎之间传送指令。

  3. 呈现引擎

    显示请求的内容。如果请求的是html和css,他就解析并显示在屏幕上。

    (这个浏览器的工作原理很长..这一篇写的只是开头介绍...后面还会详细讲这一块,就不在这里费时间了)

    对于chrome,每个标签页都分别对应一个呈现引擎,是独立的进程。

    默认情况下,呈现引擎显示html和xml和图片,也可以扩展插件来显示pdf等。

  4. Ui界面后端

    负责绘制基本控件(即由页面(浏览器)触发事件的部件,例如组合选择框、输入框等等。)这些控件根据浏览器不同展示出来的样子也不同,但是都公开了一些事件,在底层会调用操作系统的用户界面方法来处理。

  5. 网络

    用于网络调用,比如HTTP请求。

  6. Js解析器

    用于解析和执行 Javascript代码。

  7. 数据存储

    浏览器需要在硬盘上保存各种数据,被称为“网络数据库”,例如 Cookie。

    cookie,localstorage,sessionstorage,userData和IndexedDB

疑问

  1. 每个标签页都分别对应一个呈现引擎

    • chrome如何开启线程?

      我们可以,重启浏览器,打开一个隐身窗口。这个时候,点击 Chrome 浏览器右上角的“选项”菜单,选择“更多工具”子菜单,点击“任务管理器”,打开 Chrome 的任务管理器的窗口,然后看看都开了哪些进程。

      可以看出浏览器从关闭状态进行启动,然后新开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个进程。

      后续再新开标签页,浏览器、网络进程、GPU进程是共享的,不会重新启动,如果2个页面属于同一站点的话,并且从a页面中打开的b页面,那么他们也会共用一个渲染进程,否则新开一个渲染进程。

      默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

    • 多进程的优势?

      • 防⼀个⻚⾯崩溃影响整个浏览器

      • 操作系统提供了限制进程权限的方法,可以控制某些进程的权限

      • 进程有⾃⼰的私有内存空间,可以拥有更多的内存

    • 站点隔离?

      通常,Chrome通常把一个标签默认为一个进程,但是如果网页之间存在共享内容,就会共享同一个进程。站点隔离是 Chrome 中最近推出的一项功能,消除共享进程,可以为每个跨网站 iframe 运行单独的渲染器进程,确保不同网站在不同的进程上。以此防止发生类似Spectre和Meltdown的攻击。

    • Spectre和Meltdown的攻击?

      【计算机】15分钟读懂英特尔熔断幽灵漏洞-Emory

  2. 网络模块

    域名解析?

    1. chrome的DNS缓存:1分钟左右,1000条,chrome://net-internals/#dns

    2. 操作系统的DNS缓存:ipconfig /displaydns

    3. host文件:C:\Windows\System32\drivers\etc

    4. 向本地配置的首选DNS服务器发起域名解析递归请求必须返回地址。

      DNS服务器?

      一般是电信运营商提供的,也可以使用像Google提供的DNS服务器

      域名解析请求协议?

      DNS同时占用UDP和TCP的53端口,进行区域传送时使用TC协议,域名解析时使用UDP协议。

      有两种类型的DNS服务器:主DNS服务器和辅助DNS服务器,在一个区中,主服务器读取本机数据文件获取DNS数据,辅助DNS服务器读取主DNS服务器获取数据,他启动时,会和主服务器通信并加载数据信息,这一行为称为区域传送,使用TCP协议传输。

      辅助服务器会定时向主服务器查询数据是否变动,变动了就会执行区域传送并同步数据,使用TCP的原因是同步传送的数据量较多,且TCP稳定性较高。

      向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。不用经过TCP三次握手,这样DNS服务器负载更低,响应更快。

      1. DNS服务器查找自身的缓存

      2. 代我们的服务器发起迭代DNS解析请求

        1. 首先找根域(.com)的ip地址

        2. 找(.baidu.com)的ip地址

          这个DNS地址一般就是由域名注册商提供的,像万网,新网等

        3. 如果查到www.baidu.com的域名地址,返回给系统内核,内核返回给浏览器。

      3. 如果查不到,操作系统就会查找NetBIOS name Cache(Network Basic Input/Output System,NetBIOS名称缓存,就存在客户端电脑中的),里面缓存了近一段时间(几分钟)和计算机成功通讯的ip地址。nbtstat -c

      4. 查询WINS服务器(存储NETBIOS名称和IP地址对应关系)

      5. 客户端广播查找

      6. 客户端读取LMHOSTS文件(和HOSTS文件同一个目录下,写法也一样)

      TCP的3次握手?

      User-Agent(一般是指浏览器)会以一个随机端口(1024 < 端口 < 65535)向服务器的WEB程序(常用的有httpd,nginx等)80端口发起TCP的连接请求。

      http请求经过TCP/IP4层模型的层层封包,到达服务端,进入到网卡,然后是进入到内核的TCP/IP协议栈一层一层解封,然后到达web程序建立连接。

      image.png image.png

      服务器端接收到http请求后是怎么样生成html文件?

      nginx读取配置文件,匹配到对应的文件路径,向内核发起IO系统调用。内核找到文件后,从硬盘上读取到内核自身的内存空间,然后再把这个文件复制到nginx进程所在的内存空间。

      在文件系统层面,内核知道要获取的文件路径/a/b/c.html之后,读取元数据区中的/的inode,找到/对应的数据块编号,找到数据块中存储的目录,于是找到a这个名称在元数据区的inode号。

      读取到a的inode号之后,找到他的数据块,读取目录,得到b的inode号,找到b的数据块之后,读取目录得到c.html所在的inode,最后找到c.html对应的数据块,于是获取到完整内容。

      (获取到inode号 -> 找到数据块 -> 读取目录,获取到下一个目标的inode号,直到找到文件位置)

      读取到html文件之后,获取静态资源?(以chrome为例)

      1. 开始加载

        chrome把网络资源分为:(不完全)

        key意义
        MainRescouce地址栏输入得到的页面、使用frame/iframe嵌套的页面、通过超链接点击的⻚面、表单提交后跳转的页面。
        kImage图片资源
        kCSSStyleSheetcss资源
        kScriptscript资源
        kFont字体
        kRaw混合类型资源,动态资源,如ajax
        kSVGDocumentsvg资源
        kXSLStyleSheet扩展样式表语言XSLT,是一种转换语言
        kLinkPrefetchHTML5页面的预读取资源
        kTextTrackvideo的字幕
        kImportResourceHTML Imports,将一个HTML文件导入到其他HTML文档中,例如<link href="import/post.html" rel="import" />
        kMedia媒体资源
        kManifestHTML5 应用程序缓存资源
        kMock预留的测试类型
      2. 预处理请求

        先生成url、http header、http body,优先级等信息。

        接下来检查请求是否合法,把请求做些修改。如果检查合法性返回kAbort或者kBlock,说明资源被废弃了或者被阻止了,就不去加载了。

        被block的原因可能有以下几种:

        key意义
        kCSP内容安全策略检查,减少XSS攻击。
        kMixedContentMixed Content混合内容block。
        kOrigin这个主要是svg,使用use的获取svg资源的时候必须不能跨域,如下以下资源将会被阻塞
        kInspectordevtools的检查器
        kSubresourceFilter子资源过滤器
        kOther
        kNone

        kCSP

        经过某类型资源的CSP设定检查的时候没有通过就会返回堵塞原因,然后根据要求改变请求。

        如果设置了content="upgrade-insecure-requests",会改变request对象,将网页的http请求强制升级为https。

        如果我们只允许加载自己域的图片的话,可以加上meta标签:content="img-src 'self',或者后端设置该响应头。

        kMixedContent

        在https网站请求http内容,例如加载一个http的JS脚本,容易受到中间人的攻击,如修改JS的内容,从而控制整个https的⻚面。

        而图片(不使用srcset而是使用src)、音频资源即使内容被修改可能只是展示出问题,如果没有设置content="block-all-mixed-content",不会被block掉,被称为被动混合内容。

        如果用户允许,则可以加载主动混合内容。但是如果⻚面设置了"block-all-mixed-content",用户设置的允许blockable资源加载的设置将会失效。

        如果是嵌套页面,子页面允许加载主动混合内容,而父页面不允许加载,则不允许加载。

        Origin Block

        svg使用use的获取svg资源的时候必须不能跨域。如果协议、域名、端口号都一样则通过检查。需要这里和同源策略是两码事,这里的源阻塞是连请求都发不出去,而同源策略只是阻塞请求的返回结果。

      3. 资源优先级

        计算资源加载优先级,首先每个资源都有一个默认的优先级。

        优先级总共分为五级:very-high、high、medium、low、very-low,判断顺序为:MainRescource⻚面、CSS、字体这种一下子就能被看到的优先级是最高的,然后就是Script、Ajax这种,而图片、音视频的默认优先级是比较低的,最低的是prefetch预加载的资源(link,rel=prefetch)。

        在设定了资源默认的优先级之后,会再对一些情况做一些调整,主要是对prefetch/preload的资源。

        • 降低preload的字体的优先级

          预加载字体的优先级从very-high变成high

        • 降低defer/async的script的优先级

          script如果是defer的话,那么它优先级会变成最低

        • ⻚面底部preload的script优先级变成medium

          如果是preload的script,并且如果⻚面已经加载了一张图片就认为这个script是在⻚面偏底部的位置,就把它的优先级调成medium。

          资源在第一张非preload的图片前认为是early,而在后面认为是late,late的script的优先级会偏低。

          prefetch:在早期浏览器中,一旦遇到script,就要下载并执行完,才会继续解析剩下的DOM。

          preload:遇到script 的时候,DOM会停止构建,但是继续搜索页面加载所需的资源(img,script等)并且进行预加载,不用等到DOM再开始执行的时候才加载。

        • 把同步即堵塞加载的资源的优先级调成very-high

          ajax同步请求在初始化的时候会被调整成very-high

          本来是hight的ajax同步请求,在最后会执行max(switch判断得到的优先级,同步请求当前的优先级),所以该请求被调整成very-high

      4. 定级之后,在发请求之前,在渲染线程被转化成Net的优先级,对应关系如下:

        优先级资源类型
        HEIGHEST(very high)css/font/页面/同步请求
        MEDIUM(high)js/ajax
        LOW(medium)manifest/页面底部preload script(缓存和预加载的script资源)
        LOWEST(low)img/video/audio(图片视频音频)
        IDLE(very low)prefetch/defer script(会阻塞线程的js资源)

        Net Priority是请求资源的时候使用的,是在Chrome的IO线程里面进行的,好处是如果两个⻚面请求了相同资源,有缓存就能避免重复请求。

        请求资源的过程,是在IO线程进行的。渲染线程和IO线程间的通信是通过Chrome封装的Mojo框架进行的。在渲染线程会发一个消息给IO线程通知它要加载资源了。

      资源加载?

      有一个ScheduleRequest函数,他会判断当前资源是否能开始加载了,如果能的话就准备加载了,如果不能的话就继续把它放到pending request队列里面。

      有两个地方会调用它:

      1. 收到来自渲染线程IPC::Mojo的请求加载资源的消息

      2. 另外有个LoadAnyStartablePendingRequests调用了他,该函数的逻辑是遍历pending request,每次取出优先级最高的一个请求,调用ScheduleRequest判断是否可以加载了,可以的话就拿出来运行。

        有三个地方会调用它:

        1. 要插入body标签的时候
        2. 每个请求完成之后,触发加载pending requests里还未加载的请求(调用LoadAnyStartablePendingRequests)
        3. IO线程定时循环未完成的任务,触发加载

      none delayable:优先级大于等于Medium的是不可推迟的none delayable请求。

      layout-blocking:layout-blocking请求是当还没有渲染body标签,并且优先级在Medium之上的如CSS的请求。(css/font/页面)

      <!DOCType html> 
        <html> 
        <head>     
          <meta charset="utf-8">     
          <link rel="icon" href="4.png">     
          <img src="0.png">     
          <img src="1.png">     
          <link rel="stylesheet" href="1.css">     
          <link rel="stylesheet" href="2.css">     
          <link rel="stylesheet" href="3.css">     
          <link rel="stylesheet" href="4.css">     
          <link rel="stylesheet" href="5.css">     
          <link rel="stylesheet" href="6.css">     
          <link rel="stylesheet" href="7.css"> 
        </head> 
        <body>     
            <p>hello</p>     
            <img src="2.png">     
            <img src="3.png">     
            <img src="4.png">     
            <img src="5.png">     
            <img src="6.png">     
            <img src="7.png">     
            <img src="8.png">     
            <img src="9.png">     
            <script src="1.js"></script>     
            <script src="2.js"></script>     
            <script src="3.js"></script>     
            <img src="3.png"> 
            <script> 
              !function(){     
                let xhr = new XMLHttpRequest();     
                xhr.open("GET", "https://baidu.com");     
                xhr.send();     
                document.write("hi"); 
              }(); 
            </script> 
            <link rel="stylesheet" href="9.css"> 
          </body> 
      </html>
      
      1. 高优先级的资源(>=Medium)、同步请求和非http(s)的请求能够立刻加载
      2. 只要有一个layout blocking的资源在加载,最多只能加载一个delayable的资源,这个就解释了为什么0.png能够先加载
      3. 只有当layout blocking和high priority的资源加载完了,才能开始加载delayable的资源,这个就解释了为什么要等CSS加载完了才能加载其它的js/图片。
      4. 同时加载的delayable资源同一个域只能有6个,同一个client即同一个⻚面最多只能有10 个,否则要进行排队。

      可以得出结论:

      1. 由于1.css到9.css这几个CSS文件是high priority或者是none delayable的,所以⻢上in flight,但是还受到了同一个域最多只能有6个的限制,所以6/7/9.css这三个进入stalled的状态

      2. 1.css到5.css是layout-blocking的,所以最多只能再加载一个delayable的0.png,在它相邻的1.png就得排队了

      3. 等到high priority和layout-blocking的资源7.css/9.css/1.js加载完了,就开始加载delayable 的资源,主要是preload的js和图片

        为什么1.js是high priority的而2.js和3.js却是delayable的?

        一开始是Low是因为它是推测加载的(页面底部的资源),所以是优先级比较低,但是当DOM构建到那里的时候它就不是preload的,变成正常的JS加载了,所以它的优先级变成了Medium,这个可以从has_html_body标签进行推测,而2.js要等到1.js下载和解析完,它能算是正常加载,否则还是推测加载,因此它的优先级没有得到提高。

  3. Js解析器

    3101904741-b5e8b7dbcd6a30ba.png

    • scanner(词法分析器)

      扫描所有的源代码->词法分析->单词流(词法单元)

      2689508157-9c8814484c29032f.png

      Tokens 在线查看网站:esprima.org/demo/pars..…

    • Parser(解析器)

      单词流->语法树,其中会分析语法错误,确定词法作用域(在哪里声明的作用域就在哪里)

      var a = 2=>

      1960233382-5d95a723017c144c.png

      3025093354-74c56d287c7be7a8-1621749358594.png

      大概可以看出,type为声明变量,id(标识符、变量名)为a,初始值为常量2。

      AST 在线查看网站:astexplorer.net/

      • Pre-Parser(惰性解析,预解析)只解析没有被立刻执行的代码,用于确定作用域,预解析后代码开始执行,才开始进行Parser(全量解析)

        function foo() {
            console.log('a');
            function inline() {
                console.log(''b)
            }
        }
        
        (function bar() {
            console.log('c')
        })();
        
        foo();
        
        1. foo:不是立即执行的,所以使用预解析,里面的inline也会被解析
        2. bar:立即执行,直接用Parser解析
        3. foo():被调用的时候,用Parser进行解析,对inline又进行一次预解析。
    • lgnition(解释器)

      语法树->字节码(要在直译机转移之后才能成为机器码)->解释执行

      将 AST 转换为字节码,然后开始逐行解释执行。

      1399390824-f5fc0b9b222e7808_fix732.png

      早期版本的 V8 ,并没有生成中间字节码的过程,而是将所有源码转换为了机器代码。机器代码虽然执行速度更快,但是占用内存大。

    • TurboFan(编译器)

      输入字节码和一些分析数据并生成优化

      3310143149-833d49ccd6590db7_fix732.png

      当Ignition执行代码后,v8会观察代码执行情况并记录执行信息,比如执行次数,参数类型。如果一个函数被调用的次数超过设定的值,就会被标记为热点函数,将函数的字节码和执行信息发送给TurboFan,他会做出进一步优化代码的假设(如假设参数类型是数字,之后就无须检查类型了),然后直接编译为机器指令。如果后面有一次发现不是数字,就表示假设错误,要进行优化回退,还原为字节码。

    • 执行js

      语法分析阶段:对加载完成的代码进行语法检验,检验完成后进入预编译阶段;

      预编译阶段:全局、函数预编译。该阶段会进行执行上下文的创建,包括函数名以及变量提升、建立作用域链、确定 this 指向等。

      执行阶段:事件循环、将编译阶段中创建的执行上下文压入调用栈,并成为正在运行的执行上下文。代码执行结束后,将其弹出调用栈。

      (很多基础知识点,可以看你不知道的js和红宝书的时候在整理)

现代浏览器架构漫谈.md

Google 图解:Chrome 快是有原因的,科普浏览器架构

Inside look at modern web browser (part 1)

给程序员解释Spectre和Meltdown漏洞

【计算机】15分钟读懂英特尔熔断幽灵漏洞-Emory

一次完整的HTTP事务是怎样一个过程

从Chrome源码看浏览器如何加载资源

浏览器页面资源加载过程与优化

虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩

链接帖 各JavaScript引擎的简介,及相关资料/博客收集帖

segmentfault.com/a/119000002…