前言
在基於 HTTP 協議的網路傳輸中 GZip 經常被使用,Nginx 中也可以使用半行程式碼開啟 GZip。GZip 壓縮的原理是什麼呢?本篇文章是我在網上閱讀了一些文件後做的簡單總結。
從 RFC 1952 看起
RFC 1952 是 GZIP file format specification version 4.3
。該規範主要定義了 GZip 壓縮的在資料格式方面的規範,以方便不同的作業系統、CPU、檔案系統等之間進行檔案傳輸交換。下面挑有意思的幾個點說,感興趣的可以閱讀 RFC 1952 的原文。
GZIP 的檔案格式在設計上其實是可以允許一個檔案裡有多個壓縮資料集(compressed data sets)—— GZIP 壓縮後的片段拼接而成的。但就我們大多數應用場景來說,基本上都是一個檔案一個壓縮資料集,如果是多個檔案一起打包的話,也往往是將多個包合併成一個 tar 檔案。
每個壓縮資料集都是下面的結構:
| ID1 | ID2 | CM | FLG | MTIME(4位元組) | XFL | OS | ---> more
|
與|
之間是 1 byte,都是大端位元組(Big Edian)
-
其中 ID1 和 ID2 分別是 0x1f 和 0x8b,用來標識檔案格式是 gzip
-
CM 標識 加密演算法,目前 0-7是保留字,8 指的是 deflate 演算法
-
FLG 從低地址到高地址分別是 FTEXT、FHCRC、FEXTRA、FNAME、FCOMMENT、reserved、 reserved、reserved,這裡每個 bit 被設定了之後有什麼意義感興趣的話可以詳細參考 RFC 1952。比較有意思的是 FEXTRA,如果它被設定了表示存在額外的擴充欄位。擴充欄位的結構如下:
- | SI1 | SI2 | LEN | ... LEN bytes of subfield data ... |
- SI1、SI2 是對子域的 ID,由 ASCII 碼組成。如果你需要使用的話,可以向他的維護者 Jean-Loup Gailly
<gzip@prep.ai.mit.edu>
發郵件申請。目前 Apollo file 就有自己的專屬 ID
-
MTIME 指的是原始檔最近一次修改時間,存的是 Unix 時間戳
-
XFL 是給壓縮演算法傳的一些引數,用來標識如何解壓。defalte 演算法中 2 表示使用壓縮率最高的演算法,4 表示使用壓縮速度最快的演算法
-
OS 標識壓縮程式執行的檔案系統,以處理 EOF 等的問題
-
more 後面是根據 FLG 的開啟情況決定的,可能會有 迴圈冗餘校驗碼、原始檔長度、附加資訊等多種其他資訊
壓縮核心之 Deflate
GZIP 的核心是 Deflate,在 RFC 1951 中被標準化,並且在當時作為 LZW 的替代品有了非常廣泛的使用。
Deflate 是一個同時使用 LZ77 與 Huffman Coding 的演算法,這裡簡單介紹下這兩種演算法的大致思路:
LZ77
LZ77 的核心思路是如果一個串中有兩個重複的串,那麼只需要知道第一個串的內容和後面串相對於第一個串起始位置的距離 + 串的長度。
比如: ABCDEFGABCDEFH → ABCDEFG(7,6)H。7 指的是往前第 7 個數開始,6 指的是重複串的長度,ABCDEFG(7,6)H 完全可以表示前面的串,並且是沒有二義性的。
LZ77 用 滑動視窗(sliding-window compression)來實現這個演算法。具體思路是掃描頭從串的頭部開始掃描串,在掃描頭的前面有一個長度為 N 的滑動視窗。如果發現掃描頭處的串和視窗裡的 最長匹配串 是相同的,則用(兩個串之間的距離,串的長度)來代替後一個重複的串,同時還需要新增一個表示是真實串還是替換後的“串”的位元組在前面以方便解壓(此串需要在 真實串和替換“串” 之前都有存在)。
實際過程中滑動視窗的大小是固定的,匹配的串也有最小長度限制,以方便 標識+兩個串之間的距離+串的長度 所佔用的位元組是固定的 以及 不要約壓縮體積越大。更加詳細的實現可以參考:Standford Edu. lz77 algorithm、 LZ77 Compression Algorithm、 LZ77壓縮演算法編碼原理詳解(結合圖片和簡單程式碼)
這裡通過這個壓縮機制也就能比較容易的解釋為啥 CSS BEM 寫法 GZIP 壓縮之後可以忽略長度以及 JPEG 圖片 GZIP 之後可能會變大 的情況了
解壓:GZIP 的壓縮因為要在視窗裡尋找重複串相對來說效率是比較低的(LZ77 還是通過 Hash 等系列方法提高了很多),那解壓又是怎麼個情況呢?觀察壓縮後的整個串,每個小串前都有一個標識要標記是原始串還是替換“串”,通過這個標識就能以 O(1)的複雜度直接讀完並且替換完替換“串”,整體上效率是非常可觀的。
Huffman Coding
Huffman Coding 是大學課本中一般都會提到的演算法。核心思路是通過構造 Huffman Tree 的方式給字元重新編碼(核心是避免一個葉子的路徑是另外一個葉子路徑的字首),以保證出現頻路越高的字元佔用的位元組越少。關於 Huffman Tree 的構造這裡不再細說,不太清楚的可以參考:Huffman Coding。
解壓:Huffman Coding 之後需要維護一張 Huffman Map 表,來記錄重新編碼後的字串,根據這張表,還原原始串也是非常高效的。
Deflate 綜合使用了 LZ77 和 Huffman Coding 來壓縮檔案,相對而言又提升了很多。詳細可以參考 gzip原理與實現
網站中的使用
在 RFC 2016 中 GZIP 已經成為了規定的三種標準HTTP壓縮格式之一。目前絕大多數的網站都在使用 GZIP 傳輸 HTML、CSS、JavaScript 等資原始檔。
Nginx 開啟
Nginx 的 ngx_http_gzip_module 也提供了開啟 GZIP 壓縮的方式,有下面的一些常用配置:
# 開啟
gzip on;
# 壓縮等級,1-9。設定多少可以參考:http://serverfault.com/questions/253074/what-is-the-best-nginx-compression-gzip-level
gzip_comp_level 2;
# "MSIE [1-6]\." 比如禁止 IE6 使用 GZIP
gzip_disable regex ...
# 最小壓縮檔案長度
gzip_min_length 20;
# 使用 GZIP 壓縮的最小 HTTP 版本
gzip_http_version 1.1;
# 壓縮的檔案型別,值是 [MIME type](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types)
gzip_types text/html;
複製程式碼
相關探測
Nginx 上開啟 GZIP 之後,理論上會按照 GZIP 配置開啟壓縮。那如何檢測是否開啟成功了呢?
開啟瀏覽器,訪問你的網站,看 Chrome 的 Network,如果 Size 上有兩個不一樣大小的體積(如:222KB 和 613KB),則代表 GZIP 已經成功開啟。
那瀏覽器又是如何和伺服器配合的呢?
瀏覽器在請求資源的時候再 header 裡面帶上 accept-encoding: gzip
的引數。Nginx 在接收到 Header 之後,發現如果有這個配置,則傳送 GZIP 之後的檔案(返回的 header 裡也包含相關的說明),如果沒有則傳送原始檔。瀏覽器根據 response header 來處理要不要針對返回的檔案進行解壓縮然後展示。
參考文件
- RFC 1952 - GZIP file format specification version 4.3
- RFC 1951 - DEFLATE Compressed Data Format Specification version 1.3
- DEFLATE - 維基百科,自由的百科全書
- LZW - 維基百科,自由的百科全書
- LZ77 - 維基百科,自由的百科全書
- Standford Edu. lz77 algorithm
- LZ77 Compression Algorithm
- LZ77壓縮演算法編碼原理詳解(結合圖片和簡單程式碼)
- 霍夫曼編碼 - 維基百科,自由的百科全書
- gzip原理與實現
- Module ngx_http_gzip_module
- What is the best nginx compression gzip level?
- MDN - 完整的MIME型別列表
- 你真的瞭解 gzip 嗎?