HTTP中的gzip压缩

目前的HTTP服务器都能够支持对响应内容的gzip处理,即当请求头中含有Accept-Encoding: gzip时,会返回gzip压缩过的响应,并且响应头中包含Content-Encoding: gzip,例如以下的请求和响应头

GET / HTTP/1.1
User-Agent: curl/7.39.0
Host: localhost
Accept: */*
Accept-Encoding: gzip

HTTP/1.1 200 OK
Date: Fri, 08 Apr 2016 14:34:39 GMT
Server: Apache/2.4.18 (Unix) PHP/7.0.5
X-Powered-By: PHP/7.0.5
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 22578
Content-Type: text/html; charset=UTF-8

1. 对请求发送内容进行gzip

而有时候我们希望在请求中发送的内容也使用gzip压缩,虽然浏览器都不支持这一特性,但在手机App中可以使用gzip压缩提交的数据来提高速度也帮助用户节约数据流量。同时我们又希望这一压缩过程对于服务器端的代码是透明的,即Web服务器对收到的内容进行解压。

POST / HTTP/1.1
User-Agent: curl/7.39.0
Host: localhost
Accept: */*
Content-Encoding: gzip
Content-Length: 32
Content-Type: application/x-www-form-urlencoded

HTTP/1.1 200 OK
Date: Fri, 08 Apr 2016 14:43:09 GMT
Server: Apache/2.4.18 (Unix) PHP/7.0.5
X-Powered-By: PHP/7.0.5
Content-Length: 14
Content-Type: text/html; charset=UTF-8

在Apache中只需要增加SetInputFilter deflate,即可实现这一效果。而Nginx中并没有实现这一功能的模块,需要借助Lua来实现这一功能。 在需要使用请求压缩的位置增加以下配置

--开始处加入
init_by_lua '
	zlib = require("zlib");
';

--需要接受压缩请求的位置加入
rewrite_by_lua '
local content_encoding = ngx.req.get_headers()["Content-Encoding"];
if content_encoding == "gzip" then
    ngx.req.read_body();
    local data = ngx.req.get_body_data();
    if data ~= nil then
        local inflated = zlib.inflate()(data);
        ngx.req.clear_header("Content-Encoding");
        ngx.req.clear_header("Content-Length");
        ngx.req.set_body_data(inflated);
    end
end
';

另外需要对Lua的zlib模块进行简单的修改,当发送的内容不是gzip格式时,zlib模块会直接报错退出导致Nginx出现服务器错误,解决方式是取消模块中的报错退出

diff --git a/lua_zlib.c b/lua_zlib.c
index 84d1721..6ebfa47 100644
--- a/lua_zlib.c
+++ b/lua_zlib.c
@@ -92,7 +92,7 @@ static int lz_assert(lua_State *L, int result, const z_stream* stream, const cha
         lua_pushfstring(L, "ZLibError: unknown code %d (%s) at %s line %d",
                         result, stream->msg, file, line);
     }
-    lua_error(L);
+    //lua_error(L);
     return result;
 }

2. 缓存中对gzip的处理

在对HTTP内容的缓存过程中,对于压缩的处理方式一般是保存两份,或是保存一份不要缩的内容然后根据客户端的情况来选择进行压缩或者不进行压缩,然而如今一般的客户端都是支持压缩的,如果能够只缓存一份压缩的内容然后只对不支持压缩的客户端进行内容的解压缩,则能够节约不少的缓存空间。

传统的Nginx+Memcache缓存的配置如下,其中通过gzip hack强制后段换回不压缩的内容,然后再根据客户端的情况进行压缩

location / {
    gzip on;
    set $key $host$request_uri;
    srcache_fetch GET /memc $key;
    srcache_store PUT /memc $key;
    proxy_set_header Accept-Encoding "";
    proxy_pass http://127.0.0.1:8080;
}   

location /memc {
    internal;
    memc_connect_timeout 100ms;
    memc_send_timeout 100ms;
    memc_read_timeout 100ms;
    memc_ignore_client_abort on; 
    set $memc_key $query_string;
    set $memc_exptime 300;
    memc_pass 127.0.0.1:11211;
}

而实际可以通过Nginx gunzip模块实现根据客户端的情况将保存的压缩内容进行解压处理,其配置和上面非常相似,具体如下

location / {
    gunzip on;
    set $key $host$request_uri;
    srcache_fetch GET /memc $key;
    srcache_store PUT /memc $key;
    proxy_set_header Accept-Encoding "gzip";
    proxy_pass http://127.0.0.1:8080;
}   

location /memc {
    internal;
    memc_connect_timeout 100ms;
    memc_send_timeout 100ms;
    memc_read_timeout 100ms;
    memc_ignore_client_abort on; 
    set $memc_key $query_string;
    set $memc_exptime 300;
    memc_pass 127.0.0.1:11211;
}

这里还需要处理一个问题,srcache模块对于返回带有Content-Encoding响应头的内容会强制不予缓存,需要在其代码中去掉这一逻辑

diff --git a/src/ngx_http_srcache_store.c b/src/ngx_http_srcache_store.c
index ed96ff2..27f78db 100644
--- a/src/ngx_http_srcache_store.c
+++ b/src/ngx_http_srcache_store.c
@@ -150,7 +150,7 @@ ngx_http_srcache_header_filter(ngx_http_request_t *r)
         return ngx_http_srcache_next_header_filter(r);
     }
 #endif
-
+    /*
     if (!slcf->ignore_content_encoding
         && r->headers_out.content_encoding
         && r->headers_out.content_encoding->value.len)
@@ -162,7 +162,7 @@ ngx_http_srcache_header_filter(ngx_http_request_t *r)
                       &r->headers_out.content_encoding->value);

         return ngx_http_srcache_next_header_filter(r);
-    }
+    }*/

     if (slcf->resp_cache_control
         && ngx_http_srcache_response_no_cache(r, slcf, ctx) == NGX_OK)