浏览器

介绍目前常见前端面试题,浏览器基础题

img

❤ 浏览器架构

单进程浏览器时代

单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript运行环境、渲染引擎和页面等。其实早在2007年之前,市面上浏览器都是单进程的

img

  • 缺点
    • 不稳定:一个插件的意外崩溃会引起整个浏览器的崩溃
    • 不流畅:所有页面的渲染模块、JavaScript执行环境以及插件都是运行在同一个线程中的,这就意味着同一时刻只能有一个模块可以执行
    • 不安全:可以通过浏览器的漏洞来获取系统权限,这些脚本获取系统权限之后也可以对你的电脑做一些恶意的事情,同样也会引发安全问题
  • 以上这些就是当时浏览器的特点,不稳定,不流畅,而且不安全

多进程浏览器时代

  • 由于进程是相互隔离的,所以当一个页面或者插件崩溃时,影响到的仅仅是当前的页面进程或者插件进程,并不会影响到浏览器和其他页面,这就完美地解决了页面或者插件的崩溃会导致整个浏览器崩溃,也就是不稳定的问题
  • JavaScript也是运行在渲染进程中的,所以即使JavaScript阻塞了渲染进程,影响到的也只是当前的渲染页面,而并不会影响浏览器和其他页面,因为其他页面的脚本是运行在它们自己的渲染进程中的
  • Chrome把插件进程和渲染进程锁在沙箱里面,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限。

img

最新的Chrome浏览器包括:1个浏览器(Browser)主进程1个 GPU 进程1个网络(NetWork)进程多个渲染进程多个插件进程

  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • GPU进程。其实,Chrome刚开始发布的时候是没有GPU进程的。而GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制,这使得GPU成为浏览器普遍的需求。最后,Chrome在其多进程架构上也引入了GPU进程。
  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响

介绍一下HTTPS和HTTP区别

HTTPS 要比 HTTPS 多了 secure 安全性这个概念,实际上, HTTPS 并不是一个新的应用层协议,它其实就是 HTTP + TLS/SSL 协议组合而成,而安全性的保证正是 SSL/TLS 所做的工作。

SSL

安全套接层(Secure Sockets Layer)

TLS

(传输层安全,Transport Layer Security)

现在主流的版本是 TLS/1.2, 之前的 TLS1.0、TLS1.1 都被认为是不安全的,在不久的将来会被完全淘汰。

HTTPS 就是身披了一层 SSL 的 HTTP

img

那么区别有哪些呢👇

  • HTTP 是明文传输协议,HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。
  • HTTPS比HTTP更加安全,对搜索引擎更友好,利于SEO,谷歌、百度优先索引HTTPS网页。
  • HTTPS标准端口443,HTTP标准端口80。
  • HTTPS需要用到SSL证书,而HTTP不用。

我觉得记住以下两点HTTPS主要作用就行👇

  1. 对数据进行加密,并建立一个信息安全通道,来保证传输过程中的数据安全;
  2. 对网站服务器进行真实身份认证。

HTTPS的缺点

  • 证书费用以及更新维护。
  • HTTPS 降低一定用户访问速度(实际上优化好就不是缺点了)。
  • HTTPS 消耗 CPU 资源,需要增加大量机器。

❤ Chrome 打开一个页面需要启动多少进程?分别有哪些进程?

打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。

  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • GPU 进程:其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
  • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

❤ 渲染机制

1. 浏览器如何渲染网页

概述:浏览器渲染一共有五步

  1. 处理 HTML 并构建 DOM 树。
  2. 处理 CSS构建 CSSOM 树。
  3. DOMCSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,计算每个节点的位置。
  5. 调用 GPU 绘制,合成图层,显示在屏幕上

第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染

具体如下图过程如下图所示

img

img

渲染

  • 网页生成的时候,至少会渲染一次
  • 在用户访问的过程中,还会不断重新渲染

重新渲染需要重复之前的第四步(重新生成布局)+第五步(重新绘制)或者只有第五个步(重新绘制)

  • 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢
  • HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且CSS也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM

2. 浏览器渲染五个阶段

2.1 第一步:解析HTML标签,构建DOM树

在这个阶段,引擎开始解析html,解析出来的结果会成为一棵domdom的目的至少有2

  • 作为下个阶段渲染树状图的输入
  • 成为网页和脚本的交互界面。(最常用的就是getElementById等等)

当解析器到达script标签的时候,发生下面四件事情

  1. html解析器停止解析,
  2. 如果是外部脚本,就从外部网络获取脚本代码
  3. 将控制权交给js引擎,执行js代码
  4. 恢复html解析器的控制权

由此可以得到第一个结论1

  • 由于<script>标签是阻塞解析的,将脚本放在网页尾部会加速代码渲染。
  • deferasync属性也能有助于加载外部脚本。
  • defer使得脚本会在dom完整构建之后执行;
  • async标签使得脚本只有在完全available才执行,并且是以非阻塞的方式进行的

2.2 第二步:解析CSS标签,构建CSSOM树

  • 我们已经看到html解析器碰到脚本后会做的事情,接下来我们看下html解析器碰到样式表会发生的情况
  • js会阻塞解析,因为它会修改文档(document)。css不会修改文档的结构,如果这样的话,似乎看起来css样式不会阻塞浏览器html解析。但是事实上 css样式表是阻塞的。阻塞是指当cssom树建立好之后才会进行下一步的解析渲染

通过以下手段可以减轻cssom带来的影响

  • script脚本放在页面底部
  • 尽可能快的加载css样式表
  • 将样式表按照media typemedia query区分,这样有助于我们将css资源标记成非阻塞渲染的资源。
  • 非阻塞的资源还是会被浏览器下载,只是优先级较低

2.3 第三步:把DOM和CSSOM组合成渲染树(render tree)

img

2.4 第四步:在渲染树的基础上进行布局,计算每个节点的几何结构

布局(layout):定位坐标和大小,是否换行,各种position, overflow, z-index属性

2.5 调用 GPU 绘制,合成图层,显示在屏幕上

将渲染树的各个节点绘制到屏幕上,这一步被称为绘制painting

3. 渲染优化相关

3.1 Load 和 DOMContentLoaded 区别

  • Load 事件触发代表页面中的 DOMCSSJS,图片已经全部加载完毕。
  • DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSSJS,图片加载

3.2 图层

一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用。

通过以下几个常用属性可以生成新图层

  • 3D 变换:translate3dtranslateZ
  • will-change
  • videoiframe 标签
  • 通过动画实现的 opacity 动画转换
  • position: fixed

3.3 重绘(Repaint)和回流(Reflow)

重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大

  • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘
  • 回流是布局或者几何属性需要改变就称为回流。

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流

以下几个动作可能会导致性能问题

  • 改变 window 大小
  • 改变字体
  • 添加或删除样式
  • 文字改变
  • 定位或者浮动
  • 盒模型

很多人不知道的是,重绘和回流其实和 Event loop 有关

  • Event loop 执行完Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
  • 然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resizescroll 事件也是至少 16ms才会触发一次,并且自带节流功能。
  • 判断是否触发了 media query
  • 更新动画并且发送事件
  • 判断是否有全屏操作事件
  • 执行 requestAnimationFrame 回调
  • 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
  • 更新界面
  • 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调

常见的引起重绘的属性

  • color
  • border-style
  • visibility
  • background
  • text-decoration
  • background-image
  • background-position
  • background-repeat
  • outline-color
  • outline
  • outline-style
  • border-radius
  • outline-width
  • box-shadow
  • background-size

3.4 常见引起回流属性和方法

任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排,下面列一些栗子

  • 添加或者删除可见的DOM元素;
  • 元素尺寸改变——边距、填充、边框、宽度和高度
  • 内容变化,比如用户在input框中输入文字
  • 浏览器窗口尺寸改变——resize事件发生时
  • 计算 offsetWidthoffsetHeight 属性
  • 设置 style 属性的值

回流影响的范围

由于浏览器渲染界面是基于流失布局模型的,所以触发重排时会对周围DOM重新排列,影响的范围有两种

  • 全局范围:从根节点html开始对整个渲染树进行重新布局。
  • 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局

全局范围回流

1
2
3
4
5
6
7
8
9
10
11
<body>
<div class="hello">
<h4>hello</h4>
<p><strong>Name:</strong>BDing</p>
<h5>male</h5>
<ol>
<li>coding</li>
<li>loving</li>
</ol>
</div>
</body>

p节点上发生reflow时,hellobody也会重新渲染,甚至h5ol都会收到影响

局部范围回流

用局部布局来解释这种现象:把一个dom的宽高之类的几何信息定死,然后在dom内部触发重排,就只会重新渲染该dom内部的元素,而不会影响到外界

3.5 减少重绘和回流

使用 translate 替代 top

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="test"></div>
<style>
.test {
position: absolute;
top: 10px;
width: 100px;
height: 100px;
background: red;
}
</style>
<script>
setTimeout(() => {
// 引起回流
document.querySelector('.test').style.top = '100px'
}, 1000)
</script>
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • DOM 离线后修改,比如:先把 DOMdisplay:none (有一次 Reflow),然后你修改100次,然后再把它显示出来
  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
1
2
3
4
for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS选择符从右往左匹配查找,避免 DOM深度过深
  • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video标签,浏览器会自动将该节点变为图层。

img

服务端渲染

  • 得分点 服务器端生成HTML直接返回给浏览器、减少网络传输、首屏渲染快、对搜索引擎友好
  • 标准回答
    • SSR是Server Side Render简称;页面上的内容是通过服务端渲染生成的,浏览器直接显示服务端返回的html就可以了。
    • CSR是Client Side Render简称;客户端在请求时,服务端不做任何处理,直接将前端资源打包后生成的html返回给客户端,此时的html中无任何网页内容,需要客户端去加载执行js代码才能渲染生成页面内容,同时完成事件绑定,然后客户端再去通过ajax请求后端api获取数据更新视图。
    • 服务端渲染的优势:减少网络传输,响应快,用户体验好,首屏渲染快,对搜索引擎友好,搜索引擎爬虫可以看到完整的程序源码,有利于SEO。
    • 在Vue项目中实现服务端渲染方法:Vue在客户端渲染中也是采用一定方法将虚拟DOM渲染为真实DOM的,那么服务端的渲染流程也是通过虚拟DOM的编译来完成的,编译虚拟DOM的方法是renderToString。在Vue中,vue-server-renderer 提供一个名为 createBundleRenderer 的 API,这个API用于创建一个 render,并且自带renderToString方法。
  • 加分回答
    • 使用服务器端渲染 (SSR) 时还需要有一些权衡之处:
      • 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。
      • 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
      • 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。

浏览器渲染过程

  • 得分点 dom树、stylesheet、布局树、分层、光栅化、合成
  • 标准回答
    • 浏览器拿到HTML,先将HTML转换成dom树,再将CSS样式转换成stylesheet,根据dom树和stylesheet创建布局树,对布局树进行分层,为每个图层生成绘制列表,再将图层分成图块,紧接着光栅化将图块转换成位图,最后合成绘制生成页面。
  • 加分回答
    • 分层的目的:避免整个页面渲染,把页面分成多个图层,尤其是动画的时候,把动画独立出一个图层,渲染时只渲染该图层就ok,transform,z-index等,浏览器会自动优化生成图层
    • 光栅化目的:页面如果很长但是可视区很小,避免渲染非可视区的样式造成资源浪费,所以将每个图层又划分成多个小个子,当前只渲染可视区附近区域

❤ 缓存机制

1. 首先得明确 http 缓存的好处

  • 减少了冗余的数据传输,减少网费
  • 减少服务器端的压力
  • Web 缓存能够减少延迟与网络阻塞,进而减少显示某个资源所用的时间
  • 加快客户端加载网页的速度

2. 常见 http 缓存的类型

  • 私有缓存(一般为本地浏览器缓存)
  • 代理缓存

3. 然后谈谈本地缓存

本地缓存是指浏览器请求资源时命中了浏览器本地的缓存资源,浏览器并不会发送真正的请求给服务器了。它的执行过程是

  • 第一次浏览器发送请求给服务器时,此时浏览器还没有本地缓存副本,服务器返回资源给浏览器,响应码是200 OK,浏览器收到资源后,把资源和对应的响应头一起缓存下来
  • 第二次浏览器准备发送请求给服务器时候,浏览器会先检查上一次服务端返回的响应头信息中的Cache-Control,它的值是一个相对值,单位为秒,表示资源在客户端缓存的最大有效期,过期时间为第一次请求的时间减去Cache-Control的值,过期时间跟当前的请求时间比较,如果本地缓存资源没过期,那么命中缓存,不再请求服务器
  • 如果没有命中,浏览器就会把请求发送给服务器,进入缓存协商阶段。

与本地缓存相关的头有:Cache-ControlExpiresCache-Control有多个可选值代表不同的意义,而Expires就是一个日期格式的绝对值。

3.1 Cache-Control

Cache-ControlHTPP缓存策略中最重要的头,它是HTTP/1.1中出现的,它由如下几个值

  • no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载
  • no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源
  • public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
  • private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
  • max-age:从当前请求开始,允许获取的响应被重用的最长时间(秒)。
  • must-revalidate,当缓存过期时,需要去服务端校验缓存的有效性。
1
2
3
# 例如:
Cache-Control: public, max-age=1000
# 表示资源可以被所有用户以及代理服务器缓存,最长时间为1000秒。

注意,虽然你可能在其他资料中看到可以使用 meta 标签来设置缓存,比如像下面的形式:

1
<meta http-equiv="expires" content="Wed, 20 Jun 2021 22:33:00 GMT"

但在 HTML5 规范中,并不支持这种方式,所以尽量不要使用 meta 标签来设置缓存

3.2 Expires

ExpiresHTTP/1.0出现的头信息,同样是用于决定本地缓存策略的头,它是一个绝对时间,时间格式是如Mon, 10 Jun 2015 21:31:12 GMT,只要发送请求时间是在Expires之前,那么本地缓存始终有效,否则就会去服务器发送请求获取新的资源。如果同时出现Cache-Control:max-ageExpires,那么max-age优先级更高。他们可以这样组合使用

1
2
Cache-Control: public
Expires: Wed, Jan 10 2018 00:27:04 GMT

3.3 所谓的缓存协商

当第一次请求时服务器返回的响应头中存在以下情况时

  • 没有 Cache-ControlExpires
  • Cache-ControlExpires 过期了
  • Cache-Control 的属性设置为 no-cache

那么浏览器第二次请求时就会与服务器进行协商,询问浏览器中的缓存资源是不是旧版本,需不需要更新,此时,服务器就会做出判断,如果缓存和服务端资源的最新版本是一致的,那么就无需再次下载该资源,服务端直接返回304 Not Modified 状态码,如果服务器发现浏览器中的缓存已经是旧版本了,那么服务器就会把最新资源的完整内容返回给浏览器,状态码就是200 Ok,那么服务端是根据什么来判断浏览器的缓存是不是最新的呢?其实是根据HTTP的另外两组头信息,分别是:Last-Modified/If-Modified-SinceETag/If-None-Match

Last-Modified 与 If-Modified-Since

具体工作流程如下:

  • 浏览器第一次请求资源时,服务器会把资源的最新修改时间Last-Modified:Thu, 29 Dec 2011 18:23:55 GMT放在响应头中返回给浏览器
  • 第二次请求时,浏览器就会把上一次服务器返回的修改时间放在请求头If-Modified-Since:Thu, 29 Dec 2011 18:23:55发送给服务器,服务器就会拿这个时间跟服务器上的资源的最新修改时间进行对比
  • 服务端再次收到请求,根据请求头 If-Modified-Since 的值,判断相关资源是否有变化,如果没有,则返回 304 Not Modified,并且不返回资源内容,浏览器使用资源缓存值;否则正常返回资源内容,且更新Last-Modified 响应头内容。

如果两者相等或者大于服务器上的最新修改时间,那么表示浏览器的缓存是有效的,此时缓存会命中,服务器就不再返回内容给浏览器了,同时Last-Modified头也不会返回,因为资源没被修改,返回了也没什么意义。如果没命中缓存则最新修改的资源连同Last-Modified头一起返回

这种方式虽然能判断缓存是否失效,但也存在两个问题:

  • 精度问题Last-Modified 的时间精度为秒,如果在 1 秒内发生修改,那么缓存判断可能会失效;
  • 准度问题,考虑这样一种情况,如果一个文件被修改,然后又被还原,内容并没有发生变化,在这种情况下,浏览器的缓存还可以继续使用,但因为修改时间发生变化,也会重新返回重复的内容。
1
2
3
4
5
6
7
# 第一次请求返回的响应头
Cache-Control:max-age=3600
Expires: Fri, Jan 12 2018 00:27:04 GMT
Last-Modified: Wed, Jan 10 2018 00:27:04 GMT

# 第二次请求的请求头信息
If-Modified-Since: Wed, Jan 10 2018 00:27:04 GMT

这组头信息是基于资源的修改时间来判断资源有没有更新,另一种方式就是根据资源的内容来判断,就是接下来要讨论的 ETagIf-None-Match

ETag 与 If-None-Match

为了解决精度问题和准度问题,HTTP 提供了另一种不依赖于修改时间,而依赖于文件哈希值的精确判断缓存的方式,那就是响应头部字段 ETag 和请求头部字段 If-None-Match。

ETag/If-None-MatchLast-Modified/If-Modified-Since的流程其实是类似的,唯一的区别是它基于资源的内容的摘要信息(比如MD5 hash)来判断

浏览器发送第二次请求时,会把第一次的响应头信息ETag的值放在If-None-Match的请求头中发送到服务器,与最新的资源的摘要信息对比,如果相等,取浏览器缓存,否则内容有更新,最新的资源连同最新的摘要信息返回。用ETag的好处是如果因为某种原因到时资源的修改时间没改变,那么用ETag就能区分资源是不是有被更新。

具体工作流程如下:

  • 浏览器第一次请求资源,服务端在返响应头中加入 Etag 字段,Etag 字段值为该资源的哈希值
  • 当浏览器再次跟服务端请求这个资源时,在请求头上加上 If-None-Match,值为之前响应头部字段 ETag 的值;
  • 服务端再次收到请求,将请求头 If-None-Match 字段的值和响应资源的哈希值进行比对,如果两个值相同,则说明资源没有变化,返回 304 Not Modified;否则就正常返回资源内容,无论是否发生变化,都会将计算出的哈希值放入响应头部的 ETag 字段中

这种缓存比较的方式也会存在一些问题,具体表现在以下两个方面。

  • 计算成本。生成哈希值相对于读取文件修改时间而言是一个开销比较大的操作,尤其是对于大文件而言。如果要精确计算则需读取完整的文件内容,如果从性能方面考虑,只读取文件部分内容,又容易判断出错。
  • 计算误差。HTTP 并没有规定哈希值的计算方法,所以不同服务端可能会采用不同的哈希值计算方式。这样带来的问题是,同一个资源,在两台服务端产生的 Etag 可能是不相同的,所以对于使用服务器集群来处理请求的网站来说,使用 Etag 的缓存命中率会有所降低。

需要注意的是,强制缓存的优先级高于协商缓存,在协商缓存中,Etag 优先级比 Last-Modified

1
2
3
4
5
6
# 第一次请求返回的响应头:
Cache-Control: public, max-age=31536000
ETag: "15f0fff99ed5aae4edffdd6496d7131f"

# 第二次请求的请求头信息:
If-None-Match: "15f0fff99ed5aae4edffdd6496d7131f"

缓存位置

浏览器缓存的位置的话,可以分为四种,优先级从高到低排列分别👇

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

Service Worker

这个应用场景比如PWA,它借鉴了Web Worker思路,由于它脱离了浏览器的窗体,因此无法直接访问DOM。它能完成的功能比如:离线缓存消息推送网络代理,其中离线缓存就是Service Worker Cache

Memory Cache

指的是内存缓存,从效率上讲它是最快的,从存活时间来讲又是最短的,当渲染进程结束后,内存缓存也就不存在了。

Disk Cache

存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,优势在于存储容量和存储时长。

Disk Cache VS Memory Cache

两者对比,主要的策略👇

  • 内容使用率高的话,文件优先进入磁盘
  • 比较大的JS,CSS文件会直接放入磁盘,反之放入内存。

Push Cache

推送缓存,这算是浏览器中最后一道防线吧,它是HTTP/2的内容

浏览器缓存总结

浏览器缓存分为 强缓存 和 协商缓存。当客户端请求某个资源时,获取缓存的流程如下

  • 先根据这个资源的一些 http header 判断它是否命中强缓存,先检查Cache-Control,如果命中,则直接从本地获取缓存资源,不会发请求到服务器;
  • 当强缓存没有命中时,客户端会发送请求到服务器,服务器通过另一些 request header 验证这个资源是否命中协商缓存,称为 http 再验证,如果命中,服务器将请求返回,但不返回资源,而是返回304告诉客户端直接从缓存中获取,客户端收到返回后就会从缓存中获取资源;(服务器通过请求头中的If-Modified-Since或者If-None-Match字段检查资源是否更新)
  • 强缓存和协商缓存共同之处在于,如果命中缓存,服务器都不会返回资源; 区别是,强缓存不对发送请求到服务器,但协商缓存会。
  • 当协商缓存也没命中时,服务器就会将资源发送回客户端。
  • 当 ctrl+f5 强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;
  • 当 f5 刷新网页时,跳过强缓存,但是会检查协商缓存;

强缓存

  • Expires(该字段是 http1.0 时的规范,值为一个绝对时间的 GMT 格式的时间字符串,代表缓存资源的过期时间)
  • Cache-Control:max-age(该字段是 http1.1的规范,强缓存利用其 max-age 值来判断缓存资源的最大生命周期,它的值单位为秒)

协商缓存

  • Last-Modified(值为资源最后更新时间,随服务器response返回)
  • If-Modified-Since(通过比较两个时间来判断资源在两次请求期间是否有过修改,如果没有修改,则命中协商缓存)
  • ETag(表示资源内容的唯一标识,随服务器response返回)
  • If-None-Match(服务器通过比较请求头部的If-None-Match与当前资源的ETag是否一致来判断资源是否在两次请求之间有过修改,如果没有修改,则命中协商缓存)

缓存场景

对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策

  • 对于某些不需要缓存的资源,可以使用 Cache-control: no-store ,表示该资源不需要缓存
  • 对于频繁变动的资源,可以使用 Cache-Control: no-cache 并配合 ETag 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新
  • 对于代码文件来说,通常使用 Cache-Control: max-age=31536000 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件

讲讲 304

如果客户端发送了一个带条件的 GET 请求且该请求已被允许,而文档的内容(自 上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个 304 状态码

强缓存、协商缓存什么时候用哪个

因为服务器上的资源不是一直固定不变的,大多数情况下它会更新,这个时候如果我们 还访问本地缓存,那么对用户来说,那就相当于资源没有更新,用户看到的还是旧的资 源;所以我们希望服务器上的资源更新了浏览器就请求新的资源,没有更新就使用本地 的缓存,以最大程度的减少因网络请求而产生的资源浪费。

缓存总结

缓存分为两种: 强缓存和协商缓存,根据响应的 header 内容来决定。

获取资源形式 状态码 发送请求到服务器
强缓存 从缓存取 200(from cache) 否,直接从缓存取
协商缓存 从缓存取 304(not modified) 是,通过服务器来告知缓存是否可 用
  • 强缓存相关字段有 expirescache-control。如果 cache-controlexpires 同时存在的话, cache-control 的优先级高于 expires
  • 协商缓存相关字段有 Last-Modified/If-Modified-SinceEtag/If-None-Match

❤ 浏览器存储

我们经常需要对业务中的一些数据进行存储,通常可以分为 短暂性存储 和 持久性储存。

  • 短暂性的时候,我们只需要将数据存在内存中,只在运行时可用
  • 持久性存储,可以分为 浏览器端 与 服务器端
    • 浏览器:
      • cookie: 通常用于存储用户身份,登录状态等
        • http 中自动携带, 体积上限为 4K, 可自行设置过期时间
      • localStorage / sessionStorage: 长久储存/窗口关闭删除, 体积限制为 4~5M
      • indexDB
    • 服务器:
      • 分布式缓存 redis
      • 数据库

cookie 和 localSrorage、session、indexDB 的区别

特性 cookie localStorage sessionStorage indexDB
数据生命周期 一般由服务器生成,可以设置过期时间 除非被清理,否则一直存在 页面关闭就清理 除非被清理,否则一直存在
数据存储大小 4K 5M 5M 无限
与服务端通信 每次都会携带在 header 中,对于请求性能影响 不参与 不参与 不参与

从上表可以看到,cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

对于 cookie,我们还需要注意安全性

属性 作用
value 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识
http-only 不能通过 JS访问 Cookie,减少 XSS攻击
secure 只能在协议为 HTTPS 的请求中携带
same-site 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击

img

  • Name,即该 Cookie 的名称。Cookie 一旦创建,名称便不可更改。
  • Value,即该 Cookie 的值。如果值为 Unicode 字符,需要为字符编码。如果值为二进制数据,则需要使用 BASE64 编码。
  • Max Age,即该 Cookie 失效的时间,单位秒,也常和 Expires 一起使用,通过它可以计算出其有效时间。Max Age如果为正数,则该 CookieMax Age 秒之后失效。如果为负数,则关闭浏览器时 Cookie 即失效,浏览器也不会以任何形式保存该 Cookie
  • Path,即该 Cookie 的使用路径。如果设置为 /path/,则只有路径为 /path/ 的页面可以访问该 Cookie。如果设置为 /,则本域名下的所有页面都可以访问该 Cookie
  • Domain,即可以访问该 Cookie 的域名。例如如果设置为 .zhihu.com,则所有以 zhihu.com,结尾的域名都可以访问该 CookieSize 字段,即此 Cookie 的大小。
  • Http 字段,即 Cookiehttponly 属性。若此属性为 true,则只有在 HTTP Headers 中会带有此 Cookie 的信息,而不能通过 document.cookie 来访问此 Cookie。
  • Secure,即该 Cookie 是否仅被使用安全协议传输。安全协议。安全协议有 HTTPS、SSL 等,在网络上传输数据之前先将数据加密。默认为 false

CookiesSessionStorageLocalStorage 的区别?

  • 相同点:都是存储在浏览器本地的
  • 不同点:
    • 写入方式:cookie是由服务器端写入的,而SessionStorageLocalStorage都是由前端写入的
    • 生命周期:cookie的生命周期是由服务器端在写入的时候就设置好的,LocalStorage是写入就一直存在,除非手动清除,SessionStorage是页面关闭的时候就会自动清除
    • 存储大小:cookie的存储空间比较小大概4KB,SessionStorageLocalStorage存储空间比较大,大概5M
    • 数据共享:CookieSessionStorageLocalStorage数据共享都遵循同源原则,SessionStorage还限制必须是同一个页面
    • 发送请求时是否携带:在前端给后端发送请求的时候会自动携带Cookie中的数据,但是SessionStorageLocalStorage不会
    • 应用场景:Cookie一般用于存储登录验证信息SessionID 或者 tokenLocalStorage常用于存储不易变动的数据,减轻服务器的压力,SessionStorage可以用来检测用户是否是刷新进入页面,如音乐播放器恢复播放进度条的功能

实现可过期的localstorage数据

  • 得分点 惰性删除、定时删除
  • 标准回答 localStorage只能用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。所以要实现可过期的localStorage缓存的中重点就是:如何清理过期的缓存。
    • 目前有两种方法,一种是惰性删除,另一种是定时删除。
    • 惰性删除是指某个键值过期后,该键值不会被马上删除,而是等到下次被使用的时候,才会被检查到过期,此时才能得到删除。实现方法是,存储的数据类型是个对象,该对象有两个key,一个是要存储的value值,另一个是当前时间。获取数据的时候,拿到存储的时间和当前时间做对比,如果超过过期时间就清除Cookie。
    • 定时删除是指,每隔一段时间执行一次删除操作,并通过限制删除操作执行的次数和频率,来减少删除操作对CPU的长期占用。另一方面定时删除也有效的减少了因惰性删除带来的对localStorage空间的浪费。实现过程,获取所有设置过期时间的key判断是否过期,过期就存储到数组中,遍历数组,每隔1S(固定时间)删除5个(固定个数),直到把数组中的key从localstorage中全部删除。
  • 加分回答
    • LocalStorage清空应用场景:
      • token存储在LocalStorage中,要清空

实现Storage,使得该对象为单例,并对localStorage进行封装设置值setItem(key,value)和getItem(key)

1
2
3
4
5
6
7
8
9
10
11
var instance = null;
class Storage {
static getInstance() {
if (!instance) {
instance = new Storage();
}
return instance;
}
setItem = (key, value) => localStorage.setItem(key, value),
getItem = key => localStorage.getItem(key)
}

token 能放在cookie中吗

  • 得分点 能、不设置cookie有效期、重新登录重写cookie覆盖原来的cookie
  • 标准回答
    • 能。 token一般是用来判断用户是否登录的,它内部包含的信息有:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
    • token可以存放在Cookie中,token 是否过期,应该由后端来判断,不该前端来判断,所以token存储在cookie中只要不设置cookie的过期时间就ok了,如果 token 失效,就让后端在接口中返回固定的状态表示token 失效,需要重新登录,再重新登录的时候,重新设置 cookie 中的 token 就行。
  • 加分回答
  • token认证流程
    1. 客户端使用用户名跟密码请求登录
    2. 服务端收到请求,去验证用户名与密码
    3. 验证成功后,服务端签发一个 token ,并把它发送给客户端
    4. 客户端接收 token 以后会把它存储起来,比如放在 cookie 里或者 localStorage
    5. 客户端每次发送请求时都需要带着服务端签发的 token(把 token 放到 HTTP 的 Header 里)
    6. 服务端收到请求后,需要验证请求里带有的 token ,如验证成功则返回对应的数据

请你谈谈Cookie的弊端

cookie虽然在持久保存客户端数据提供了方便,分担了服务器存储的负担,但还是有很多局限性的

  • 每个特定的域名下最多生成20cookie
  • IE6或更低版本最多20cookie
  • IE7和之后的版本最后可以有50cookie
  • Firefox最多50个cookie
  • chromeSafari没有做硬性限制
  • IE 和 Opera 会清理近期最少使用的 cookieFirefox 会随机清理 cookie
  • cookie 的最大大约为 4096 字节,为了兼容性,一般设置不超过 4095 字节
  • 如果 cookie 被人拦截了,就可以取得所有的 sessioln 信息

❤ 跨域方案

很多种方法,但万变不离其宗,都是为了搞定同源策略。重用的有 jsonpiframecorsimgHTML5 postMessage等等。其中用到 html 标签进行跨域的原理就是 html 不受同源策略影响。但只是接受 Get 的请求方式,这个得清楚。

延伸1:img iframe script 来发送跨域请求有什么优缺点?

1. iframe

  • 优点:跨域完毕之后DOM操作和互相之间的JavaScript调用都是没有问题的
  • 缺点:1.若结果要以URL参数传递,这就意味着在结果数据量很大的时候需要分割传递,巨烦。2.还有一个是iframe本身带来的,母页面和iframe本身的交互本身就有安全性限制。

2. script

  • 优点:可以直接返回json格式的数据,方便处理
  • 缺点:只接受GET请求方式

3. 图片ping

  • 优点:可以访问任何url,一般用来进行点击追踪,做页面分析常用的方法
  • 缺点:不能访问响应文本,只能监听是否响应

延伸2:配合 webpack 进行反向代理?

webpackdevServer 选项里面提供了一个 proxy 的参数供开发人员进行反向代理

1
2
3
4
5
6
7
'/api': {
target: 'http://www.example.com', // your target host
changeOrigin: true, // needed for virtual hosted sites
pathRewrite: {
'^/api': '' // rewrite path
}
},

然后再配合 http-proxy-middleware 插件对 api 请求地址进行代理

1
2
3
4
5
6
7
8
9
const express = require('express');
const proxy = require('http-proxy-middleware');
// proxy api requests
const exampleProxy = proxy(options); // 这里的 options 就是 webpack 里面的 proxy 选项对应的每个选项

// mount `exampleProxy` in web server
const app = express();
app.use('/api', exampleProxy);
app.listen(3000);

然后再用 nginx 把允许跨域的源地址添加到报头里面即可

说到 nginx ,可以再谈谈 CORS 配置,大致如下

1
2
3
4
5
6
7
8
9
10
11
12
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'DNT, X-Mx-ReqToken, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 200;
}
}
  • 得分点:同源限制、协议、域名、端口、CORS、node中间件、JSONP、postmessage

  • 标准回答:
    跨域:当前页面中的某个接口请求的地址和当前页面的地址如果协议、域名、端口其中有一项不同,就说该接口跨域了。
    跨域限制的原因:浏览器为了保证网页的安全,出的同源协议策略。

    跨域报错信息:

    img

    跨域解决方案

    • cors:目前最常用的一种解决办法,通过设置后端允许跨域实现。
    1
    2
      res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader("Access-Control-Allow-Methods", "GET, PUT, OPTIONS, POST");
    • node中间件、nginx反向代理:跨域限制的时候浏览器不能跨域访问服务器,node中间件和nginx反向代理,都是让请求发给代理服务器,静态页面面和代理服务器是同源的,然后代理服务器再向后端服务器发请求,服务器和服务器之间不存在同源限制。
  • JSONP:利用的原理是script标签可以跨域请求资源,将回调函数作为参数拼接在url中。后端收到请求,调用该回调函数,并将数据作为参数返回去,注意设置响应头返回文档类型,应该设置成javascript。

    • postmessage:H5新增API,通过发送和接收API实现跨域通信。
  • 加分回答:

    • 跨域场景:前后端分离式开发、调用第三方接口

跨域

因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax请求会失败。

我们可以通过以下几种常用方法解决跨域的问题

1. JSONP

JSONP 的原理很简单,就是利用 <script>标签没有跨域限制的漏洞。通过 <script>标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时

1
2
3
4
5
6
<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
function jsonp(data) {
console.log(data)
}
</script>

JSONP 使用简单且兼容性不错,但是只限于 get 请求

  • 在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP,以下是简单实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function jsonp(url, jsonpCallback, success) {
let script = document.createElement("script");
script.src = url;
script.async = true;
script.type = "text/javascript";
window[jsonpCallback] = function(data) {
success && success(data);
};
document.body.appendChild(script);
}
jsonp(
"http://xxx",
"callback",
function(value) {
console.log(value);
}
);

2. CORS

  • ORS需要浏览器和后端同时支持。IE 89 需要通过 XDomainRequest 来实现。
  • 浏览器会自动进行 CORS 通信,实现CORS通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
  • 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
1
2
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader("Access-Control-Allow-Methods", "GET, PUT, OPTIONS, POST");

3. document.domain

  • 该方式只能用于二级域名相同的情况下,比如 a.test.comb.test.com 适用于该方式。
  • 只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域

1.)父窗口:(http://www.domain.com/a.html)

1
2
3
4
5
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>

2.)子窗口:(http://child.domain.com/b.html)

1
2
3
document.domain = 'domain.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);

4. postMessage

这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

1
2
3
4
5
6
7
8
9
10
// 发送消息端
window.parent.postMessage('message', 'http://test.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
var origin = event.origin || event.originalEvent.origin;
if (origin === 'http://test.com') {
console.log('验证通过')
}
});

5. nginx代理跨域

nginx 配置跨域,可以为全局配置和单个代理配置(两者不能同时配置)

  1. 全局配置,在nginx.conf文件中的 http 节点加入跨域信息
1
2
3
4
5
6
7
http {
# 跨域配置
add_header 'Access-Control-Allow-Origin' '$http_origin' ;
add_header 'Access-Control-Allow-Credentials' 'true' ;
add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;
}
  1. 局部配置(单个代理配置跨域), 在路径匹配符中加入跨域信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server {
listen 8080;
server_name server_name;

charset utf-8;

location / {
# 这里配置单个代理跨域,跨域配置
add_header 'Access-Control-Allow-Origin' '$http_origin' ;
add_header 'Access-Control-Allow-Credentials' 'true' ;
add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;

#配置代理 代理到本机服务端口
proxy_pass http://127.0.0.1:9000;
proxy_redirect off;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

6. Node 中间层接口转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const router = require('koa-router')()
const rp = require('request-promise');

// 通过node中间层转发实现接口跨域
router.post('/github', async (ctx, next) => {
let {category = 'trending',lang = 'javascript',limit,offset,period} = ctx.request.body
lang = lang || 'javascript'
limit = limit || 30
offset = offset || 0
period = period || 'week'
try {
let res = await rp({
method: 'POST',
// 跨域的接口
uri: `https://e.juejin.cn/resources/github`,
body: {
category,
lang,
limit,
offset,
period
},
json: true
})

ctx.body = res
} catch(error) {
ctx.body = {
code: -1,
message: error.message || '查询github接口失败'
}
}
})

module.exports = router

7.本地代理服务devServer

1
2
3
4
5
6
7
8
9
10
// vue或react项目中设置devServer配置
devServer: {
proxy: {
// 接口前缀
"/api": {
target: "http://XX.XX.XX.XX:8081", // 跨域请求的接口地址
changeOrigin: true,
},
},
},

❤ XSS 和 CSRF

img

1. XSS

涉及面试题:什么是 XSS 攻击?如何防范 XSS 攻击?什么是 CSP

  • XSS 简单点来说,就是攻击者想尽一切办法将可以执行的代码注入到网页中。
  • XSS 可以分为多种类型,但是总体上我认为分为两类:持久型和非持久型。
  • 持久型也就是攻击的代码被服务端写入进数据库中,这种攻击危害性很大,因为如果网站访问量很大的话,就会导致大量正常访问页面的用户都受到攻击。

举个例子,对于评论功能来说,就得防范持久型 XSS 攻击,因为我可以在评论中输入以下内容

image.png

  • 这种情况如果前后端没有做好防御的话,这段评论就会被存储到数据库中,这样每个打开该页面的用户都会被攻击到。
  • 非持久型相比于前者危害就小的多了,一般通过修改 URL 参数的方式加入攻击代码,诱导用户访问链接从而进行攻击。

举个例子,如果页面需要从 URL 中获取某些参数作为内容的话,不经过过滤就会导致攻击代码被执行

1
2
<!-- http://www.domain.com?name=<script>alert(1)</script> -->
<div>{{name}}</div>

但是对于这种攻击方式来说,如果用户使用 Chrome 这类浏览器的话,浏览器就能自动帮助用户防御攻击。但是我们不能因此就不防御此类攻击了,因为我不能确保用户都使用了该类浏览器。

img

对于 XSS 攻击来说,通常有两种方式可以用来防御。

  1. 转义字符

首先,对于用户的输入应该是永远不信任的。最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义

1
2
3
4
5
6
7
8
9
10
function escape(str) {
str = str.replace(/&/g, '&amp;')
str = str.replace(/</g, '&lt;')
str = str.replace(/>/g, '&gt;')
str = str.replace(/"/g, '&quto;')
str = str.replace(/'/g, '&#39;')
str = str.replace(/`/g, '&#96;')
str = str.replace(/\//g, '&#x2F;')
return str
}

通过转义可以将攻击代码 <script>alert(1)</script> 变成

1
2
// -> &lt;script&gt;alert(1)&lt;&#x2F;script&gt;
escape('<script>alert(1)</script>')

但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式

1
2
3
4
const xss = require('xss')
let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')
// -> <h1>XSS Demo</h1>&lt;script&gt;alert("xss");&lt;/script&gt;
console.log(html)

以上示例使用了 js-xss 来实现,可以看到在输出中保留了 h1 标签且过滤了 script标签

  1. CSP

CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。

通常可以通过两种方式来开启 CSP

  • 设置 HTTP Header 中的 Content-Security-Policy
  • 设置 meta 标签的方式 <meta http-equiv="Content-Security-Policy">

这里以设置 HTTP Header 来举例

只允许加载本站资源

1
Content-Security-Policy: default-src ‘self’

只允许加载 HTTPS 协议图片

1
Content-Security-Policy: img-src https://*

允许加载任何来源框架

1
Content-Security-Policy: child-src 'none'

当然可以设置的属性远不止这些,你可以通过查阅 文档 (opens new window)的方式来学习,这里就不过多赘述其他的属性了。

对于这种方式来说,只要开发者配置了正确的规则,那么即使网站存在漏洞,攻击者也不能执行它的攻击代码,并且 CSP 的兼容性也不错。

img

2 CSRF

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法

CSRF 就是利用用户的登录态发起恶意请求

如何攻击

假设网站中有一个通过 Get 请求提交用户评论的接口,那么攻击者就可以在钓鱼网站中加入一个图片,图片的地址就是评论接口

1
<img src="http://www.domain.com/xxx?comment='attack'"/>

img

1
res.setHeader('Set-Cookie', `username=poetry2;sameSite = strict;path=/;httpOnly;expires=${getCookirExpires()}`)

在B网站,危险网站向A网站发起请求

1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
<body>
<!-- 利用img自动发送请求 -->
<img src="http://localhost:8000/api/user/login" />
</body>
</html>

会带上A网站的cookie

img

1
2
3
4
5
6
7
8
9
10
11
12
// 在A网站下发cookie的时候,加上sameSite=strict,这样B网站在发送A网站请求,不会自动带上A网站的cookie,保证了安全


// NAME=VALUE 赋予Cookie的名称及对应值
// expires=DATE Cookie 的有效期
// path=PATH 赋予Cookie的名称及对应值
// domain=域名 作为 Cookie 适用对象的域名 (若不指定则默认为创建 Cookie 的服务器的域名) (一般不指定)
// Secure 仅在 HTTPS 安全通信时才会发送 Cookie
// HttpOnly 加以限制,使 Cookie 不能被 JavaScript 脚本访问
// SameSite Lax|Strict|None 它允许您声明该Cookie是否仅限于第一方或者同一站点上下文

res.setHeader('Set-Cookie', `username=poetry;sameSite=strict;path=/;httpOnly;expires=${getCookirExpires()}`)

img

如何防御

  • Get 请求不对数据进行修改
  • 不让第三方网站访问到用户 Cookie
  • 阻止第三方网站请求接口
  • 请求时附带验证信息,比如验证码或者 token
  • SameSite Cookies: 只能当前域名的网站发出的http请求,携带这个Cookie。当然,由于这是新的cookie属性,在兼容性上肯定会有问题

CSRF攻击,仅仅是利用了http携带cookie的特性进行攻击的,但是攻击站点还是无法得到被攻击站点的cookie。这个和XSS不同,XSS是直接通过拿到Cookie等信息进行攻击的

在CSRF攻击中,就Cookie相关的特性:

  • http请求,会自动携带Cookie。
  • 携带的cookie,还是http请求所在域名的cookie。

3 密码安全

加盐

对于密码存储来说,必然是不能明文存储在数据库中的,否则一旦数据库泄露,会对用户造成很大的损失。并且不建议只对密码单纯通过加密算法加密,因为存在彩虹表的关系

  • 通常需要对密码加盐,然后进行几次不同加密算法的加密
1
2
// 加盐也就是给原密码添加字符串,增加原密码长度
sha256(sha1(md5(salt + password + salt)))

但是加盐并不能阻止别人盗取账号,只能确保即使数据库泄露,也不会暴露用户的真实密码。一旦攻击者得到了用户的账号,可以通过暴力破解的方式破解密码。对于这种情况,通常使用验证码增加延时或者限制尝试次数的方式。并且一旦用户输入了错误的密码,也不能直接提示用户输错密码,而应该提示账号或密码错误

前端加密

虽然前端加密对于安全防护来说意义不大,但是在遇到中间人攻击的情况下,可以避免明文密码被第三方获取

4. 总结

  • XSS:跨站脚本攻击,是一种网站应用程序的安全漏洞攻击,是代码注入的一种。常见方式是将恶意代码注入合法代码里隐藏起来,再诱发恶意代码,从而进行各种各样的非法活动

防范:记住一点 “所有用户输入都是不可信的”,所以得做输入过滤和转义

  • CSRF:跨站请求伪造,也称 XSRF,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。与 XSS 相比,XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。

防范:用户操作验证(验证码),额外验证机制(token使用)等

XSS攻击

  • 得分点 XSS是跨站脚本攻击、向目标网站插入恶意代码、大量用户访问网站时运行恶意脚本获取信息
  • 标准回答
    • XSS是跨站脚本攻击(Cross Site Scripting),不写为CSS是为了避免和层叠样式表(Cascading Style Sheets)的缩写混淆,所以将跨站脚本攻击写为XSS。攻击者可以通过向Web页面里面插入script代码,当用户浏览这个页面时,就会运行被插入的script代码,达到攻击者的目的。XSS的危害一般是泄露用户的登录信息cookie,攻击者可以通过cookie绕过登录步骤直接进入站点。
    • XSS的分类分为反射型存储型
      • 反射型就是临时通过url访问网站,网站服务端将恶意代码从url中取出,拼接在HTML中返回给浏览器,用户就会执行恶意代码。
      • 存储型就是将恶意代码以留言的形式保存在服务器数据库,任何访问网站的人都会受到攻击。预防XSS攻击的方案基本是对数据进行严格的输出编码,比如HTML元素的编码,JavaScript编码,css编码,url编码等等。
  • 加分回答
    • XSS的危害:
      • 获取cookie:网站中的登录一般都是用cookie作为某个用户的身份证明,这是服务器端返回的一串字符。如果cookie被攻击者拿到,那么就可以绕过密码登录。当空间、论坛如果可以被插入script代码,那么进入空间或者论坛的人的账号就可以轻易被攻击者获取。
      • 恶意跳转:直接在页面中插入window.location.href进行跳转。 XSS的分类:
        • 反射型XSS(非持久型XSS):通过URL参数直接注入
        • 存储型XSS(持久型XSS):存储到数据库后读取时注入
    • XSS的预防:
      • 浏览器的防御和“X-XSS-Protection”有关,默认值为1,即默认打开XSS防御,可以防御反射型的XSS,不过作用有限,只能防御注入到HTML的节点内容或属性的XSS,例如URL参数中包含script标签。不建议只依赖此防御手段。
      • 防御HTML节点内容,通过转义<为<以及>为>来实现防御HTML节点内容。
      • 预防HTML属性,通过转义”->&quto来实现防御,一般不转义空格,但是这要求属性必须带引号。
      • 预防JavaScript代码,通过将数据进行JSON序列化。
      • 防御富文本是比较复杂的工程,因为富文本可以包含HTML和script,这些难以预测与防御,建议是通过白名单的方式来过滤允许的HTML标签和标签的属性来进行防御,大概的实现方式是:
        • 将HTML代码段转成树级结构的数据
        • 遍历树的每一个节点,过滤节点的类型和属性,或进行特殊处理
        • 处理完成后,将树级结构转化成HTML代码
        • 开启浏览器XSS防御:Http Only cookie,禁止 JavaScript 读取某些敏感 Cookie,攻击者完成XSS注入后也无法窃取此 Cookie。

CSRF攻击

  • 得分点 CSRF时跨站请求伪造、盗用用户身份发起请求
  • 标准回答
    • CSRF跨站点请求伪造(Cross Site Request Forgery)和XSS攻击一样,有巨大的危害性,就是攻击者盗用了用户的身份,以用户的身份发送恶意请求,但是对服务器来说这个请求是合理的,这样就完成了攻击者的目标。
    • CSRF攻击的过程原理是:
      • 用户打开浏览器,访问目标网站A,输入用户名和密码请求登录
      • 用户信息在通过认证后,网站A产生一个cookie信息返回给浏览器,这个时候用户以可正常发送请求到网站A
      • 用户在没有退出网站A之前在同一个浏览器打开了另一个新网站B。
      • 新网站B收到用户请求之后返回一些攻击代码,并发出一个请求要求访问返回cookie的网站A
      • 浏览器收到这些攻击性代码之后根据新网站B的请求在用户不知道的情况下以用户的权限操作了cookie并向网站A服务器发起了合法的请求。
    • 预防CSRF攻击主要有以下策略:
      • 使用验证码,在表单中添加一个随机的数字或者字母验证码,强制要求用户和应用进行直接的交互。
      • HTTP中Referer字段,检查是不是从正确的域名访问过来,它记录了HTTP请求的来源地址。
      • 使用token验证,在HTTP请求头中添加token字段,并且在服务器端建立一个拦截器验证这个token,如果token不对,就拒绝这个请求。
  • 加分回答
    • 验证HTTP Referer字段的好处就是实施起来特别简单,普通的网站开发不需要特别担心CSRF漏洞,只需要在最后面设置一个拦截器来验证referer的值就可以了,不需要改变已有的代码逻辑,非常便捷。但是这个方法也不是万无一失的,虽然referer是浏览器提供的,但是不同的浏览器可能在referer的实现上或多或少有自身的漏洞,所以使用referer的安全保证是通过浏览器实现的。
    • 使用token验证的方法要比referer更安全一些,需要把token放在一个HTTP自定义的请求头部中,解决了使用get或者post传参的不便性。

常见web安全及防护原理

  • sql注入原理
    • 就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令
  • 总的来说有以下几点
    • 永远不要信任用户的输入,要对用户的输入进行校验,可以通过正则表达式,或限制长度,对单引号和双"-"进行转换等
    • 永远不要使用动态拼装SQL,可以使用参数化的SQL或者直接使用存储过程进行数据查询存取
    • 永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接
    • 不要把机密信息明文存放,请加密或者hash掉密码和敏感的信息

XSS原理及防范

  • Xss(cross-site scripting)攻击指的是攻击者往Web页面里插入恶意html标签或者javascript代码。比如:攻击者在论坛中放一个看似安全的链接,骗取用户点击后,窃取cookie中的用户私密信息;或者攻击者在论坛中加一个恶意表单,当用户提交表单的时候,却把信息传送到攻击者的服务器中,而不是用户原本以为的信任站点

XSS防范方法

  • 首先代码里对用户输入的地方和变量都需要仔细检查长度和对”<”,”>”,”;”,”’”等字符做过滤;其次任何内容写到页面之前都必须加以encode,避免不小心把html tag 弄出来。这一个层面做好,至少可以堵住超过一半的XSS 攻击

XSS与CSRF有什么区别吗?

  • XSS是获取信息,不需要提前知道其他用户页面的代码和数据包。CSRF是代替用户完成指定的动作,需要知道其他用户页面的代码和数据包。要完成一次CSRF攻击,受害者必须依次完成两个步骤
  • 登录受信任网站A,并在本地生成Cookie
  • 在不登出A的情况下,访问危险网站B

CSRF的防御

  • 服务端的CSRF方式方法很多样,但总的思想都是一致的,就是在客户端页面增加伪随机数
  • 通过验证码的方法

❤ Service Worker

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API

浏览器对 ServiceWorker 做了很多限制

  • ServiceWorker 中无法直接访问 DOM,但可以通过 postMessage 接口发送的消息来与其控制的页面进行通信
  • ServiceWorker 只能在本地环境下或 HTTPS 网站中使用
  • ServiceWorker 有作用域的限制,一个 ServiceWorker 脚本只能作用于当前路径及其子路径;

目前该技术通常用来做缓存文件,提高首屏速度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register("sw.js")
.then(function(registration) {
console.log("service worker 注册成功");
})
.catch(function(err) {
console.log("servcie worker 注册失败");
});
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener("install", e => {
e.waitUntil(
caches.open("my-cache").then(function(cache) {
return cache.addAll(["./index.html", "./index.js"]);
})
);
});

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener("fetch", e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response;
}
console.log("fetch source");
})
);
});

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了

img

在 Cache 中也可以发现我们所需的文件已被缓存

img

当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的

❤ DOM 节点操作

(1)创建新节点

1
2
3
createDocumentFragment()    //创建一个DOM片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点

(2)添加、移除、替换、插入

1
2
3
4
appendChild(node)
removeChild(node)
replaceChild(new,old)
insertBefore(new,old)

(3)查找

1
2
3
4
5
6
getElementById();
getElementsByName();
getElementsByTagName();
getElementsByClassName();
querySelector();
querySelectorAll();

(4)属性操作

1
2
3
4
getAttribute(key);
setAttribute(key, value);
hasAttribute(key);
removeAttribute(key);

❤ 掌握页面的加载过程

网页加载流程

  • 当我们打开网址的时候,浏览器会从服务器中获取到 HTML 内容
  • 浏览器获取到 HTML 内容后,就开始从上到下解析 HTML 的元素
  • <head>元素内容会先被解析,此时浏览器还没开始渲染页面
    • 我们看到<head>元素里有用于描述页面元数据的<meta>元素,还有一些<link>元素涉及外部资源(如图片、CSS 样式等),此时浏览器会去获取这些外部资源。除此之外,我们还能看到<head>元素中还包含着不少的<script>元素,这些<script>元素通过src属性指向外部资源
  • 当浏览器解析到这里时(步骤 3),会暂停解析并下载 JavaScript 脚本
  • 当 JavaScript 脚本下载完成后,浏览器的控制权转交给 JavaScript 引擎。当脚本执行完成后,控制权会交回给渲染引擎,渲染引擎继续往下解析 HTML 页面
  • 此时<body>元素内容开始被解析,浏览器开始渲染页面
  • 在这个过程中,我们看到<head>中放置的<script>元素会阻塞页面的渲染过程:把 JavaScript 放在<head>里,意味着必须把所有 JavaScript 代码都下载、解析和解释完成后,才能开始渲染页面
  • 如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,用户体验会变得很糟糕
  • 因此,对于对性能要求较高、需要快速将内容呈现给用户的网页,常常会将 JavaScript 脚本放在<body>的最后面。这样可以避免资源阻塞,页面得以迅速展示。我们还可以使用defer/async/preload等属性来标记<script>标签,来控制 JavaScript 的加载顺序

延迟加载的方式有哪些

js 的加载、解析和执行会阻塞页面的渲染过程,因此我们希望 js 脚本能够尽可能的延迟加载,提高页面的渲染速度。

几种方式是:

  • 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行
  • 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样
  • 给 js 脚本添加 async属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行
  • 动态创建 DOM 标签的方式,我们可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本

img

怎么判断页面是否加载完成

  • Load 事件触发代表页面中的 DOMCSSJS,图片已经全部加载完毕。
  • DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSSJS,图片加载

❤ 从输入URL到页面展示过程

img

1. DNS域名解析

img

  • 根 DNS 服务器 :返回顶级域 DNS 服务器的 IP 地址
  • 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址
  • 权威 DNS 服务器 :返回相应主机的 IP 地址

DNS的域名查找,在客户端和浏览器,本地DNS之间的查询方式是递归查询;在本地DNS服务器与根域及其子域之间的查询方式是迭代查询;

img

在客户端输入 URL 后,会有一个递归查找的过程,从浏览器缓存中查找->本地的hosts文件查找->找本地DNS解析器缓存查找->本地DNS服务器查找,这个过程中任何一步找到了都会结束查找流程。

如果本地DNS服务器无法查询到,则根据本地DNS服务器设置的转发器进行查询。若未用转发模式,则迭代查找过程如下图:

img

结合起来的过程,可以用一个图表示:

img

在查找过程中,有以下优化点:

  • DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存
  • 在域名和 IP 的映射过程中,给了应用基于域名做负载均衡的机会,可以是简单的负载均衡,也可以根据地址和运营商做全局的负载均衡。

2. 建立TCP连接

首先,判断是不是https的,如果是,则HTTPS其实是HTTP + SSL / TLS 两部分组成,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据

进行三次握手,建立TCP连接。

  • 第一次握手:建立连接。客户端发送连接请求报文段
  • 第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认
  • 第三次握手:客户端收到服务器的SYN+ACK报文段,向服务器发送ACK报文段

SSL握手过程

  • 第一阶段 建立安全能力 包括协议版本 会话Id 密码构件 压缩方法和初始随机数
  • 第二阶段 服务器发送证书 密钥交换数据和证书请求,最后发送请求-相应阶段的结束信号
  • 第三阶段 如果有证书请求客户端发送此证书 之后客户端发送密钥交换数据 也可以发送证书验证消息
  • 第四阶段 变更密码构件和结束握手协议

完成了之后,客户端和服务器端就可以开始传送数据

发送HTTP请求,服务器处理请求,返回响应结果

TCP连接建立后,浏览器就可以利用 HTTP/HTTPS 协议向服务器发送请求了。服务器接受到请求,就解析请求头,如果头部有缓存相关信息如if-none-match与if-modified-since,则验证缓存是否有效,若有效则返回状态码为304,若无效则重新返回资源,状态码为200

这里有发生的一个过程是HTTP缓存,是一个常考的考点,大致过程如图:

img

3. 关闭TCP连接

4. 浏览器渲染

按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、栅格化和显示。如图:

img

  • 渲染进程将 HTML 内容转换为能够读懂DOM 树结构。
  • 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
  • 创建布局树,并计算元素的布局信息。
  • 对布局树进行分层,并生成分层树。
  • 为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。
  • 合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。

构建 DOM 树

img

  • 转码(Bytes -> Characters)—— 读取接收到的 HTML 二进制数据,按指定编码格式将字节转换为 HTML 字符串
  • Tokens 化(Characters -> Tokens)—— 解析 HTML,将 HTML 字符串转换为结构清晰的 Tokens,每个 Token 都有特殊的含义同时有自己的一套规则
  • 构建 Nodes(Tokens -> Nodes)—— 每个 Node 都添加特定的属性(或属性访问器),通过指针能够确定 Node 的父、子、兄弟关系和所属 treeScope(例如:iframe 的 treeScope 与外层页面的 treeScope 不同)
  • 构建 DOM 树(Nodes -> DOM Tree)—— 最重要的工作是建立起每个结点的父子兄弟关系

样式计算

渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。

CSS 样式来源主要有 3 种,分别是通过 link 引用的外部 CSS 文件、style标签内的 CSS、元素的 style 属性内嵌的 CSS。

页面布局

布局过程,即排除 script、meta 等功能化、非视觉节点,排除 display: none 的节点,计算元素的位置信息,确定元素的位置,构建一棵只包含可见元素布局树。如图:

img

其中,这个过程需要注意的是回流和重绘

生成分层树

页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)

栅格化

合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图

img

通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。

显示

最后,合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上,渲染过程完成。

❤ WebSocket

由于 http 存在一个明显的弊端(消息只能有客户端推送到服务器端,而服务器端不能主动推送到客户端),导致如果服务器如果有连续的变化,这时只能使用轮询,而轮询效率过低,并不适合。于是 WebSocket被发明出来

相比与 http 具有以下优点

  • 支持双向通信,实时性更强;
  • 可以发送文本,也可以二进制文件;
  • 协议标识符是 ws,加密后是 wss
  • 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部;
  • 支持扩展。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)
  • 无跨域问题。

实现比较简单,服务端库如 socket.iows,可以很好的帮助我们入门。而客户端也只需要参照 api 实现即可

WebSocket 的实现和应用

参考链接

Websocket 是一个 持久化的协议, 基于 http , 服务端可以 主动 push

兼容:

  • FLASH Socket
  • 长轮询: 定时发送 ajax
  • long poll: 发送 –> 有消息时再 response
  • new WebSocket(url)
  • ws.onerror = fn
  • ws.onclose = fn
  • ws.onopen = fn
  • ws.onmessage = fn
  • ws.send()

什么是 WebSocket

WebSocket 是 HTML5 中的协议,支持持久连续,http 协议不支持持久性连接。Http1.0 和 HTTP1.1 都不支持持久性的链接,HTTP1.1 中的 keep-alive,将多个 http 请求合并为 1个

WebSocket 是什么样的协议,具体有什么优点

  • HTTP 的生命周期通过 Request 来界定,也就是 Request 一个 Response,那么在 Http1.0 协议中,这次 Http 请求就结束了。在 Http1.1 中进行了改进,是的有一个 connection: Keep-alive,也就是说,在一个 Http 连接中,可以发送多个 Request,接收多个 Response。 但是必须记住,在 Http 中一个 Request 只能对应有一个 Response,而且这个 Response 是被动的,不能主动发起。
  • WebSocket 是基于 Http 协议的,或者说借用了 Http 协议来完成一部分握手,在握手阶段 与 Http 是相同的。我们来看一个 websocket 握手协议的实现,基本是 2 个属性,upgrade, connection。

基本请求如下:

1
2
3
4
5
6
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
Origin: http://example.com

多了下面 2 个属性:

1
2
Upgrade:webSocket 
Connection:Upgrade

告诉服务器发送的是 websocket

理解WebSocket协议的底层原理、与HTTP的区别

WebSocket 是一个持久化的网络通信协议,可以在单个 TCP 连接上进行 全双工通讯 ,没有了 RequestResponse 的概念,两者地位完全平等,连接一旦建立,客户端和服务端之间可以实时进行双向数据传输。

  1. HTTP 是非持久协议,客户端想知道服务端的处理进度只能通过长轮询或者是 long poll 的方式,但是前者对服务器压力大,后者则会因为一直等待响应造成阻塞。

img

  1. 虽然 http1.1 默认开启了 keep-alive 长连接保持了这个 TCP 通道使得在一个 HTTP 连接中可以发送多个请求,接受多个响应,但是一个请求只能有一个响应,而且这个响应也是被动的,不能主动发起
  2. WebSocket 虽然是独立于 HTTP 的一种协议,但是 WebSocket 必须依赖 HTTP 协议进行一次握手(在握手阶段是一样的),握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了,可以用一张图理解两者有交集,但并不是全部。

img

socket

  1. socket 也被称为套接字,与 HTTP 和 WebSocket 不一样,socket 不是协议,它是在程序层面上对传输层协议(可以主要理解为 TCP / IP)的接口封装。可以理解为一个能够提供端对端的通信的调用接口(API)。
  2. 对于程序员而言,其需要在 A 端创建一个 socket 实例,并为这个实例提供其所要连接的 B 端的 IP 地址和端口号,而在 B 端创建另一个 socket 实例,并且绑定本地端口号来进行监听。当 A 和 B 建立连接后,双方就建立了一个端对端的 TCP

应用场景

WebSocket 可以做弹幕、消息订阅、多玩家游戏、协同编辑、股票基金实时报价、视频会议、在线教育、聊天室等应用实时监听服务端变化。

WebCocket 握手

  • WebSocket 握手请求报文
1
2
3
4
5
6
7
8
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

下面是与传统 HTTP 报文不同的地方:

1
2
Upgrade: websocket
Connection: Upgrade

表示发起的是 WebSocket 协议

1
2
3
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
  • Sec-WebSocket-Key 是由浏览器随机生成的,验证是否可以进行 WebSocket 通信,防止恶意或者无意的连接;

  • Sec-WebSocket-Protocol 是用户自定义的字符串,用来标识服务所以需要的协议;

  • Sec-WebSocket-Version 表示支持的 WebSocket 版本。

  • 服务端响应

1
2
3
4
5
6
HTTP/1.1 101 
Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
  • 101 响应码 表示要转换协议。
  • Connection: Upgrade 表示升级新协议请求。
  • Upgrade: websocket 表示升级为 WebSocket 协议。
  • Sec-WebSocket-Accept 是经过服务器确认,并加密过后的 Sec-WebSocket-Key ,用来证明客户端和服务端之间能够进行通信了。
  • Sec-WebSocket-Protocol 表示最终使用的协议。

至此,客户端和服务器握手成功建立了 WebSocket 连接,HTTP 已经完成了他所有工作,接下来就是完全按照 WebSocket 协议进行通信。

WebSocket 心跳

可能会有某些未知情况导致 socket 断开,而客户端和服务端却不知道,需要客户端定时发送一个 心跳 ping 让服务端知道自己在线,服务端也需要回复一个 心跳 pong 告诉客户端自己可用,否则视为断开。

WebSocket 状态

WebSocket 对象中的 readyState 属性有四种状态:

  • 0:表示正在连接
  • 1:表示连接成功,可以通信了
  • 2:表示连接正在关闭
  • 3:表示连接已经关闭,或者打开连接失败

websocket和HTTP有什么不一样小结

  • 双向通信
  • 数据格式比较轻量,性能开销小,通信高效
    • 协议控制的数据包头部较小,而HTTP协议每次通信都需要携带完整的头部
  • 更好的二进制支持
  • 没有同源限制,客户端可以与任意服务器通信
  • HTTP 协议有着良好的兼容性。默认端口也是80443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器

WEB 应用从服务器主动推送 Data 到客户端有那些方式

  • AJAX 轮询

  • html5 服务器推送事件 (new EventSource(SERVER_URL)).addEventListener("message", func);

  • html5 Websocket

    • (new WebSocket(SERVER_URL)).addEventListener("message", func);

检测浏览器版本版本有哪些方式?

  • 根据 navigator.userAgent UA.toLowerCase().indexOf('chrome')
  • 根据 window 对象的成员 'ActiveXObject' in window