前言
大家好,我是老馬。很高興遇到你。
我們為 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壓縮的一些關鍵點:
-
壓縮演算法:
- GZIP:最常用的壓縮格式,廣泛支援各種壓縮級別。
- DEFLATE:與GZIP類似,但通常不包含檔案後設資料。
- Brotli:一種較新的壓縮演算法,提供比GZIP更好的壓縮比率,被現代瀏覽器支援。
- 其他:如Zstandard(Zstd),也是一種高效的壓縮演算法,但瀏覽器支援度較低。
-
HTTP頭資訊:
Content-Encoding
:響應頭,指示資料使用的壓縮格式。Accept-Encoding
:請求頭,客戶端透過此頭通知伺服器它支援的壓縮格式。Vary
:響應頭,用於指示響應內容會根據不同的請求頭(如Accept-Encoding
)而變化。
-
伺服器配置:
- 伺服器需要配置相應的模組或中介軟體來處理HTTP壓縮。例如,在Nginx中,可以使用
gzip
模組,在Apache中可以使用mod_deflate
。
- 伺服器需要配置相應的模組或中介軟體來處理HTTP壓縮。例如,在Nginx中,可以使用
-
內容型別:
- 並非所有內容型別都適合壓縮。文字內容(如HTML、CSS、JavaScript)通常會從壓縮中受益,而已經壓縮的媒體型別(如JPEG、GIF圖片,MP3音訊)則不會。
-
壓縮級別:
- 伺服器可以配置不同的壓縮級別,以平衡壓縮比率和CPU負載。壓縮級別越高,壓縮比率越好,但CPU負載也越大。
-
快取處理:
- 壓縮內容應該被適當地快取。如果客戶端支援條件請求(如ETag或Last-Modified),快取的壓縮內容可以被重複使用。
-
安全性:
- 在某些情況下,壓縮可能會暴露敏感資訊,尤其是當內容是動態生成的。因此,需要確保壓縮的內容不會洩露敏感資料。
-
配置示例:
- 以下是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位元組的響應,並且指定了可以被壓縮的內容型別。
- 以下是Nginx配置GZIP壓縮的示例:
-
測試和驗證:
- 使用工具(如Chrome開發者工具或線上服務)來測試和驗證壓縮是否按預期工作。
-
客戶端支援:
- 客戶端(瀏覽器)必須支援併傳送
Accept-Encoding
頭,才能接收壓縮內容。
- 客戶端(瀏覽器)必須支援併傳送
HTTP壓縮是一個強大的工具,可以顯著提高網站效能。然而,它也增加了伺服器的CPU負載,因此需要根據伺服器的處理能力和網站的需求來適當配置。
如果一個 txt 檔案很大,指定壓縮響應。那麼 response 中的 Content-Type 應該是什麼?響應中會有哪些資訊標識壓縮相關的?
對於一個大型的文字檔案(如.txt
),如果伺服器配置了壓縮響應,那麼Content-Type
通常設定為text/plain
,因為.txt
檔案是純文字格式。
當響應被壓縮時,響應頭中會包含一些標識壓縮相關的資訊:
-
Content-Encoding
:
這個響應頭用於指示響應體使用的壓縮格式。例如,如果使用了GZIP壓縮,Content-Encoding
將被設定為gzip
。如果使用了Brotli壓縮,它將被設定為br
。Content-Encoding: gzip
-
Vary
:
Vary
響應頭用於指示響應內容會根據不同的請求頭而變化。當啟用壓縮時,伺服器可能會設定Vary
頭為Accept-Encoding
,表示響應內容會根據請求中的Accept-Encoding
頭變化。Vary: Accept-Encoding
-
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