最后更新于 .

nginx可以轻松实现根据不同的url 或者 get参数来转发到不同的服务器,然而当我们需要根据http包体来进行请求路由时,nginx默认的配置规则就捉襟见肘了,但是没关系,nginx提供了强大的自定义模块功能,我们只要进行需要的扩展就行了。 我们来理一下思路,我们的需求是:

nginx根据http包体的参数,来选择合适的路由

在这之前,我们先来考虑另一个问题: 在nginx默认配置的支持下,能否实现服务器间的跳转呢?即类似于状态机,从一个服务器执行OK后,跳转到另一台服务器,按照规则依次传递下去。 答案是可以的,这也是我之前写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协议的返回码,所有请求进入时都默认进入 http://localhost:8888;,然后再根据返回码是 433 还是 434 来选择进入 http://localhost:6788 还是 http://localhost:6789。 OK,也许你已经猜到我将这个例子的用意了,是的,我们只要在我们的自定义模块中,根据http的包体返回不同的返回码,进而 proxy_pass 到不同的后端服务器即可。 好吧,接下来,我们正式进入nginx自定义模块的编写中来。 一. nginx 自定义模块编写 由于这也是我第一次写nginx模块,所以也是参考了非常多文档,我一一列在这里,所以详细的入门就不说了,只说比较不太一样的地方。 参考链接:
  1. nginx的helloworld模块的helloworld
  2. nginx 一个例子模块,简单的将http请求的内容返输出
  3. nginx 自定义协议 扩展模块开发
  4. Emiller的Nginx模块开发指南
而我们这个模块一个最大的特点就是,需要等包体整个接收完才能进行处理,所以有如下代码:
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。我们来看一下它定义的原型:
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->connection->log
的结果总是null,至今也不知道为什么。(知道了是lua头文件和ngx头文件顺序的问题,把ngx头文件放到最前面即可) OK,接下来我们只要在get_route_id中增加逻辑代码,读几行配置,判断一下就可以了~ 但是,我想要的远不止如此。 二.lua解析器的加入 老博友应该都看过我之前写的一篇博客: 代码即数据,数据即代码(1)-把难以变更的代码变成易于变更的数据,而这一次的需求也非常符合使用脚本的原则: 只需要告诉我返回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的很多函数都变了,比如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
        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.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

Pingbacks

  1. nginx自定义模块编写-实时统计模块热资讯 | 热资讯 on #

    [...] 不是第一次写nginx的自定义模块了,之前有写过根据POST数据转发请求的模块(参见nginx自定义模块编写-根据post参数路由到不同服务器),不过上次写的是处理模块,而这次写的是过滤模块,还是有一些区别的。 [...]

Pingbacks已打开。

Trackbacks

引用地址

评论

  1. Heiher

    Heiher on #

    好东西,感谢分享!

    Reply

  2. cold night

    cold night on #

    好东西...

    Reply

  3. 甄码农

    甄码农 on #

    为啥不用正则呢,用正则映射到不同的服务器,不是更简单吗?

    Reply

  4. 任伟

    任伟 on #

    有一个问题,如果我的location @433 { proxy_pass http://localhost:6788/app; }会出错,原因是named location里面不能加url part(app),我该怎么解决?

    Reply

    1. Dante

      Dante on #

      这个不太清楚。。之前没有考虑过这个需求。

      Reply

  5. 码狂

    码狂 on #

    rb-&gt;buf-&gt;pos 不知道为什么无法读取buf数据。GET请求现在OK,但POST请求一直无法获取body,可能会是哪方面的原因呢?

    Reply

    1. Dante

      Dante on #

      好久之前的代码了。。已经忘记了。。

      Reply

      1. 码狂

        码狂 on #

        那请教一下,我该如何获取post请求中的参数呢?你还记得吗?我现在通过循环rb-&gt;bufs获取到了body值用char*保存,打印出来是这样的-----------------------------start-----------------------------------------WebKitFormBoundaryPzdfB3NsQYPHovryContent-Disposition: form-data; name="securityCode"A1B2------WebKitFormBoundaryPzdfB3NsQYPHovry------------------------------end----------------------------------我怎么获取securityCode和A1B2?

        Reply

      2. 码狂

        码狂 on #

        这个跟使用的版本会不会有没有关系?

        Reply

  6. 学习Nginx

    学习Nginx on #

    loc, pf ,appid = get_need_vals(method, uri, args, body)可以参考一下你get_need_vals里面的代码吗?

    Reply

发表评论