
打開網絡上的 ZIP 檔案需要幾步?下載下傳,解壓,拿到所有檔案。面對一個 ZIP,能不能「邊下邊播」、「按需下載下傳」?
今年 6 月,優酷繪本技術團隊開發出新的解壓方式——ZIP 流式解壓技術,并成功應用在優酷繪本秒開項目中,30M+ 繪本平均加載時長隻需 0.91s,加載耗時比傳統的解壓方式降低了 88.3%,讓使用者的閱讀體驗直線提升。實際對比效果如下:
本文将介紹 ZIP 流式解壓的原理和技術實作路徑,希望為大家帶來啟發,将 ZIP 流式解壓技術更多的應用到業務中。
一 什麼是ZIP
ZIP 是一種檔案格式,定義了如何将多個檔案、資料塊組織在一起形成一個完整的檔案。例如我們常見的 .apk,.ipa,.sketch,都是ZIP檔案。通常程式是這樣建立 ZIP 檔案的:
- 壓縮單個檔案形成單檔案資料塊;
- 在資料塊前後添加檔案描述資訊;
- 對每個待壓縮的檔案重複以上步驟後,拼接所有資料形成更大的資料塊;
提取所有檔案描述資訊,生成一份「檔案目錄」,附在最後一個資料塊的尾部。
我們将檔案前部描述資訊稱為 Local File Header,檔案後部描述資訊稱為 Data Descriptor, 被壓縮的檔案本身稱為 File Data,将最後的檔案目錄稱為 Central Directory。以上所有合在一起,就是一個标準的 ZIP 檔案。如下圖:
一個标準的解壓方式總是從讀取 ZIP 檔案末尾開始的,我們以解壓上圖的 File Data 1 為例:
- 首先在 ZIP 檔案末尾找到 Central Directory 資料塊;
- 在 Central Directory 資料塊中找到 File Header 1;
- 從 File Header 1 中讀取 Local File Header 1 的偏移量和 File Data 1 的相關資訊;
- 根據偏移量找到 Local File Header 1;
- 讀取 Local File Header 1;
- 解密 File Data 1(如果需要);
- 解壓 File Data 1;
- 讀取 Data Descriptor 1;
- 使用 File Header 1 中儲存的 CRC-32 做校驗步驟 7 中計算的 CRC-32,以確定解壓後的資料完整性。
标準解壓方式存在的不足
可以發現,标準的解壓強依賴尾部的 Central Directory。當 ZIP 檔案存儲在 cdn 上時,哪怕我們隻想通路其中的一個檔案,也必須下載下傳整個 ZIP 解壓後才可通路。假如 ZIP 檔案有 100 MB,但是我們隻需要通路其中的某一個 10 KB 的檔案,那麼下載下傳整個 ZIP 将是對流量的巨大浪費。
二 優酷技術方案:ZIP流式解壓
我們的一個初步的想法是能不能邊下載下傳邊解壓?
要實作這點,首先需要改變解壓方式,使其不能再依賴尾部的 Central Directory。
根據 ZIP 檔案格式标準可知,除了 Central Directory,每個 File Data 頭部的 Loca File Header 部分也包含了該檔案的相關資訊。
假如 Local File Header 中包含了充分的資訊,我們也許可以基于 Local File Header 去解壓檔案資料,其解壓流程就可以變為:
- 從頭開始,搜尋到 Local File Header 1;
- CRC32 的校驗。
- 那麼 Local File Header 裡到底存儲了什麼?是否滿足解密解壓所需?
了解 Local File Header
我們根據文檔對 Local File Header 的描述,畫出其二進制檔案中的排列:
其中的關鍵資訊為:
中繼資料簽名是一個 Magic Number,用來标記接下來資料是什麼内容。例如 Local File Header 的簽名是 0x04034b50,用 char 表示也就是 { 'P', 'K', '3', '4' }。當讀取到對應資料簽名時,則意味着接下來的資料結構符合對應中繼資料的定義,需要使用對應規則解析。
Compress Method 指明資料塊用何種算法壓縮,解壓需要使用對應的算法。
Compressed Size 和 UnCompressed Size可以幫助确定檔案的結尾位址和 Data Descriptor 的偏移量。這兩個 Size 也是檔案解密時 HMAC 計算的關鍵。
有了 Magic Number 作為中繼資料簽名,我們隻需要逐位元組周遊去比對這個 Number,就可以找到 Loca File Header,而不再需要依賴尾部的定位資訊。而且 Local File Header 中存儲的中繼資料足夠我們決定解壓算法、計算大小、校驗 CRC-32 了。
還有一個問題是,解壓縮算法是否支援流式解壓縮?是否有特定的上下文依賴?通過了解壓縮算法的原理[1],我們知道,所有的壓縮算法都是支援從頭部開始流式解壓的。
而下載下傳方面,檔案是以從頭到尾連續的方式下載下傳,這又天然地和和從頭解壓的方式配合,便可以初步實作邊下邊解!
加密 ZIP 檔案的問題
一切都相當順利,直到遇到了加密後的 ZIP 檔案。加密後的 ZIP 檔案的 Local File Header 中的關鍵資訊除了簽名和檔案名以外,其他資訊都被隐去,需要去 Central Directory 中讀取。
再一次,我們回到了依賴 Central Directory 的狀态。
在失去如此多關鍵資訊的情況下能否繼續做到流式解壓?我們需要先挖掘一下 ZIP 的加密方式。
ZIP 的加密方式
ZIP 檔案支援多種加密方式,最常見的是 Traditional PKWARE Encryption 和 AES Encryption 。
Traditional PKWARE Encryption 是 ZIP 自定義的一種基于密碼的對稱加密方式,每個位元組的加密僅和密碼有關,加密前後的資料長度不變。這種不依賴上下文的加密方式可以實作我們需要的流式解密。
AES 加密采用的是 CTR 模式。CTR 模式将明文分組,并生成一個計數器。使用密鑰對計數器進行加密生成二進制位元組流。利用這個位元組流和明文進行 XOR 操作進行加密。其解密方式也是一樣的。
這種方式也支援流式解密。
兩種常用的加密方式都支援流式解密,那麼加解密需要的關鍵資訊,在 Local File Header 中是否有存儲就成了能否流式解密的關鍵。
流式解密的關鍵資訊
無論是 Traditional PKWARE Encryption 還是 AES Encryption,在解密時都需要一些除密碼之外的關鍵資訊,例如鹽值,加密算法的強度等。此外,在 AES 加密的 ZIP 檔案中, Local File Header 中的 Compress Method 字段被抹去,這樣我們便無法知曉壓縮算法,是以無法解壓。
至此,問題集中為:
- Local File Header 中是否有足夠的加密所需資訊。
- 加密的 ZIP 檔案,是否能在除 Central Directory 以外的位置找到 Compress Method 字段。
Local File Header 中加密相關的資訊
ZIP 格式的設計者在設計 ZIP 檔案格式的初期就提供了檔案拓展能力,一些額外的拓展資料可以存放在 Local File Header 的 Extra Field 中。ZIP AES 加密說明書[2]告訴我們 AES 的相關資訊就存放在這裡。其關鍵資訊如下:
原來壓縮算法被藏到了 Extra Data 中。那麼鹽值被存放在哪裡了?答案是存放在 File Data 的頭尾。
綜上,我們找到解密所需的所有關鍵資訊,整個流式解密解壓的所有技術點都被我們探索完。剩下的便是按原理實作,以及細節的打磨。
三 總結
說了那麼多,流式解壓究竟有什麼價值呢?
由于流式解壓實作了邊下載下傳邊解壓,将整個操作的時長從下載下傳 + 解壓縮變成了約等于純下載下傳的時長,直接抹掉了解壓的耗時。在 39.1 MB 大小的 ZIP 包下載下傳解壓測試中,耗時從 9.08 秒降低至 4.17 秒,有将近 100% 的提速!同時,你可以不必等待整個 ZIP 下載下傳解壓完,而是在解壓完一小部分資料的時候,就直接展示 UI。使用者側看起來就好像一瞬間就解壓完了。
是以,流式解壓可以應用在許多時間敏感的操作裡,也可以用來優化基于 ZIP 檔案的相關業務。例如基于 ZIP 的全局換膚加速、基于 ZIP 的 Web 資源緩存加載的加速等等。前言中的優酷繪本秒開就是基于這一技術實作。
參考
[1]
https://houbb.github.io/2018/11/09/althgorim-compress-althgorim-12-zip-02[2]AES Encryption Information: Encryption Specification AE-1 and AE-2
https://www.winzip.com/win/en/aes_info.html[3]ZIP File Format Specification
https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.2.1.TXT[4]AES Coding Tips for Developers
https://www.winzip.com/win/en/aes_tips.html