Node侧实现内容压缩(gzip/br/deflate)介绍与实践

简介: Node侧实现内容压缩(gzip/br/deflate)介绍与实践

背景


在查看自己的应用日志时,发现进入日志页面后总是要几秒钟才会加载(接口没做分页),于是打开网络面板查看


网络异常,图片无法展示
|

网络异常,图片无法展示
|


这才发现接口返回的数据都没有被压缩,本以为接口用Nginx反向代理了,Nginx会自动帮我做这一层(这块后面探究一下,理论上是可行的)


这里的后端是 Node 服务


本文就分享一下 HTTP数据压缩相关知识以及在Node侧的实践


前置知识


下面的客户端均指浏览器


accept-encoding


网络异常,图片无法展示
|


客户端在向服务端发起请求时,会在请求头(request header)中添加accept-encoding字段,其值标明客户端支持的压缩内容编码格式


content-encoding


网络异常,图片无法展示
|


服务端在对返回内容执行压缩后,通过在响应头(response header)中添加content-encoding,来告诉浏览器内容实际压缩使用的编码算法


deflate/gzip/br


deflate是同时使用了LZ77算法与哈夫曼编码(Huffman Coding)的一个无损数据压缩算法。


gzip 是基于 DEFLATE 的算法


br指代Brotli,该数据格式旨在进一步提高压缩比,对文本的压缩相对deflate能增加20%的压缩密度,而其压缩与解压缩速度则大致不变


zlib模块


Node.js包含一个zlib 模块,提供了使用 GzipDeflate/Inflate、以及 Brotli 实现的压缩功能


这里以gzip为例分场景列举多种使用方式,Deflate/InflateBrotli使用方式一样,只是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)的操作


使用createGzipcreateUnzip


  • 注:所有 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的操作


利用 gzipunzip API,这两个方法包含同步异步类型


  • 压缩
  • gzip
  • gzipSync
  • 解压
  • unzip
  • unzipSync


方式1:readStreamBuffer,然后进行进一步操作


  • gzip:异步


// 压缩
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)
    })
})


  • gzipSync:同步


// 压缩
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 Server中的实践


这里直接使用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)


参考


相关文章
|
29天前
|
缓存 负载均衡 JavaScript
构建高效后端服务:Node.js与Express框架实践
在数字化时代的浪潮中,后端服务的重要性不言而喻。本文将通过深入浅出的方式介绍如何利用Node.js及其强大的Express框架来搭建一个高效的后端服务。我们将从零开始,逐步深入,不仅涉及基础的代码编写,更会探讨如何优化性能和处理高并发场景。无论你是后端新手还是希望提高现有技能的开发者,这篇文章都将为你提供宝贵的知识和启示。
|
1月前
|
Web App开发 JavaScript 前端开发
构建高效后端服务:Node.js与Express框架的实践
【10月更文挑战第33天】在数字化时代的浪潮中,后端服务的效率和可靠性成为企业竞争的关键。本文将深入探讨如何利用Node.js和Express框架构建高效且易于维护的后端服务。通过实践案例和代码示例,我们将揭示这一组合如何简化开发流程、优化性能,并提升用户体验。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
2月前
|
Web App开发 JavaScript API
构建高效后端系统:Node.js与Express框架的实践之路
【9月更文挑战第37天】在数字化时代的浪潮中,后端开发作为技术架构的核心,承载着数据处理和业务逻辑的重要职责。本文将深入探讨如何利用Node.js及其强大的Express框架来搭建一个高效、可扩展的后端系统。我们将从基础概念讲起,逐步引导读者理解并实践如何设计、开发和维护一个高性能的后端服务。通过实际代码示例和清晰的步骤说明,本文旨在为初学者和有经验的开发者提供一个全面的指南,帮助他们在后端开发的旅途上走得更远。
55 3
|
2月前
|
Web App开发 JSON JavaScript
深入浅出:Node.js后端开发入门与实践
【10月更文挑战第4天】在这个数字信息爆炸的时代,了解如何构建一个高效、稳定的后端系统对于开发者来说至关重要。本文将引导你步入Node.js的世界,通过浅显易懂的语言和逐步深入的内容组织,让你不仅理解Node.js的基本概念,还能掌握如何使用它来构建一个简单的后端服务。从安装Node.js到实现一个“Hello World”程序,再到处理HTTP请求,文章将带你一步步走进Node.js的大门。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开一扇通往后端开发新世界的大门。
|
4月前
|
JavaScript 前端开发 API
深入浅出:使用Node.js搭建RESTful API的实践之旅
【8月更文挑战第31天】本文将带你踏上一次Node.js的探险之旅,通过实际动手构建一个RESTful API,我们将探索Node.js的强大功能和灵活性。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供宝贵的实践经验和深刻的技术洞见。
|
4月前
|
存储 缓存 JavaScript
深入Node.js身份验证:策略与实践
【8月更文挑战第20天】
56 4
|
4月前
|
JavaScript 安全 前端开发
Node.js身份验证全攻略:策略与实践,打造坚不可摧的Web应用安全防线!
【8月更文挑战第22天】Node.js作为强大的服务器端JavaScript平台,对于构建高效网络应用至关重要。本文探讨其身份验证策略,涵盖从基于token至复杂的OAuth 2.0及JWT。Passport.js作为认证中间件,支持本地账号验证及第三方服务如Google、Facebook登录。同时介绍JWT轻量级验证机制,确保数据安全传输。开发者可根据应用需求选择合适方案,注重安全性以保护用户数据。
89 1
|
6月前
|
JavaScript 前端开发 Shell
深入Node.js的进程与子进程:从文档到实践
深入Node.js的进程与子进程:从文档到实践
|
7月前
|
消息中间件 监控 JavaScript
Node.js中的微服务架构:构建与实践
【4月更文挑战第30天】本文探讨了在Node.js中构建微服务的实践,包括定义服务边界、选择框架(如Express、Koa或NestJS)、设计RESTful API、实现服务间通信(HTTP、gRPC、消息队列)、错误处理、服务发现与负载均衡,以及监控和日志记录。微服务架构能提升应用的可伸缩性、灵活性和可维护性。
|
7月前
|
数据采集 JavaScript 前端开发
利用axios库在Node.js中进行代理请求的实践
利用axios库在Node.js中进行代理请求的实践