nginx可以轻松实现根据不同的url 或者 get参数来转发到不同的服务器,然而当我们需要根据http包体来进行请求路由时,nginx默认的配置规则就捉襟见肘了,但是没关系,nginx提供了强大的自定义模块功能,我们只要进行需要的扩展就行了。
我们来理一下思路,我们的需求是:
nginx根据http包体的参数,来选择合适的路由
在这之前,我们先来考虑另一个问题:
在nginx默认配置的支持下,能否实现服务器间的跳转呢?即类似于状态机,从一个服务器执行OK后,跳转到另一台服务器,按照规则依次传递下去。
答案是可以的,这也是我之前写bayonet之后,在nginx上特意尝试的功能。
一个示例的配置如下:
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模块,所以也是参考了非常多文档,我一一列在这里,所以详细的入门就不说了,只说比较不太一样的地方。
参考链接:
而我们这个模块一个最大的特点就是,需要等包体整个接收完才能进行处理,所以有如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
->
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结束
}
|
我们注册了一个回调函数 ngx_http_foo_post_handler,当包体全部接受完成时就会调用。之后我们调用了get_route_id来获取返回码,然后通过 ngx_http_finalize_request(r, result); 来告诉nginx处理的结果。
这里有个小插曲,即get_route_id。我们来看一下它定义的原型:
1
|
第一个参数是 ngx_log_t *log,是为了方便在报错的时候打印日志。然而在最开始的时候,get_route_id 的原型是这样: |
第一个参数是 ngx_log_t *log,是为了方便在报错的时候打印日志。然而在最开始的时候,get_route_id 的原型是这样:
1
|
结果在 get_route_id 函数内部,调用 |
结果在 get_route_id 函数内部,调用
的结果总是null,至今也不知道为什么。(知道了是lua头文件和ngx头文件顺序的问题,把ngx头文件放到最前面即可)
OK,接下来我们只要在get_route_id中增加逻辑代码,读几行配置,判断一下就可以了~ 但是,我想要的远不止如此。
二.lua解析器的加入
老博友应该都看过我之前写的一篇博客: 代码即数据,数据即代码(1)-把难以变更的代码变成易于变更的数据,而这一次的需求也非常符合使用脚本的原则:
只需要告诉我返回nginx哪个返回码,具体怎么算出来的,再复杂,再多变,都放到脚本里面去。
所以接下来我又写了c调用lua的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
)
)
;
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的很多函数都变了,比如lua_open废弃,变成luaL_newstate等,不过总体来说还算没浪费太多时间。
接下来是req_route.lua的内容,我只截取入口函数如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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
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配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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.googlecode.com/svn/trunk/nginx_req_route
用perl or lua的版本如下
http://www.php-oa.com/2010/09/25/perl-perl-nginx.html
https://github.com/chaoslawful/lua-nginx-module