天天看點

nginx自定義子產品編寫-根據post參數路由到不同伺服器

nginx可以輕松實作根據不同的url 或者 get參數來轉發到不同的伺服器,然而當我們需要根據http包體來進行請求路由時,nginx預設的配置規則就捉襟見肘了,但是沒關系,nginx提供了強大的自定義子產品功能,我們隻要進行需要的擴充就行了。

我們來理一下思路,我們的需求是:

nginx根據http包體的參數,來選擇合适的路由

在這之前,我們先來考慮另一個問題:

在nginx預設配置的支援下,能否實作伺服器間的跳轉呢?即類似于狀态機,從一個伺服器執行OK後,跳轉到另一台伺服器,按照規則依次傳遞下去。

一個示例的配置如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

server {

    listen       8080;

    server_name  localhost;

    location / {

        proxy_pass http://localhost:8888;

        error_page 433 = @433;

        error_page 434 = @434;

    }

    location @433 {

        proxy_pass http://localhost:6788;

    location @434 {

        proxy_pass http://localhost:6789;

    error_page   500 502 503 504  /50x.html;

    location = /50x.html {

        root   html;

}

看明白了吧?我們使用了 433和434 這兩個非标準http協定的傳回碼,所有請求進入時都預設進入 http://localhost:8888;,然後再根據傳回碼是 433 還是 434 來選擇進入 http://localhost:6788 還是 http://localhost:6789。

OK,也許你已經猜到我将這個例子的用意了,是的,我們隻要在我們的自定義子產品中,根據http的包體傳回不同的傳回碼,進而 proxy_pass 到不同的後端伺服器即可。

好吧,接下來,我們正式進入nginx自定義子產品的編寫中來。

一. nginx 自定義子產品編寫

由于這也是我第一次寫nginx子產品,是以也是參考了非常多文檔,我一一列在這裡,是以詳細的入門就不說了,隻說比較不太一樣的地方。

參考連結:

<a href="http://haoningabc.iteye.com/blog/1283098" target="_blank">nginx的helloworld子產品的helloworld</a>

<a href="http://lijinxing17.blog.163.com/blog/static/349777082010627104929343/" target="_blank">nginx 一個例子子產品,簡單的将http請求的内容返輸出</a>

<a href="http://blog.chinaunix.net/space.php?uid=26443921&amp;do=blog&amp;id=3018781" target="_blank">nginx 自定義協定 擴充子產品開發</a>

<a href="http://yaoweibin.cn/maindoc/nginx-modules-guide-cn.pdf" target="_blank">Emiller的Nginx子產品開發指南</a>

而我們這個子產品一個最大的特點就是,需要等包體整個接收完才能進行處理,是以有如下代碼:

24

25

26

27

28

29

30

31

32

33

34

void ngx_http_foo_post_handler(ngx_http_request_t *r){

    // 請求全部讀完後從這裡入口, 可以産生響應

    ngx_http_request_body_t* rb = r-&gt;request_body;

    char* body = NULL;

    int body_size = 0;

    if (rb &amp;&amp; rb-&gt;buf)

    {

        body = (char*)rb-&gt;buf-&gt;pos;

        body_size = rb-&gt;buf-&gt;last - rb-&gt;buf-&gt;pos;

    int result = get_route_id(r-&gt;connection-&gt;log,

                              (int)r-&gt;method,

                              (char*)r-&gt;uri.data,

                              (char*)r-&gt;args.data,

                              body,

                              body_size

                              );

    if (result &lt; 0)

        ngx_log_error(NGX_LOG_ERR, r-&gt;connection-&gt;log, 0, "get_route_id fail, result:%d", result);

        result = DFT_ROUTE_ID;

    ngx_http_finalize_request(r, result);

static ngx_int_t ngx_http_req_route_handler(ngx_http_request_t *r)

{

    ngx_http_read_client_request_body(r, ngx_http_foo_post_handler);

    return NGX_DONE; // 主handler結束

我們注冊了一個回調函數 ngx_http_foo_post_handler,當包體全部接受完成時就會調用。之後我們調用了get_route_id來擷取傳回碼,然後通過 ngx_http_finalize_request(r, result); 來告訴nginx處理的結果。

這裡有個小插曲,即get_route_id。我們來看一下它定義的原型:

extern int get_route_id(ngx_log_t *log, int method, char* uri, char* args, char* body, int body_size);

第一個參數是 ngx_log_t *log,是為了友善在報錯的時候列印日志。然而在最開始的時候,get_route_id 的原型是這樣:

extern int get_route_id(ngx_http_request_t *r, int method, char* uri, char* args, char* body, int body_size);

結果在 get_route_id 函數内部,調用

r-&gt;connection-&gt;log

的結果總是null,至今也不知道為什麼。(知道了是lua頭檔案和ngx頭檔案順序的問題,把ngx頭檔案放到最前面即可)

OK,接下來我們隻要在get_route_id中增加邏輯代碼,讀幾行配置,判斷一下就可以了~ 但是,我想要的遠不止如此。

二.lua解析器的加入

隻需要告訴我傳回nginx哪個傳回碼,具體怎麼算出來的,再複雜,再多變,都放到腳本裡面去。

是以接下來我又寫了c調用lua的代碼:

35

36

37

38

39

40

41

42

43

44

int get_route_id(ngx_log_t *log, int method, char* uri, char* args, char* body, int body_size)

    const char lua_funcname[] = "get_route_id";

    lua_State *L = luaL_newstate();

    luaL_openlibs(L);

    if (luaL_loadfile(L, LUA_FILENAME) || lua_pcall(L, 0, 0, 0))

        ngx_log_error(NGX_LOG_ERR, log, 0, "cannot run configuration file: %s", lua_tostring(L, -1));

        lua_close(L);

        return -1;

    lua_getglobal(L, lua_funcname); /* function to be called */

    lua_pushnumber(L, method);

    lua_pushstring(L, uri);

    lua_pushstring(L, args);

    lua_pushlstring(L, body, body_size);

    /* do the call (1 arguments, 1 result) */

    if (lua_pcall(L, 4, 1, 0) != 0)

        ngx_log_error(NGX_LOG_ERR, log, 0, "error running function %s: %s", lua_funcname, lua_tostring(L, -1));

        return -2;

    /* retrieve result */

    if (!lua_isnumber(L, -1))

        ngx_log_error(NGX_LOG_ERR, log, 0, "function %s must return a number", lua_funcname);

        return -3;

    int result = (int)lua_tonumber(L, -1);

    lua_pop(L, 1); /* pop returned value */

    lua_close(L);

    return result;

比較郁悶的是,lua 5.2的很多函數都變了,比如lua_open廢棄,變成luaL_newstate等,不過總體來說還算沒浪費太多時間。

接下來是req_route.lua的内容,我隻截取入口函數如下:

function get_route_id(method, uri, args, body)

    loc, pf ,appid = get_need_vals(method, uri, args, body)

    if loc == nil or pf == nil or appid == nil then

        return OUT_CODE

    end

    --到這裡位置,就把所有的資料都拿到了

    --print (loc, pf, appid)

    -- 找是否在對應的url, loc中

    if not is_match_pf_and_loc(pf, loc) then

    -- 找是否在對應的appid中

    if not is_match_appid(appid) then

    return IN_CODE

end

OK,結合了lua解析器之後,無論多複雜的調整,我們都基本可以做到隻修改lua腳本而不需要重新修改、編譯nginx子產品代碼了。

接下來,就該是體驗我們的成果了。

三.nginx配置

    location /req_route {

        req_route;

OK,enjoy it!

最後,放出代碼如下:

<a href="https://vimercode.googlecode.com/svn/trunk/nginx_req_route" target="_blank">https://vimercode.googlecode.com/svn/trunk/nginx_req_route</a>

用perl or lua的版本如下

<a href="http://www.php-oa.com/2010/09/25/perl-perl-nginx.html" target="_blank">http://www.php-oa.com/2010/09/25/perl-perl-nginx.html</a>

<a href="https://github.com/chaoslawful/lua-nginx-module" target="_blank">https://github.com/chaoslawful/lua-nginx-module</a>

繼續閱讀