從零手寫實現 nginx-09-compress http 檔案壓縮

老马啸西风發表於2024-06-07

前言

大家好,我是老馬。很高興遇到你。

我們為 java 開發者實現了 java 版本的 nginx

https://github.com/houbb/nginx4j

如果你想知道 servlet 如何處理的,可以參考我的另一個專案:

手寫從零實現簡易版 tomcat minicat

手寫 nginx 系列

如果你對 nginx 原理感興趣,可以閱讀:

從零手寫實現 nginx-01-為什麼不能有 java 版本的 nginx?

從零手寫實現 nginx-02-nginx 的核心能力

從零手寫實現 nginx-03-nginx 基於 Netty 實現

從零手寫實現 nginx-04-基於 netty http 出入參最佳化處理

從零手寫實現 nginx-05-MIME型別(Multipurpose Internet Mail Extensions,多用途網際網路郵件擴充套件型別)

從零手寫實現 nginx-06-資料夾自動索引

從零手寫實現 nginx-07-大檔案下載

從零手寫實現 nginx-08-範圍查詢

從零手寫實現 nginx-09-檔案壓縮

從零手寫實現 nginx-10-sendfile 零複製

從零手寫實現 nginx-11-file+range 合併

從零手寫實現 nginx-12-keep-alive 連線複用

從零手寫實現 nginx-13-nginx.conf 配置檔案介紹

從零手寫實現 nginx-14-nginx.conf 和 hocon 格式有關係嗎?

從零手寫實現 nginx-15-nginx.conf 如何透過 java 解析處理?

從零手寫實現 nginx-16-nginx 支援配置多個 server

什麼是 http 壓縮

HTTP壓縮是一種網路最佳化技術,用於減少在客戶端和伺服器之間傳輸的資料量。

透過壓縮響應內容(如HTML、CSS、JavaScript、圖片等),可以加快載入速度,減少頻寬消耗,提升使用者體驗。

HTTP壓縮通常在伺服器端進行,客戶端接收到壓縮後的資料後進行解壓縮。

以下是HTTP壓縮的一些關鍵點:

  1. 壓縮演算法

    • GZIP:最常用的壓縮格式,廣泛支援各種壓縮級別。
    • DEFLATE:與GZIP類似,但通常不包含檔案後設資料。
    • Brotli:一種較新的壓縮演算法,提供比GZIP更好的壓縮比率,被現代瀏覽器支援。
    • 其他:如Zstandard(Zstd),也是一種高效的壓縮演算法,但瀏覽器支援度較低。
  2. HTTP頭資訊

    • Content-Encoding:響應頭,指示資料使用的壓縮格式。
    • Accept-Encoding:請求頭,客戶端透過此頭通知伺服器它支援的壓縮格式。
    • Vary:響應頭,用於指示響應內容會根據不同的請求頭(如Accept-Encoding)而變化。
  3. 伺服器配置

    • 伺服器需要配置相應的模組或中介軟體來處理HTTP壓縮。例如,在Nginx中,可以使用gzip模組,在Apache中可以使用mod_deflate
  4. 內容型別

    • 並非所有內容型別都適合壓縮。文字內容(如HTML、CSS、JavaScript)通常會從壓縮中受益,而已經壓縮的媒體型別(如JPEG、GIF圖片,MP3音訊)則不會。
  5. 壓縮級別

    • 伺服器可以配置不同的壓縮級別,以平衡壓縮比率和CPU負載。壓縮級別越高,壓縮比率越好,但CPU負載也越大。
  6. 快取處理

    • 壓縮內容應該被適當地快取。如果客戶端支援條件請求(如ETag或Last-Modified),快取的壓縮內容可以被重複使用。
  7. 安全性

    • 在某些情況下,壓縮可能會暴露敏感資訊,尤其是當內容是動態生成的。因此,需要確保壓縮的內容不會洩露敏感資料。
  8. 配置示例

    • 以下是Nginx配置GZIP壓縮的示例:
      gzip on;
      gzip_vary on;
      gzip_proxied any;
      gzip_comp_level 5;
      gzip_min_length 256;
      gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
      
    • 這個配置啟用了GZIP壓縮,根據Accept-Encoding頭變化響應不同的內容,對所有代理請求進行壓縮,設定壓縮級別為5(快速壓縮),只壓縮大於256位元組的響應,並且指定了可以被壓縮的內容型別。
  9. 測試和驗證

    • 使用工具(如Chrome開發者工具或線上服務)來測試和驗證壓縮是否按預期工作。
  10. 客戶端支援

    • 客戶端(瀏覽器)必須支援併傳送Accept-Encoding頭,才能接收壓縮內容。

HTTP壓縮是一個強大的工具,可以顯著提高網站效能。然而,它也增加了伺服器的CPU負載,因此需要根據伺服器的處理能力和網站的需求來適當配置。

如果一個 txt 檔案很大,指定壓縮響應。那麼 response 中的 Content-Type 應該是什麼?響應中會有哪些資訊標識壓縮相關的?

對於一個大型的文字檔案(如.txt),如果伺服器配置了壓縮響應,那麼Content-Type通常設定為text/plain,因為.txt檔案是純文字格式。

當響應被壓縮時,響應頭中會包含一些標識壓縮相關的資訊:

  1. Content-Encoding
    這個響應頭用於指示響應體使用的壓縮格式。例如,如果使用了GZIP壓縮,Content-Encoding將被設定為gzip。如果使用了Brotli壓縮,它將被設定為br

    Content-Encoding: gzip
    
  2. Vary
    Vary響應頭用於指示響應內容會根據不同的請求頭而變化。當啟用壓縮時,伺服器可能會設定Vary頭為Accept-Encoding,表示響應內容會根據請求中的Accept-Encoding頭變化。

    Vary: Accept-Encoding
    
  3. Content-Length
    Content-Length響應頭指示響應體的大小(以位元組為單位)。當響應被壓縮時,Content-Length將反映壓縮後的大小。

    Content-Length: 1200
    

    注意:在某些情況下,如果伺服器使用分塊傳輸編碼(chunked transfer encoding),Content-Length可能不會被設定。

當客戶端接收到包含Content-Encoding頭的響應時,它知道需要對接收到的資料進行解壓縮,以恢復原始內容。

以下是一個示例HTTP響應,假設伺服器對一個大型文字檔案的響應進行了GZIP壓縮:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Encoding: gzip
Content-Length: 1200
Vary: Accept-Encoding
Date: Wed, 31 Dec 2024 12:00:00 GMT
Server: YourServer/1.0

<壓縮的響應體>

在這個示例中:

  • Content-Type: text/plain; charset=UTF-8 表示響應體是一個文字檔案,使用UTF-8編碼。
  • Content-Encoding: gzip 表示響應體被GZIP壓縮。
  • Content-Length 表示壓縮後的響應體大小。
  • Vary: Accept-Encoding 表示響應內容可能會根據請求頭中的Accept-Encoding變化。

客戶端在接收到這個響應後,會根據Content-Encoding頭來解壓縮響應體,然後以原始的文字格式進行處理或顯示。

核心實現

/**
 * 檔案壓縮
 *
 * @since 0.8.0
 * @author 老馬嘯西風
 */
public class NginxRequestDispatchFileCompress extends AbstractNginxRequestDispatchFullResp {

    private static final Log logger = LogFactory.getLog(AbstractNginxRequestDispatchFullResp.class);

    @Override
    protected FullHttpResponse buildFullHttpResponse(FullHttpRequest request,
                                                     final NginxConfig nginxConfig,
                                                     NginxRequestDispatchContext context) {
        final File targetFile = context.getFile();
        logger.info("[Nginx] match compress file, path={}", targetFile.getAbsolutePath());


        // 壓縮內容
        byte[] compressData = getCompressData(context);
        // 建立一個帶有GZIP壓縮內容的ByteBuf
        ByteBuf compressedContent = Unpooled.copiedBuffer(compressData);
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, compressedContent);

        // 設定壓縮相關的響應頭
        response.headers().set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, InnerMimeUtil.getContentTypeWithCharset(targetFile, context.getNginxConfig().getCharset()));

        // 檢查請求是否接受GZIP編碼
        if (request.headers().contains(HttpHeaderNames.ACCEPT_ENCODING) &&
                request.headers().get(HttpHeaderNames.ACCEPT_ENCODING).contains(HttpHeaderValues.GZIP)) {

            // 新增Vary頭,告知存在多個版本的響應
            response.headers().set(HttpHeaderNames.VARY, HttpHeaderNames.ACCEPT_ENCODING);
        }

        return response;
    }

    public static byte[] getCompressData(NginxRequestDispatchContext context) {
        final File targetFile = context.getFile();

        byte[] inputData = FileUtil.getFileBytes(targetFile);
        // 使用try-with-resources語句自動關閉資源
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
             GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) {
            // 寫入要壓縮的資料
            gzipOutputStream.write(inputData);
            // 強制重新整理輸出流以確保所有資料都被壓縮和寫入
            gzipOutputStream.finish();
            // 獲取壓縮後的資料
            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            logger.error("[Nginx] getCompressData failed", e);
            throw new Nginx4jException(e);
        }
    }

}

測試

測試程式碼

public static void main(String[] args) {
    NginxGzipConfig gzipConfig = new NginxGzipConfig();
    gzipConfig.setGzip("on");
    gzipConfig.setGzipMinLength(256);
    Nginx4jBs.newInstance()
            .nginxGzipConfig(gzipConfig)
            .init()
            .start();
}

頁面訪問

http://192.168.1.12:8080/c.txt

可以直接返回 c.txt 的內容。

而且後端命中了壓縮處理。

[INFO] [2024-05-26 21:01:26.319] [nioEventLoopGroup-3-1] [c.g.h.n.s.r.d.h.AbstractNginxRequestDispatchFullResp.buildFullHttpResponse] - [Nginx] match compress file, path=D:\data\nginx4j\c.txt
[INFO] [2024-05-26 21:01:26.324] [nioEventLoopGroup-3-1] [c.g.h.n.u.InnerMimeUtil.getContentTypeWithCharset] - file=D:\data\nginx4j\c.txt, contentType=text/plain; charset=UTF-8
[INFO] [2024-05-26 21:01:26.327] [nioEventLoopGroup-3-1] [c.g.h.n.s.r.d.h.AbstractNginxRequestDispatchFullResp.doDispatch] - [Nginx] channelRead writeAndFlush DONE response=DefaultFullHttpResponse(decodeResult: success, version: HTTP/1.1, content: UnpooledHeapByteBuf(freed))
HTTP/1.1 200 OK
content-encoding: gzip
content-type: text/plain; charset=UTF-8
vary: accept-encoding
content-length: 696
[INFO] [2024-05-26 21:01:26.328] [nioEventLoopGroup-3-1] [c.g.h.n.s.h.NginxNettyServerHandler.channelRead0] - [Nginx] channelRead writeAndFlush DONE id=40a5effffe257be0-00006394-00000001-0af1170bb1114311-b3ae0da9

http 資訊

請求基本資訊

Request URL:
http://192.168.1.12:8080/c.txt
Request Method:
GET
Status Code:
200 OK
Remote Address:
192.168.1.12:8080
Referrer Policy:
strict-origin-when-cross-origin

請求頭

GET /c.txt HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Host: 192.168.1.12:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36

響應頭

HTTP/1.1 200 OK
content-encoding: gzip
content-type: text/plain; charset=UTF-8
vary: accept-encoding
content-length: 696

可見,雖然返回的是 gzip 內容,但是瀏覽器會自動解壓處理。

小結

本節我們實現了檔案的壓縮處理,這個對於檔案的傳輸效能提升比較大。

當然,壓縮+解壓本身也是對效能有損耗的。要結合具體的壓縮比等考慮。

下一節,我們考慮實現一下 cors 的支援。

我是老馬,期待與你的下次重逢。

開源地址

為了便於大家學習,已經將 nginx 開源

https://github.com/houbb/nginx4j

相關文章