如何壓縮 HTTP 請求正文

JerryQu發表於2016-04-19

上篇文章中,我介紹了 HTTP 協議中的 Accept-Encoding 和 Content-Encoding 機制。它可以很好地用於文字類響應正文的壓縮,減少網路資料的傳輸,已被廣泛使用。但 HTTP 請求的發起方瀏覽器,無法事先知曉要訪問的服務端是否支援解壓,所以現階段的瀏覽器沒有壓縮請求正文

有一些通訊協議基於 HTTP 做了擴充套件,他們的客戶端和服務端是專用的,完全可以針對請求正文進行壓縮,例如 WebDAV 客戶端就是這麼做的。

實際的 Web 專案中,存在請求正文非常大的場景,例如發表長篇部落格,上報用於除錯的網路資料等等。這些資料如果能在本地壓縮後再提交,就可以節省大量流量、減少傳輸時間。本文介紹如何對 HTTP 請求正文進行壓縮,包含如何在服務端解壓、如何在客戶端壓縮兩個部分。

開始之前,先來介紹本文涉及的三種資料壓縮格式:

  • DEFLATE,是一種使用 Lempel-Ziv 壓縮演算法(LZ77)和哈夫曼編碼的壓縮格式。詳見 RFC 1951
  • ZLIB,是一種使用 DEFLATE 的壓縮格式,對應 HTTP 中的 Content-Encoding: deflate。詳見 RFC 1950
  • GZIP,也是一種使用 DEFLATE 的壓縮格式,對應 HTTP 中的 Content-Encoding: gzip。詳見 RFC 1952

Content-Encoding 中的 deflate,實際上是 ZLIB。為了清晰,本文將 DEFLATE 稱之為 RAW DEFLATE,ZLIB 和 GZIP 都是 RAW DEFLATE 的不同 Wrapper。

解壓請求正文

服務端收到請求正文後,需要分析請求頭中的 Content-Encoding 欄位,才能知道正文采用了哪種壓縮格式。本文規定用 gzip、deflate 和 deflate-raw 分別表示請求正文采用 GZIP、ZLIB 和 RAW DEFLATE 壓縮格式。

Nginx

Nginx 沒有類似於 Apache 的 SetInputFilter 指令,不能直接給請求新增處理邏輯,還好有 OpenResty。OpenResty 通過整合 Lua 及大量 Lua 庫,極大地提升了 Nginx 的功能豐富度和可擴充套件性。而 LuaJIT 中的 FFI 庫,允許純 Lua 程式碼呼叫外部 C 函式,使用 C 資料結構。

把這一切結合起來,就能方便地實現這個需求:首先安裝 OpenResty;下載並解壓 Zlib 庫的 FFI 版;然後在 Nginx 的配置中,通過 lua_package_path 指令將這個庫引入;再新建一個 lua 檔案,如 request-compress.lua,呼叫 Zlib 庫實現解壓功能:

我們的 Nginx 一般都是擋在最前面,背後還有 PHP、Node.js 等實際服務。這段程式碼從 Content-Encoding 請求頭中獲取請求壓縮格式,並在解壓後移除了這個頭部。這樣對於 Nginx 背後的服務來說,完全感知不到跟平常有什麼不一樣。

現在還差最後一步,找到 Nginx 中配置 xxx_pass(proxy_pass、uwsgi_pass、fastcgi_pass 等)的地方,加入 lua 處理邏輯:

這個配置目的是讓這個 lua 邏輯工作在 Nginx 的 Access 階段。

到此為止,基於 OpenResty 的解壓方案已經寫好。它能否按預期正常工作呢?我決定先放一放,後面再驗證。

Node.js

Node.js 內建了對 Zlib 庫的封裝。使用 Node.js 也可以輕鬆應對壓縮內容。直接上程式碼:

這段程式碼將請求正文解壓之後,直接做出輸出返回,它可以正常工作,但僅作示意。實際專案中,這些通用邏輯應該放在框架層統一處理,業務層程式碼無需關心。後面我會基於 ThinkJS 寫一個外掛,專門處理這個邏輯。

PHP

PHP 也內建了處理這些壓縮格式的函式,以下是例項程式碼:

可以看到,ZLIB 格式的壓縮資料去掉頭尾,就是 RAW DEFLATE,可以直接用 gzinflate 解壓。跟前面一樣,如果採用 PHP 解壓方案,也應該在框架層統一處理。

小結一下:在 Nginx 統一解壓的好處是無論後端掛接什麼服務,都可以做到無感知,壞處是需要升級為 OpenResty;在 Web 框架中處理更靈活,但不同語言不同專案需要分別處理,效能方面應該也有差別。如何選擇,要看各自實際情況。

壓縮請求正文

瀏覽器

通過 pako 這個 JS 庫,可以在瀏覽器中使用 ZLib 庫的大部分功能。它也能用於 Node.js 環境,但 Node.js 中一般用官方的 Zlib 就可以了。

pako 的瀏覽器版可以在這裡下載,我們只需要壓縮功能,使用 pako_deflate.min.js 即可。這個檔案有 27.3KB,gzip 後 9.1KB,算很小的了。它同時支援 GZIP、ZLIB 和 RAW DEFLATE 三種壓縮格式,如果只保留一種應該還能更小。

下面是使用 pako 庫在瀏覽器中實現壓縮請求正文的示例程式碼:

這段程式碼本身沒什麼好多說的,十分簡單。這裡有一個最終的 DEMO 頁面,大家可以實際體驗下。在這個 DEMO 中,針對 Zepto 原始碼壓縮後能夠減少 70% 的體積,十分可觀。這個 DEMO 服務端使用的是前面介紹的 Node.js 解壓方案。

Gzip + Curl

使用 Curl 命令,可以將 Gzip 命令生成的 GZIP 壓縮資料 POST 給服務端。例如:

通過下圖可以清晰的看到整個資料傳輸過程:

request body compress

本文到此馬上就要結束了。對於本文沒有提及的移動 APP,如果有 POST 大資料的場景,也可以使用本文方案,以較小的成本換取使用者流量的節省和網路效能的提升,更妙的是這個方案具有良好的相容性(不支援請求正文壓縮的老版本 APP,自然不會在請求頭帶上 Content-Encoding 欄位,直接會跳過服務端的解壓邏輯),非常值得嘗試!

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

如何壓縮 HTTP 請求正文 如何壓縮 HTTP 請求正文

相關文章