九、控制响应头

HTTP响应头需要配置很多重要的信息,例如添加CDN缓存时间、操作set-cookie、标记业务数据类型等。利用Lua的API可以轻松完成这些配置,并且它有丰富的模块可供选择。

9.1 获取响应头

ngx.resp.get_headers

语法:headers = ngx.resp.get_headers(max_headers?, raw?)
配置环境:set_by_lua,rewrite_by_lua,access_by_lua,content_by_lua,headerfilter by_lua,body_filter_by_lua,log_by_lua,balancer_by_lua

含义:读取当前请求的响应头,并返回一个Lua的table类型的数据。

示例:

server {
    listen       80;
    server_name  testnginx.com;
     location  / {              
        content_by_lua_block { 
           local ngx = require "ngx";
           local h = ngx.resp.get_headers()
           for k, v in pairs(h) do
               ngx.say('Header name: ',k, ' value: ',v)
           end    
           --因为是table,所以可以使用下面的方式读取单个响应头的值
           ngx.say(h["content-type"])   
        }
    }
}

执行结果如下:

# curl -i 'ttp://testnginx.com/test?=12132&a=2&b=c&dd'
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Fri, 08 Jun 2018 07:36:35 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

Header name:content-type value: application/octet-stream
Header name:connection value: keep-alive
application/octet-stream

9.2 修改响应头

ngx.header.HEADER

语法:ngx.header.HEADER = VALUE

语法:value = ngx.header.HEADER

配置环境:rewrite_by_lua,access_by_lua,content_by_lua,header_filter_by_lua,body_filter_by_lua,log_by_lua

含义:对响应头进行修改、清除、添加等操作。
此API在输出响应头时,默认会将下划线替换成中横线,示例如下:

server {
    listen       80;
    server_name  testnginx.com;

    location / {
        content_by_lua_block {
            local ngx = require "ngx"
            ngx.header.content_type = 'text/plain';
            --在代码里面是下划线,输出时就变成中横线了
            ngx.header.Test_Nginx = 'Lua';
            --下面的代码等同于ngx.header.A_Ver = 'aaa' 
            ngx.header["A_Ver"] = 'aaa';
            --读取响应头,并赋值给变量a
            local a = ngx.header.Test_Nginx;
        }
    }
}

执行代码,下划线都被替换成了中横线,如下所示:

# curl -i 'http://testnginx.com/?test=12132&a=2&b=c&dd'
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Fri, 08 Jun 2018 03:18:16 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive
test-type: ttt
Test-Nginx: Lua
A-Ver: aaa

有时需要在一个响应头中存放多个值,例如,当访问/test 路径时,需要为set-cookie设置两个Cookie:

location = /test {
    content_by_lua_block {
       local ngx = require "ngx"
       --以逗号分隔两个Cookie
       ngx.header['Set-Cookie'] = {'test1=1; path=/test', 'test2=2; path=/test'}
    }
}

输出结果如下:

# curl -i 'http://testnginx.com/test?=12132&a=2&b=c&dd'
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Fri, 08 Jun 2018 03:21:59 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive
Set-Cookie: test1=1; path=/test
Set-Cookie: test2=2; path=/test

9.3 清除响应头
如果需要清除一个响应头,将它赋值为nil即可,如下所示:

ngx.header["X-Test"] = nil;

十、读取请求体

$request_body表示请求体被读取到内存中的数据,一般由proxy_pass、fastcgi_pass、uwsgi_pass和scgi_pass等指令进行处理。由于Nginx默认不读取请求体的数据,所以当Lua通过ngx.var.request_body的方式获取请求体时会发现数据为空。那么,该如何获得请求体的数据呢?下面将介绍几种可行的方式。

10.1 强制获取请求体

lua_need_request_body

语法:lua_need_request_body <on|off>

默认:off

配置环境:http,server,location,location if

含义:默认为off,即不读取请求体。如果设置为on,则表示强制读取请求体,此时,可以通过ngx.var.request_body来获取请求体的数据。但需要注意一种情况,$request_body存在于内存中,如果它的字节大小超过Nginx配置的client_body_buffer_size的值,Nginx就会把请求体存放到临时文件中,此时数据就不在内存中了,这会导致$request_body为空,所以需要设置client_body_buffer_size和client_max_body_size的值相同,避免出现这种情况。
这种配置方式不够灵活,Ngx_lua官网也不推荐使用此方法。下面将介绍一种更合适的方式去获取请求体的数据。

10.2 用同步非阻塞方式获取请求体

ngx.req.read_body

语法:ngx.req.read_body()

环境:rewrite_by_lua,access_by_lua,content_by_lua*

含义:同步读取客户端请求体,且不会阻塞Nginx的事件循环。使用此指令后,就可以通过ngx.req.get_body_data来获取请求体的数据了。但如果是使用临时文件来存放请求体的话,就需要先使用函数ngx.req.get_body_file来获取临时文件名,再去读取临时文件中的请求体数据了。

ngx.req.get_body_data
语法:data = ngx.req.get_body_data()
配置环境:rewrite_by_lua,access_by_lua,content_by_lua,log_by_lua
含义:执行ngx.req.read_body指令后,可以使用本指令在内存中获取请求体数据,结果会返回一个Lua的字符串类型的数据。如果要获取Lua 的table类型的数据,则需要使用ngx.req.get_post_args。

ngx.req.get_post_args
语法: args, err = ngx.req.get_post_args(max_args?)
配置环境:rewrite_by_lua,access_by_lua,content_by_lua,header_filter_by_lua,body_filter_by_lua,log_by_lua
含义:在执行ngx.req.read_body指令后,可以使用本指令读取包含当前请求在内的所有POST请求的查询参数,返回一个Lua的table类型。max_args参数的作用是限制参数的数量,为了服务的安全,最多支持使用100个参数(包括重复的参数),超过限制的参数会被忽略。如果max_args为0,则表示关闭此限制,但为了避免被无穷多的参数***,不要设置max_args为0。如果最多支持使用10个参数,则应配置为ngx.req.get_post_args(10)。

ngx.req.get_body_file
语法:file_name = ngx.req.get_body_file()
配置环境:rewrite_by_lua,access_by_lua,content_by_lua*
含义:在执行ngx.req.read_body指令后,可以使用本指令获取存放请求体的临时文件名(绝对路径),如果请求体被存放在内存中,获取的值就是nil。通过本指令获取的文件是只读的,不可以被修改,且会在被Nginx读取后被删除掉。

10.3 使用场景示例

下面将对这些指令的使用方式和使用场景进行展示。
获取string类型的请求体
要获取string类型的请求体,可以使用如下配置:

server {
    listen       80;
    server_name  testnginx.com;        
    location / {
      client_max_body_size 10k;
      client_body_buffer_size 1k;

      content_by_lua_block { 
       local ngx = require "ngx"
         --开启读取请求体模式
         ngx.req.read_body()
         --获取内存中的请求体
         local data = ngx.req.get_body_data()
         if data then
             ngx.print('ngx.req.get_body_data: ',data, ' ---- type is ', type(data))
             return
         else
         --如果没有获取到内存中的请求体数据,则去临时文件中读取
             local file = ngx.req.get_body_file()
             if file then
                 ngx.say("body is in file ", file)
             else
                 ngx.say("no body found")
             end
         end
      }
    }

配置好后,重载Nginx配置(重载是指使用HUP信号或reload命令来重新加载配置),先用一个小于1KB的请求体(在Nginx配置中设置client_body_buffer_size为1k)执行请求,输出的是string字符串类型,如下所示:

# curl -i http://testnginx.com/ -d 'test=12132&a=2&b=c&dd'
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Wed, 06 Jun 2018 11:03:35 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

ngx.req.get_body_data: test=12132&a=2&b=c&dd ---- type is string

获取table类型的请求体
要获取table类型的请求体,可以使用如下配置:

server {
    listen       80;
    server_name  testnginx.com;

    location / {
      client_max_body_size 10k;
      client_body_buffer_size 1k;

      content_by_lua_block {
         --开启读取请求体模式
         ngx.req.read_body()
         -- 获取内存中的请求体,返回的结果是Lua的table类型的数据
         local args, err = ngx.req.get_post_args()
         if args then
            for k, v in pairs(args) do
                if type(v) == "table" then
                    --如果存在相同的参数名,就会将相同的参数并列在一起,以逗号分隔
                    ngx.say(k, ": ", table.concat(v, ", "))
                else
                    ngx.say(k, ": ", v)
                end
             end
         else
             --如果没有获取到内存中的请求体数据,则去临时文件中读取
             local file = ngx.req.get_body_file()
             if file then
                 ngx.say("body is in file ", file)
             else
                 ngx.say("no body found")
             end
         end
     }
    }
}

发送测试请求,其中a参数有2个,c参数值为空,d参数连等号都没有。执行结果如下所示:

#  curl -i http://testnginx.com/ -d 'test=12132&a=2&b=c&dd=1&a=354&c=&d'

b: c
dd: 1
d: true
c: 
test: 12132
a: 2, 354

可以看到参数a的两个值并列显示,并以逗号分隔,参数c显示为空,参数d的结果为布尔值true。
获取临时文件中的请求体
如果使用一个大小在1KB~10KB之间的请求体,会发生什么呢?测试执行结果如下:

# curl -i http://testnginx.com/ -d 'test=12132&a=2&b=kls204120312saldkk12 easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2je204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2je204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2je204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jesk20312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2je204120312saldkk12easjdiasasd3ej12i3j12io3jeioq2jeskls204120312saldkk12easjdiasasd3ej11'
HTTP/1.1 100 Continue

HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Wed, 06 Jun 2018 10:14:32 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

body is in file /usr/local/nginx_1.12.2/client_body_temp/0000000051

因为请求体数据的大小大于client_body_buffer_size的值,所以使用了临时文件存储请求体的数据。因此,需要先获取存放数据的临时文件名,再去读取请求体数据。

注意:读取临时文件中的请求体数据是不被推荐的,因此本书不对相关操作进行,有兴趣的读者可以使用io.open完成读取。

10.4 使用建议
在实际应用中,关于读取请求体,有如下几条建议。
1.尽量不要使用lua_need_request_body去获取请求体。
2.获取请求体前,必须执行ngx.req.read_body()。
3.获取请求体数据时尽量不要使用硬盘上的临时文件,否则会对性能有很大影响;务必要确认请求体数字字节大小的范围,并确保client_body_buffer_size和client_max_body_size的值一致,这样只需到内存中去读取数据就可以了。它既提高了Nginx自身的吞吐能力,也提升了Lua的读取性能。
4.如果请求体存放在临时文件中,Nginx会在处理完请求后自动清理临时文件。
5.对ngx.req.get_post_args参数的限制可以灵活控制,但不能关闭限制,以避免被恶意***。

十一、输出响应体

在Lua中,响应体的输出可以使用ngx.print 和 ngx.say 这两个指令完成。

11.1 异步发送响应体
ngx.print
语法:ok, err = ngx.print(...)
配置环境:rewrite_by_lua,access_by_lua,content_by_lua*
含义:用来输出内容,输出的内容会和其他的输出合并,然后再发送给客户端。如果响应头还未发送的话,发送前会优先将响应头发送出去。
示例:

location  / {

    content_by_lua_block { 
        local ngx = require "ngx";
        local h = ngx.req.get_headers()
        for k, v in pairs(h) do
            ngx.print('Header name: ',k, ' value: ',v)
        end

    }
}

执行结果如下(所有的数据会合并到一起进行发送):

# curl -i 'http://testnginx.com/test?=12132&a=2&b=c&dd'
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Fri, 08 Jun 2018 08:11:40 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

Header name:host value: testnginx.comHeader name:accept value: */*Header name:user-agent value: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.19.1 Basic ECC zlib/1.2.3 libidn/1.18 libssh2/1.4.

ngx.say
语法:ok, err = ngx.say(...)
配置环境:rewrite_by_lua,access_by_lua,content_by_lua*
含义:功能和ngx.print一样,只是输出结果多了1个回车符。

11.2 同步发送响应体
ngx.print和ngx.say为异步调用,执行后并不会立即输出响应体,可以通过执行ngx.flush(true)来实现同步输出响应体的功能。

ngx.flush
语法:ok, err = ngx.flush(wait?)
配置环境:rewrite_by_lua,access_by_lua,content_by_lua*
含义:在默认情况下会发起一个异步调用,即不等后续的数据到达缓冲区就会直接将内容输出到客户端。如果将wait的参数值设置为true,表示同步执行,即会等内容全部输出到缓冲区后再输出到客户端。

server {
    listen       80;
    server_name  testnginx.com;
    default_type 'text/plain';
    location /test1 {
        content_by_lua_block {
           ngx.say("test ")
           ngx.say("nginx ")
           ngx.sleep(3)
           ngx.say("ok!")
           ngx.say("666!")
        }
    }

    location /test2 {
        content_by_lua_block {
           ngx.say("test ")
           ngx.say("nginx ")
           ngx.flush(true)
           ngx.sleep(3)
           ngx.say("ok!")
           ngx.say("666!")
        }
    }

}

访问/test1 和 /test2后,从执行结果可以看出,带有ngx.flush(true) 指令的内容会先输出test nginx,然后,等待大约3秒后再输出ok! 666!。如果没有配置ngx.flush(true)指令,请求会在等待3秒后输出完整的一句话。
注意:指令ngx.flush不支持HTTP1.0,可以使用如下方式进行测试:
# curl -i 'http://testnginx.com/test2' --http1.0

十二、正则表达式

虽然Lua支持正则匹配且功能齐全,但在Nginx上推荐使用Lua-lua提供的指令。
12.1 单一捕获

ngx.re.match
语法:captures, err = ngx.re.match(subject, regex, options?, ctx?, res_table?)
配置环境:init_worker_by_lua,set_by_lua,rewrite_by_lua,access_by_lua,content_ by_lua,header_filter_by_lua,body_filter_by_lua,log_by_lua,ngx.timer.,balancerby lua,ssl_certificate_by_lua,ssl_session_fetch_by_lua,ssl_session_store_by_lua*
含义:使用Perl兼容的正则表达式来匹配subject参数,只返回匹配到的第一个结果。如果匹配失败,则返回nil;如果有异常,则返回nil和一个描述错误信息的err。
示例:

location / {
    content_by_lua_block {
        local ngx = require "ngx";
        --匹配多个数字+aaa的正则表达式
    local m, err = ngx.re.match(ngx.var.uri, "([0-9]+)(aaa)");
        if m then
           --匹配成功后输出的信息
           ngx.say(ngx.var.uri, '---match success---', 'its type: ',type(m))
           ngx.say(ngx.var.uri, '---m[0]--- ', m[0])
           ngx.say(ngx.var.uri, '---m[1]--- ', m[1])
           ngx.say(ngx.var.uri, '---m[2]--- ', m[2])
        else
           if err then
               ngx.log(ngx.ERR, "error: ", err)
               return
           end
           ngx.say("match not found")
        end

    }
}

执行结果如下:

# curl  'http://testnginx.com/test/a123aaa/b456aaa/c'
/test/a123aaa/b456aaa/c---match success---its type: table
/test/a123aaa/b456aaa/c---m[0]---123aaa
/test/a123aaa/b456aaa/c---m[1]---123
/test/a123aaa/b456aaa/c---m[2]---aaa

从执行结果可以看出:
1.ngx.re.match只返回匹配到的第一个结果,所以后面的456aaa并没有被输出。
2.ngx.re.match返回的结果是table类型的。
3.ngx.re.match匹配成功后,m[0] 的值是匹配到的完整数据,而m[1]、m[2] 是被包含在括号内的单个匹配结果。

12.2 全部捕获
ngx.re.match只返回第一次匹配成功的数据,如果想获取所有符合正则表达式的数据,可以使用ngx.re.gmatch。
ngx.re.gmatch
语法:iterator, err = ngx.re.gmatch(subject, regex, options?)
配置环境:init_worker_by_lua,set_by_lua,rewrite_by_lua,access_by_lua,content_ by_lua,header_filter_by_lua,body_filter_by_lua,log_by_lua,ngx.timer.,balancerby lua,ssl_certificate_by_lua,ssl_session_fetch_by_lua,ssl_session_store_by_lua*
含义:和ngx.re.match功能相似,但返回的是一个Lua迭代器,可以通过迭代的方式获取匹配到的全部数据。

location / {
   content_by_lua_block {
      local ngx = require "ngx";
      --参数i表示忽略大小写
      local m_table, err = ngx.re.gmatch(ngx.var.uri, "([0-9]+)(aaa)", "i");
      if not m_table then
          ngx.log(ngx.ERR,  err)
          return
      end
      while true do
         local m, err = m_table()
         if err then
            ngx.log(ngx.ERR,  err)
            return
         end
         if not m then
              break
         end
         ngx.say(m[0])
         ngx.say(m[1])
      end

    }
}

执行结果如下:

# curl  'http://testnginx.com/test/a123aaa/b456AAA/c'
123aaa
123
456AAA
456

ngx.re.match和ngx.re.gmatch都有一个options参数,用来控制匹配的执行方式,options常用参数说明见表7-1。
表7-1 options常用参数说明
Lua-Nginx-Module常用指令(中)
12.3 更高效的匹配和捕获

ngx.re.match和ngx.re.gmatch在使用过程中都会生成Lua table,如果只需确认正则表达式是否可以匹配成功,推荐使用如下指令。

ngx.re.find
语法:from, to, err = ngx.re.find(subject, regex, options?, ctx?, nth?)
配置环境:init_worker_by_lua,set_by_lua,rewrite_by_lua,access_by_lua,content_ by_lua,header_filter_by_lua,body_filter_by_lua,log_by_lua,ngx.timer.,balancerby lua,ssl_certificate_by_lua,ssl_session_fetch_by _lua,ssl_session_store_by_lua*
含义:与ngx.re.match类似,但只返回匹配结果的开始位置索引和结束位置索引。
因为ngx.re.find不会创建table来存储数据,所以性能上比ngx.re.match和ngx.re.gmatch要好很多。此时,如果需要捕获匹配到的数据,可以使用Lua的函数string.sub。

location / {
   content_by_lua_block {
      local ngx = require "ngx";
      local uri = ngx.var.uri
      --使用o、j两个参数进行匹配,以提升性能
      local find_begin,find_end,err = ngx.re.find(uri, "([0-9]+)(aaa)","oj");
      if find_begin then
          ngx.say('begin: ',find_begin)
          ngx.say('end: ',find_end)
       --利用Lua的string.sub函数来获取数据
          ngx.say('find it: ' ,string.sub(uri, find_begin,find_end))
          return
      end
    }
}

执行结果如下:

# curl  'http://testnginx.com/test/a123aaa/b456AAAa/c'
begin:8
end:13
find it: 123aaa

ngx.re.match、ngx.re.gmatch和 ngx.re.find 都支持ctx参数,有关ctx参数的说明如下。
1.ctx是Lua table类型的,是可选的第4个参数,但若用到第5个参数nth,那么,此位置需要用nil作为占位符。
2.当ctx有值(键是pos,如pos=1)时,ngx.re.find将从pos位置开始进行匹配(位置的下标从1开始)。
3.无论ctx表中是否有值,ngx.re.find都会在正则表达式匹配成功后,将ctx值设置为所匹配字符串之后的位置;若匹配失败,ctx表将保持原有的状态。
nth是ngx.re.find的第5个参数,是在Lua-Nginx-Module 0.9.3版本之后新增加的参数,它的作用和ngx.re.match中的m[1]、m[2]类似。当nth等于1时,获取的结果等同于ngx.re.match中的m[1],示例如下:

location / {
   content_by_lua_block {
      local ngx = require "ngx";
      local uri = ngx.var.uri

      --从uri位置为10的地方开始进行匹配,下标默认从1开始,只匹配nth是1的数据,即([0-9]+)的值
      local ctx = { pos = 10 }
      local find_begin,find_end,err = ngx.re.find(uri, "([0-9]+)(aaa)","oji",ctx,1);
      if find_begin then
          ngx.say('begin: ',find_begin)
          ngx.say('end: ',find_end)
          ngx.say('find it: ' ,string.sub(uri, find_begin,find_end))
          return
      end
    }
}

执行结果如下:

# curl  'http://testnginx.com/test/a123aaa/b456AAAa/c'
begin:10
end:10
find it: 3

因为ctx的位置是10,所以uri前面的“/test/a12”这9个字符被忽略了,匹配到的就只有3aaa,又因为nth为1,所以捕获到的值是3。

12.4 替换数据
Lua API也支持匹配对应数据并对其进行替换的指令。
ngx.re.sub
语法:newstr, n, err = ngx.re.sub(subject, regex, replace, options?)
配置环境:init_worker_by_lua,set_by_lua,rewrite_by_lua,access_by_lua,content_ by_lua,header_filter_by_lua,body_filter_by_lua,log_by_lua,ngx.timer.,balancerby lua,ssl_certificate_by_lua,ssl_session_fetchby lua,ssl_session_store_by_lua*
含义:若subject中含有参数regex的值,则将之替换为参数replace的值。options为可选参数。替换后的内容将赋值给newstr,n表示匹配到的次数。
示例:

location / {
    content_by_lua_block {
        local ngx = require "ngx";
        local uri = ngx.var.uri
        local n_str, n, err = ngx.re.sub(uri,"([0-9]+)", 'zzzz')
        if n_str then
            ngx.say(uri)
            ngx.say(n_str)
            ngx.say(n)
        else
            ngx.log(ngx.ERR, "error: ", err)
            return
        end
    }
}

执行结果如下:

# curl   'http://testnginx.com/test188/x2/1231'
/test188/x2/1231
/testzzzz/x2/1231
1

从结果可以看出,只在第一次匹配成功时进行了替换操作,并且只替换了1次,所以n的结果是1。如果要替换匹配到的全部结果可以使用ngx.re.gsub,示例如下:

local n_str, n, err = ngx.re.gsub(uri,"([0-9]+)", 'zzzz')

从执行结果可知,替换了3次:

# curl   'http://testnginx.com/test188/x2/1231'
/test188/x2/1231
/testzzzz/xzzzz/zzzz
3

12.5 转义符号
正则表达式包括\d、\s、\w 等匹配方式,但在Ngx_Lua中使用时,反斜线 \ 会被Lua处理掉,从而导致匹配异常。所以需要对带有 \ 的字符进行转义,转义方式和其他语言有些区别,转义后的格式为\\d、\\s、\\w,因为反斜线会被Nginx和Lua各处理一次,所以\\会先变成\,再变成\。
还可以通过[[]]的方式将正则表达式直接传入匹配指令中,以避免被转义,如下所示:

local find_regex = [[\d+]]
local m = ngx.re.match("xxx,43", find_regex)
ngx.say(m[0])   --输出 43

通常建议使用[[]]的方式。

十三、子请求

Nginx一般分两种请求类型,一种是主请求;一种是子请求,即subrequest。主请求从Nginx的外部进行访问,而子请求则在Nginx内部进行访问。子请求不是HTTP请求,不会增加网络开销。它的主要作用是将一个主请求分解为多个子请求,用子请求去访问指定的location服务,最后汇总到一起完成主请求的任务。
Nginx的请求方法有很多种,如GET、POST、 PUT 、DELETE等,同样,子请求也支持这些请求方法。

13.1 请求方法
Lua API中提供了多个指令来实现子请求,Lua API常见的请求方法说明见表7-2。

表7-2 Lua API常见的请求方法说明
Lua-Nginx-Module常用指令(中)
13.2 单一子请求

ngx.location.capture
语法:res = ngx.location.capture(uri, options?)
配置环境:rewrite_by_lua,access_by_lua,content_by_lua*
含义:发出同步但不阻塞Nginx的子请求。可以用来访问指定的location,但不支持访问命名location(如@abc 就是命名location)。location中可以有静态文件,如ngx_proxy、ngx_fastcgi、ngx_memc、ngx_postgres、ngx_drizzle,甚至是Ngx_Lua和Nginx的c模块。
子请求总是会把整个请求体缓存到内存中,如果要处理一个较大的子请求,使用cosockets是最好的选择(cosockets是与ngx.socket.tcp有关的API)。
子请求一般在内部进行访问,建议在被子请求访问的location上配置internal,即只允许内部访问。
子请求返回的结果res,它是一个table类型的数据,包含4个元素:res.status、res.header、res.body和res.truncated,res的元素名及其用途见表7-3。

表7-3 res的元素名及其用途
Lua-Nginx-Module常用指令(中)
ngx.location.capture的第2个参数options是可选参数,也可以包含多个参数,示例如下:

server {
    listen       80;
    server_name  testnginx.com;
    default_type 'text/plain';

    location = /main {
        set $m 'hello';
        content_by_lua_block {
            local ngx = require "ngx";
            --发起子请求,访问/test,请求方式是GET,请求体是test nginx,子请求的URL参数是a=1&b=2,并使用copy_all_vars将主请求的Nginx变量($m)全部复制到子请求中
              local res = ngx.location.capture(
              '/test',  { method = ngx.HTTP_GET , body = 'test nginx',
               args = { a = 1, b = 2 },copy_all_vars = true }
            )
            ngx.say(res.status)
            ngx.say(res.body)
            ngx.say(type(res.header))
            ngx.say(type(res.truncated))
        }
    }
    location = /test
{
    #只能在Nginx内部进行访问 
        internal;
        content_by_lua_block {
            local ngx = require "ngx";
            --获取请求体,在这里是获取主请求的请求体
            ngx.req.read_body()
            local body_args = ngx.req.get_body_data() 
            --输出请求的参数,获取主请求的m变量的值,并与world进行字符串拼接
            ngx.print('request_body: ' ,body_args, ' capture_args: ', ngx.var.args, '---  copy_all_vars : ', ngx.var.m .. 'world! ')
        }
    }
}

执行结果如下:

# curl   'http://testnginx.com/main'
200
request_body:test nginx capture_args:a=1&b=2---  copy_all_vars : helloworld!
table
boolean

从示例中可以看出:

1.ngx.location.capture的第2个参数options可以包含多个table类型的参数。

2.子请求的请求方法由参数method进行配置,示例中的请求方法为GET。

3.子请求通过参数body可以定义新的请求体。

4.子请求通过参数args可以配置新的URL的args,args是table类型的。

5.copy_all_vars = true的作用是将主请求的全部变量传递给子请求,如果没有此配置就不会传递过去。

6.从子请求的返回结果可以获取状态码、响应体、响应头、结果是否被截断。
根据上面的介绍可知,下面两种方式是等价的:
local res = ngx.location.capture('/test?a=1&b=2')
local res = ngx.location.capture('/test , args = { a = 1, b = '2' }')
ngx.location.capture 还支持更丰富的参数操作,具体如下。

1.vars参数,table类型,可以设置子请求中的变量值,前提是该变量在Nginx中被声明过。如果配置copy_all_vars = true,且vars里有和主请求相同的变量,则会使用vars中变量的值;如果vars里是新变量,就会和主请求的变量一起传递过去。

2.share_all_vars参数,用来共享主请求和子请求的变量,如果在子请求中修改了共享变量的值,主请求的变量值也会被改变。不推荐使用此参数,因为可能会导致很多意外问题的出现。

3.always_forward_body参数,默认值为false,此时,如果不设置body参数,且请求方法是PUT或POST,则主请求的请求体可以传给子请求。如果把always_forward_body设置为 true,且不设置body参数,无论请求方法是什么,主请求的请求体都会传给子请求。

4.ctx参数,指定一个table作为子请求的ngx.ctx表,它可以使主请求和子请求共享请求头的上下文环境。

关于参数vars的使用方式,示例如下:

location = /main {
    set $m 'hello';
    set $mm '';
    content_by_lua_block {
        local ngx = require "ngx";
        local res = ngx.location.capture(
            '/test',
            { method = ngx.HTTP_POST ,
            vars = {mm = 'MMMMM',m = 'hhhh'}}
        )
        ngx.say(res.body)
    }
}
location = /test {
    content_by_lua_block {
        local ngx = require "ngx";
        ngx.print(ngx.var.m .. ngx.var.mm )
    }
}

执行结果如下:

# curl   'http://testnginx.com/main'
hhhhMMMMM

主请求的变量在子请求中被修改了,并传给了子请求指定的/test:
注意:使用ngx.location.capture发送子请求时,默认会将主请求的请求头全部传入子请求中,这可能会带来一些不必要的麻烦。例如,如果浏览器发送的压缩头Accept-Encoding:gzip被传入子请求中,且子请求是ngx_proxy的标准模块,则请求的结果会被压缩后再返回,导致Lua无法读取子请求返回的数据。因此应将子请求的 proxy_pass_request_headers设置为off,避免把请求头传递给后端服务器。

13.3 并发子请求

有时需要发送多条子请求去获取信息,这时,就要用到并发操作了。

ngx.location.capture_multi
语法:res1, res2, ... = ngx.location.capture_multi({ {uri, options?}, {uri, options?}, ... })
配置环境:rewrite_by_lua,access_by_lua,content_by_lua*
含义:与ngx.location.capture相似,但可以支持多个子请求并行访问,并按配置顺序返回数据。返回的数据也是多个结果集。
示例:

server {
    listen       80;
    server_name  testnginx.com;
    default_type 'text/plain';
    location = /main {
        set $m 'hello';
        set $mm '';
        content_by_lua_block {
            local ngx = require "ngx";
            --发送两个子请求,会返回两个结果集
            local res1, res2 = ngx.location.capture_multi{
                { "/test1?a=1&b=2" },
                { "/test2",{ method = ngx.HTTP_POST},body = "test nginx" },
            }
            --返回的body的方式和ngx.location.capture一样
            if res1.status == ngx.HTTP_OK then
                ngx.say(res1.body)
            end

            if res2.status == ngx.HTTP_OK then
                ngx.say(res2.body)
            end
        }
    }
    location = /test1 {
         echo 'test1';
    }
    location = /test2 {
         echo 'test2';
    }

}

执行结果如下:

# curl   'http://testnginx.com/main'
test1
test2

主请求需要等到所有的子请求都返回后才会结束子请求的执行,最慢的子请求的执行时间就是整体的消耗时间,所以在实际业务中需要对子请求的超时时间做好限制。
注意:Nginx对子请求有并发数量限制,目前Nginx 1.1以上的版本限制子请求并发数量为200个,老版本是50个。