在查看自己的應用程式日誌時,發現進入日誌頁面後總是要幾秒鐘才會載入(介面沒做分頁),於是開啟網路面板查看
這才發現介面回傳的資料都沒有被壓縮,本來以為介面用Nginx反向代理了,Nginx會自動幫我做這一層(這塊後面探究一下,理論上是可行的)
這裡的後端是Node服務
本文就分享HTTP数据压缩
相關知識以及在Node侧的实践
下面的客戶端均指瀏覽器
客戶端在向服務端發起請求時,會在請求頭(request header)中加入accept-encoding
字段,其值標示客戶端支持的压缩内容编码
格式
服務端在對回傳內容執行壓縮後,透過在回應頭(response header)中加入content-encoding
,來告訴瀏覽器內容实际压缩使用的编码算法
deflate
是同時使用了LZ77
演算法與哈夫曼编码(Huffman Coding)
的一個無損資料壓縮演算法。
gzip
是基於DEFLATE
的演算法
br
指涉Brotli
,該資料格式旨在進一步提高壓縮比,對文字的壓縮相對deflate
能增加20%
的壓縮密度,而其壓縮與解壓縮速度則大致不變
Node. js包含一個zlib 模块
,提供了使用Gzip
、 Deflate/Inflate
、以及Brotli
實現的壓縮功能
這裡以gzip
為例分場景列舉多種使用方式, Deflate/Inflate
與Brotli
使用方式一樣,只是API不一樣
基於stream
的操作
基於buffer
的操作
引入幾個所需的模組
const zlib = require('zlib') const fs = require('fs') const stream = require('stream') const testFile = 'tests/origin.log' const targetFile = `${testFile}.gz` const decodeFile = `${testFile}.un.gz`
解/壓縮結果查看,這裡使用du
指令直接統計解壓縮前後結果
# 執行du -ah tests # 結果如下108K tests/origin.log.gz 2.2M tests/origin.log 2.2M tests/origin.log.un.gz 4.6M tests
流(stream)
的操作使用createGzip
與createUnzip
zlib
API,除了那些明確同步的API,都使用Node.js 內部線程池,可以看做是異步的方式1:直接利用實例上的pipe
方法傳遞流
// 壓縮const readStream = fs.createReadStream(testFile) const writeStream = fs.createWriteStream(targetFile) readStream.pipe(zlib.createGzip()).pipe(writeStream) // 解壓縮const readStream = fs.createReadStream(targetFile) const writeStream = fs.createWriteStream(decodeFile) readStream.pipe(zlib.createUnzip()).pipe(writeStream)
方式2:利用stream
上的pipeline
,可在回掉中單獨做其它的處理
// 壓縮const readStream = fs.createReadStream(testFile) const writeStream = fs.createWriteStream(targetFile) stream.pipeline(readStream, zlib.createGzip(), writeStream, err => { if (err) { console.error(err); } }) // 解壓縮const readStream = fs.createReadStream(targetFile) const writeStream = fs.createWriteStream(decodeFile) stream.pipeline(readStream, zlib.createUnzip(), writeStream, err => { if (err) { console.error(err); } })
方式3: Promise化pipeline
方法
const { promisify } = require('util') const pipeline = promisify(stream.pipeline) // 壓縮const readStream = fs.createReadStream(testFile) const writeStream = fs.createWriteStream(targetFile) pipeline(readStream, zlib.createGzip(), writeStream) .catch(err => { console.error(err); }) // 解壓縮const readStream = fs.createReadStream(targetFile) const writeStream = fs.createWriteStream(decodeFile) pipeline(readStream, zlib.createUnzip(), writeStream) .catch(err => { console.error(err); })
Buffer
的操作利用gzip
與unzip
API,這兩個方法包含同步
與异步
類型
gzip
gzipSync
unzip
unzipSync
方式1:將readStream
轉Buffer
,然後進行進一步操作
//壓縮const buff = [] readStream.on('data', (chunk) => { buff.push(chunk) }) readStream.on('end', () => { zlib.gzip(Buffer.concat(buff), targetFile, (err, resBuff) => { if(err){ console.error(err); process.exit() } fs.writeFileSync(targetFile,resBuff) }) })
// 壓縮const buff = [] readStream.on('data', (chunk) => { buff.push(chunk) }) readStream.on('end', () => { fs.writeFileSync(targetFile,zlib.gzipSync(Buffer.concat(buff))) })
方式2:直接透過readFileSync
讀取
// 壓縮const readBuffer = fs.readFileSync(testFile) const decodeBuffer = zlib.gzipSync(readBuffer) fs.writeFileSync(targetFile,decodeBuffer) // 解壓縮const readBuffer = fs.readFileSync(targetFile) const decodeBuffer = zlib.gzipSync(decodeFile) fs.writeFileSync(targetFile,decodeBuffer)
除了對檔案壓縮,有時也許要對傳輸的內容進行直接進行解壓縮
這裡以壓縮文字內容為例
// 測試資料const testData = fs.readFileSync( testFile, { encoding: 'utf-8' })
流(stream)
操作這塊就考慮string
=> buffer
=> stream
的轉換就行
string
=> buffer
const buffer = Buffer.from(testData)
buffer
=> stream
const transformStream = new stream.PassThrough() transformStream.write(buffer) // or const transformStream = new stream.Duplex() transformStream.push(Buffer.from(testData)) transformStream.push(null)
這裡以寫入到檔案範例,當然也可以寫到其它的流裡,如HTTP的Response
(後面會單獨介紹)
transformStream .pipe(zlib.createGzip()) .pipe(fs.createWriteStream(targetFile))
Buffer
操作同樣利用Buffer.from
將字串轉buffer
const buffer = Buffer.from(testData)
然後直接使用同步API轉換,這裡result就是壓縮後的內容
const result = zlib.gzipSync(buffer)
可以寫入文件,在HTTP Server
中也可直接對壓縮後的內容進行回傳
fs.writeFileSync(targetFile, result)
這裡直接使用Node中http
模組建立一個簡單的Server進行示範
在其他的Node Web
框架中,處理思路類似,當然一般也有現成的插件,一鍵接入
const http = require('http') const { PassThrough, pipeline } = require('stream') const zlib = require('zlib') // 測試資料const testTxt = '測試資料123'.repeat(1000) const app = http.createServer((req, res) => { const { url } = req // 讀取支援的壓縮演算法const acceptEncoding = req.headers['accept-encoding'].match(/(br|deflate|gzip)/g) // 預設回應的資料型別res.setHeader('Content-Type', 'application/json; charset=utf-8') // 幾個範例的路由const routes = [ ['/gzip', () => { if (acceptEncoding.includes('gzip')) { res.setHeader('content-encoding', 'gzip') // 使用同步API直接壓縮文字內容res.end(zlib.gzipSync(Buffer.from(testTxt))) return } res.end(testTxt) }], ['/deflate', () => { if (acceptEncoding.includes('deflate')) { res.setHeader('content-encoding', 'deflate') // 基於流的單次操作const originStream = new PassThrough() originStream.write(Buffer.from(testTxt)) originStream.pipe(zlib.createDeflate()).pipe(res) originStream.end() return } res.end(testTxt) }], ['/br', () => { if (acceptEncoding.includes('br')) { res.setHeader('content-encoding', 'br') res.setHeader('Content-Type', 'text/html; charset=utf-8') // 基於流的多次寫入操作const originStream = new PassThrough() pipeline(originStream, zlib.createBrotliCompress(), res, (err) => { if (err) { console.error(err); } }) originStream.write(Buffer.from('<h1>BrotliCompress</h1>')) originStream.write(Buffer.from('<h2>測試資料</h2>')) originStream.write(Buffer.from(testTxt)) originStream.end() return } res.end(testTxt) }] ] const route = routes.find(v => url.startsWith(v[0])) if (route) { route[1]() return } // 兜底res.setHeader('Content-Type', 'text/html; charset=utf-8') res.end(`<h1>404: ${url}</h1> <h2>已註冊路由</h2> <ul> ${routes.map(r => `<li><a href="${r[0]}">${r[0]}</a></li>`).join('') } </ul> `) res.end() }) app.listen(3000)