1. 引言

摘要:本文从 Nginx 模块开发视角,深入剖析 subrequest 子请求机制的源码实现。内容涵盖 ngx_http_request_t 核心数据结构、ngx_http_subrequest 创建函数的调用链、响应捕获机制(ngx_http_send_special 与完成回调),以及引用计数与内存池的生命周期管理。文章提供两个完整的 C 模块开发示例:单个 subrequest 的发起与响应捕获、并行 subrequest 聚合模块(BFF 场景),并附有 config 编译配置、nginx.conf 配置示例及 GDB 调试技巧,帮助读者掌握在自定义 Nginx 模块中使用 subrequest 的完整能力。

Nginx 的 subrequest(子请求)机制是其内部请求处理的核心特性之一。与普通 HTTP 请求不同,subrequest 不经过网络层,而是在 Nginx 事件循环内部直接创建和处理,主请求(main request)可以发起一个或多个子请求,并将子请求的响应捕获后用于权限校验、内容聚合、动态组装等场景。

本文将从 Nginx 模块开发 的视角出发,深入分析 subrequest 的源码实现,包括 ngx_http_subrequest 函数的调用链、请求上下文传递机制、响应捕获方式,并给出完整的 C 模块开发示例,帮助读者掌握在自定义 Nginx 模块中使用 subrequest 的能力。

2. subrequest 源码核心数据结构

2.1 请求对象 ngx_http_request_t

在 Nginx 源码 src/http/ngx_http_request.h 中,每个 HTTP 请求由一个 ngx_http_request_t 结构体表示。subrequest 相关的关键字段如下:

// src/http/ngx_http_request.h(简化)
typedef struct ngx_http_request_s {
    // 主请求指针,subrequest 指向其父请求
    struct ngx_http_request_s  *main;

    // 父请求指针,subrequest 指向发起者
    struct ngx_http_request_s  *parent;

    // 请求方法(GET/POST 等)
    ngx_uint_t                  method;

    // 请求 URI
    ngx_str_t                   uri;

    // 请求参数 args
    ngx_str_t                   args;

    // 请求头
    ngx_http_headers_in_t       headers_in;

    // 响应头
    ngx_http_headers_out_t      headers_out;

    // 响应体链表
    ngx_chain_t                *out;

    // 请求池,subrequest 拥有独立的内存池
    ngx_pool_t                 *pool;

    // 请求阶段
    ngx_uint_t                  phase_handler;

    // 标志位:是否为 subrequest
    unsigned                    subrequest:1;

    // 标志位:父请求是否已完成(用于 subrequest 完成时的判断)
    unsigned                    parent_done:1;

    // ... 其他字段
} ngx_http_request_t;

关键设计

  • main 指向整个请求链的根请求,parent 指向直接发起者。
  • 每个 subrequest 拥有独立的 pool(内存池),subrequest 完成后可独立释放。
  • subrequest:1 标志位用于区分主请求与子请求。

2.2 subrequest 的创建函数 ngx_http_subrequest

subrequest 的创建入口在 src/http/ngx_http_core_module.c 中:

// src/http/ngx_http_core_module.c
ngx_int_t
ngx_http_subrequest(ngx_http_request_t *r,
    ngx_str_t *uri, ngx_str_t *args,
    ngx_http_request_t **psr,
    ngx_http_post_subrequest_t *ps,
    ngx_uint_t flags)
{
    ngx_http_request_t  *sr;
    ngx_http_core_srv_conf_t  *cscf;

    // 1. 从主请求的内存池中分配 subrequest 结构体
    sr = ngx_pcalloc(r->pool, sizeof(ngx_http_request_t));
    if (sr == NULL) {
        return NGX_ERROR;
    }

    // 2. 设置父子关系
    sr->main = r->main ? r->main : r;
    sr->parent = r;
    r->main->count++;

    // 3. 复制关键字段
    sr->method = NGX_HTTP_GET;
    sr->uri = *uri;
    sr->args = *args;
    sr->schema = r->schema;

    // 4. 设置 subrequest 标志
    sr->subrequest = 1;

    // 5. 创建独立的内存池
    sr->pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, r->connection->log);
    if (sr->pool == NULL) {
        return NGX_ERROR;
    }

    // 6. 复制请求头(可选)
    sr->headers_in = r->headers_in;

    // 7. 注册完成回调
    if (ps) {
        sr->post_subrequest = ps;
    }

    // 8. 将 subrequest 加入事件循环
    cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
    ngx_http_handler(sr, cscf);

    *psr = sr;
    return NGX_OK;
}

源码分析要点

  • subrequest 不创建新的 TCP 连接,直接从连接的空闲请求池分配。
  • sr->main->count++ 增加主请求的引用计数,确保主请求在所有 subrequest 完成前不会结束。
  • ngx_http_handler 将 subrequest 加入 Nginx 的阶段处理流程,与主请求走相同的处理管道。

3. 开发第一个 subrequest 模块:从源码编译

3.1 模块目录结构

my_subrequest_module/
├── config          # Nginx 模块编译配置
└── ngx_http_my_subrequest_module.c  # 模块源码

3.2 config 文件

# my_subrequest_module/config
ngx_addon_name=ngx_http_my_subrequest_module
HTTP_MODULES="$HTTP_MODULES ngx_http_my_subrequest_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_my_subrequest_module.c"

3.3 模块源码:发起 subrequest 并捕获响应

// my_subrequest_module/ngx_http_my_subrequest_module.c
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

// 模块上下文结构体
typedef struct {
    ngx_str_t  backend_uri;
} ngx_http_my_subrequest_loc_conf_t;

// 前向声明
static ngx_int_t ngx_http_my_subrequest_handler(ngx_http_request_t *r);
static void *ngx_http_my_subrequest_create_loc_conf(ngx_conf_t *cf);
static char *ngx_http_my_subrequest_merge_loc_conf(ngx_conf_t *cf,
    void *parent, void *child);

// subrequest 完成回调函数
static ngx_int_t
ngx_http_my_subrequest_post_handler(ngx_http_request_t *sr, void *data,
    ngx_int_t rc)
{
    ngx_http_request_t *r = data;  // 主请求

    ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
        "subrequest completed with status %i, body length %z",
        sr->headers_out.status, sr->out ? ngx_chain_length(sr->out) : 0);

    // 将 subrequest 的响应体复制到主请求的输出链
    if (sr->out) {
        // 注意:这里简化处理,实际需要将响应体数据复制到主请求的 pool 中,避免 subrequest 释放后内存失效
        r->out = sr->out;
        r->headers_out.content_length_n = sr->headers_out.content_length_n;
    }

    r->headers_out.status = sr->headers_out.status;
    return NGX_OK;
}

// 模块主处理函数
static ngx_int_t
ngx_http_my_subrequest_handler(ngx_http_request_t *r)
{
    ngx_http_my_subrequest_loc_conf_t *mlcf;
    ngx_http_request_t *sr;
    ngx_http_post_subrequest_t *ps;
    ngx_int_t rc;

    // 只处理主请求,避免 subrequest 递归
    if (r->subrequest) {
        return NGX_DECLINED;
    }

    mlcf = ngx_http_get_module_loc_conf(r, ngx_http_my_subrequest_module);

    // 1. 分配完成回调结构体
    ps = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t));
    if (ps == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
    ps->handler = ngx_http_my_subrequest_post_handler;
    ps->data = r;

    // 2. 发起 subrequest
    rc = ngx_http_subrequest(r, &mlcf->backend_uri, NULL, &sr, ps,
                             NGX_HTTP_SUBREQUEST_WAITED);
    if (rc != NGX_OK) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    // 3. 挂起主请求,等待 subrequest 完成
    ngx_http_set_ctx(r, sr, ngx_http_my_subrequest_module);
    return NGX_DONE;
}

// 配置指令定义
static ngx_command_t ngx_http_my_subrequest_commands[] = {
    { ngx_string("my_backend_uri"),
      NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_my_subrequest_loc_conf_t, backend_uri),
      NULL },

    ngx_null_command
};

// 模块上下文
static ngx_http_module_t ngx_http_my_subrequest_module_ctx = {
    NULL,                                  // preconfiguration
    NULL,                                  // postconfiguration
    NULL,                                  // create main configuration
    NULL,                                  // init main configuration
    NULL,                                  // create server configuration
    NULL,                                  // merge server configuration
    ngx_http_my_subrequest_create_loc_conf, // create location configuration
    ngx_http_my_subrequest_merge_loc_conf   // merge location configuration
};

// 模块定义
ngx_module_t ngx_http_my_subrequest_module = {
    NGX_MODULE_V1,
    &ngx_http_my_subrequest_module_ctx,    // module context
    ngx_http_my_subrequest_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_my_subrequest_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_my_subrequest_loc_conf_t *conf;

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_my_subrequest_loc_conf_t));
    if (conf == NULL) {
        return NULL;
    }

    ngx_str_set(&conf->backend_uri, "/internal/default");
    return conf;
}

// 配置结构体合并
static char *
ngx_http_my_subrequest_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_my_subrequest_loc_conf_t *prev = parent;
    ngx_http_my_subrequest_loc_conf_t *conf = child;

    ngx_conf_merge_str_value(conf->backend_uri, prev->backend_uri,
                             "/internal/default");
    return NGX_CONF_OK;
}

3.4 编译模块

# 下载 Nginx 源码
wget http://nginx.org/download/nginx-1.26.0.tar.gz
tar -zxvf nginx-1.26.0.tar.gz
cd nginx-1.26.0

# 配置时添加自定义模块
./configure \
  --prefix=/usr/local/nginx \
  --add-module=/path/to/my_subrequest_module

make -j$(nproc)
sudo make install

3.5 nginx.conf 配置示例

server {
    listen 80;
    server_name example.com;

    location / {
        my_backend_uri /internal/data;
        # 模块处理函数会自动发起 subrequest
    }

    location /internal/data {
        internal;
        return 200 '{"status": "ok", "data": "from subrequest"}';
        add_header Content-Type application/json;
    }
}

4. subrequest 响应捕获机制源码分析

4.1 响应体传递:ngx_http_send_special

subrequest 的响应体通过 Nginx 的过滤器链传递。关键源码在 src/http/ngx_http_request.c

// src/http/ngx_http_request.c
ngx_int_t
ngx_http_send_special(ngx_http_request_t *r, ngx_uint_t flags)
{
    ngx_chain_t  *cl;
    ngx_buf_t    *b;

    // 创建特殊标记的 buffer
    b = ngx_calloc_buf(r->pool);
    if (b == NULL) {
        return NGX_ERROR;
    }

    // 设置 subrequest 完成标记
    if (flags & NGX_HTTP_LAST) {
        b->last_buf = 1;  // 标记为最后一个 buffer
    }

    cl = ngx_alloc_chain_link(r->pool);
    if (cl == NULL) {
        return NGX_ERROR;
    }

    cl->buf = b;
    cl->next = NULL;

    // 通过过滤器链发送
    return ngx_http_output_filter(r, cl);
}

当 subrequest 的过滤器链处理完所有响应体后,会发送一个带有 last_buf 标记的 buffer,触发 ngx_http_finalize_request 完成 subrequest。

4.2 完成回调触发机制

// src/http/ngx_http_request.c(简化)
void
ngx_http_finalize_request(ngx_http_request_t *r, ngx_int_t rc)
{
    // 如果是 subrequest,调用 post_subrequest 回调
    if (r->subrequest && r->post_subrequest) {
        rc = r->post_subrequest->handler(r, r->post_subrequest->data, rc);
    }

    // 减少主请求引用计数
    if (r != r->main) {
        r->main->count--;
    }

    // 如果主请求引用计数为 0,真正结束主请求
    if (r->main->count == 0) {
        ngx_http_finalize_request(r->main, NGX_DONE);
    }
}

关键设计

  • subrequest 结束时调用 post_subrequest->handler,将控制权交回主请求。
  • 主请求通过 count 引用计数管理 subrequest 的生命周期,跟踪所有活跃的 subrequest。
  • 只有当所有 subrequest 都完成后,主请求才会真正结束。

5. 实战:并行 subrequest 聚合模块

5.1 场景描述

构建一个 BFF(Backend for Frontend)模块,并行发起多个 subrequest 获取用户信息和订单数据,然后组装成统一的 JSON 响应。

5.2 模块源码:使用 ngx_http_subrequest 并行调用

// ngx_http_aggregate_module.c(核心片段)

/*
 * 聚合上下文结构体 —— 用于在多个 subrequest 之间共享状态。
 * 每个主请求对应一个 ctx 实例,贯穿整个并行请求的生命周期。
 */
typedef struct {
    ngx_http_request_t *requests[2];  // 保存两个 subrequest 的请求指针,用于在回调中区分是哪个子请求完成
    ngx_str_t           responses[2]; // 分别存储两个 subrequest 的响应体字符串
    ngx_uint_t          completed;    // 已完成的 subrequest 计数,当等于 2 时触发最终组装
    ngx_http_request_t *main_request; // 保存主请求指针,回调中通过它发送最终响应
} ngx_http_aggregate_ctx_t;

/*
 * subrequest 完成回调函数 —— 每个 subrequest 结束时自动调用。
 * 参数 sr:刚刚完成的 subrequest 请求对象
 * 参数 data:发起 subrequest 时传入的 ctx 指针
 * 参数 rc:subrequest 的结束状态码
 */
static ngx_int_t
ngx_http_aggregate_post_handler(ngx_http_request_t *sr, void *data,
    ngx_int_t rc)
{
    ngx_http_aggregate_ctx_t *ctx = data;  // 恢复聚合上下文
    ngx_http_request_t *r = ctx->main_request;  // 获取主请求
    ngx_uint_t i;

    /* 步骤一:遍历 requests 数组,定位当前完成的 subrequest 是哪一个 */
    for (i = 0; i < 2; i++) {
        if (ctx->requests[i] == sr) {
            /* 步骤二:保存 subrequest 的响应体到 ctx 中 */
            if (sr->out) {
                /*
                 * 注意:sr->out 属于 subrequest 的内存池,subrequest 结束后
                 * 该内存池可能被销毁,因此必须将数据复制到主请求的内存池中。
                 * 这里先分配主请求 pool 的内存,实际生产代码需要遍历链表逐段复制。
                 */
                ctx->responses[i].data = ngx_palloc(r->pool,
                    ngx_chain_length(sr->out) + 1);
                // 实际需要遍历链表复制,此处为简化处理
            }
            break;  // 找到后退出循环
        }
    }

    ctx->completed++;  // 递增已完成计数

    /* 步骤三:检查是否所有 subrequest 都已完成,若是则组装最终响应 */
    if (ctx->completed == 2) {
        ngx_str_t json;
        /*
         * 在主请求的 pool 中分配 JSON 组装缓冲区。
         * 使用主请求 pool 确保数据在发送完成前始终有效。
         */
        json.data = ngx_palloc(r->pool, 256);
        json.len = ngx_sprintf(json.data,
            "{\"user\":%V,\"orders\":%V}",
            &ctx->responses[0], &ctx->responses[1]) - json.data;

        /* 设置响应状态码和 Content-Length 头 */
        r->headers_out.status = NGX_HTTP_OK;
        r->headers_out.content_length_n = json.len;
        ngx_http_send_header(r);  // 发送响应头

        /* 构造响应体 buffer 并挂接到输出链 */
        ngx_buf_t *b = ngx_create_temp_buf(r->pool, json.len);
        b->last = ngx_copy(b->pos, json.data, json.len);
        ngx_chain_t *cl = ngx_alloc_chain_link(r->pool);
        cl->buf = b;
        cl->next = NULL;

        /* 通过过滤器链发送最终响应给客户端 */
        return ngx_http_output_filter(r, cl);
    }

    return NGX_OK;  // 还有 subrequest 未完成,继续等待
}

/*
 * 模块主处理函数 —— 入口函数,负责初始化上下文并并行发起 subrequest。
 * 注意:该函数返回 NGX_DONE 表示主请求被挂起,等待 subrequest 完成后恢复。
 */
static ngx_int_t
ngx_http_aggregate_handler(ngx_http_request_t *r)
{
    ngx_http_aggregate_ctx_t *ctx;
    ngx_http_post_subrequest_t *ps[2];  // 两个 subrequest 的完成回调结构体
    ngx_str_t uris[2] = {
        ngx_string("/internal/user_info"),   // 第一个 subrequest 的目标 URI
        ngx_string("/internal/order_stats")  // 第二个 subrequest 的目标 URI
    };
    ngx_int_t rc;

    /* 步骤一:获取或创建聚合上下文(分配上下文) */
    ctx = ngx_http_get_module_ctx(r, ngx_http_aggregate_module);
    if (ctx == NULL) {
        /*
         * 首次进入时,在主请求的 pool 中分配上下文结构体。
         * 使用主请求 pool 确保 ctx 在整个请求生命周期内有效。
         */
        ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_aggregate_ctx_t));
        if (ctx == NULL) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;  // 内存分配失败
        }
        ngx_http_set_ctx(r, ctx, ngx_http_aggregate_module);  // 将 ctx 绑定到主请求
    }

    ctx->main_request = r;  // 保存主请求指针,供回调函数使用

    /* 步骤二:并行发起两个 subrequest(发起并行请求) */
    for (ngx_uint_t i = 0; i < 2; i++) {
        /*
         * 为每个 subrequest 分配回调结构体。
         * 注意:必须在主请求 pool 中分配,因为 subrequest 完成后其 pool 可能被销毁。
         */
        ps[i] = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t));
        ps[i]->handler = ngx_http_aggregate_post_handler;  // 设置完成回调函数
        ps[i]->data = ctx;  // 传入 ctx,回调中通过 data 恢复上下文

        /*
         * 调用 ngx_http_subrequest 创建 subrequest。
         * 参数说明:
         *   r          - 主请求(父请求)
         *   &uris[i]   - subrequest 的目标 URI
         *   NULL       - 无查询参数
         *   &ctx->requests[i] - 输出参数,返回 subrequest 的请求指针
         *   ps[i]      - 完成回调结构体
         *   NGX_HTTP_SUBREQUEST_WAITED - 标志位:父请求是否已完成(用于 subrequest 完成时的判断)
         */
        rc = ngx_http_subrequest(r, &uris[i], NULL, &ctx->requests[i],
                                 ps[i], NGX_HTTP_SUBREQUEST_WAITED);
        if (rc != NGX_OK) {
            /*
             * 注意:如果某个 subrequest 创建失败,应清理已创建的 subrequest。
             * 生产代码中需要遍历已创建的 subrequest 并调用 ngx_http_finalize_request 清理。
             */
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }
    }

    /*
     * 返回 NGX_DONE 挂起主请求的处理流程。
     * Nginx 事件循环会继续调度 subrequest,当所有 subrequest 完成后,
     * 通过回调函数 ngx_http_aggregate_post_handler 恢复主请求并发送响应。
     */
    return NGX_DONE;
}

5.3 配置示例

server {
    listen 80;
    server_name api.example.com;

    location /dashboard {
        aggregate_module on;
    }

    location /internal/user_info {
        internal;
        proxy_pass http://user_service:8083;
    }

    location /internal/order_stats {
        internal;
        proxy_pass http://order_service:8084;
    }
}

6. subrequest 与主请求的生命周期管理

6.1 引用计数机制

Nginx 通过 r->main->count 管理 subrequest 的生命周期。每次创建 subrequest 时 count++,每次 subrequest 完成时 count--。只有当 count == 0 时,主请求才会真正结束。

// 创建 subrequest 时
sr->main = r->main ? r->main : r;
r->main->count++;

// subrequest 完成时
if (r != r->main) {
    r->main->count--;
}
if (r->main->count == 0) {
    ngx_http_finalize_request(r->main, NGX_DONE);
}

6.2 内存池管理

每个 subrequest 拥有独立的内存池,subrequest 完成后可以立即释放,不阻塞主请求的内存回收:

// subrequest 完成时释放内存池
if (r->pool) {
    ngx_destroy_pool(r->pool);
    r->pool = NULL;
}

7. 源码级调试技巧

7.1 使用 GDB 调试 subrequest

# 编译时保留调试符号
./configure --with-debug --prefix=/usr/local/nginx
make -j$(nproc)
sudo make install

# GDB 调试
gdb /usr/local/nginx/sbin/nginx
(gdb) set follow-fork-mode child
(gdb) b ngx_http_subrequest
(gdb) b ngx_http_finalize_request
(gdb) run

7.2 关键断点位置

函数 文件 行号(1.26.0) 作用
ngx_http_subrequest ngx_http_core_module.c ~1800 创建和处理 subrequest
ngx_http_finalize_request ngx_http_request.c ~2900 结束请求
ngx_http_send_special ngx_http_request.c ~2100 发送完成标记
ngx_http_handler ngx_http_core_module.c ~900 进入阶段处理

7.3 日志调试

// 在模块中添加调试日志
ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0,
    "subrequest created: uri=%V, main=%p, parent=%p",
    &sr->uri, sr->main, sr->parent);

ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0,
    "subrequest completed: count=%ui", r->main->count);

8. 注意事项与最佳实践

8.1 性能考虑

  • 使用 NGX_HTTP_SUBREQUEST_WAITED 标志时,主请求会同步等待 subrequest 完成。使用 NGX_HTTP_SUBREQUEST_WAITED 标志让主请求等待 subrequest 完成。
  • 多个 subrequest 应使用 ngx_http_subrequest 依次发起,Nginx 的事件循环会自动调度。
  • 避免 subrequest 嵌套过深,建议不超过 3 层。

8.2 内存安全

  • subrequest 的响应体数据属于 subrequest 的内存池,在主请求中使用前需要复制到主请求的内存池中。
  • 使用 ngx_palloc(r->pool, ...) 分配主请求内存,使用 ngx_palloc(sr->pool, ...) 分配 subrequest 内存。

8.3 错误处理

  • subrequest 可能返回 NGX_HTTP_NOT_FOUND 等错误码,需要在回调中检查 sr->headers_out.status
  • 使用 NGX_HTTP_SUBREQUEST_BACKGROUND 标志可以让 subrequest 在后台运行,主请求不等待。使用 NGX_HTTP_SUBREQUEST_WAITED 标志时,主请求会同步等待 subrequest 完成。

9. 总结

本文从 Nginx 模块开发的角度,深入分析了 subrequest 的源码实现。核心要点包括:

  1. 数据结构ngx_http_request_t 通过 mainparent 指针构建请求树,subrequest 标志位区分请求类型。
  2. 创建函数ngx_http_subrequest 从连接池分配请求结构体,设置父子关系,通过 ngx_http_handler 加入处理流程。
  3. 生命周期:引用计数 count 管理所有 subrequest 的完成状态,独立内存池支持 subrequest 的独立释放,避免内存泄漏。
  4. 回调机制post_subrequest 回调函数在 subrequest 完成时被调用,将控制权交回主请求。
  5. 实战开发:通过自定义模块示例,展示了如何发起 subrequest、捕获响应、并行聚合数据。

掌握 subrequest 的源码级原理,可以帮助开发者编写高性能的 Nginx C 模块,实现复杂的请求处理逻辑。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐