Nginx可以輕松實現(xiàn)根據(jù)不同的url 或者 get參數(shù)來轉發(fā)到不同的服務器,然而當我們需要根據(jù)http包體來進行請求路由時,Nginx默認的配置規(guī)則就捉襟見肘了,但是沒關系,Nginx提供了強大的自定義模塊功能,我們只要進行需要的擴展就行了。
我們來理一下思路,我們的需求是:
Nginx根據(jù)http包體的參數(shù),來選擇合適的路由
在這之前,我們先來考慮另一個問題:
在Nginx默認配置的支持下,能否實現(xiàn)服務器間的跳轉呢?即類似于狀態(tài)機,從一個服務器執(zhí)行OK后,跳轉到另一臺服務器,按照規(guī)則依次傳遞下去。
答案是可以的,這也是我之前寫bayonet之后,在nginx上特意嘗試的功能。
一個示例的配置如下:
- 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協(xié)議的返回碼,所有請求進入時都默認進入 http://localhost:8888;,然后再根據(jù)返回碼是 433 還是 434 來選擇進入 http://localhost:6788 還是 http://localhost:6789。
OK,也許你已經(jīng)猜到我將這個例子的用意了,是的,我們只要在我們的自定義模塊中,根據(jù)http的包體返回不同的返回碼,進而 proxy_pass 到不同的后端服務器即可。
好吧,接下來,我們正式進入nginx自定義模塊的編寫中來。
一. nginx 自定義模塊編寫 由于這也是我第一次寫nginx模塊,所以也是參考了非常多文檔,我一一列在這里,所以詳細的入門就不說了,只說比較不太一樣的地方。 參考鏈接:
- nginx的helloworld模塊的helloworld
- nginx 一個例子模塊,簡單的將http請求的內容返輸出
- nginx 自定義協(xié)議 擴展模塊開發(fā)
- Emiller的Nginx模塊開發(fā)指南
而我們這個模塊一個最大的特點就是,需要等包體整個接收完才能進行處理,所以有如下代碼:
- void ngx_http_foo_post_handler(ngx_http_request_t *r){
- // 請求全部讀完后從這里入口, 可以產(chǎn)生響應
- ngx_http_request_body_t* rrb = r->request_body;
-
- char* body = NULL;
- int body_size = 0;
-
- if (rb && rb->buf)
- {
- body = (char*)rb->buf->pos;
- body_size = rb->buf->last - rb->buf->pos;
- }
-
- int result = get_route_id(r->connection->log,
- (int)r->method,
- (char*)r->uri.data,
- (char*)r->args.data,
- body,
- body_size
- );
- if (result < 0)
- {
- ngx_log_error(NGX_LOG_ERR, r->connection->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結束
- }
我們注冊了一個回調函數(shù) 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)
第一個參數(shù)是 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 函數(shù)內部,調用:
- r->connection->log
的結果總是null,至今也不知道為什么。
OK,接下來我們只要在get_route_id中增加邏輯代碼,讀幾行配置,判斷一下就可以了~ 但是,我想要的遠不止如此。
二、lua解析器的加入
老博友應該都看過我之前寫的一篇博客: 代碼即數(shù)據(jù),數(shù)據(jù)即代碼(1)-把難以變更的代碼變成易于變更的數(shù)據(jù),而這一次的需求也非常符合使用腳本的原則:
只需要告訴我返回nginx哪個返回碼,具體怎么算出來的,再復雜,再多變,都放到腳本里面去。
所以接下來我又寫了c調用lua的代碼:
- 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));
- lua_close(L);
- 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);
- lua_close(L);
- return -3;
- }
- int result = (int)lua_tonumber(L, -1);
-
- lua_pop(L, 1); /* pop returned value */
-
- lua_close(L);
- return result;
- }
比較郁悶的是,lua 5.2的很多函數(shù)都變了,比如lua_open廢棄,變成luaL_newstate等,不過總體來說還算沒浪費太多時間。
接下來是req_route.lua的內容,我只截取入口函數(shù)如下:
- 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
- --到這里位置,就把所有的數(shù)據(jù)都拿到了
- --print (loc, pf, appid)
- -- 找是否在對應的url, loc中
- if not is_match_pf_and_loc(pf, loc) then
- return OUT_CODE
- end
- -- 找是否在對應的appid中
- if not is_match_appid(appid) then
- return OUT_CODE
- end
- return IN_CODE
- end
OK,結合了lua解析器之后,無論多復雜的調整,我們都基本可以做到只修改lua腳本而不需要重新修改、編譯nginx模塊代碼了。
接下來,就該是體驗我們的成果了。
三、Nginx配置
- server {
- listen 8080;
- server_name localhost;
-
- location /req_route {
- req_route;
- 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;
- }
- }
OK,enjoy it!
最后,放出代碼如下:
https://vimercode./svn/trunk/nginx_req_route
|