Nginx与Gzip请求

天,移动端的同事跑来问:某些API需要传输大数据,Nginx服务器能否支持Gzip请求?一方面可以节省移动端流量;另一方面还可以加快传输速度,提升用户体验。对于Apache来说,利用SetInputFilter,可以很轻松的实现这个功能,那么Nginx如何做呢?

  既然移动端发送的是Gzip请求,自然需要想想如何在服务端解压缩。搜索一下现成的Nginx的模块,发现和Gzip相关的模块有如下几个:

  •   Gzip: Gzip responses.

  •   Gzip Precompression: Serves precompressed versions of static files.

  •   Gunzip: On-the-fly decompressing of gzipped responses.

      可惜它们都是和Response相关的Gzip,而我们需要的是和Request相关的Gzip。

      在我们的实际情况里,很多接口都是用PHP做的,于是自然想到用PHP的gzdecode方法来解压缩Gzip请求,不过最终出于效率的担心放弃了。

      每当我遇到难题的时候就会想起lua-nginx-module,它总是能屡建奇功,这次自然也不例外,仔细搜索了一下OpenResty社区,发现有人遇到了同样的问题,春哥在讨论中给出了建议,不过并没有涉及具体的实现逻辑,于是我查了资料总结了一下。

    方案

      第一个选择是使用lua-zlib:

    local zlib = require "zlib"
    
    local encoding = ngx.req.get_headers()["Content-Encoding"]
    
    if encoding == "gzip" then
        local body = ngx.req.get_body_data()
    
        if body then
            local stream = zlib.inflate()
            ngx.req.set_body_data(stream(body))
        end
    end

      第二个选择是通过LuaJIT的FFI库来包装ZLIB模块,官方教程里有一些现成的可供参考的的例子,不过例子里介绍的是Deflate,而不是Gzip,自己用FFI封装Gzip的话又有点小复杂,好在别人已经做了相关的工作,那就是lua-files:

    local ffi  = require "ffi"
    local zlib = require "zlib"
    
    local function reader(s)
        local done
        return function()
            if done then return end
            done = true
            return s
        end
    end
    
    local function writer()
        local t = {}
        return function(data, sz)
            if not data then return table.concat(t) end
            t[#t + 1] = ffi.string(data, sz)
        end
    end
    
    local encoding = ngx.req.get_headers()["Content-Encoding"]
    
    if encoding == "gzip" then
        local body = ngx.req.get_body_data()
    
        if body then
            local write = writer()
            zlib.inflate(reader(body), write, nil, "gzip")
            ngx.req.set_body_data(write())
        end
    end

      如上例子代码源自zlib_test.lua,乍看上去,代码里的reader和writer可能会令人费解,其实你可以把它们理解成输入输出接口,可以修改成文件,数据库等等形式。

      别高兴太早,当你运行时,很可能会遇到如下错误:

      libzlib.so: cannot open shared object file.

      实际上这是因为如下zlib.lua代码的缘故:

    local C = ffi.load 'zlib'

      运行时,ffi.load会自动补全文件名,如果是Windows,则加载zlib.dll文件,如果是Linux,则加载libzlib.so,但实际上在Linux下,ZLIB扩展的名字是libz.so,而非libzlib.so。

      知道的问题的原委,我们自然就知道如何修改代码了:

    local C
    
    if ffi.os == "Windows" then
        C = ffi.load "zlib"
    else
        C = ffi.load "z"
    end

      有时候我们不推荐直接修改第三方库的代码,因为这样的话,每次第三库更新代码,我们都要做对应的修改,一旦忘记就会出错,这时候可以考虑做一个软连接别名。

    测试

      开篇说过,接口都是用PHP做的,不过请求里的Gzip数据是用LUA处理的,如何让PHP使用LUA处理后的数据呢?不同的语言似乎是个难题,好在Nginx有Phases一说,PHP作为FastCGI模块工作在content阶段,LUA可以工作在access阶段,这样它们就和谐了:

    location ~ \.php$ {
        access_by_lua_file /path/to/lua/file;
    
        include fastcgi.conf;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
    }

      那么lua-zlib和lua-files两种方案效率如何?下面是我用PHP写的测试脚本:

    <?php
    
    $url = 'http://url';
    
    $header = implode("\r\n", array(
        'Content-Type: application/x-www-form-urlencoded',
        'Content-Encoding: gzip',
        'Connection: close',
    ));
    
    $content = gzencode(http_build_query(
        'foo' => str_repeat('x', 100),
        'bar' => str_repeat('y', 100),
    ));
    
    $options = array(
        'http' => array(
            'protocol_version' => '1.1',
            'method' => 'POST',
            'header' => $header,
            'content' => $content,
        ),
    );
    
    $context = stream_context_create($options);
    
    for ($i = 0; $i < 1000; $i++) {
        file_get_contents($url, false, $context);
    }
    
    ?>

      很多人写测试脚本的时候,喜欢在开始结束部分加上时间,这样相减就得到了代码实际运行的时间,其实这是不必要的,利用Linux自带的time就可以获取运行时间:

    shell> time php /path/to/php/file

      按春哥说的,FFI应该更高效,不过从我的测试结果看,lua-zlib比lua-files更快一些。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值