【Linux网络编程】Nginx -- 模块开发(upstream / subrequest)

【Linux网络编程】Nginx -- 模块开发(upstream / subrequest)

【1】upstream VS subrequest 简介

upstream 为访问上游服务器,它把Nginx定义为代理服务器,首要功能是透传,其次才是以TCP获取第三方服务器的内容;Nginx的HTTP反向代理模块是基于upstream方式实现的;
subrequest是子请求,即subrequest将会为用户创建子请求,即将一个复杂的请求分解为多个子请求,每个子请求负责一种功能项,而最初的原始请求负责构成并发送响应给用户;当subrequest访问第三方服务时,首先派生出子请求访问上游服务器,父请求在完全取得上游服务器的响应后再决定处理来自客户端请求的方式;
upstream是从属于用户请求的,subrequest与原始的用户请求相比是一个或多个独立的新请求,并且新的子请求与原始请求之间可以并发的处理;

【2】基于 upstream 的模块开发

upstream模块不产生自己的内容,而是通过请求后端服务器得到内容;Nginx内部封装了请求并取得响应内容的整个过程,所以upstream模块只需要开发若干回调函数,以完成构造请求和解析响应等具体的工作即可;

【2.1】upstream 模块的处理流程

upstream嵌入到一个请求中的方式是设置ngx_http_request_t中的upstream成员,若没有使用upstream机制,则ngx_http_request_t中的upstream成员是NULL空指针,若使用upstream机制,则必须设置upstream成员;

【2.1.1】HTTP 模块启动 upstream 模块的流程

  • 1.调用函数ngx_http_upstream_create为请求创建upstream;
  • 2.设置上游服务器的地址,可通过配置文件nginx.conf配置好上游服务器地址,也可以通过ngx_http_request_t中的成员resolved设置上游服务器地址;
  • 3.设置upstream的回调方法;
  • 4.调用函数ngx_http_upstream_init启动upstream;

【2.1.2】upstream 模块执行的一般流程

整个处理过程中会调用 upstream 的回调方法进行相应的处理,其中最常用的回调方法为 create_request、process_header、finalize_request;

注,upstream 模块处理上游包体的方式

  • 1.当请求结构体ngx_http_request_t中的成员subrequest_in_memory标志位为1时,upstream不转发响应包体到下游,并由HTTP模块实现的input_filter()方法处理包体;
  • 2.当请求结构体ngx_http_request_t中的成员subrequest_in_memory标志位为0时,且ngx_http_upstream_conf_t配置结构体中的成员buffering标志位为1时,upstream将开启更多的内存和磁盘文件用于缓存上游的响应包体(此时,上游网速更快),并转发响应包体;
  • 3.当请求结构体ngx_http_request_t中的成员subrequest_in_memory标志位为0时,且ngx_http_upstream_conf_t配置结构体中的成员buffering标志位为0时,upstream将使用固定大小的缓冲区来转发响应包体;

【2.2】upstream 模块开发代码示例

【2.2.1】Nginx 配置

...

http {

    ...

    server {

        ...

        location /myupstream {
            myupstream;
        }

        ...
        
    }

    ...

}

...

【2.2.2】示例代码

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

// 定义存放上下文的结构体
typedef struct
{
    // 保存 HTTP 处理的状态信息
    ngx_http_status_t           status;
    ngx_str_t					backendServer;
} ngx_http_myupstream_ctx_t;

typedef struct
{
    // 配置 upstream 的数据结构
    ngx_http_upstream_conf_t upstream;
} ngx_http_myupstream_conf_t;

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

static ngx_int_t ngx_http_myupstream_handler(ngx_http_request_t *r);
static void* ngx_http_myupstream_create_loc_conf(ngx_conf_t *cf);
static char *ngx_http_myupstream_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);

static ngx_int_t
mytest_upstream_process_header(ngx_http_request_t *r);
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
};

// commands数组用于定义模块的配置文件参数,每一个数组元素都是ngx_command_t类型
// 数组以ngx_null_command结尾
// #define ngx_null_command  { ngx_null_string, 0, NULL, 0, 0, NULL }
// 空指令,用于在指令数组的最后当做哨兵,结束数组,避免指定长度,类似NULL的作用
static ngx_command_t  ngx_http_myupstream_commands[] =
{
    // 定义mytest配置项的处理,此处指定mytest配置项由ngx_http_mytest函数处理
    {
        ngx_string("myupstream"),
        NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_NOARGS,
        // ngx_http_mytest 是 ngx_command_t 结构体中的 set 成员
        // 当在某个配置块中出现 mytest 配置项时,nginx 将会调用 ngx_http_mytest 方法
        ngx_http_mytest,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },

    ngx_null_command
};

// 定义 HTTP 框架各阶段的回调方法
// 若HTTP框架初始化时无需完成特定的工作则可将回调置为NULL
// 此处设置了 create_loc_conf/merge_loc_conf 的回调
static ngx_http_module_t  ngx_http_myupstream_module_ctx =
{
    NULL,                                           /* preconfiguration */
    NULL,                  		                    /* postconfiguration */
    NULL,                                           /* create main configuration */
    NULL,                                           /* init main configuration */
    NULL,                                           /* create server configuration */
    NULL,                                           /* merge server configuration */
    ngx_http_myupstream_create_loc_conf,       		/* create location configuration */
    ngx_http_myupstream_merge_loc_conf         		/* merge location configuration */
};

// 定义 myupstream 模块
// 回调方法,init_module、init_process、exit_process、exit_master 由 Nginx 框架代码调用,与 HTTP 模块无关
ngx_module_t  ngx_http_myupstream_module =
{
    NGX_MODULE_V1,
    &ngx_http_myupstream_module_ctx,       /* module context */
    ngx_http_myupstream_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
};

//create_loc_conf回调
static void* ngx_http_myupstream_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_myupstream_conf_t  *mycf;
    // 分配ngx_http_myupstream_conf_t结构体内存空间
    mycf = (ngx_http_myupstream_conf_t  *)ngx_pcalloc(cf->pool, sizeof(ngx_http_myupstream_conf_t));
    if (mycf == NULL)
    {
        return NULL;
    }

    //以下简单的硬编码ngx_http_upstream_conf_t结构中的各成员,例如
    //超时时间都设为1分钟,这也是http反向代理模块的默认值
    mycf->upstream.connect_timeout = 60000;
    mycf->upstream.send_timeout = 60000;
    mycf->upstream.read_timeout = 60000;
    mycf->upstream.store_access = 0600;
    //实际上buffering已经决定了将以固定大小的内存作为缓冲区来转发上游的
    //响应包体,这块固定缓冲区的大小就是buffer_size,如果buffering为1
    //就会使用更多的内存缓存来不及发往下游的响应,例如最多使用bufs.num个
    //缓冲区、每个缓冲区大小为bufs.size,另外还会使用临时文件,临时文件的
    //最大长度为max_temp_file_size
    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成员必须要初始化(upstream在解析
    //完上游服务器返回的包头时,会调用
    //ngx_http_upstream_process_headers方法按照hide_headers成员将
    //本应转发给下游的一些http头部隐藏),这里将它赋为
    //NGX_CONF_UNSET_PTR 是为了在merge合并配置项方法中使用
    //upstream模块提供的ngx_http_upstream_hide_headers_hash
    //方法初始化hide_headers 成员
    mycf->upstream.hide_headers = NGX_CONF_UNSET_PTR;
    mycf->upstream.pass_headers = NGX_CONF_UNSET_PTR;

    return mycf;
}

//merge_loc_conf回调
//hide_headers的类型是ngx_array_t动态数组
//(实际上,upstream模块将会通过hide_headers来构造hide_headers_hash散列表)
//由于upstream模块要求hide_headers不可以为NULL,因此必须要初始化hide_headers成员;
//upstream模块提供了ngx_http_upstream_hide_headers_hash方法来初始化hide_headers
//  但仅可在合并配置项方法内使用;
static char *ngx_http_myupstream_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_myupstream_conf_t *prev = (ngx_http_myupstream_conf_t *)parent;
    ngx_http_myupstream_conf_t *conf = (ngx_http_myupstream_conf_t *)child;

    ngx_hash_init_t             hash;
    hash.max_size = 100;
    hash.bucket_size = 1024;
    hash.name = "proxy_headers_hash";
    // 初始化 hide_headers 成员,upstream 模块要求 hide_headers 不为 NULL
    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;
}

//create_request的回调处理函数,在其中构造请求
//mytest_upstream_create_request方法用于创建发送给上游服务器的HTTP请求,upstream模块将回调该函数
static ngx_int_t
mytest_upstream_create_request(ngx_http_request_t *r)
{
    //发往baidu上游服务器的请求很简单,就是模仿正常的搜索请求,
    //以/index.html?q=...的URL来发起搜索请求
    static ngx_str_t backendQueryLine =
        ngx_string("GET /index.html?q=%V HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n");
    ngx_int_t queryLineLen = backendQueryLine.len + r->args.len - 2;
    //必须在内存池中申请内存,这有两点好处:
    //1.在网络情况不佳的情况下,向上游服务器发送请求时,
    //  可能需要epoll多次调度send发送才能完成,
    //  这时必须保证这段内存不会被释放;
    //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_chain_t结构
    //  其中包含着要发送给上游服务器的请求
    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;
}

//process_header的回调处理函数,在其中解析包头
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_myupstream_ctx_t* ctx = ngx_http_get_module_ctx(r, ngx_http_myupstream_module);
    if (ctx == NULL)
    {
        return NGX_ERROR;
    }

    u = r->upstream;

    //http框架提供的ngx_http_parse_status_line方法可以解析http响应行
    //  其输入是收到的字符流和上下文中的ngx_http_status_t结构
    rc = ngx_http_parse_status_line(r, &u->buffer, &ctx->status);
    //返回NGX_AGAIN表示还没有解析出完整的http响应行,需要接收更多的字符流再继续解析
    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;
    }

    //以下表示解析到完整的http响应行,这时会做一些简单的赋值操作,将解析出
    //的信息设置到r->upstream->headers_in结构体中,upstream解析完所
    //有的包头时,就会把headers_in中的成员设置到将要向下游发送的
    //r->headers_out结构体中,即现在向headers_in中设置的信息,最终都会发往下游客户端;
    //为什么不是直接设置r->headers_out而要这样多此一举呢?
    //这是因为upstream希望能够按照ngx_http_upstream_conf_t配置结构体
    //  中的hide_headers等成员对发往下游的响应头部做统一处理
    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头部,设置process_header回调方法为
    //mytest_upstream_process_header,
    //之后再收到的新字符流将由mytest_upstream_process_header解析
    u->process_header = mytest_upstream_process_header;

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

// 解析 HTTP 响应的头部,
// 本例只是简单的将上游服务器发送的 HTTP 头部添加到了请求 r->upstream->headers_in.headers 链表中
// 若有需要特殊处理的HTTP头部,则也应该在mytest_upstream_process_header方法中进行
static ngx_int_t
mytest_upstream_process_header(ngx_http_request_t *r)
{
    ngx_int_t                       rc;
    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响应头部作统一处理;
    //该结构体中存储了需要做统一处理的http头部名称和回调方法
    umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module);

    //循环的解析所有的http头部
    for ( ;; )
    {
        // http框架提供了基础性的ngx_http_parse_header_line方法,用于解析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头部
            h = ngx_list_push(&r->upstream->headers_in.headers);
            if (h == NULL)
            {
                return NGX_ERROR;
            }
            //构造刚刚添加到headers链表中的http头部
            h->hash = r->header_hash;

            h->key.len = r->header_name_end - r->header_name_start;
            h->value.len = r->header_end - r->header_start;
            //必须由内存池中分配存放http头部的内存
            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头部都解析完毕
        //  接下来再接收到的都将是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;
                }

                h->hash = ngx_hash(ngx_hash(ngx_hash(ngx_hash(
                                                         ngx_hash('s', 'e'), 'r'), 'v'), 'e'), 'r');

                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;
                }

                h->hash = ngx_hash(ngx_hash(ngx_hash('d', 'a'), 't'), 'e');

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

            return NGX_OK;
        }

        //如果返回NGX_AGAIN则表示状态机还没有解析到完整的http头部,
        //  要求upstream模块继续接收新的字符流再交由process_header回调方法解析
        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;
    }
}

// 主要完成资源释放的工作
// upstream模块要求必须实现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 可以是 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 方法处理该请求
    //
    // 请求处理函数的原型,见 src/http/ngx_http_request.h
    // typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);
    clcf->handler = ngx_http_myupstream_handler;

    return NGX_CONF_OK;
}

//启动upstream
static ngx_int_t
ngx_http_myupstream_handler(ngx_http_request_t *r)
{
    //首先建立http上下文结构体ngx_http_myupstream_ctx_t
    ngx_http_myupstream_ctx_t* myctx = ngx_http_get_module_ctx(r, ngx_http_myupstream_module);
    if (myctx == NULL)
    {
        myctx = ngx_palloc(r->pool, sizeof(ngx_http_myupstream_ctx_t));
        if (myctx == NULL)
        {
            return NGX_ERROR;
        }
        //将新建的上下文与请求关联起来
        ngx_http_set_ctx(r, myctx, ngx_http_myupstream_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;
    }

    //得到配置结构体ngx_http_myupstream_conf_t
    ngx_http_myupstream_conf_t  *mycf = (ngx_http_myupstream_conf_t  *) ngx_http_get_module_loc_conf(r, ngx_http_myupstream_module);
    ngx_http_upstream_t *u = r->upstream;
    //这里用配置文件中的结构体来赋给r->upstream->conf成员
    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);
    char* pDmsIP = inet_ntoa(*(struct in_addr*) (pHost->h_addr_list[0]));
    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->resolved->port = htons((in_port_t)80);


    //设置三个必须实现的回调方法
    //create_request、process_header、finalize_request
    u->create_request = mytest_upstream_create_request;
    u->process_header = mytest_process_status_line;
    u->finalize_request = mytest_upstream_finalize_request;

    //这里必须将count成员加1
    //  告知ngx_http_myupstream_handler方法暂时不要销毁请求
    //  从而upstream机制接下来可以接管请求的处理工作
    //当前请求的引用计数增1
    r->main->count++;
    //启动upstream
    ngx_http_upstream_init(r);
    //必须返回NGX_DONE
    return NGX_DONE;
}

【3】基于 subrequest 的模块开发

subrequest 是分解复杂请求的一种设计模式,可以把原始请求分解为多个子请求,使得诸多请求协同完成一个用户请求,并且每个请求只关注一个功能;若不是完全将上游服务器的响应包体转发到下游客户端,基本都会使用 subrequest 创建子请求,并由子请求使用 upstream 机制访问上游服务器,并由父请求根据上游响应重新构造返回给下游客户端的响应;

开发 subrequest 模块的主要步骤

  • 1.在 nginx.conf 配置文件中配置好子请求的处理方式;
  • 2.启动 subrequest 子请求;
  • 3.实现子请求执行结束时的回调函数;
  • 4.实现父请求被激活时的回调函数;

【3.1】subrequest 模块的处理流程

【3.1.1】启动 subrequest

启动 subrequest 时序图示

  • 1) Nginx主循环中会定期地调用事件模块,检查是否有网络事件发生
  • 2) 事件模块发现该请求的回调方法属于HTTP框架,则交由HTTP框架来处理请求
  • 3) 根据解析完的URI来决定使用哪个location下的模块来处理该请求
  • 4) 调用mytest模块的ngx_http_mytest_handler方法处理该请求
  • 5) 设置subrequest子请求的URI及回调方法
  • 6) 调用ngx_http_subrequest方法创建子请求
  • 7) 创建的子请求会添加到原始请求的posted_requests链表中,这样保证第10步时会在父请求返回NGX_DONE的情况下开始执行子请求
  • 8) ngx_http_subrequest方法执行完毕,子请求创建成功
  • 9) ngx_http_mytest_handler方法执行完毕,返回NGX_DONE,这样父请求不会被销毁,将等待以后的再次激活
  • 10) HTTP框架执行完当前请求(父请求)后,检查posted_requests链表中是否还有子请求,若存在子请求,则调用子请求的write_event_handler方法
  • 11) 根据子请求的URI(第5步中建立),检查nginx.conf文件中所有的location配置,确定应由哪个模块来执行子请求
  • 12) 调用反向代理模块的入口方法ngx_http_proxy_handler来处理子请求
  • 13) 由于反向代理模块使用了upstream机制,所以它也要通过许多次的异步调用才能完整地处理完子请求,这时它的入口方法会返回NGX_DONE
  • 14) 再次检查是否还有子请求,这时会发现已经没有子请求需要执行了,当然,子请求可以继续建立新的子请求;
  • 15) 当第2步中的网络读取事件处理完毕后,交还控制权给事件模块
  • 16) 当本轮网络事件处理完毕后,交还控制权给Nginx主循环

【3.1.2】转发多个子请求的响应包体

子请求转发 HTTP 包体过程的时序图示

  • 1) Nginx主循环中会定期地调用事件模块,检查是否有网络事件发生
  • 2) 事件模块发现这个请求的回调方法属于反向代理模块的接收HTTP包体阶段,于是交由反向代理模块来处理
  • 3) 读取上游服务器发来的包体
  • 4) 对于接收到的字符流,会依次调用所有的HTTP过滤器模块来转发包体;其中,还会调用到postpone过滤模块,该模块将会处理设置在子请求中的ngx_http_postponed_request_t链表
  • 5) postpone模块使用ngx_http_postpone_filter方法将待转发的包体以合适的顺序再进行整理发送到下游客户端;若ngx_http_postpone_filter方法没有通过ngx_http_next_filter方法则继续调用其他HTTP过滤模块(如由于顺序的原因而暂停转发某个子请求的响应包体),将会直接跳到第7步,否则继续处理这段接收到的包体(第6步)
  • 6) 继续调用其他HTTP过滤模块,待所有的过滤模块执行完毕后将控制权交还给反向代理模块
  • 7) 当第2步中的网络读取事件处理完毕后,交还控制权给事件模块
  • 8) 当本轮网络事件处理完毕后,交还控制权给Nginx主循环

【3.1.3】激活父请求

子请求激活父请求时序图示

  • 1) Nginx主循环中会定期地调用事件模块,检查是否有网络事件发生
  • 2) 如果事件模块检测到连接关闭事件,而这个请求的处理方法属于upstream模块,则交由upstream模块来处理请求
  • 3) upstream模块开始调用ngx_http_upstream_finalize_request方法来结束upstream机制下的请求
  • 4) 调用HTTP框架提供的ngx_http_finalize_request方法来结束子请求
  • 5) ngx_http_finalize_request方法会检查当前的请求是否是子请求,如果是子请求,则会回调post_subrequest成员中的handler方法即调用mytest_subrequest_post_handler方法
  • 6) 在实现的子请求回调方法中,解析子请求返回的响应包;
    • 注意,这时需要通过write_event_handler设置父请求被激活后的回调方法(因为此时父请求的回调方法已经被HTTP框架设置为ngx_http_request_empty_handler方法(不做任何处理)
  • 7) 子请求的回调方法执行完毕后,交由HTTP框架的ngx_http_finalize_request方法继续向下执行
  • 8) ngx_http_finalize_request方法执行完毕
  • 9) HTTP框架如果发现当前请求后还有父请求需要执行,则调用父请求的write_event_handler回调方法
  • 10) 这里可以根据第6步中解析子请求响应后的结果来构造响应包
  • 11) 调用无阻塞的ngx_http_send_header、ngx_http_output_filter发送方法向客户端发送响应包
  • 12) 无阻塞发送方法会立刻返回;即使目前未发送完,Nginx之后也会异步地发送完所有的响应包,然后再结束请求
  • 13) 父请求的回调方法执行完毕
  • 14) 当第2步中的上游服务器连接关闭事件处理完毕后,交还控制权给事件模块
  • 15) 当本轮网络事件处理完毕后,交还控制权给Nginx主循环

【3.2】subrequest 模块开发代码示例

【3.2.1】Nginx 配置

...

http {

    ...

    server {

        ...

        location /list {
            proxy_pass http://221.229.218.22:10812/api/v1/web/dict/query;
            # 不希望第三方服务发来的 HTTP 包体做过 gzip 压缩
            proxy_set_header  Accept-Encoding  "";
        }

        location /query {
            mysubrequest;
        }

        ...

    }

    ...

}

...

【3.2.2】示例代码

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

// 请求上下文用于保存子请求回调方法中解析出来的数据信息
typedef struct
{
    ngx_str_t		stock[6];
} ngx_http_mysubrequest_ctx_t;

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

static ngx_int_t ngx_http_mysubrequest_handler(ngx_http_request_t *r);
static ngx_int_t mytest_subrequest_post_handler(ngx_http_request_t *r, void *data, ngx_int_t rc);
static void
mytest_post_handler(ngx_http_request_t * r);

// commands数组用于定义模块的配置文件参数,每一个数组元素都是ngx_command_t类型
// 数组以ngx_null_command结尾
// #define ngx_null_command  { ngx_null_string, 0, NULL, 0, 0, NULL }
// 空指令,用于在指令数组的最后当做哨兵,结束数组,避免指定长度,类似NULL的作用
static ngx_command_t  ngx_http_mysubrequest_commands[] =
{
    // 定义mysubrequest配置项的处理,此处指定mytest配置项由ngx_http_mytest函数处理
    {
        ngx_string("mysubrequest"),
        NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_NOARGS,
        // ngx_http_mytest 是 ngx_command_t 结构体中的 set 成员
        // 当在某个配置块中出现 mysubrequest 配置项时,nginx 将会调用 ngx_http_mytest 方法        
        ngx_http_mytest,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },

    ngx_null_command
};

// 定义 HTTP 框架各阶段的回调方法
// 若HTTP框架初始化时无需完成特定的工作则可将回调置为NULL
static ngx_http_module_t  ngx_http_mysubrequest_module_ctx =
{
    NULL,                               /* preconfiguration */
    NULL,                  		        /* postconfiguration */
    NULL,                               /* create main configuration */
    NULL,                               /* init main configuration */
    NULL,                               /* create server configuration */
    NULL,                               /* merge server configuration */
    NULL,       			            /* create location configuration */
    NULL         			            /* merge location configuration */
};

// 定义 mysubrequest 模块
// 回调方法,init_module、init_process、exit_process、exit_master 由 Nginx 框架代码调用,与 HTTP 模块无关
ngx_module_t  ngx_http_mysubrequest_module =
{
    NGX_MODULE_V1,
    &ngx_http_mysubrequest_module_ctx,     /* module context */
    ngx_http_mysubrequest_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 ngx_int_t mytest_subrequest_post_handler(ngx_http_request_t *r,
                                                void *data, ngx_int_t rc)
{
    //当前请求r是子请求,它的parent成员就指向父请求
    ngx_http_request_t          *pr = r->parent;
    //注意,上下文是保存在父请求中的,所以要由pr中取上下文
    //其实有更简单的方法,即参数data就是上下文,初始化subrequest时指定
    ngx_http_mysubrequest_ctx_t* myctx = ngx_http_get_module_ctx(pr, ngx_http_mysubrequest_module);
    //获取请求状态码
    pr->headers_out.status = r->headers_out.status;
    //如果返回NGX_HTTP_OK(也就是200)意味着访问成功,接着将开始解析http包体
    if (r->headers_out.status == NGX_HTTP_OK)
    {
        int flag = 0;
        //在不转发响应时,buffer中会保存着上游服务器的响应;特别是在使用
        //反向代理模块访问上游服务器时,如果它使用upstream机制时没有重定义
        //input_filter方法,upstream机制默认的input_filter方法会试图
        //把所有的上游响应全部保存到buffer缓冲区中
        ngx_buf_t* pRecvBuf = &r->upstream->buffer;

        //解析上游服务器的响应,并将解析出的值赋到上下文结构体myctx->stock数组中
        for (; pRecvBuf->pos != pRecvBuf->last; pRecvBuf->pos++)
        {
            if (*pRecvBuf->pos == ',' || *pRecvBuf->pos == '\"')
            {
                if (flag > 0)
                {
                    myctx->stock[flag - 1].len = pRecvBuf->pos - myctx->stock[flag - 1].data;
                }
                flag++;
                myctx->stock[flag - 1].data = pRecvBuf->pos + 1;
            }

            if (flag > 6)
                break;
        }
    }

    //这一步很重要,设置接下来父请求的回调方法
    pr->write_event_handler = mytest_post_handler;

    return NGX_OK;
}

//父请求的回调方法
static void
mytest_post_handler(ngx_http_request_t * r)
{
    //如果没有返回200则直接把错误码发回用户
    if (r->headers_out.status != NGX_HTTP_OK)
    {
        ngx_http_finalize_request(r, r->headers_out.status);
        return;
    }

    //当前请求是父请求,直接取其上下文
    ngx_http_mysubrequest_ctx_t* myctx = ngx_http_get_module_ctx(r, ngx_http_mysubrequest_module);

    //定义发给用户的http包体内容,格式为
    //stock[…],Today current price: …, volumn: …
    ngx_str_t output_format = ngx_string("stock[%V],Today current price: %V, volumn: %V");

    //计算待发送包体的长度
    int bodylen = output_format.len + myctx->stock[0].len
                  + myctx->stock[1].len + myctx->stock[4].len - 6;
    r->headers_out.content_length_n = bodylen;

    //在内存池上分配内存保存将要发送的包体
    ngx_buf_t* b = ngx_create_temp_buf(r->pool, bodylen);
    ngx_snprintf(b->pos, bodylen, (char*)output_format.data,
                 &myctx->stock[0], &myctx->stock[1], &myctx->stock[4]);
    b->last = b->pos + bodylen;
    b->last_buf = 1;

    // 构造发送时的 ngx_chain_t 结构体
    ngx_chain_t out;
    out.buf = b;
    out.next = NULL;
    //设置Content-Type,注意汉字编码
    static ngx_str_t type = ngx_string("text/plain; charset=GBK");
    r->headers_out.content_type = type;
    r->headers_out.status = NGX_HTTP_OK;

    r->connection->buffered |= NGX_HTTP_WRITE_BUFFERED;
    // ngx_http_send_header 方法向客户端发送 HTTP 响应包头
    ngx_int_t ret = ngx_http_send_header(r);
    // ngx_http_output_filter 方法向客户端发送 HTTP 响应包体
    ret = ngx_http_output_filter(r, &out);

    //注意,这里发送完响应后必须手动调用ngx_http_finalize_request
    //结束请求,因为这时http框架不会再帮忙调用它
    ngx_http_finalize_request(r, ret);
}


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

    // 首先找到 mytest 配置项所属的配置块,clcf 可以是 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_mysubrequest_handler 方法处理该请求
    //
    // 请求处理函数的原型,见 src/http/ngx_http_request.h
    // typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);
    clcf->handler = ngx_http_mysubrequest_handler;

    return NGX_CONF_OK;
}

// ngx_http_request_t中保存了请求的信息
static ngx_int_t
ngx_http_mysubrequest_handler(ngx_http_request_t * r)
{
    //创建http上下文
    ngx_http_mysubrequest_ctx_t* myctx = ngx_http_get_module_ctx(r, ngx_http_mysubrequest_module);
    if (myctx == NULL)
    {
        myctx = ngx_palloc(r->pool, sizeof(ngx_http_mysubrequest_ctx_t));
        if (myctx == NULL)
        {
            return NGX_ERROR;
        }

        //将上下文设置到原始请求r中
        ngx_http_set_ctx(r, myctx, ngx_http_mysubrequest_module);
    }

    // ngx_http_post_subrequest_t结构体会决定子请求的回调方法
    ngx_http_post_subrequest_t *psr = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t));
    if (psr == NULL)
    {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    //设置子请求回调方法为mytest_subrequest_post_handler
    psr->handler = mytest_subrequest_post_handler;

    //data设为myctx上下文,这样回调mytest_subrequest_post_handler时传入的data参数就是myctx
    psr->data = myctx;

    //子请求的URI前缀是/list
    //与nginx.conf中配置的子请求location中的URI是一致的
    //此处创建了子请求从而向/list发送请求
    ngx_str_t sub_prefix = ngx_string("/list");
    ngx_str_t sub_location;
    sub_location.len = sub_prefix.len + r->args.len;
    sub_location.data = ngx_palloc(r->pool, sub_location.len);
    ngx_snprintf(sub_location.data, sub_location.len,
                 "%V%V", &sub_prefix, &r->args);

    //sr就是子请求
    ngx_http_request_t *sr;
    //调用ngx_http_subrequest创建子请求
    //该函数只会返回NGX_OK或者NGX_ERROR
    //返回NGX_OK时,sr就已经是合法的子请求;
    //注意,此处的NGX_HTTP_SUBREQUEST_IN_MEMORY参数将告诉upstream模块把上
    //游服务器的响应全部保存在子请求的sr->upstream->buffer内存缓冲区中
    ngx_int_t rc = ngx_http_subrequest(r, &sub_location, NULL, &sr, psr, NGX_HTTP_SUBREQUEST_IN_MEMORY);
    if (rc != NGX_OK)
    {
        return NGX_ERROR;
    }

    //必须返回NGX_DONE
    return NGX_DONE;
}

参考致谢
本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。

【1】深入理解 Nginx 模块开发与架构解析

【2】nginx subrequest的实现解析

【3】nginx模块开发(三)upstream模块

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值