從Chrome源碼看HTTP

本篇解讀基於Chromium 66。HTTP協議起很大做用的是http頭,它主要是由一個個鍵值對組成的,例如Content-Type: text/html表示發送的數據是html格式,而Content-Encoding: gzip指定了內容是使用gzip壓縮的,Transfer-Encoding: chunked又表示它使用分塊傳輸編碼,等等。javascript

從Chrome發的請求複製一個原始的請求報文頭以下所示,如訪問http://payment-admin.com/list將會發送如下請求報文:php

"GET /list HTTP/1.1\r\nHost: payment-admin.com\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3345.0 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: en-US,en;q=0.9\r\nIf-None-Match: W/\"68104920-260-\"2018-02-13T14:16:35.000Z\"\"\r\nIf-Modified-Since: Tue, 13 Feb 2018 14:16:35 GMT\r\n\r\n"複製代碼

這個是按照http報文格式拼接的字符串,以下圖所示:css

對於每一個請求,Chrome都會自動設置UA字段:html

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3345.0 Safari/537.36java

Chrome的UA字段是這麼拼的:jquery

Mozilla/5.0 ([os_info]) AppleWebKit/[webkit_major_version].[webkit_minor_version] (KHTML, like Gecko) [chrome_version] Safari/[webkit_major_version].[webkit_minor_version]linux

以下源碼所示:nginx

而且咱們看到源碼的註釋還說明了爲何UA要帶上Safari——爲了以最大限度地與Safari兼容的方式展現產品名稱。web

當前請求收到了如下響應報文頭算法

"HTTP/1.1 200 OK\0Server: nginx/1.8.0\0Date: Fri, 16 Feb 2018 03:31:51 GMT\0Content-Type: text/html; charset=UTF-8\0Transfer-Encoding: chunked\0Connection: keep-alive\0last-modified: Tue, 13 Feb 2018 14:16:35 GMT\0etag: W/\"68104920-260-\"2018-02-13T14:16:35.000Z\"\"\0 cache-control: max-age=10\0Expires: Fri, 16 Feb 2018 03:32:01 GMT\0Content-Encoding: gzip\0\0"

這個請求報文頭和響應報文頭有個小區別,它的字段間的分隔符是\0,而不是上面的\r\n了。

對於請求報文頭字段,咱們重點討論如下兩個問題:

(1)緩存是以什麼作爲鍵值的,即如何區分兩個不一樣的資源,緩存瀏覽器是如何組織管理的?

(2)gzip是如何壓縮和解壓的,爲何經過gzip壓縮體積常常能小一半以上?


對於緩存,首先怎麼設定資源的緩存時間呢?若是使用nginx,能夠這樣:

server {
    listen       80;
    server_name  www.rrfed.com;

    # .json不要緩存,時間爲0
    location ~* \.sw.json$ {
        expires 0;
    }   
    # 若是是圖片的話緩存30天
    location ~* \.(jpg|jpeg|png|gif|webp)$ {
        expires 30d;
    }   
    # css/js緩存7天
    location ~* \.(css|js)$ {
        expires 7d;
    }   
}複製代碼

上述代碼根據對不一樣的文件名後綴區分設置緩存時間,如圖片緩存30天,js/css緩存7天。

若是使用Node.js等在請求裏面單獨添加的,能夠直接添加Cache-Control的頭:

// 設置30天=2592000s緩存
response.setHeader("Cache-Control", "max-age=2592000");複製代碼

這樣瀏覽器就能收到緩存的http頭了:

那麼瀏覽器是如何區分不一樣的資源進行緩存的?你可能已經猜到了,根據url,以下圖所示:

Chrome使用一個生成Cache Key的函數,這個函數是使用請求的url做爲緩存的key值

若是這樣的話,POST等請求是否是也能夠被緩存?實際上並非的,由於它上面還有一個判斷,以下圖所示:

這個ShouldPassThrough會對請求方式進行判斷:

若是是普通的POST/PUT,是返回true的,也就是說,這種請求是直接返回true的,是須要pass的,不用取緩存。而對於DELETE和HEAD,在另一個地方作的判斷:

若是mode爲NONE的話,就會去發請求了。也就是說除了GET以外,Chrome基本上不會對其它請求方式進行緩存

請求完以後會對cache進行存儲,經過打斷點檢查能夠發現是放在了這個路徑下

~/Library/Caches/Chromium/Default/Cache/

以下圖所示:

這個目錄下的緩存文件是以key值(即url)的SHA1哈希值作爲文件名:

查看這個Cache目錄,能夠發現文件名是以哈希值加上一個0或1的後綴組成,0/1是file index(具體不深刻討論),以下圖所示:

緩存文件不是把文件內容寫到硬盤,而是把Chrome封裝的Entry實例內存內容序列化寫到硬盤,它是變量在內存的表示。若是用文本編輯器打開緩存文件是這樣的:

可直接讀取成相應的變量。

同時會把這個Entry放在entries_set_內存變量裏面,它是一個unordered_map,即普通的哈希Map,key值就是url的sha1值,value值是一個MetaData,它保存了文件大小等幾個信息,EntrySet的數據結構以下代碼所示:

using EntrySet = std::unordered_map<uint64_t, EntryMetadata>;複製代碼

這個entries_set_最主要的做用仍是記錄緩存的key值,因此它的命名是叫set而不是map。這個變量會保存它的序列化格式到硬盤,叫作索引文件index:

~/Library/Caches/Chromium/Default/Cache/index-dir/the-real-index

Chrome在啓動的時候就會去加載這個文件到entries_set_裏面,加載資源的時候就會先這個哈希Map裏面找:

若是找獲得就直接去加載硬盤文件,不去發請求了。

數據取出來以後,就會對緩存是否過時進行驗證:

驗證是否過時須要先計算當前的緩存的有效期,以下源碼的註釋:

// From RFC 2616 section 13.2.4:
//
// The max-age directive takes priority over Expires, so if max-age is present
// in a response, the calculation is simply:
//
//   freshness_lifetime = max_age_value
//
// Otherwise, if Expires is present in the response, the calculation is:
//
//   freshness_lifetime = expires_value - date_value
//
// Note that neither of these calculations is vulnerable to clock skew, since
// all of the information comes from the origin server.
//
// Also, if the response does have a Last-Modified time, the heuristic
// expiration value SHOULD be no more than some fraction of the interval since
// that time. A typical setting of this fraction might be 10%:
//
//   freshness_lifetime = (date_value - last_modified_value) * 0.10
//複製代碼

結合代碼實現邏輯,這個步驟是這樣的:

(1)若是給了max-age,那麼有效期就是max-age指定的時間:

cache-control: max-age=10
另外若是指定了no-cache或者no-store的話,那麼有效期就是0:
cache-control: no-cache
cache-control: no-store

(2)若是沒有給max-age,可是給了expires,那麼就使用expires指定的時間減去當前時間獲得有效期:

Expires: Wed, 21 Feb 2018 07:28:00 GMT

這個日期是http-date格式,使用GMT時間。

(3)若是max-age和expires都沒有,而且沒有指定must-revalidate,就使用當前時間減掉last modified time乘以一個調整係數0.1作爲有效期:

last-modified: Tue, 13 Feb 2018 08:16:27 GMT
若是指定了must-revalidate,如:
cache-control: max-age=10, must-revalidate
cache-control: must-revalidate

那麼就不能直接使用緩存,要發個請求,若是服務返回304那麼再使用緩存。

有了有效期以後再和當前的年齡進行比較,若是有效期比年齡還大則認爲有效,不然無效。而這個年齡是用當前時間減掉資源響應時間,再加上一個調整時間獲得:

//     resident_time = now - response_time;
//     current_age = corrected_initial_age + resident_time;複製代碼

由於考慮到請求還須要花費時間等因素,current_age須要作一個修正。

關於緩存就說到這裏,接下來討論gzip壓縮


gzip壓縮常常能把一個文件的體積壓到一半如下,如jquery-3.3.1.min.js有85kb,經過gzip壓縮就剩下35kb:

減少了58%的體積。因此gzip是怎麼壓的呢?這個是我一直很好奇的問題。

在linux/mac上常常能夠看到.tar.gz後綴的文件名,.tar表示打成了一個tar包,而.gz表示把tar包用gzip壓縮了一下,能夠用如下命令壓縮和解壓:

# 把html目錄打包成一個壓縮文件
tar -zcvf html.tar.gz html/

# 解壓到當前目錄
tar -zxvf html.tar.gz
複製代碼

gzip已經被標準化成RFC1952,nginx開啓gzip可經過添加如下配置:

server {
    gzip                on;
    gzip_min_length     1k;
    gzip_buffers        4 16k;
    # gzip_http_version 1.1;
    gzip_comp_level     2;
    gzip_types          text/plain application/javascript application/x-javascript text/javascript text/xml text/css application/x-httpd-php image/jpeg image/gif image/png;
}複製代碼

Chrome是使用第三方的zlib庫作爲壓縮和解壓的庫,其解壓使用的庫文件是third_party/zlib/contrib/optimizations/inflate.c,這個代碼看起來比較晦澀,具體過程能夠參考這個deflate的說明這一個,gzip依賴於deflate,deflate是結合了霍夫曼編碼和LZ77壓縮。以壓縮如下文本作爲說明:

"In the beginning God created the heaven and the earth. And the earth was without form, and void."

先對它進行LZ77壓縮變成:

In the beginning God created<25, 5>heaven an<14, 6>earth. A<23, 12> was without form,<55, 5>void.

其中<25, 5>表明<distance, length>,表示字符串" the ",25是距離distance,在當前位置往前25個字節,再取長度length = 5,就是最開始那個" the "。同理,後面的<14, 6>表示"d the "。

一個字節有8位能夠表示的最大數字爲255,假設用一個字節表示distance,一個字節表示length,那麼上述文本由沒有壓縮的96B變成76B,其壓縮率已達到80%,若是文本越長,那麼重複的機率越大,壓縮率越高。標準建議最大的塊長度爲32kb,即超過32kb後重復字符從新開始算。

可是有個問題是:如何區分正常的內容和表示<distance, length>的長度對?標準是這麼解決的,值爲0 ~ 255的爲正常內容,而256表示塊結束,257 ~ 285表示長度對。

爲了表示數字285最小須要9個位,也就是說能夠每9位 9位地讀取值(同理以9位爲單位進行壓縮),這樣能夠解決問題,可是會大量地浪費空間,由於9位最大能表示511.因此引入了可變長度編碼霍夫曼編碼,數據的存儲再也不是固定長度的(如每個字節表示一個內容),而是可變的,最短多是1位表示一個字符,最長多是9位。

可是這樣可能會區分不了,如A、B、C 3個字符分別表示爲:

A:0

B:1

C:01

那麼當遇到01的時候就不知道是C仍是AB了。


因此霍夫曼編碼就是爲了解決保證前綴不衝突的問題,以下圖所示:

先統計每一個字符出現的次數,而後每次選取兩個次數最小的字符造成左右子結點,它們的和作爲父結點作爲一個新的結點,直到全部結點造成一棵樹,左子樹表明0,右子樹表明1,從根結點到葉子結點的路徑就是當前字符的編碼,如z的編碼就是001,而e是1,這樣高頻率出現的符號的編碼會比較短,就達到了壓縮的目的。同時須要有一個表記錄編碼的對應關係,在解壓的時候進行查找。(標準還對這個算法進行了優化)。

剛纔提到長度對的範圍是257 ~ 285共29個,這樣是不夠用的,由於一個塊最大有32kb(取決於壓縮率),重複字符串若是最長只能有29個或者只能往前找29個,那麼不能進行充分地壓縮,所以標準還在後面添加了額外的位進行加大,以下所示:

例如若是length是266,那麼後面還要再讀1位,若是這1位是0,那麼length就爲13,若是這1位是1,那麼length就是14,依次類推。length後面緊接着就是distance,distance也會相似地處理。咱們看到length最大爲258,而distance最大爲32kb。


gzip的特色是壓縮比較費時,可是解壓比較容易。壓縮須要統計字符,查找重複字符串,而解壓只須要查下可變長度編碼表,而後讀取比較value大小看是否爲內容仍是長度對再進行輸出。gzip壓縮率好壞取決於內容的重複度,重複率越高,則壓縮率越高。


本篇對HTTP的解讀就到這裏,主要講述了三個內容:HTTP報文頭、HTTP緩存、Gzip壓縮。看完了本文應該會了解HTTP請求頭和響應頭分別是長什麼樣的,Chrome的UA是怎麼拼出來的,HTTP緩存瀏覽器是怎麼組織管理的、緩存時間又是怎麼計算的,Gzip壓縮的過程是怎麼樣的、爲何Gzip的壓縮效果廣泛較好等問題。對於HTTP其它感興趣的內容咱們下回再分解。

相關文章
相關標籤/搜索