持久化存储与HTTP缓存

2018-02-12 10:47:33来源:https://juejin.im/post/5a7e92696fb9a06336114aca作者:稀土掘金人点击

分享

本文主要学习一下一些高级的HTTP知识,例如 Session LocalStorage Cache-Control Expires ETag


其实主要就是涉及到了 持久化存储与缓存的技术


在此之前已经学习了 Cookie 的相关知识,其中 Cookie 有个缺点可以人为修改,有一定的安全隐患。


所以,针对这个缺点,诞生了 Session


Session

一般来说 Session 是基于Cookie实现的,它利用一个 sessionId 把用户的敏感数据隐藏起来,除非暴力穷举才有可能获得敏感数据。


sessionId

我们使用 Cookie 的时候,一般是服务器给用户一个响应头,设置 Cookie


response.setHeader('Set-Cookie', 'sign_in_email=...;HTTPOnly')

既然Session还是基于 Cookie 实现的,那么还是应该在 Set-Cookie 上搞事情。


//预先在服务器端预留对象准备存储各种session
let sessions = {
}
...
let sessionId = Math.random() * 100000
sessions[sessionId] = {sign_in_email: email}
response.setHeader('Set-Cookie', `sessionId=${sessionId};HTTPOnly`)

使用随机数来做 sessionId ,最终只是把这串随机数暴露给外界,而真正的信息却保存在了服务器端的 sessions 对象里面。它就像一个密码簿一样,有效的信息与 sessionId 一一对应,这是服务器的事,保证了安全性。


当下次用户访问该网站的其他页面的时候,就会带着登录时服务器给的这个 sessionId ,服务器获得这个 sessionId 后,然后一转化就知道是正确的用户了。


let sessions = {
sessionId: {
sign_in_email: ...
}
}
持久化存储

在HTML里面 js文件 里面的变量或对象,每当网页刷新的时候,就会死掉,又重新生成,虽然还是那个 a ,但是刷新后已经是另一块内存了。既然它也没变,我们为什么不把它一直保留着呢,即使刷新了 a 还是那个 a ,也就是持久化存储的意义。以前使用 Cookie 做这个功能,不过 Cookie 每次发请求会把Cookie里面的所有东西都带着去服务器,加重内存的负担,而且请求响应时间长,所以 html5 给了一个新的API localStorage


关于Cookie如何工作的,我发现这篇文章写得特别好


LocalStorage

它本质上还是个 hash ,不过是存在于浏览器端的,不同于 session 存在与服务器端的 hash 。一般存储的都是没有用的或者不敏感的信息。


localStorage 是window的全局属性,常用的有三个方法


//1. 添加键、值
localStorage.setItem('a', '...')
//2. 获得键、值
localStorage。getItem('a')
//3.清空localStorage
localStorage.clear()

注意,它存的值全是字符串,即使你写的像对象也没有卵用。


如果想存储字符串需要用到 JSON.stringify( )





一个实际应用

很简单的一个例子:网站进行更新了,用户登录进来了,想提示用户一下---我有新东西啦,这个提示并不应该在每次刷新的时候反复告诉用户,只是在第一次用户进来的时候告诉他即可。


let already = localStorage.getItem('已经提示过了')
if (!already) {
alert('我们的网站新进了一些货物,您看一下有没有您需要的啊O(∩_∩)O~')
localStorage.setItem('已经提示过了', true)
} else {
}

当第一次访问的时候, already 为null,所以进入 if 代码片段,提示用户一次,接着把 already 设为 true ,不会进入 if ,也就不再提示了。





不基于 Cookie 的 session

学习了 localStorage ,就可以搞一些黑科技了,前面说了, session 一般是基于 Cookie 的,那么有没有例外呢。


有的。利用查询参数和 localStorage 可是实现 session Id`。


小结一下
Cookie的特点
服务器通过 Set-Cookie 头给客户端一串字符串
客户端每次访问相同域名的网页时,必须带上这段字符串
客户端要在一段时间内保存这个Cookie
Cookie 默认在用户关闭页面后就失效,后台代码可以任意设置 Cookie 的过期时间。比如max-age和后面要讲的 Expires
大小大概在 4kb 以内

Session的特点
将 SessionID(随机数)通过 Cookie 发给客户端
客户端访问服务器时,服务器读取 SessionID
服务器有一块内存(哈希表)保存了所有 session
通过 SessionID 我们可以得到对应用户的隐私信息,如 id、email
这块内存(哈希表)就是服务器上的所有 session

LocalStorage的特点
LocalStorage 跟 HTTP 无关
也就是说发送任何请求都不会带上 LocalStorage 的值
只有相同域名的页面才能互相读取 LocalStorage(没有同源那么严格)
每个域名 localStorage 最大存储量为 5Mb 左右(每个浏览器不一样)
常用场景:记录有没有提示过用户(没有用的信息,不能记录密码等敏感信息)
LocalStorage 永久有效,除非用户清理缓存




SessionStorage

会话存储主要特点与 localStorage 基本相同,最大的不同是 SessionStorage 在用户关闭页面(会话结束)后就失效。


HTTP缓存技术三兄弟

假如说我们要访问的的文件比较大,我们请求完之后,下载需要花很长时间,当我们刷新页面的时候,虽然文件没有任何更新,但是我们又从服务器端下载了一遍大文件,导致每次响应时间依然很长。





通过上图的实验可以看到 localhost 的请求响应很快,10ms;而 default.css 、 main.js 文件较大,响应时间是 localhost 的25倍,而 jq 文件使用了 cdn 加速,是从内存的缓存中获得的,几乎瞬间。如果每次都这样的话,用户体验肯定很差。


那么我们能不能在第一次响应完毕之后,如果资源没有更新,就不去服务器端下载,而是去某个地方获得呢?


答案是肯定的,可以实现,通过缓存,正如上图的 jq 实现的方法一样。


这部分可以作为web性能优化的一个方法。


Cache-Control

通过 max-age 设置缓存的有效时间(持续时间)


if (path === '/css/default.css'){
let string = fs.readFileSync('./css/default.css', 'utf8')
response.setHeader('Content-Type', 'text/css;charset=utf-8')
response.setHeader('Cache-Control', 'max-age=1000000')
response.write(string)
response.end()
}

在响应头里面加上 Cache-Control ,表示在100000秒内不要再去向服务器要这个资源了,就从我的内存缓存里面获得。








虽然使用了缓存技术,不过有一点疑惑的就是有时候从硬盘的缓存里面获得,这个速度提升并不大,但是仍然避免了向服务器再次发起请求获得资源的过程;有时候从内存的缓存里面获得,这个就特别快了。大概是因为内存的缓存特别快吧。


通常我们把 Cache-Control 的有效时间设的很长。


以经常逛得知乎为例。





如果一个文件长期不变,把它设为从缓存里面获得,知乎设置了32596169秒的有效时间,超过了1年=31536000秒的时间。


首页尽量不用缓存技术

我们刷一些论坛性质的或者新闻性质的网站,注重时效性,一般会把爆炸性的、高质量的内容放到首页去,如果我们看了一会,想刷新看看新的更新的内容,而你设了缓存,看到的还是10分钟之前的首页,那就太尴尬了☺……


所以首页尽量不用缓存技术,只对那些长期不变的文件、图片等使用缓存技术。


还是以知乎为例。





对于知乎的 Cache-Control 的写法我是比较懵逼的。


MDN的语法 上



public

Indicates that the response may be cached by any cache.



private

Indicates that the response is intended for a single user and must not be stored by a shared cache. A private cache may store the response.



no-cache

Forces caches to submit the request to the origin server for validation before releasing a cached copy.



no-store

The cache should not store anything about the client request or server response.



must-revalidate

The cache must verify the status of the stale resources before using it and expired ones should not be used.


MDN推荐关闭缓存的写法是 Cache-Control: no-cache, no-store, must-revalidate 。


那么如果有的资源确实被更新了,如何去更新缓存呢。


更新缓存

通过服务器端代码 server.js 我们可以发现


if (path === '/js/main.js') {
...
response.setHeader('Cache-Control', 'max-age=1000000')
...
} else if (path === '/css/default.css'){
...
response.setHeader('Cache-Control', 'max-age=1000000')
...
}

只要当 URL 符合要求的时候,会使用缓存技术,不去发起请求重新下载资源。


所以当文件确实被更新了之后,我们可以改变 URL ,那么就会去重新下载新的文件了。


既然我们的网页入口是 html ,可以在这里面动手脚


...
<script src="./js/main.js?V2"></script>
...

当你更新代码之后,理论上只需要在URL上添加查询参数 ?V2 即可。


我们还是去知乎看看他们的例子。





可以看到知乎也是把 URL 改了,只不过比我那种高级,它在文件名字动了手脚,大概是用了什么框架或者处理工具吧,不过更新缓存的思路上是一样的。文件变了,知乎就把文件缓存的 URL 填点东西;没变的话,就缓存一年,在你的硬盘某处睡一年^_^。


小结一下

使用缓存就用 response.setHeader('Cache-Control', 'max-age=100000') ,当你想更新的时候就改变文件的 URL 。





当然,缓存存多了,你的硬盘估计就爆了,浏览器会去权衡这些的,应该优先清楚哪些缓存,是浏览器的事。


俗话说得好啊,吃井不忘挖井人啊,要学会忆苦思甜啊,我们现在用的可爽的 Cache-Control 也不是凭空冒出来的,是有历史原因的,以前呢,是用 Expires 实现缓存的技术。


Expires

Expires 的英文是到期的意思,很明显是与缓存有关的技术,不过从其英文意思也能看出它是到某个时间点截止的意思,不是 Cache-Control 的有效时间。





从语法和示例可以看出它是基于格林威治时间的。


我们还要处理一下时间


var d = new Date() //Sat Feb 10 2018 11:18:54 GMT+0800 (CST)
d.toGMTString() //"Sat, 10 Feb 2018 03:18:54 GMT"

能看出来,这个响应头的最大的弊端在于, 时间戳是与你的本地时间关联的


如果本地电脑的时间系统错乱了,而且这种毛病还真的时常发生,那你的缓存就毫无作用了。maybe这就是HTTP要升级这个响应头的原因吧O(∩_∩)O~


当 Cache-Control 和 Expires 共同存在的时候


如果还有一个 设置了 "max-age" 或者 "s-max-age" 指令的Cache-Control响应头,那么 Expires 头就会被忽略。


关于缓存的技术,还有最后一个兄弟 ETag ,在搞定它之前,先来学习一下它的小跟班 MD5


MD5

MD5 是一个摘要算法。经常用于比较两个文件是否完全一样,如果有一点不一样,误差会放大。例如我们经常重装系统的话,有良心的系统提供者会给你一个对应的 MD5 值,当你下载完毕后,查看你下载的系统的MD5值是否与官方提供给你的一样,确保是否会因为网络原因导致你下载的东西不完整。


在 Linux 系统里面使用 md5sum 指令进行MD5校验





第一个红框里面就是 1.txt 文件(内容设定为123456)的MD5值,第二个红框里面就是 1-copy 文件(内容被我改为了123460)的MD5值。


在 nodejs 里面如何使用呢,Google后发现有 npm 的 MD5 。


npm install md5
...
//在server.js引入
var md5 = require('md5');

准备工作做完,可以搞 ETag 了。


ETag

The ETag HTTP response header is an identifier for a specific version of a resource.It allows caches to be more efficient, and saves bandwidth, as a web server does not need to send a full response if the content has not changed. On the other side, if the content has changed, etags are useful to help prevent simultaneous updates of a resource from overwriting each other ("mid-air collisions").


If the resource at a given URL changes, a new Etag value must be generated. Etags are therefore similar to fingerprints and might also be used for tracking purposes by some servers. A comparison of them allows to quickly determine whether two representations of a resource are the same, but they might also be set to persist indefinitely by a tracking server.


这个响应头是特定资源版本的标识符。
如果给定URL中的资源更改,则一定要生成新的Etag值。因此Etags类似于指纹,也可能被某些服务器用于跟踪。 比较etags能快速确定此资源是否变化,但也可能被跟踪服务器永久存留。

可以看出 ETag 应该是一串值,此时上一节的 MD5 就派上用场了,我们使用MD5来比较前后两次请求文件的内容。


当某个URL来访问服务器的资源的时候,如果服务器设置了响应头 ETag:一串md5值 ,那么





现在没有什么其他变化,如果第二次刷新的话,你会发现





请求头多了一个 If-None-Match:一串MD5值 。


比较上述两图,我的 main.js 没有改变过,发现 ETag:一串md5值 和 If-None-Match:一串MD5值 的一样,稍微一思考的话,就能明白,第二次刷新的时候如果我的 main.js 变了的话,那么





第二次向服务器发起请求,下载的 main.js 的 ETag 的MD5值必然不同了。


根据这个现象,然后结合MDN文档


ETag头的另一个典型用例是缓存未更改的资源。 如果用户再次访问给定的URL(设有ETag字段),显示资源过期了且不可用,客户端就发送值为ETag的If-None -Matchheader字段:


If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

服务器将客户端的ETag(作为If-None-Match字段的值一起发送)与其当前版本的资源的ETag进行比较,如果两个值匹配(即资源未更改),服务器将返回不带任何内容的304未修改状态,告诉客户端缓存版本可用(新鲜)。


可以推理出如下的代码了:


if (path === '/js/main.js') {
let string = fs.readFileSync('./js/main.js', 'utf8')
response.setHeader('Content-Type', 'application/javascript;charset=utf-8')
let fileMd5 = md5(string)
response.setHeader('ETag', fileMd5)
if (request.headers['if-none-match'] === fileMd5) {
response.statusCode = 304
} else {
response.write(string)
}
response.end()
}
304状态码的含义

HTTP 304 说明无需再次传输请求的内容,也就是说可以使用缓存的内容。这通常是在一些安全的方法(safe),例如GET或HEAD或在请求中附带了头部信息:If-None-Match或If-Modified-Since。


304和缓存的区别:


缓存不会发起请求了,直接从内存或者硬盘中获得
304依然会发起请求与响应,只不过响应的第四部分不用再次下载了,因为没有更改,所以还是第一次下载的资源。



几个常见的考题
Cookie和Session的区别
Cookie是存放在浏览器端的数据,每次都随请求发送给 Server。存储 cookie 是浏览器提供的功能。 cookie 其实是存储在浏览器中的纯文本,浏览器的安装目录下会专门有一个 cookie 文件夹来存放各个域下设置的 cookie 。
而Session是存放在服务器端的内存中,其 Session ID 是通过 Cookie 发送给客户端的,这个Session ID每次都随请求发送给 Server。
Cookie 和 LocalStorage 的区别
Set-Cookie 之后,用户的每次访问服务器,请求里面都会带着 Cookie 到服务器上,与HTTP有关,而 LocalStorage 不用发到服务器端,它是存储在浏览器里面的,与HTTP无关,是浏览器的属性, window.localStorage 。
Cookie 一般比较小,大约4k左右,而 LocalStorage 大约能用5M
Cookie 默认会在用户关闭页面后失效,不过后端可以设置保存时间,而 LocalStorage 永久有效,除非用户手动清理。
LocalStorage 和 SessionStorage 的区别
LocalStorage 永久有效,除非用户手动清理 localStorage.clear() 。不会自动过期
但是SessionStorage在会话结束后就会失效,也就是用户关闭了页面,就失效了。会自动过期
Cookie 如何设置过期时间?如何删除 Cookie?

设置过期时间: Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>

data`是格林威治时间,响应头里里面应该这么写代码

response.setHeader('Expires', 'Fri, 09 Feb 2018 11:29:48 GMT')

也就是说Cookie在格林威治时间的2018年2月9号的11点29分48秒失效。


设置cookie过期时间小于当前时间,那么就会删除该cookie。


function deleteCookie(name) {
document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:01 GMT;'
}

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台