ngx_lua學習筆記 -- capture + proxy 實現httpclient


題注

最近我在學習nginx的lua插件,發現結合nginx的異步io和lua的流程控制能力,還是有很豐富的想象空間的:幾乎所有常見的http請求的處理邏輯都能搞定,諸如查查數據庫,訪問一下memcache,讀寫一下本地文件等,都不在話下。恰好我正在研究一個第三方http服務的調用api,就產生了將其移植到lua上的想法。

其中涉及到了構造https請求的需求。我先采用ngx.socket.tcp()自己構造了一個http格式的請求,測試結果為可用,然后考慮能不能找到封裝好了的httpclient。放狗搜索了一番后,只找到了這個庫:lua-resty-http (https://github.com/liseen/lua-resty-http)。無奈下載下來后發現其不支持https,遂fork並修改了一番(https://github.com/kyriosli/lua-resty-http),加上了https支持。

到此貌似整個移植過程就完了?NO,nginx本身就是玩http的,為啥我們還要自己構造http請求,解析請求結果,管理連接復用呢?想到這一層,我又把眼光轉到了nginx自帶的api上

原理

ngx_lua本身不提供httpclient功能,但提供了ngx.location.capture接口。這個接口可以發送一個子請求(subrequest),並獲取子請求的響應結果。子請求可以調用lua,或者返回文件,甚至通過proxy_pass訪問另一個地址。

關鍵就在這里!如果我們使用proxy_pass機制結合subrequest,不就能實現http接口請求了嗎?

且慢!

一般來說,httpclient要求能夠指定method,scheme,域名,端口,請求頭,以及數據。但默認的subrequest將繼承所有當前請求的信息。好在ngx.location.capture接口支持指定method和data。那么只要搞定scheme,域名,端口,請求頭就可以了。

實現

使用過nginx的proxy_pass的同學都知道,proxy_pass支持從變量中獲取值(如常見的proxy_pass $scheme://xxx$request_uri;)。而rewrite能夠利用捕獲組設置變量,所以,我們可以在nginx的配置文件中加入這么一條location規則:

location /proxy/ {
     internal;
    rewrite ^/proxy/(https?)/([^/]+)/(\d+)/(.*)     /$4 break;
    proxy_pass      $1://$2:$3;
}
  • 第一行,指定規則為internal規則,防止外部請求命中此規則
  • 第二行,使用rewrite將地址中的scheme,host,port匹配出來;使用break終止匹配,不再檢查其它location
  • 第三行,使用proxy_pass將scheme,host,port拼裝成服務器地址

這樣,我們要請求如https://www.example.com:443/foo/bar時,只需要在lua代碼中構造一個子請求,地址為/proxy/https/www.example.com/443/foo/bar即可。

接下來就是header問題,通過ngx.req.set_header可以在lua中添加需要的請求頭。

由於header的繼承性,我們需要考慮來自客戶端的哪些header不能被傳遞到遠端接口中:

  • Host: 因為proxy_pass默認會重寫host,所以無需處理(在常規的proxy_pass中,可能需要使用proxy_set_header命令修正Host頭)
  • Content-Type: 如果來自客戶端的請求類型為POST,需要將該頭去掉
  • Content-Length: 同上
  • Accept-Encoding: 絕大多數時候,瀏覽器接受gzip, deflate格式的壓縮,如果遠端接口啟用了壓縮,此頭將導致返回的數據格式被壓縮過,從而lua無法正確處理。因此需要去掉此頭
  • Cookie: 涉及到隱私問題,所以這個頭也需要去掉

 通過ngx.req.clear_header可以將需要去掉的頭去掉。注意為了不影響后續處理,所有被去掉/改寫的頭應該被恢復,所有被添加的頭應該被去掉。

下面是一個簡單的示例代碼:

-- POST https://www.example.com:443/foo/bar?hello=world

ngx.req.set_header("Content-Type", "application/json;charset=utf8");
ngx.req.set_header("Accept", "application/json");

local res = ngx.location.capture('/proxy/https/www.example.com/443/foo/bar', {
    method = ngx.HTTP_POST,
    body = body,
    args = {hello = 'world'}
});

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM