Lua-Nginx-Module常用指令(中)

九、控制響應頭

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個。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章