nginx访问第三方服务之upstream使用

本文介绍了Nginx中使用upstream模块与第三方服务通信的方法,包括ngx_http_upstream_t和ngx_http_upstream_conf_t结构体的设置,以及如何配置回调方法处理请求和响应。通过设置upstream,可以实现高效地反向代理第三方服务内容。

当需要访问第三方服务时,Nginx提供了两种全异步方式来与第三方服务器通信:upstream与subrequest。本文将介绍upstream的使用方式,Nginx的HTTP反向代理模块就是基于upstream方式实现,当我们希望把第三方服务的内容几乎原封不懂地返回给用户时,一般使用upstream方式,它可以非常高效地透传HTTP。

1.upstream的使用方式

upstream的使用方式并不复杂,它提供了8个回调方法,见文件http/ngx_http_upstream_t.h,用户需要实现的其中的几个回调方法,本例中实现了3个回调方法:

struct ngx_http_upstream_s{
//...
//构造发往上游服务器的请求
    ngx_int_t                      (*create_request)(ngx_http_request_t *r);
//收到上游服务器的TCP流时回调,直到不返回NGX_AGAIN,处理http响应
    ngx_int_t                      (*process_header)(ngx_http_request_t *r);
//销毁upstream时回调
    void                           (*finalize_request)(ngx_http_request_t *r,ngx_int_t rc);
//...
};

那么upstream是如何嵌入到一个请求中的?
模块在处理任何一个请求时都有ngx_http_request_t结构对象r,该结构中有一个ngx_http_upstream_t类型的成员upstream,见文件http/ngx_http_request.h

struct ngx_http_request_s
{
    //...
    ngx_http_upstream_t    *upstream;
    //...
}

如果没有使用upstream机制,则需要将相应的成员设置为NULL指针,否则需要对r->upstream进行设置。
HTTP模块启用upstream机制的步骤如下:

Created with Raphaël 2.1.0Start调用ngx_http_upstream_create方法为请求创建upstream设置第三方服务器地址设置upstream的回调方法调用ngx_http_upstream_init方法启动upstreamEnd

使用upstream模块提供的ngx_http_upstream_init方法后,HTTP如何运行upstream框架,大致流程如下:

Created with Raphaël 2.1.0Start创建发往上游服务器的请求与上游服务器建立无阻塞TCP连接发送请求到第三方服务接收第三方服务响应并处理包头和包体回调finalize_request销毁请求End

2.upstream的配置

2.1 ngx_http_upstream_t结构体设置

需要大致了解struct ngx_http_upstream_s结构体及其相关成员,详情见文件http/ngx_http_upstream.h
除了之前介绍到回调函数之外,还有一些需要了解的参数:

struct ngx_http_upstream_s {
    //...
    //发什么样的请求给上游服务器,create_request中实现
    ngx_chain_t                     *request_bufs;
    //可直接指定上游服务器的地址
    ngx_http_upstream_resolved_t    *resolved;

    //buffer存储接收上游服务器的响应内容。复用
    ngx_buf_t                        buffer;

    //向客户端转发上游服务器的包体时才有用。
    //1-多个缓冲区以及磁盘文件转发(Nginx与上游间的网速远大于Nginx与下游客户端之间的网速)
    //0-只使用该结构体中的buffer缓冲区向下游转发响应包体
    unsigned                         buffering:1;
    //...
};

关于buffering参数,upstream提供了3种处理上游服务器包体的方式:

1.交由HTTP模块使用input filter回调方法直接处理包体。
2.以固定缓冲区(buffer成员)转发包体(本文使用的方式 buffering设置为0)
3.以多个缓冲加磁盘文件的方式转发包体等(buffering设置为1)。

2.2 ngx_http_upstream_conf_t结构体设置

该结构是ngx_http_upstream_t中的成员之一,通过nginx.conf和设置commands使用预设的方法可以对该结构体赋值。在深入理解Nginx书中,为了简便期间直接在create_loc_conf回调函数中创建对象并硬编码赋值。下列指出结构体中必须设置的成员。

// http/ngx_http_upstream.h

typedef struct{
//...
     //连接上游服务器的超时时间
    ngx_msec_t                       connect_timeout;
    //发送tcp包到上游服务器的超时时间
    ngx_msec_t                       send_timeout;
    //接收Tcp包的超时时间
    ngx_msec_t                       read_timeout;
//...
}ngx_http_upstream_conf_t;

2.3 设置需要访问的第三方服务地址resolved成员

这部分就是熟悉套接字编程了,看到resovlved结构的成员就清楚了(见文件http/ngx_http_upstream.h):

typedef struct{
    //...
    ngx_uint_t naddrs;
    //上游服务器地址
    struct sockaddr *sockaddr;
    socklen_t socklen;
    //...
}ngx_http_upstream_resolved_t;

2.4 设置回调方法

ngx_http_upstream_t结构体中有8个回调方法,不过必须实现的有3个回调方法:

create_request:创建请求
process_header:处理响应
finalize_request:销毁请求


3.upstream的使用示例

书中给出google的URL搜索示例,由于墙的原因,我们使用baidu,其URL搜索方法是www.baidu.com/s?wd=hahanginx.conf配置location,例如:

location /test{
    mytest;
}

如果访问URL是/test?lumia则可以通过upstream机制向www.baidu.com发送搜索请求。

完整代码

源码来源自深入理解Nginx,注释包含自己的理解。

//ngx_http_mytest_module.c

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

/*上下文结构体*/
typedef struct
{
    ngx_http_status_t status;
    ngx_str_t backendServer;
}ngx_http_mytest_ctx_t;

/*uptream相关配置,例如超时等待时间等*/
typedef struct
{
    ngx_http_upstream_conf_t upstream;
} ngx_http_mytest_conf_t;


/*配置项处理函数*/
static char *
ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);


/*真正的处理函数*/
/*设置upstream的host、回调函数、启动upstream等*/
static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r);


/*创建ngx_http_mytest_conf_t结构体,硬编码参数*/
static void* ngx_http_mytest_create_loc_conf(ngx_conf_t *cf);


/*设置hide_headers_hash*/
static char *ngx_http_mytest_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);


/*解析http头部*/
static ngx_int_t
mytest_upstream_process_header(ngx_http_request_t *r);


/*处理http响应行*/
static ngx_int_t
mytest_process_status_line(ngx_http_request_t *r);


/*安全考虑隐藏头部*/
static ngx_str_t  ngx_http_proxy_hide_headers[] =
{
    ngx_string("Date"),
    ngx_string("Server"),
    ngx_string("X-Pad"),
    ngx_string("X-Accel-Expires"),
    ngx_string("X-Accel-Redirect"),
    ngx_string("X-Accel-Limit-Rate"),
    ngx_string("X-Accel-Buffering"),
    ngx_string("X-Accel-Charset"),
    ngx_null_string
};


/*command 结构体数组*/
static ngx_command_t  ngx_http_mytest_commands[] =
{

    {
        ngx_string("mytest"),
        NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_NOARGS,
        ngx_http_mytest,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },

    ngx_null_command
};


/*模块上下文*/
static ngx_http_module_t  ngx_http_mytest_module_ctx =
{
    NULL, /* preconfiguration */
    NULL, /* postconfiguration */

    NULL, /* create main configuration */
    NULL, /* init main configuration */

    NULL, /* create server configuration */
    NULL, /* merge server configuration */

    ngx_http_mytest_create_loc_conf,/* create location configuration */
    ngx_http_mytest_merge_loc_conf  /* merge location configuration */
};


/*nginx 模块*/
ngx_module_t  ngx_http_mytest_module =
{
    NGX_MODULE_V1,
    &ngx_http_mytest_module_ctx,           /* module context */
    ngx_http_mytest_commands,              /* module directives */
    NGX_HTTP_MODULE,                       /* module type */
    NULL,                                  /* init master */
    NULL,                                  /* init module */
    NULL,                                  /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    NULL,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};


static void*
ngx_http_mytest_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_mytest_conf_t *mycf;

    mycf = (ngx_http_mytest_conf_t  *)ngx_pcalloc(cf->pool, sizeof(ngx_http_mytest_conf_t));
    if (mycf == NULL)
    {
        return NULL;
    }

    /*对结构体变量中的成员进行硬编码,超时连接相关*/
    mycf->upstream.connect_timeout = 60000;
    mycf->upstream.send_timeout = 60000;
    mycf->upstream.read_timeout = 60000;
    mycf->upstream.store_access = 0600;

    /*处理上游服务器包体方式相关,这里以固定buffer转发包体*/
    mycf->upstream.buffering = 0;
    mycf->upstream.bufs.num = 8;
    mycf->upstream.bufs.size = ngx_pagesize;
    mycf->upstream.buffer_size = ngx_pagesize;
    mycf->upstream.busy_buffers_size = 2 * ngx_pagesize;
    mycf->upstream.temp_file_write_size = 2 * ngx_pagesize;
    mycf->upstream.max_temp_file_size = 1024 * 1024 * 1024;


    /*upstream模块要求hide_headers必须初始化*/
    mycf->upstream.hide_headers = NGX_CONF_UNSET_PTR;
    mycf->upstream.pass_headers = NGX_CONF_UNSET_PTR;

    return mycf;
}


static char *
ngx_http_mytest_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_mytest_conf_t *prev = (ngx_http_mytest_conf_t *)parent;
    ngx_http_mytest_conf_t *conf = (ngx_http_mytest_conf_t *)child;

    ngx_hash_init_t hash;
    hash.max_size = 100;
    hash.bucket_size = 1024;
    hash.name = "proxy_headers_hash";
    /*hide header*/
    if (ngx_http_upstream_hide_headers_hash(cf, &conf->upstream,
     &prev->upstream, ngx_http_proxy_hide_headers, &hash)
        != NGX_OK)
    {
        return NGX_CONF_ERROR;
    }
    return NGX_CONF_OK;
}

/*upstream  create_request 回调,构建到上游服务器的请求*/
static ngx_int_t
mytest_upstream_create_request(ngx_http_request_t *r)
{
    /*模仿baidu搜索请求 /s?wd= */
    static ngx_str_t backendQueryLine =
        ngx_string("GET /s?wd=%V HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n");

    /*请求长度 -2表示 %V*/
    ngx_int_t queryLineLen = backendQueryLine.len + r->args.len - 2;

    /*内存池申请内存,请求结束时内存被自动释放*/
    ngx_buf_t* b = ngx_create_temp_buf(r->pool, queryLineLen);
    if (b == NULL)
        return NGX_ERROR;

    /*last要指向请求的末尾*/
    b->last = b->pos + queryLineLen;

    /*作用相当于snprintf*/
    ngx_snprintf(b->pos, queryLineLen ,(char*)backendQueryLine.data, &r->args);

    /*发送给上游服务器的请求*/
    r->upstream->request_bufs = ngx_alloc_chain_link(r->pool);
    if (r->upstream->request_bufs == NULL)
        return NGX_ERROR;

    /*request_bufs这里只包含1个ngx_buf_t缓冲区*/
    r->upstream->request_bufs->buf = b;
    r->upstream->request_bufs->next = NULL;

    r->upstream->request_sent = 0;
    r->upstream->header_sent = 0;

    /*header_hash不可以为0*/
    r->header_hash = 1;
    return NGX_OK;
}



static ngx_int_t
mytest_process_status_line(ngx_http_request_t *r)
{
    size_t len;
    ngx_int_t rc;
    ngx_http_upstream_t *u;

    /*取出http请求的上下文*/
    ngx_http_mytest_ctx_t* ctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    if (ctx == NULL)
    {
        return NGX_ERROR;
    }

    u = r->upstream;

    /*解析http响应行*/
    rc = ngx_http_parse_status_line(r, &u->buffer, &ctx->status);

    /*NGX_AGAIN表示继续解析*/
    if (rc == NGX_AGAIN)
    {
        return rc;
    }

    /*NGX_ERROR没有接收到合法的http响应行*/
    if (rc == NGX_ERROR)
    {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "upstream sent no valid HTTP/1.0 header");

        r->http_version = NGX_HTTP_VERSION_9;
        u->state->status = NGX_HTTP_OK;

        return NGX_OK;
    }

    /*解析到完整到响应行,将解析出来的内容赋值到headers_in结构体中*/
    if (u->state)
    {
        u->state->status = ctx->status.code;
    }

    /*赋值操作*/
    u->headers_in.status_n = ctx->status.code;

    len = ctx->status.end - ctx->status.start;

    u->headers_in.status_line.len = len;

    u->headers_in.status_line.data = ngx_pnalloc(r->pool, len);
    if (u->headers_in.status_line.data == NULL)
    {
        return NGX_ERROR;
    }
    ngx_memcpy(u->headers_in.status_line.data, ctx->status.start, len); 


    /*开始解析http响应头部*/
    /*之后收到的新字符流将有新的回调函数解析*/
    u->process_header = mytest_upstream_process_header;

    /*如果本次收到的字符流除了http响应行外,还有多余的字符*/
    /*将由mytest_upstream_process_header方法解析*/
    return mytest_upstream_process_header(r);
}


/*处理http头部*/
static ngx_int_t
mytest_upstream_process_header(ngx_http_request_t *r)
{
    ngx_int_t rc;

    /*为http头部量身定制*/
    /*例如 key 存储"Content-Length"*/
    /*value 存储 "1024" */
    ngx_table_elt_t *h;

    ngx_http_upstream_header_t     *hh;

    ngx_http_upstream_main_conf_t  *umcf;

    /*将upstream模块配置项ngx_http_upstream_main_conf_t取了*/
    /*该结构体中存储了需要做统一处理的http头部名称和回调方法*/
    umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module);

    /*循环的解析所有的http头部*/
    for ( ;; )
    {
    /*解析http头部*/
        rc = ngx_http_parse_header_line(r, &r->upstream->buffer, 1);

        /*NGX_OK解析出一行http头部*/
        if (rc == NGX_OK)
        {
            /*向headers_in.headers这个ngx_list_t链表中添加http头部*/
        /*ngx_list_t是先插入再赋值*/
            h = ngx_list_push(&r->upstream->headers_in.headers);
            if (h == NULL)
            {
                return NGX_ERROR;
            }

            /*构造刚刚添加到headers链表中的http头部*/
            h->hash = r->header_hash;

        /*key-头部名称 value-对应的值*/
            h->key.len = r->header_name_end - r->header_name_start;
            h->value.len = r->header_end - r->header_start;


        /*zx:作者这里分配这么大空间的原因在于,在结构体中有三个变量*/
        //1.key
        //2.value
        //3.lowcase_case
        /*一次申请这一整段空间,+1用来'\0'来区分*/
        /*这也在于ngx_str_t的特性在于data字段记录的只是字串地址*/
            h->key.data = ngx_pnalloc(r->pool,
                                      h->key.len + 1 + h->value.len + 1 + h->key.len);
            if (h->key.data == NULL)
            {
                return NGX_ERROR;
            }

        /*赋值操作*/
            h->value.data = h->key.data + h->key.len + 1;
            h->lowcase_key = h->key.data + h->key.len + 1 + h->value.len + 1;

            ngx_memcpy(h->key.data, r->header_name_start, h->key.len);
            h->key.data[h->key.len] = '\0';
            ngx_memcpy(h->value.data, r->header_start, h->value.len);
            h->value.data[h->value.len] = '\0';

            if (h->key.len == r->lowcase_index)
            {
                ngx_memcpy(h->lowcase_key, r->lowcase_header, h->key.len);
            }
            else
            {
                ngx_strlow(h->lowcase_key, h->key.data, h->key.len);
            }

            /*upstream模块会对一些http头部做特殊处理*/
            hh = ngx_hash_find(&umcf->headers_in_hash, h->hash,
                               h->lowcase_key, h->key.len);

            if (hh && hh->handler(r, h, hh->offset) != NGX_OK)
            {
                return NGX_ERROR;
            }

            continue;
        }

        /*返回NGX_HTTP_PARSE_HEADER_DONE表示响应中所有的http头部都解析*/
        if (rc == NGX_HTTP_PARSE_HEADER_DONE)
        {
            /*如果之前解析http头部时没有发现server和date头部*/
        /*据http协议添加这两个头部*/
            if (r->upstream->headers_in.server == NULL)
            {
                h = ngx_list_push(&r->upstream->headers_in.headers);
                if (h == NULL)
                {
                    return NGX_ERROR;
                }

        /*zx:查看ngx_hash_key_lc/ngx_hash_key源码可知,可以改写*/
        ngx_str_t str=ngx_string("server");
        h->hash=ngx_hash_key_lc(str.data,str.len);
#if 0
                h->hash = ngx_hash(ngx_hash(ngx_hash(ngx_hash(
                                                      ngx_hash('s', 'e'), 'r'), 'v'), 'e'), 'r');
#endif

                ngx_str_set(&h->key, "Server");
                ngx_str_null(&h->value);
                h->lowcase_key = (u_char *) "server";
            }

            if (r->upstream->headers_in.date == NULL)
            {
                h = ngx_list_push(&r->upstream->headers_in.headers);
                if (h == NULL)
                {
                    return NGX_ERROR;
                }

        ngx_str_t str=ngx_string("date");
        /*zx:同上*/
        h->hash=ngx_hash_key_lc(str.data,str.len);
#if 0
                h->hash = ngx_hash(ngx_hash(ngx_hash('d', 'a'), 't'), 'e');
#endif

                ngx_str_set(&h->key, "Date");
                ngx_str_null(&h->value);
                h->lowcase_key = (u_char *) "date";
            }

            return NGX_OK;
        }

    /*没有解析到完整的http头部,继续接收新的字符流*/
        if (rc == NGX_AGAIN)
        {
            return NGX_AGAIN;
        }

        /*其他返回值都是非法的*/
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "upstream sent invalid header");
        return NGX_HTTP_UPSTREAM_INVALID_HEADER;
    }
}

/*finalize_request回调 请求结束前会调用*/
static void
mytest_upstream_finalize_request(ngx_http_request_t *r, ngx_int_t rc)
{
    ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0,
                  "mytest_upstream_finalize_request");
}


static char *
ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_core_loc_conf_t  *clcf;

    /*首先找到mytest配置项所属的配置块,clcf貌似是location块内的数据*/
    /*结构,其实不然,它可以是main、srv或者loc级别配置项,也就是说在每个*/
    /*http{}和server{}内也都有一个ngx_http_core_loc_conf_t结构体*/
    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);

    /*http框架在处理用户请求进行到NGX_HTTP_CONTENT_PHASE阶段时,如果*/
    /*请求的主机域名、URI与mytest配置项所在的配置块相匹配,就将调用我们*/
    /*实现的ngx_http_mytest_handler方法处理这个请求*/
    clcf->handler = ngx_http_mytest_handler;

    return NGX_CONF_OK;
}

static ngx_int_t
ngx_http_mytest_handler(ngx_http_request_t *r)
{
    /*首先建立http上下文结构体ngx_http_mytest_ctx_t*/
    ngx_http_mytest_ctx_t* myctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    if (myctx == NULL)
    {
        myctx = ngx_palloc(r->pool, sizeof(ngx_http_mytest_ctx_t));
        if (myctx == NULL)
        {
            return NGX_ERROR;
        }
        /*将新建的上下文与请求关联起来*/
        ngx_http_set_ctx(r, myctx, ngx_http_mytest_module);
    }

    /*对每1个要使用upstream的请求,必须调用且只能调用1次*/
    /*ngx_http_upstream_create方法,它会初始化r->upstream成员*/
    if (ngx_http_upstream_create(r) != NGX_OK)
    {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "ngx_http_upstream_create() failed");
        return NGX_ERROR;
    }

    /*得到配置upstream结构体ngx_http_mytest_conf_t*/
    ngx_http_mytest_conf_t  *mycf = (ngx_http_mytest_conf_t*) ngx_http_get_module_loc_conf(r, ngx_http_mytest_module);

    /*配置upstream*/
    ngx_http_upstream_t *u = r->upstream;

    /*这里用配置文件中的结构体来赋给r->upstream->conf成员*/
    /*ngx_http_upstream_conf_t结构*/
    u->conf = &mycf->upstream;

    /*决定转发包体时使用的缓冲区*/
    u->buffering = mycf->upstream.buffering;

    /*以下代码开始初始化resolved结构体,用来保存上游服务器的地址*/

    u->resolved = (ngx_http_upstream_resolved_t*) ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_resolved_t));
    if (u->resolved == NULL)
    {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "ngx_pcalloc resolved error. %s.", strerror(errno));
        return NGX_ERROR;
    }

    /*这里的上游服务器就是www.baidu.com*/
    static struct sockaddr_in backendSockAddr;
    struct hostent *pHost = gethostbyname((char*) "www.baidu.com");
    if (pHost == NULL)
    {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "gethostbyname fail. %s", strerror(errno));

        return NGX_ERROR;
    }

    /*访问上游服务器的80端口*/
    backendSockAddr.sin_family = AF_INET;
    backendSockAddr.sin_port = htons((in_port_t) 80);

    memcpy(&backendSockAddr.sin_addr,pHost->h_addr_list[0],sizeof(struct in_addr));
    char* pDmsIP = inet_ntoa(*(struct in_addr*) (pHost->h_addr_list[0]));
    /*inet_addr*/
    //backendSockAddr.sin_addr.s_addr = inet_addr(pDmsIP);
    myctx->backendServer.data = (u_char*)pDmsIP;
    myctx->backendServer.len = strlen(pDmsIP);

    /*将地址设置到resolved成员中*/
    u->resolved->sockaddr = (struct sockaddr *)&backendSockAddr;
    u->resolved->socklen = sizeof(struct sockaddr_in);
    u->resolved->naddrs = 1;

    /*设置三个必须实现的回调方法:创建请求、处理头部、请求销毁*/
    u->create_request = mytest_upstream_create_request;
    u->process_header = mytest_process_status_line;
    u->finalize_request = mytest_upstream_finalize_request;

    /*这里必须将count成员加1*/
    r->main->count++;
    /*启动upstream*/
    ngx_http_upstream_init(r);
    /*必须返回NGX_DONE*/
    return NGX_DONE;
}

将模块编入nginx,启动nginx服务,curl一下,百度就出来了。

curl -v localhost:1024/test?lumia

如图:

这里写图片描述


4.参考

深入理解Nginx

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值