阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

手把手教你开发Nginx模块

232次阅读
没有评论

共计 15102 个字符,预计需要花费 38 分钟才能阅读完成。

前面的哪些话

关于 Nginx 模块开发的博客资料,网上很多,很多。但是,每篇博客都只提要点,无法 ”step by step” 照着做,对于初次接触 Nginx 开发的同学,只能像只盲目的蚂蚁瞎燥急!该篇文章没有太多技术深度,只是一步一步说明白 Nginx 模块的开发过程。

开发环境搭建

工欲善其事,必先利其器。个人推荐 Eclipse CDT 作为 IDE,原因很简单,代码提示与补全功能很全,完胜 Codeblock 这类 … 相信与否,试过就知道。

在 Ubuntu 下搭建开发环境:

  • 安装 GCC 编译器

    apt-get install build-essential
  • 安装 pcre/openssl/zlib 开发库

    apt-get install libpcre3-dev
    apt-get install libssl-dev
    apt-get install libzip-dev

    必需安装 nginx 核心模块依赖的 pcre,openssl,zilib 开发库

  • 安装 JRE/Eclipse CDT

    apt-get install openjdk-8-jre
    wget http://ftp.yz.yamagata-u.ac.jp/pub/eclipse//technology/epp/downloads/release/neon/R/eclipse-cpp-neon-R-linux-gtk-x86_64.tar.gz && tzr -xzvf eclipse-cpp-neon-R-linux-gtk-x86_64.tar.gz
  • 下载 nginx 源码

    wget http://nginx.org/download/nginx-1.10.1.tar.gz && tar -xzvf nginx-1.10.1.tar.gz
  • 配置 CDT Build Environment
    添加变量,值 Nginx src 下各模块路径,用冒号分隔,例如:

    /root/Workspace/nginx-1.10.1/src/core:/root/Workspace/nginx-1.10.1/src/event:/root/Workspace/nginx-1.10.1/src/http:/root/Workspace/nginx-1.10.1/src/mail:/root/Workspace/nginx-1.10.1/src/stream:/root/Workspace/nginx-1.10.1/src/os/unix 

    添加环境变量,创建 C 项目时自动作为 - I 选项
    手把手教你开发 Nginx 模块
    手把手教你开发 Nginx 模块

Nginx 模块编译流程

Nginx 使用 configure 脚本分析环境,自动生成 objs 结果。哪么 configure 如何编译第三方模块?答案是 –add-module 指定第三方模块目录,并将目录存为 $ngx_addon_dir 环境变量。执行 $ngx_addon_dir/config 脚本,读取模块配置。在 config 中的环境变量分为 2 种:小写的本地环境变量,大写的全局环境变量。例如:

ngx_addon_name=ngx_http_mytest_module
HTTP_MODULES="$HTTP_MODULES ngx_http_mytest_module" 
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mytest_moudle.c"
CORE_LIBS="$CORE_LIBS -lpcre"
  • HTTP_MODULES 中的 ngx_http_mytest_module 就是 NGX_ADDON_SRCS 中源码 (如果有多个,都要写上)ngx_http_mytest_module.c 中定义的 ngx_module_t 类型的全局变量。
  • 可见,第三方模块的入口点就是 ngx_module_t 类型全局变量,该变量又关联 ngx_http_module_t 类型 static 变量,与 ngx_command_t 类型 static 数组。
  • 在 ngx_http_module_t 中定义上下文配置 nginx.conf 解析的回调方法。
  • 在 ngx_command_t 中定义配置项处理的 set 回调方法。
  • Nginx 的全部操作都是异步的。在上述的方法中根据需要又会使用其他 handler 方法。
    以上可以看成 Nginx 第三方模块的起式。

Upstream 例子源码

  • config

    ngx_addon_name=ngx_http_mytest_module
    HTTP_MODULES="$HTTP_MODULES ngx_http_mytest_module"
    NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mytest_module.c"
  • 源代码

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

typedef struct {ngx_http_upstream_conf_t upstream;
} mytest_conf_t;

typedef struct {ngx_http_status_t status;
    ngx_str_t backendServer;
} mytest_ctx_t;

staticvoid *mytest_create_loc_conf(ngx_conf_t *cf);
staticchar *mytest_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);
static ngx_int_t mytest_upstream_create_request(ngx_http_request_t *r);
static ngx_int_t mytest_upstream_process_status_line(ngx_http_request_t *r);
static ngx_int_t mytest_upstream_process_header(ngx_http_request_t *r);
staticvoid mytest_upstream_finalize_request(ngx_http_request_t *r,
        ngx_int_t rc);
static ngx_int_t mytest_handler(ngx_http_request_t *r);
staticchar *mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);


static ngx_http_module_t mytest_ctx = {NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    mytest_create_loc_conf,
    mytest_merge_loc_conf
};

static ngx_command_t mytest_commands[] = {
    {ngx_string("mytest"),
        NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_NOARGS,
        mytest,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },
    ngx_null_command
};

ngx_module_t ngx_http_mytest_module = {
    NGX_MODULE_V1,
    &mytest_ctx,
    mytest_commands,
    NGX_HTTP_MODULE,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NGX_MODULE_V1_PADDING
};

static ngx_str_t  mytest_upstream_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
};

staticvoid *mytest_create_loc_conf(ngx_conf_t *cf){mytest_conf_t *mycf;
    mycf = (mytest_conf_t *)ngx_pcalloc(cf->pool, sizeof(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;

    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;

    mycf->upstream.hide_headers = NGX_CONF_UNSET_PTR;
    mycf->upstream.pass_headers = NGX_CONF_UNSET_PTR;

    return mycf;
}

staticchar *mytest_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child){mytest_conf_t *prev = (mytest_conf_t *)parent;
    mytest_conf_t *conf = (mytest_conf_t *)child;

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

static ngx_int_t mytest_upstream_create_request(ngx_http_request_t *r){static ngx_str_t backendQueryLine = ngx_string("GET /search?q=%V HTTP/1.1\r\nHost: www.google.com.hk\r\nConnection: close\r\n\r\n");
    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;
    b->last = b->pos + queryLineLen;

    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;

    r->upstream->request_bufs->buf = b;
    r->upstream->request_bufs->next = NULL;

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

    r->header_hash = 1;

    return NGX_OK;
}

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

    mytest_ctx_t *ctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    if(ctx == NULL) return NGX_ERROR;

    u = r->upstream;

    rc = ngx_http_parse_status_line(r, &u->buffer, &ctx->status);
    if(rc == NGX_AGAIN) return rc;
    if(rc == NGX_ERROR){ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "upstream sent to valid HTTP/1.0 header");

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

        return NGX_OK;
    }

    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_pcalloc(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);

    u->process_header = mytest_upstream_process_header;

    return mytest_upstream_process_header(r);
}

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;

    umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module);

    for(;;){rc = ngx_http_parse_header_line(r, &r->upstream->buffer, 1);
        if(rc == NGX_OK){h = ngx_list_push(&r->upstream->headers_in.headers);
            if(h == NULL) return NGX_ERROR;

            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;

            h->key.data = ngx_pcalloc(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);
            }

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

        if(rc == NGX_HTTP_PARSE_HEADER_DONE){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;
        }
        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_FT_INVALID_HEADER;
    }
}

staticvoid 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");
}

staticchar *mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf){ngx_http_core_loc_conf_t *clcf;

    clcf = ngx_http_conf_get_module_loc_conf(cf,ngx_http_core_module);
    clcf->handler = mytest_handler;

    return NGX_CONF_OK;
}

static ngx_int_t mytest_handler(ngx_http_request_t *r){mytest_ctx_t *myctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    if(myctx == NULL){myctx = ngx_pcalloc(r->pool, sizeof(mytest_ctx_t));
        if(myctx == NULL) return NGX_ERROR;
        ngx_http_set_ctx(r, myctx, ngx_http_mytest_module);
    }

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

    mytest_conf_t *mycf = (mytest_conf_t *)ngx_http_get_module_loc_conf(r, ngx_http_mytest_module);
    ngx_http_upstream_t *u = r->upstream;
    u->conf = &mycf->upstream;
    u->buffering = mycf->upstream.buffering;

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

    static struct sockaddr_in backendSockAddr;

    struct hostent *pHost = gethostbyname((char *)"www.google.com.hk");
    if(pHost == NULL){ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "gethostbyname fail. %s", strerror(errno));
        return NGX_ERROR;
    }

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

    u->resolved->sockaddr = (struct sockaddr *)&backendSockAddr;
    u->resolved->port = htons((in_port_t)80);
    u->resolved->socklen = sizeof(struct sockaddr_in);
    u->resolved->naddrs = 1;

    u->create_request = mytest_upstream_create_request;
    u->process_header = mytest_upstream_process_status_line;
    u->finalize_request = mytest_upstream_finalize_request;

    r->main->count++;

    ngx_http_upstream_init(r);
    return NGX_DONE;
}

注意:《Nginx 深入解析》的 demo 少了这句:“u->resolved->port = htons((in_port_t)80);”,否则报错“2016/09/09 11:24:18 [error] 28352#0: *1 no port in upstream “”, client: 127.0.0.1, server: localhost, request: “GET /mytest?q=test HTTP/1.1”, host: “localhost””

  • 编译脚本

    ./configure --prefix=/usr/local/nginx --add-module=/root/Workspace/nginx-modules/ngx_http_mytest_module --with-debug
    sudo make
    sudo make install

    安装后即可到 /usr/local/nginx 下配置 nginx.conf 进行测试。

Subrequest 例子源码

  • config

    ngx_addon_name=ngx_http_mytest_module
    HTTP_MODULES="$HTTP_MODULES ngx_http_mytest_module"
    NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mytest_module.c"
  • 源代码

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

typedef struct {ngx_str_t stock[6];
} mytest_ctx_t;

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);
static ngx_int_t mytest_handler(ngx_http_request_t *r);
static char *mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

static ngx_http_module_t mytest_conf = {NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL
};
static ngx_command_t mytest_commands[] = {
    {ngx_string("mytest"),
        NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
        mytest,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },
    ngx_null_command
};

ngx_module_t ngx_http_mytest_module = {
    NGX_MODULE_V1,
    &mytest_conf,
    mytest_commands,
    NGX_HTTP_MODULE,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NGX_MODULE_V1_PADDING
};

static ngx_int_t mytest_subrequest_post_handler(ngx_http_request_t *r,
        void *data, ngx_int_t rc) {ngx_http_request_t *pr = r->parent;

    mytest_ctx_t *myctx = ngx_http_get_module_ctx(pr, ngx_http_mytest_module);
    pr->headers_out.status = r->headers_out.status;
    if (r->headers_out.status == NGX_HTTP_OK) {int flag = 0;
        ngx_buf_t *pRecvBuf = &r->upstream->buffer;
        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){if(r->headers_out.status != NGX_HTTP_OK){ngx_http_finalize_request(r,r->headers_out.status);
        return;
    }

    mytest_ctx_t *myctx = ngx_http_get_module_ctx(r,ngx_http_mytest_module);
    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 out;
    out.buf = b;
    out.next = NULL;

    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_int_t ret = ngx_http_send_header(r);
    ret = ngx_http_output_filter(r, &out);

    ngx_http_finalize_request(r,ret);
}

static ngx_int_t mytest_handler(ngx_http_request_t *r){mytest_ctx_t *myctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    if(myctx == NULL){myctx = ngx_pcalloc(r->pool, sizeof(mytest_ctx_t));
        if(myctx == NULL) return NGX_ERROR;
        ngx_http_set_ctx(r,myctx,ngx_http_mytest_module);
    }

    ngx_http_post_subrequest_t *psr = ngx_pcalloc(r->pool, sizeof(ngx_http_post_subrequest_t));
    if(psr == NULL) return NGX_HTTP_INTERNAL_SERVER_ERROR;

    psr->handler = mytest_subrequest_post_handler;
    psr->data = myctx;

    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_pcalloc(r->pool, sub_location.len);
    ngx_snprintf(sub_location.data, sub_location.len, "%V%V", &sub_prefix, &r->args);

    ngx_http_request_t *sr;
    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;

    return NGX_DONE;
}

static char *mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf){ngx_http_core_loc_conf_t *clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
    clcf->handler = mytest_handler;
    return NGX_CONF_OK;
}
  • 编译脚本

    ./configure --prefix=/usr/local/nginx --add-module=/root/Workspace/nginx-modules/ngx_http_mytest_module2 --with-debug
    sudo make
    sudo make install

————————————– 分割线 ————————————–

Nginx 负载均衡配置实战  http://www.linuxidc.com/Linux/2014-12/110036.htm

CentOS 6.2 实战部署 Nginx+MySQL+PHP http://www.linuxidc.com/Linux/2013-09/90020.htm

使用 Nginx 搭建 WEB 服务器 http://www.linuxidc.com/Linux/2013-09/89768.htm

搭建基于 Linux6.3+Nginx1.2+PHP5+MySQL5.5 的 Web 服务器全过程 http://www.linuxidc.com/Linux/2013-09/89692.htm

CentOS 6.3 下 Nginx 性能调优 http://www.linuxidc.com/Linux/2013-09/89656.htm

CentOS 6.3 下配置 Nginx 加载 ngx_pagespeed 模块 http://www.linuxidc.com/Linux/2013-09/89657.htm

CentOS 6.4 安装配置 Nginx+Pcre+php-fpm http://www.linuxidc.com/Linux/2013-08/88984.htm

Nginx 安装配置使用详细笔记 http://www.linuxidc.com/Linux/2014-07/104499.htm

Nginx 日志过滤 使用 ngx_log_if 不记录特定日志 http://www.linuxidc.com/Linux/2014-07/104686.htm

————————————– 分割线 ————————————–

Nginx 的详细介绍 :请点这里
Nginx 的下载地址 :请点这里

本文永久更新链接地址 :http://www.linuxidc.com/Linux/2016-09/135263.htm

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-21发表,共计15102字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中