浙江省高等学校教师教育理论培训

微信搜索“毛凌志岗前心得”小程序

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

gzip 与 deflate :gzip算法原理深入分析 -

gzip 与 deflate :gzip算法原理深入分析

  [复制链接]
  

4

主题

1

听众

35

积分

官方团队

Rank: 12Rank: 12Rank: 12

Q豆
22
Q币
0
注册时间
2011-4-22
分享
0
积分
35
UID
198
跳转到指定楼层
楼主
发表于 2011-4-22 17:05:15 |只看该作者 |倒序浏览
本帖最后由 verohan 于 2011-4-22 17:05 编辑

deflate与gzip解压的代码几乎相同,应该可以合成一块代码。
区别仅有:

  • deflate使用inflateInit(),而gzip使用inflateInit2()进行初始化,比 inflateInit()多一个参数: -MAX_WBITS,表示处理raw deflate数据。因为gzip数据中的zlib压缩数据块没有zlib header的两个字节。使用inflateInit2时要求zlib库忽略zlib header。在zlib手册中要求windowBits为8..15,但是实际上其它范围的数据有特殊作用,见zlib.h中的注释,如负数表示raw deflate。
  • Apache的deflate变种可能也没有zlib header,需要添加假头后处理。即MS的错误deflate (raw deflate).zlib头第1字节一般是0×78, 第2字节与第一字节合起来的双字节应能被31整除,详见rfc1950。例如Firefox的zlib假头为0×7801,python zlib.compress()结果头部为0x789c。

再去检查 zlib.h 中的注释说明,在 zlib-1.2.3/zlib.h Line 500 的地方发现这样一段话:

The windowBits parameter is the base two logarithm of the window size
(the size of the history buffer). It should be in the range 8..15 for this
version of the library. Larger values of this parameter result in better
compression at the expense of memory usage. The default value is 15 if
deflateInit is used instead.
windowBits can also be -8..-15 for raw deflate. In this case, -windowBits
determines the window size. deflate() will then generate raw deflate data
with no zlib header or trailer, and will not compute an adler32 check value.
windowBits can also be greater than 15 for optional gzip encoding. Add
16 to windowBits to write a simple gzip header and trailer around the
compressed data instead of a zlib wrapper. The gzip header will have no
file name, no extra data, no comment, no modification time (set to zero),
no header crc, and the operating system will be set to 255 (unknown).  If a
gzip stream is being written, strm->adler is a crc32 instead of an adler32.
回过头来看 nginx 和 apache 的实现:
nginx-0.6.34/src/http/modules/ngx_http_gzip_filter_module.c Line 335:
rc = deflateInit2(&ctx->zstream, (int) conf->level, Z_DEFLATED,
-wbits, memlevel, Z_DEFAULT_STRATEGY);
httpd-2.0.63/modules/filters/mod_deflate.c Line 374:
zRC = deflateInit2(&ctx->stream, c->compressionlevel, Z_DEFLATED,
c->windowSize, c->memlevel,
Z_DEFAULT_STRATEGY);
(Line 153: c->windowSize = i * -1; )
也就是说,nginx 和 apache 在程序里处理的都是 raw deflate data ,windowBits 都是负数,那为什么 Content-Encoding 都写的是 gzip 而不是 deflate 呢?
在 apache 的 mod_deflate.c 里,首先发现了这样一个写入 gzip header 的动作:
       /* RFC 1952 Section 2.3 dictates the gzip header:
*
* +—+—+—+—+—+—+—+—+—+—+
* |ID1|ID2|CM |FLG|     MTIME     |XFL|OS |
* +—+—+—+—+—+—+—+—+—+—+
*
* If we wish to populate in MTIME (as hinted in RFC 1952), do:
* putLong(date_array, apr_time_now() / APR_USEC_PER_SEC);
* where date_array is a char[4] and then print date_array in the
* MTIME position.  WARNING: ENDIANNESS ISSUE HERE.
*/
buf = apr_psprintf(r->pool, “%c%c%c%c%c%c%c%c%c%c”, deflate_magic[0],
deflate_magic[1], Z_DEFLATED, 0 /* flags */,
0, 0, 0, 0 /* 4 chars for mtime */,
0 /* xflags */, OS_CODE);
deflate_magic 是这样定义的:
/* magic header */
static char deflate_magic[2] = { ‘\037′, ‘\213′ };
而 OS_CODE 是在 zutil.h 里定义的,AMIGA 是 1,VAXC 是 2,OS2 是 6,WIN32 是 11,默认 unix 是 3(从这个顺序也可以看出操作系统的发展历史了)
数一下,10个字节,再联想到老大说的 18 个字节,仔细找找,终于在 Line 462 里发现这样一个附加 tail 的动作:
buf = apr_palloc(r->pool, 8);
putLong((unsigned char *)&buf[0], ctx->crc);
putLong((unsigned char *)&buf[4], ctx->stream.total_in);
b = apr_bucket_pool_create(buf, 8, r->pool, f->c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(ctx->bb, b);
不多不少,8个字节。10个字节的头加上 8 个字节的尾巴,就是老大说的多出来的 18 个字节。apache 调用 zlib 的接口产生了 raw defalte 的数据,然后手工的添加了 gzip 头和尾。
同样的,在 nginx 的 ngx_http_gzip_filter_module.c 首先在 Line 179看到 Igor Sysoev 同学很不负责任的定义了这样一个 gzip header:
static u_char  gzheader[10] = { 0x1f, 0x8b, Z_DEFLATED, 0, 0, 0, 0, 0, 0, 3 };
仔细看最后一位!居然直接写了一个 3 !这会不会导致 windows 上编译的 nginx 在输出 gzip 压缩过的页面的时候,客户端解压不正常?回头有空再去看看 zlib 里关于解压的算法代码中,对于这个 OS_CODE 是怎么处理的吧。
继续寻找,在 Line 351 的地方,作者还写了一段注释(虽然我越看越不明白他在试图表达什么意思):
        b->memory = 1;
b->pos = gzheader;
b->last = b->pos + 10;
out.buf = b;
out.next = NULL;
/*
* We pass the gzheader to the next filter now to avoid its linking
* to the ctx->busy chain.  zlib does not usually output the compressed
* data in the initial iterations, so the gzheader that was linked
* to the ctx->busy chain would be flushed by ngx_http_write_filter().
*/
大致是说把 gzheader 传给下一个 filter 去处理,这个 filter 只出来 raw deflate 数据,以及附加的 tail 吧。在 Line 605 的地方:
#if (NGX_HAVE_LITTLE_ENDIAN && NGX_HAVE_NONALIGNED)
trailer->crc32 = ctx->crc32;
trailer->zlen = ctx->zin;
#else
trailer->crc32[0] = (u_char) (ctx->crc32 & 0xff);
trailer->crc32[1] = (u_char) ((ctx->crc32 >>  & 0xff);
trailer->crc32[2] = (u_char) ((ctx->crc32 >> 16) & 0xff);
trailer->crc32[3] = (u_char) ((ctx->crc32 >> 24) & 0xff);
trailer->zlen[0] = (u_char) (ctx->zin & 0xff);
trailer->zlen[1] = (u_char) ((ctx->zin >>  & 0xff);
trailer->zlen[2] = (u_char) ((ctx->zin >> 16) & 0xff);
trailer->zlen[3] = (u_char) ((ctx->zin >> 24) & 0xff);
#endif
幸亏有 IBM MOTOROLA  们造了 Big Endian 机器,这样一来,这段代码的意思再明白不过了。
连上网搜资料加读代码,一共花了大约3个小时,到现在,大约清楚了这么几个问题:



  • deflate 是最基础的算法,在 zlib 里面有实现
  • gzip 在 deflate 的 raw data 前增加了 10 个字节的 gzheader,尾部添加了 8 个字节的校验字节(可选 crc32 和 adler32) 和长度标识字节,gzip 的 magic number 是 0x1f, 0x8b
  • zlib 自己也有 header 和尾部校验的数据,如果使用 deflateInit 而不是 deflateInit2,或者 windowBits 设置为正数8~15的话
  • zlib windowBits 设置为 16 第4位设置为1(即在原来值的基础上加16,感谢 antonio 同学的更正) 的时候,zlib 自己会产生一个 gzip 的头和尾,这种情况下 OS_CODE 被设置为 255(unknown),尾部校验使用 crc32 。问题是,既然 zlib 本身就提供了这种功能,为什么 apache 和 nginx 不用,反而都选择手工添加呢?
  • 为 nginx 添加 deflate 支持,只需要把输出中的头,尾去掉,并把 Content-Encoding 改为 deflate 即可。18 个字节,就这样省下来了。
  • 继续为 nginx 增加 deflate 压缩支持。公司居然用的是 nginx 0.7.33 的最新开发版本,虽然不时的有 502 bad gateway 出现,但老高不以为然。打开0.7.33 代码一看,比 0.6 版本的整洁了许多,gzip 添加头,尾的动作都被封装到了单独的函数中了,再也不是一个大函数从头写到尾了,有进步。
    起初是想为 deflate 压缩单独写一个与 gzip 平行的模块,拿原先 gzip 模块的 c 文件(src/http/modules/ngx_http_gzip_filter_module.c)一通 “搜索”-“替换”,编译通过了,但新模块死活没有被调用。想想也是,http 请求头的处理不在这个 c 文件里面,只改这个文件,当然不会有效果了。
    接下来就把原来的 gzip c 文件一通猛改,添加头的函数直接 return,添加尾的函数也去掉具体添加的动作,最后再把 Content-Encoding 改过来,一测试,呵呵,还真的省下来 18 个字节!
    但这样以来 gzip 就不支持了。更严重的是,如果(虽然可能性比较小),一个客户端只支持 gzip,不支持 deflate,那么它就无法解析请求的结果。在查看 src/http/ngx_http_core_module.c 里的 ngx_http_gzip_ok 函数的时候,终于发现了对于客户端提交的 header 里面的 accept encoding 的判断处理:
    ngx_strcasestrn(r->headers_in.accept_encoding->value.data,
    “gzip”, 4 – 1) == NULL
    而 ngx_http_request_t *r 几乎在每一个函数里都是可用的。为了尽量减少改动的文件数量和代码处,也是为了尽量保险起见,我选择了在 src/http/modules/ngx_http_gzip_filter_module.c 文件里每一个改动的地方都做一次这样的判断:客户端是否支持 deflate,如果支持,则按 deflate 的做法进行修改,如果不支持,则保持原来 gzip 的样子。
    编译,测试,ubuntu 8.10 + firefox 3.0.4 ,httpfox 下测试通过。
    patchhttp://code.google.com/p/fulin/s ... .7.33.deflate.patch
    效果:
    浏览器 Accept-Encoding:
    gzip:使用 gzip
    deflate:使用 deflate
    gzip,deflate:使用 deflate
    无:不压缩
    nginx 版本:nginx-0.7.33
    patch 使用方法:将 patch 文件放置于与 nginx-0.7.33 目录同级的地方,使用命令:
    patch -p0 < nginx.deflate.patch
    然后按正常流程 configure,make,make install
posted on 2013-03-31 13:41  lexus  阅读(2221)  评论(0编辑  收藏  举报