對http 的理解,一直都出於看完資料,沒過幾天又忘記了下面這篇文章只是本身對http
的整個過程一個梳理,而且從http
的請求過來,來簡單的進行性能優化進行一個梳理。 其中主要設計以下幾個環節:html
對上面的幾個環節的梳理,都是借鑑前輩們的分析成果,後面會列出全部的文章鏈接。express
參考文章 當你輸入一個網址的時候,實際會發生什麼?segmentfault
fecebook.com
由於咱們輸入的
fecebook.com
,而不是http://www.facebook.com/
因此服務器自動進行了永久重定向,返回的是301 狀態碼後端
爲何服務器必定要重定向而不是直接發會用戶想看的網頁內容呢?這個問題有好多有意思的答案。瀏覽器
其中一個緣由跟搜索引擎排名有 關。你看,若是一個頁面有兩個地址,就像http://www.igoro.com/ 和http://igoro.com/,搜索引擎會認爲它們是兩個網站,結果形成每個的搜索連接都減小從而下降排名。而搜索引擎知道301永久重定向是 什麼意思,這樣就會把訪問帶www的和不帶www的地址歸到同一個網站排名下。緩存
還有一個是用不一樣的地址會形成緩存友好性變差。當一個頁面有好幾個名字時,它可能會在緩存裏出現好幾回。性能優化
從上面咱們已經知道從輸入URL到展現頁面的整個大體過程,下面咱們會針對其中幾個關鍵步驟再次深刻分析bash
在上面咱們已經知道了,咱們輸入一個URL,發起請求,其實接收請求的最終一個服務器,而每一個服務器都一個IP地址,因此通常一個域名對一個一個IP地址(也有對應多個IP地址的,咱們暫時值分析對一個IP地址的狀況), 可是瀏覽器怎麼知道域名到底對應的是那個IP地址呢,這個就是涉及到怎麼去域名解析了。 域名解析主要是以下過程:服務器
從上面分析可知,咱們輸入一個域名須要去作DNS解析找到IP,可是在咱們的代碼中,常常將一些靜態資源放在CDN中,每一個CDN地址咱們都要去作下DNS解析,這個會浪費時間,咱們能夠經過預先進行DNS解析,而後在請求的時候,DNS已經解析完成就不用等待了cookie
<!--在head標籤中,越早越好-->
<link rel="dns-prefetch" href="//example.com">
複製代碼
參考文章探網絡系列(1)-TCP三次握手&Render Tree頁面渲染=>從輸入URL到頁面顯示的過程?
客戶端發送鏈接請求報文段,將SYN值設爲1,Sequence Number爲x。客戶端進入SYN_SEND狀態,等待服務器的確認。
服務器收到客戶端SYN報文段,須要對這個SYN報文段進行確認,設置Acknowledgment Number爲x+1(Sequence Number+1)。同時,本身本身還要發送SYN請求信息,將SYN值設爲1,Sequence Number設爲y。服務器端將上述全部信息放到一個報文段(即SYN+ACK報文段)中,一併發送給客戶端,服務器進入SYN_RECV狀態。
客戶端收到服務器的SYN+ACK報文段後將Acknowledgment Number設置爲y+1,向服務器發送ACK報文段,這個報文段發送完畢之後,客戶端和服務器端都進入ESTABLISHED狀態,完成TCP三次握手。
完成三次握手,客戶端與服務器開始傳送數據,在上述過程當中,還有一些重要的概念:
未鏈接隊列:在三次握手協議中,服務器維護一個未鏈接隊列,該隊列爲每一個客戶端的SYN包(syn=j)開設一個條目,該條目代表服務器已收到SYN包,並向客戶發出確認,正在等待客戶的確認包。這些條目所標識的鏈接在服務器處於Syn_RECV狀態,當服務器收到客戶的確認包時,刪除該條目,服務器進入ESTABLISHED狀態。 Backlog參數:表示未鏈接隊列的最大容納數目。
SYN-ACK 重傳次數:服務器發送完SYN-ACK包,若是未收到客戶確認包,服務器進行首次重傳,等待一段時間仍未收到客戶確認包,進行第二次重傳,若是重傳次數超過系統規定的最大重傳次數,系統將該鏈接信息從未鏈接隊列中刪除。注意,每次重傳等待的時間不必定相同。
未鏈接存活時間:是指未鏈接隊列的條目存活的最長時間,也即服務從收到SYN包到確認這個報文無效的最長時間,該時間值是全部重傳請求包的最長等待時間總和。有時咱們也稱未鏈接存活時間爲Timeout時間、SYN_RECV存活時間。
在謝希仁著《計算機網絡》第四版中講「三次握手」的目的是爲了防止已失效的鏈接請求報文段忽然又傳送到了服務端,於是產生錯誤
「已失效的鏈接請求報文段」的產生在這樣一種狀況下:client發出的第一個鏈接請求報文段並無丟失,而是在某個網絡結點長時間的滯留了,以至延誤到鏈接釋放之後的某個時間纔到達server。原本這是一個早已失效的報文段。但server收到此失效的鏈接請求報文段後,就誤認爲是client再次發出的一個新的鏈接請求。因而就向client發出確認報文段,贊成創建鏈接。假設不採用「三次握手」,那麼只要server發出確認,新的鏈接就創建了。因爲如今client並無發出創建鏈接的請求,所以不會理睬server的確認,也不會向server發送數據。但server卻覺得新的運輸鏈接已經創建,並一直等待client發來數據。這樣,server的不少資源就白白浪費掉了。採用「三次握手」的辦法能夠防止上述現象發生。例如剛纔那種狀況,client不會向server的確認發出確認。server因爲收不到確認,就知道client並無要求創建鏈接。」
做者:wuxinliulei
來源:知乎
參考文章 TCP四次揮手(圖解)-爲什麼要四次揮手
做者:李太白不白
來源:CSDN
從上面分析可知,每次請求資源都須要進行TCP鏈接,會有三次握手操做,才表示鏈接成功,鏈接成功後,服務器纔會向客戶端發送數據,若是每次請求資源時都才進行鏈接,是很浪費時間的,咱們能夠在請求資源以前,先預先鏈接,在真正請求的時候,就已經鏈接上,以前發送資源就能夠,咱們能夠利用以下方式:
參考文章Head標籤裏面的dns-prefetch,preconnect,prefetch和prerender
<link rel="preconnect" href="//example.com">
<link rel="preconnect" href="//cdn.example.com" crossorigin>
複製代碼
瀏覽器會進行如下步驟:
咱們已經創建了TCP鏈接,服務端已經能夠往客戶端(瀏覽器)發送資源了,可是若是若是已經請求過一次資源了,可是咱們刷新頁面,咱們還須要從新請求資源,這樣也太浪費請求了,瀏覽器解決再次請求有緩存 策略,緩存就是再次請求資源能儘可能從已經請求的資源中獲取最好從而減小了請求次數,也就是不須要再次進行TCP鏈接,可是若是瀏覽器每次都查看緩存中否已經有了資源就再也不次請求,這樣也會形成可能咱們獲取到的資源不是最新的,因此針對着這兩種狀況,瀏覽器緩存有以下兩種策略:
下面咱們來針對着兩種策略來進行簡單的分析。
強緩存主要是瀏覽器根據請求頭部的兩個字段來判斷的:
強緩存命中 from memory cache & from disk cache
在測試的時候,看到命中強緩存時,有兩種狀態,200 (from memory cache) cache & 200 (from disk cache),因而去找了一下這二者的區別:
其實若是咱們一個頁面中存在請求多個同樣的圖片資源,瀏覽器會自動處理,從內存緩存中自動獲取(from memory cache), 可是咱們關閉了頁面或者刷新了頁面,這個內存緩存就失效了, 不過這個緩存是瀏覽器自動幫咱們處理的,咱們作不了什麼處理.
expires 是http 1.0 裏面的特性,經過指定資源指定緩存到期GMT的絕對時間 來判斷資源是否過時,若是沒有過時就用緩存,不然從新請求資源.
缺點: 因爲使用具體時間,若是時間表示出錯或者沒有轉換到正確的時區均可能形成緩存生命週期出錯。
Cache-Control 是http1.1
中爲了彌補Expires的缺陷而加入的,當Expires和Cache-Control同時存在時,Cache-Control優先級高於Expires。
下面咱們梳理下cache-control
的配置:
屬性 | 描敘 |
---|---|
max-age | 設置緩存存儲的最大週期,超過這個時間緩存被認爲過時(單位秒)。cache: max-age=60 這裏是60秒 |
public/private | public 表示服務器端和瀏覽器端都能緩存, cache: max-age=60, public , private 表示只能用戶的瀏覽器才能緩存,路由器已經CDN不能緩存 |
no-cache | no-cache 不是說不緩存,而是必須須要從服務器去請求一次,若是緩存還生效,則就服務器只會返回304,不會返回請求相應體,請求不會減小,可是請求的資源可能減少( Express 緩存策略中,若是請求頭部攜帶了cache-control 並且設置了no-cache 則只會從新返回新的資源,不會返回304 ) |
no-store | 不緩存,使用協商緩存 |
must-revalidate | 緩存必須在使用以前驗證舊資源的狀態,而且不可以使用過時資源。 |
若是cache-control 表示資源過時,或者設置了no-store, 並非說明緩存的資源不能再使用,瀏覽器還能夠配合來使用協商緩存, 下面咱們就來分析協商緩存
若是強緩存(cache-control)資源失效,瀏覽器就會調用協商緩存策略,協商緩存策略主要是經過以下的兩個請求頭部來處理:
瀏覽器在請求服務器資源時,服務器會將文件的最後修改時間,賦值給相應求頭last-modified
,如: last-moified: Fri,08 Jun 2018 10:2:30: GMT
再次請求這個資源時(刷新頁面(不是強制刷新F5 + Ctrl),或者從新打開這個頁面), 請求頭部會添加一個if-modified-since
的頭部信息,其值就是last-modified
的值, 如:if-modified-since:Fri,08 Jun 2018 10:2:30: GMT, 發送給服務器,服務器會根據這個值來判斷緩存是否生效,若是緩存依舊生效,則返回一個304,和一個空的響應體 , 瀏覽器機會從緩存讀取,不然返回200 而且返回請求結果
Etag 其實和last-modified 的效果同樣,都是後端針對相應的資源,返回的一個標識,只是last-modified 是資源最後的修改時間,etag 是資源相應的標識,不一樣的服務器生成etag的策略是不同的。好比說,express 框架生成etag 的規則是 文件最後一次修改時間-文件的大小
function stattag (stat) {
// mtime 文件最後一次的修改時間
// size 文件的大小
var mtime = stat.mtime.getTime().toString(16)
var size = stat.size.toString(16)
return '"' + size + '-' + mtime + '"'
}
複製代碼
再次請求資源時(刷新頁面(不是強制刷新F5 + Ctrl),或者從新打開這個頁面),請求頭部會添加一個if-none-match
的請求頭髮送給服務器,服務器會根據這個值來判斷緩存是否生效, 若是緩存依舊生效,則返回一個304,和一個空的響應體 , 瀏覽器機會從緩存讀取,不然返回200 而且返回請求結果。
從上面的分析感受last-modified
和etag
的功能,應該同樣,爲何在HTTP 1.1 會出現etag 的概念呢,etag 主要是解決了以下問題:
last-modified
和etag
標籤,那誰的優先級更高呢若是同時設置了last-modified
和etag
標籤,那誰的優先級更高呢? 規定是etag優先生效, 那爲何etag 爲何會優先於last-modified 呢?是由瀏覽器決定的?
通過分析,不是由瀏覽器決定的,而是有服務器 決定的。瀏覽器只是在請求資源的時候攜帶last-modified 和etag 的請求頭到服務器,接下來就由服務器來決定緩存是否能夠用, 咱們能夠查看下express 的處理邏輯的源代碼來分析:
if (this.isCachable() && this.isFresh()) {
this.notModified()
return
}
複製代碼
其中this.notModified()
就是直接返回一個304
:
SendStream.prototype.notModified = function notModified () {
var res = this.res
debug('not modified')
this.removeContentHeaderFields()
res.statusCode = 304
res.end()
}
複製代碼
express 判斷緩存是否生效最主要的邏輯是在this.isFresh()
方法中實現:
function fresh (reqHeaders, resHeaders) {
// fields
var modifiedSince = reqHeaders['if-modified-since']
var noneMatch = reqHeaders['if-none-match']
// unconditional request
if (!modifiedSince && !noneMatch) {
return false
}
// Always return stale when Cache-Control: no-cache
// to support end-to-end reload requests
// https://tools.ietf.org/html/rfc2616#section-14.9.4
var cacheControl = reqHeaders['cache-control']
if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
return false
}
// if-none-match
if (noneMatch && noneMatch !== '*') {
var etag = resHeaders['etag']
if (!etag) {
return false
}
var etagStale = true
var matches = parseTokenList(noneMatch)
for (var i = 0; i < matches.length; i++) {
var match = matches[i]
if (match === etag || match === 'W/' + etag || 'W/' + match === etag) {
etagStale = false
break
}
}
if (etagStale) {
return false
}
}
// if-modified-since
if (modifiedSince) {
var lastModified = resHeaders['last-modified']
var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))
if (modifiedStale) {
return false
}
}
return true
}
複製代碼
咱們能夠根據上面的代碼來具體分析express
具體是怎樣來判斷緩存是否生效
if-modified-since
和if-none-match
頭部,就直接判斷緩存失效var modifiedSince = reqHeaders['if-modified-since']
var noneMatch = reqHeaders['if-none-match']
if (!modifiedSince && !noneMatch) {
return false
}
複製代碼
cache-control
, 而且有設置no-cache , 則直接判斷緩存失效(var CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/
)var cacheControl = reqHeaders['cache-control']
//
if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
return false
}
複製代碼
if-none-match
, 判斷的方法就是從新獲取一個etag
, 而後判斷if-none-match
是否與etag
相等, 若是不相等, 就直接判斷緩存失效// if-none-match
if (noneMatch && noneMatch !== '*') {
var etag = resHeaders['etag']
if (!etag) {
return false
}
var etagStale = true
var matches = parseTokenList(noneMatch)
for (var i = 0; i < matches.length; i++) {
var match = matches[i]
if (match === etag || match === 'W/' + etag || 'W/' + match === etag) {
etagStale = false
break
}
}
if (etagStale) {
return false
}
}
複製代碼
其中var etag = resHeaders['etag']
是在請求時,從新獲取的etag
if-modified-since
,其判斷的邏輯是,若是last-modified
的值小於等於if-modified-since
的值, 則直接判斷緩存失效// if-modified-since
if (modifiedSince) {
var lastModified = resHeaders['last-modified']
var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))
if (modifiedStale) {
return false
}
}
複製代碼
從上面的分析可知,其實Express
的緩存生效機制並無遵循etag
的優先級高於last-modified
,而是在判斷失效 的機制遵循了etag
的優先級高於last-modified
.
繼續...