本文主要學習一下一些高階的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
的寫法我是比較懵逼的。
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 -Match
header欄位:
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
大約能用5MCookie
預設會在使用者關閉頁面後失效,不過後端可以設定儲存時間,而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;'
}
複製程式碼
Cache-Control: max-age=1000 快取 與 ETag 的「快取」有什麼區別?
Cache-Control: max-age=1000
的快取 是直接不發請求的,1000秒內相同URL的使用者請求資源的時候,不會再去發請求訪問伺服器了,直接從本地記憶體的快取裡面獲取ETag
的快取是不管怎麼樣都要發起請求,第二次訪問的是時候會多一個請求頭If-None-Match : md5值
,如果兩次請求之間的MD5值相同就不會去下載新的檔案,響應體是第一次下載的;如果MD5值變了,就要去下載新的檔案。