其他

探索HTTP传输中gzip压缩的秘密

转载:探索HTTP传输中gzip压缩的秘密

背景


为什么要开启gZip

我们给某人发送邮件时,我们在传输之前把自己的文件压缩一下,接收方收到文件后再去解压获取文件。这中操作对于我们来说都已经司空见惯。我们压缩文件的目的就是为了把传输文件的体积减小,加快传输速度。我们在 http 传输中开启 gZip 的目的也是如此,但是一般文章介绍 gZip 时候总是结合一些服务端配置(nginx)或者构建工具插件(webpack)来说,列出一大堆配置让人看的云里雾里,以至于到最后还没搞懂 为什么用怎么用 这些问题。

当用户访问我们的web站点时,前端资源通过Gzip压缩,传输给客户端,比纯文本体积减少大概60%左右,能够节约客户端网络资源,提升响应速度,解决网络导致的资源加载瓶颈,和减小服务器压力。

http 与 gZip


我们下面去探讨一下这些问题

gZip 文件怎么通讯

我们传输压缩文件给别人时候一般都带着后缀名 .rar, .zip之类,对方在拿到文件后根据相应的后缀名选择不同的解压方式然后去解压文件。我们在 http 传输时候解压文件的这个角色的扮演者就是我们使用的浏览器,但是浏览器怎么分辨这个文件是什么格式,应该用什么格式去解压呢?

http/1.0 协议中关于服务端发送的数据可以配置一个 Content-Encoding 字段,这个字段用于说明数据的压缩方法

Content-Encoding: gzip
Content-Encoding: compress
Content-Encoding: deflate

客户端在接受到返回的数据后去检查对应字段的信息,然后根据对应的格式去做相应的解码。客户端在请求时,可以用 Accept-Encoding 字段说明自己接受哪些压缩方法。

Accept-Encoding: gzip, deflate


我们在浏览器的控制台中可以看到请求的相关信息

Gzip工作原理

  1. 浏览器请求url,并在请求头中设置属性accept-encoding:gzip。表明浏览器支持gzip,这个参数是浏览器自动会携带的请求头信息。(这里有个问题,所有浏览器都支持gzpi吗?)
  2. 服务器收到浏览器发送的请求之后,服务器会返回压缩后的文件,并在响应头中包含content-encoding: gzip;如果没有压缩文件,服务器会返回未压缩的请求文件。
  3. 浏览器接收到到服务器的响应,根据content-encoding: gzip 响应头使用gzip策略去解压压缩后的资源,通过content-type内容类型决定怎么编码读取该文件内容。

谁去压缩文件

这件事看起来貌似只能服务端来做,我们在网上看到最多的也是诸如 nginx 开启 gZip 配置之类的文章,但是现在前端流行 spa 应用, 用 react, vue 之类的框架时候总伴随这一套自己的脚手架,一般用 webpack 作为打包工具,其中可以配置插件 如compression-webpack-plugin 可以让我们把生成文件进行 gZip 等压缩并生成对应的压缩文件,而我们应用在构架时候有可能也会在服务区和前端文件中放置一层 node 应用来进行接口鉴权和文件转发。nodejs中我们熟悉的express 框架中也有一个compression 中间件,可以开启gZip,一时间看的人眼花缭乱,到底应该用谁怎么用呢?

服务端响应请求时候压缩

其实 nginx 压缩和 node 框架中用中间件去压缩都是一样的,当我们点击网页发送一个请求时候,我们的服务端会找到对应的文件,然后对文件进行压缩返回压缩后的内容【当然可以利用缓存减少压缩次数】,并配置好我们上面提到的 Content-Encoding 信息。对于一些应用在构架时候并没有上游代理层,比如服务端就一层 node 就可以直接用自己本身的压缩插件对文件进行压缩,如果上游配有有 nginx 转发处理层,最好交给 nginx 来处理这些,因为它们有专门为此构建的内容,可以更好的利用缓存并减小开销(很多使用c语言编写的)。

Gzip压缩的几种方案


  1. Nginx配置
  2. webpack配置
  3. Nodejs配置

Nginx配置


浏览器请求资源时,nginx服务器对文件进行gzip压缩后返回给浏览器。前端不用做任何修改正常打包,但这样会消耗服务器性能。nginx开启gzip压缩,代码如下:

介绍一下上面的配置:

指令格式说明
gzipgzip on | off控制是否开启gzip,默认不开启
gzip_min_lengthgzip_min_length length大于length的响应才压缩,默认为20字节
gzip_http_versiongzip_http_version 1.0|1.1对1.1的请求开启压缩功能
gzip_buffersgzip_buffers number size设置gzip申请内存的大小,申请number个大小为size的空间
gzip_comp_levelgzip_comp_level level设置压缩等级,1-9,1表示压缩程度最低,压缩率最高。9表示相反,最费时间。
gzip_typesgzip_types mine-types根据mime-type类型进行压缩,可以取值text/plain等。

其中的zip_comp_level,是一项重要的性能指标,取值多少最合理?以下是以umi为例,level从1-9,压缩之后的umi.742a046d.js文件分析

以上分析得出结论:随着压缩率的提高,所消耗的CPU也会越来越多。从1-4压缩比较明显,5以后基本变化不大。估建议设置gzip_comp_level 4

前端打包


打包时通过webpack配置生成的对应的.gz文件,浏览器请求文件后会自动解压未js或者css等资源文件。

前端打包借助compression-webpack-plugin插件

npm install compression-webpack-plugin –save-dev

还是借助umi,在config.ts中配置如下:

{
 ...
 chainWebpack(config, {webpack}){
    config.plugin('compression-webpack-plugin').use(CompressionPlugin, [{
      algorithm: 'gzip',
      test: /\.(js|css|html|svg|gif|png|jpeg|txt)$/,
      filename: '[base].gz',
      threshold: 2048,
      deleteOriginalAssets: false,
    }])
  },
...
}

打包之后的文件目录:

可以通过配置deleteOriginalAssets:true,删除未打包资源,但我们推荐保留未打包资源

Nginx服务器只需要配置:

gzip_static on;

nodejs启用gzip


nodejs启用gzip相对来说比较简单,以express框架为例如何启用gzip;

npm install compression 
npm install @types/compression --save-dev
import compression from 'compression';
...
const app = express();
// 尽量在其他中间件前面
app.use(compression({level: 5}));
...

问题


1.gzip是所有浏览器都支持的吗?

2.content-type和content-encoding的区别

content-type:内容类型,决定文件接收方以什么形式、什么编码读取这个文件,注重是浏览器如何读取文件。

content-encoding:设置文件内容的编码格式,传输前什么格式,传输后接收到要以什么格式解析,注重如何传输文件。

3、压缩后的文件会不会影响之前缓存文件的缓存策略?

不会。因为生成后的gz文件名同样会带上hash值。

4、除了Gzip压缩,还有别的压缩方式吗?

这个主要看,content-encoding有哪些值,还有zopfli、brotli。其他压缩方式要么效果差不过,要么浏览器兼容性不好,建议不要采用其他压缩方式。

5. nodejs的compression模块,会压缩 http json的response吗?

在应用中使用 compression 模块

以下是一个使用 Express 框架的示例,展示了如何将 compression 模块集成到你的应用中:

const express = require('express');
const compression = require('compression');

const app = express();

// 使用 compression 中间件
app.use(compression());

app.get('/data', (req, res) => {
    const jsonResponse = {
        message: 'Hello, this is a compressed JSON response!',
        data: Array(1000).fill('some data')
    };

    res.json(jsonResponse);
});

const PORT = 3000;
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});
// 启动服务
node app.js

// 发送请求
curl -H "Accept-Encoding: gzip" http://localhost:3000/data

你会发现响应是压缩过的,并且体积比未压缩的响应要小得多。

  • 客户端支持:客户端必须支持 gzip 或其他压缩算法,并在请求头中包含 Accept-Encoding: gzip
  • 响应大小compression 模块会自动处理响应大小。如果响应太小(默认阈值是 1KB),可能不会压缩,因为压缩小的响应可能得不偿失。
  • 性能:压缩和解压缩会消耗 CPU 资源,因此需要根据具体场景权衡性能和带宽的消耗。