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常用参数说明
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常见的请求方法说明
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的元素名及其用途
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个。