分享

【Linux网络编程】Nginx -- 模块开发(基本模块解析)

 印度阿三17 2021-03-03

【1】处理流程图示

Nginx 一次常规的请求和响应的处理流程

典型的 HTTP 模块在 Nginx 中调用的简化流程

【2】模块开发示例

【2.1】将模块编译进入 Nginx

将模块源代码文件放到一个目录下,并在该目录中编写一个文件用于告知Nginx编译本模块的方式,该文件名必须为config;此时只要在configure脚本执行时加入参数--add-module=PATH(PATH为给定的源代码、config文件的保存目录),便可以在执行正常编译安装流程时完成Nginx编译工作;

【2.1.1】Config 文件解析

#仅在configure执行时使用,一般设置为模块名称
ngx_addon_name=模块完整名称
#保存所有的HTTP模块名称,每个HTTP模块间由空格符相连
HTTP_MODULES="$HTTP_MODULES 模块完整名称"
#用于指定新增模块的源代码,多个待编译的源代码间以空格符相连
#在设置NGX_ADDON_SRCS时使用的参数$ngx_addon_dir的值为--add-module=PATH的PATH参数
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/源代码文件名"

【2.1.2】Nginx 模块编译安装

./configure --prefix=安装目录 --add-module=模块源代码文件目录
make
make install

【2.2】Nginx 模块示例代码解析

【2.2.1】发送内存数据

// 该 HTTP 模块接入 Nginx 的方式
// 1. 不希望该模块对整个 HTTP 请求有效
// 2. 在 nginx.conf 文件中的 http{}、server{}、location{} 块内定义 mytest 配置项
//
// 在 HTTP 框架定义的 NGX_HTTP_CONTENT_PHASE 阶段开始处理请求
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

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

static ngx_int_t ngx_http_mytest_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_mytest_commands[] =
{
    // 定义mytest配置项的处理,此处指定mytest配置项由ngx_http_mytest函数处理
    {
        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_command_t 结构体中的 set 成员
        // 当在某个配置块中出现 mytest 配置项时,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_mytest_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 */
};

// 定义 mytest 模块
// 回调方法,init_module、init_process、exit_process、exit_master 由 Nginx 框架代码调用,与 HTTP 模块无关
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 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_mytest_handler;

    return NGX_CONF_OK;
}

// ngx_http_request_t中保存了请求的信息
static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r)
{
    // 判断 HTTP 头信息中的方法是否支持
    if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD)))
    {
        return NGX_HTTP_NOT_ALLOWED;
    }

    // 丢弃 HTTP 请求包体
    ngx_int_t rc = ngx_http_discard_request_body(r);
    if (rc != NGX_OK)
    {
        return rc;
    }

    // 构造并发送响应
    // 设置返回的 Content-Type
    ngx_str_t type = ngx_string("text/plain");
    // 返回包体的内容
    ngx_str_t response = ngx_string("Hello World!");
    // 设置返回的状态码
    r->headers_out.status = NGX_HTTP_OK;
    // 响应包具有包体内容,需要设置 Content-Length 长度
    r->headers_out.content_length_n = response.len;
    // 设置 Content-Type
    r->headers_out.content_type = type;

    // 发送 HTTP 头部
    rc = ngx_http_send_header(r);
    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only)
    {
        return rc;
    }

    // 从 Nginx 的内存池中分配一块内存
    // 构造 ngx_buf_t 结构体准备发送包体
    ngx_buf_t                 *b;
    b = ngx_create_temp_buf(r->pool, response.len);
    if (b == NULL)
    {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
    // 将 Hellow World 复制到 ngx_buf_t 指向的内存中
    ngx_memcpy(b->pos, response.data, response.len);
    // 设置 b->last 指针
    b->last = b->pos   response.len;
    // 声明这是最后一块缓冲区
    b->last_buf = 1;

    // 构造发送时的 ngx_chain_t 结构体
    ngx_chain_tout;
    // 赋值 ngx_buf_t
    out.buf = b;
    // 设置 next 为 NULL
    out.next = NULL;

    // ngx_http_output_filter 方法向客户端发送 HTTP 响应包体
    return ngx_http_output_filter(r, &out);
}

【2.2.2】发送文件

// 该 HTTP 模块接入 Nginx 的方式
// 1. 不希望该模块对整个 HTTP 请求有效
// 2. 在 nginx.conf 文件中的 http{}、server{}、location{} 块内定义 mytest 配置项
//
// 在 HTTP 框架定义的 NGX_HTTP_CONTENT_PHASE 阶段开始处理请求
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

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

static ngx_int_t ngx_http_mytest_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_mytest_commands[] =
{
    // 定义mytest配置项的处理,此处指定mytest配置项由ngx_http_mytest函数处理
    {
        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_command_t 结构体中的 set 成员
        // 当在某个配置块中出现 mytest 配置项时,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_mytest_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 */
};

// 定义 mytest 模块
// 回调方法,init_module、init_process、exit_process、exit_master 由 Nginx 框架代码调用,与 HTTP 模块无关
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 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_mytest_handler;

    return NGX_CONF_OK;
}

// ngx_http_request_t中保存了请求的信息
static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r)
{
    // 判断 HTTP 头信息中的方法是否支持
    if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD)))
    {
        return NGX_HTTP_NOT_ALLOWED;
    }

    // 丢弃 HTTP 请求体
    ngx_int_t rc = ngx_http_discard_request_body(r);
    if (rc != NGX_OK)
    {
        return rc;
    }

    // 内存池中分配ngx_buf_t数据体
    ngx_buf_t *b;
    b = ngx_palloc(r->pool, sizeof(ngx_buf_t));

    // 设置文件名
    u_char* filename = (u_char*)"/tmp/intro.html";
    // 将in_file标志位置1,表示ngx_buf_t发送的是文件而不是内存缓冲数据
    // ngx_http_output_filter调用后,若检测到in_file标志为1,则从ngx_buf_t缓冲区中的file成员处获取实际的文件
    b->in_file = 1;
    // 分配内存
    b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t));
    // 打开文件
    b->file->fd = ngx_open_file(filename, NGX_FILE_RDONLY | NGX_FILE_NONBLOCK, NGX_FILE_OPEN, 0);
    // 配置日志指针
    b->file->log = r->connection->log;
    // 记录文件名称
    b->file->name.data = filename;
    // 文件名称长度
    b->file->name.len = sizeof(filename) - 1;
    if (b->file->fd <= 0)
    {
        return NGX_HTTP_NOT_FOUND;
    }

    // 允许 range 协议
    r->allow_ranges = 1;

    // 获取文件信息
    if (ngx_file_info(filename, &b->file->info) == NGX_FILE_ERROR)
    {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    // 设置文件数据起始偏移量
    b->file_pos = 0;
    // 设置文件数据结束偏移量
    b->file_last = b->file->info.st_size;
    // 内存池清理添加
    ngx_pool_cleanup_t* cln = ngx_pool_cleanup_add(r->pool, sizeof(ngx_pool_cleanup_file_t));
    if (cln == NULL)
    {
        return NGX_ERROR;
    }
    // 配置内存池清理信息
    cln->handler = ngx_pool_cleanup_file;
    ngx_pool_cleanup_file_t  *clnf = cln->data;

    clnf->fd = b->file->fd;
    clnf->name = b->file->name.data;
    clnf->log = r->pool->log;

    // 构造响应
    // 设置返回的 Content-Type
    ngx_str_t type = ngx_string("text/plain");

    // 设置返回的状态码
    r->headers_out.status = NGX_HTTP_OK;
    // 响应包具有包体内容,需要设置 Content-Length 长度
    r->headers_out.content_length_n = b->file->info.st_size;
    // 设置 Content-Type
    r->headers_out.content_type = type;

    // 发送 HTTP 头部
    rc = ngx_http_send_header(r);
    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only)
    {
        return rc;
    }

    // 构造发送时的 ngx_chain_t 结构体
    ngx_chain_tout;
    // 赋值 ngx_buf_t
    out.buf = b;
    // 设置 next 为 NULL
    out.next = NULL;

    // ngx_http_output_filter 方法向客户端发送 HTTP 响应包体
    return ngx_http_output_filter(r, &out);
}

【2.3】Nginx 模块代码配置与调式

【2.3.1】添加模块相关配置信息

...

http {
    ...
    # 模块开发对应的配置
    location /test {
        mytest;
    }
    ...
}

...

【2.3.2】运行 Nginx 并附着进程调式

启动 Nginx 并查看 Worker 进程 ID

VSCode 配置

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        { 
            "name": "nginx_worker_debug",
            "type": "cppdbg",
            "request": "attach",
            "program": "/home/shallysun/code_dev/SourceCode/nginx_1_19/nginx/bin/sbin/nginx",
            "processId": "45472",
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ]
        }
    ]
}

调试效果

【3】Nginx 模块开发典型数据结构解析

【3.1】ngx_command_s / ngx_command_t

// Nginx 在解析配置文件中的一个配置项时,遍历所有模块,对于每一个模块遍历 commands 数组,
// 在数组中检查到 ngx_null_command 时会停止使用当前模块解析该配置项;
//
// 指令结构体,用于定义nginx指令
// ngx_command_t (ngx_core.h)
struct ngx_command_s {
    // 指令的名字
    ngx_str_t             name;

    // 指令的类型,是NGX_CONF_XXX的组合,决定指令出现的位置、参数数量、类型等
    // NGX_HTTP_MAIN_CONF/NGX_HTTP_SRV_CONF/NGX_HTTP_LOC_CONF
    ngx_uint_t            type;

    // 出现了 name 中指定的配置项后,会调用 set 方法处理配置项参数
    // 指令解析函数,是函数指针
    // 预设有ngx_conf_set_flag_slot等,见本文件
    // cf:解析的环境结构体,重要的是cf->args,是指令字符串数组
    // cmd:该指令的结构体
    // conf当前的配置结构体,需转型后才能使用
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

    // 在配置文件中的偏移量
    // 专门给http/stream模块使用,决定存储在main/srv/loc的哪个层次
    // NGX_HTTP_MAIN_CONF_OFFSET/NGX_HTTP_SRV_CONF_OFFSET/NGX_HTTP_LOC_CONF_OFFSET
    // NGX_STREAM_MAIN_CONF_OFFSET
    // 其他类型的模块不使用,直接为0
    ngx_uint_t            conf;

    // 变量在conf结构体里的偏移量,可用offsetof得到
    // 主要用于nginx内置的命令解析函数,自己写命令解析函数可以置为0
    //
    // 当前配置项在整个存储配置项的结构体中的偏移位置
    ngx_uint_t            offset;

    // 解析后处理的数据
    // 配置项读取后的处理方法,必须是 ngx_conf_post_t 结构的指针
    void                 *post;
};

【3.2】ngx_http_module_t

// http 框架在读取、重载配置文件时定义了由 ngx_http_module_t 接口描述的 8 个阶段
// http 框架在启动过程中会在每个阶段调用 ngx_http_module_t 中的相应方法
// http模块的函数表,在配置解析阶段被框架调用
typedef struct {
    // 解析配置文件之前调用
    // ngx_http_block里,创建配置结构体后,开始解析之前调用
    // 常用于添加变量定义
    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);
    // 完成配置文件解析后调用
    // ngx_http_block里,解析、合并完配置后调用
    // 常用于初始化模块的phases handler
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);
    // 当需要创建数据结构用于存储 main 级别的全局配置项时,可以通过 create_main_conf 回调创建存储全局配置项的结构体
    // 创建模块的main配置,只有一个,在http main域
    void       *(*create_main_conf)(ngx_conf_t *cf);

    // 初始化模块的main配置,只有一个,在http main域
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);

    // 当需要创建数据结构用于存储 srv 级别的配置项时,可以通过 create_srv_conf 回调创建存储 srv 级别配置项的结构体
    // 创建、合并模块的srv配置
    void       *(*create_srv_conf)(ngx_conf_t *cf);
    // 用于合并 main 级别和 srv 级别下的同名配置项
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

    // 当需要创建数据结构用于存储 loc 级别的配置项时,可以通过 create_loc_conf 回调创建存储 loc 级别配置项的结构体
    // 创建、合并模块的location配置
    void       *(*create_loc_conf)(ngx_conf_t *cf);
    // 用于合并 srv 级别和 loc 级别下的同名配置项
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;

【3.3】ngx_module_s / ngx_module_t

// 重新定义了填充宏,加入了签名字符串
// 早期(1.9.11之前)的定义是
// #define NGX_MODULE_V1          0, 0, 0, 0, 0, 0, 1
// 注意前两个字段改成了unset(-1)而不是0,表示序号未初始化
#define NGX_MODULE_V1                                                             NGX_MODULE_UNSET_INDEX, NGX_MODULE_UNSET_INDEX,                               NULL, 0, 0, nginx_version, NGX_MODULE_SIGNATURE

// 填充宏,填充ngx_module_t的最后8个字段,设置为空指针
// 1.10没有变化
#define NGX_MODULE_V1_PADDING  0, 0, 0, 0, 0, 0, 0, 0


// 重要的数据结构,定义nginx模块
// 重要的模块ngx_core_module/ngx_event_module/ngx_http_module
// 1.9.11后有变化,改到了ngx_module.h,可以定义动态模块,使用了spare0等字段
struct ngx_module_s {
    // 成员变量 ctx_index index spare0 spare1 version 通常使用宏NGX_MODULE_V1填充

    // 每类(http/event)模块各自的index,表示当前模块在一类模块中的序号
    // 由管理此类模块的 Nginx 核心模块设置
    // 可用于表示优先级以及各个模块的位置
    // 初始化为-1
    ngx_uint_t            ctx_index;

    // 在ngx_modules数组里的唯一索引,main()里赋值
    // 使用计数器变量ngx_max_module设置
    // 表示当前模块在ngx_modules数组中的序号,即当前模块在所有模块中的序号
    ngx_uint_t            index;

    // 1.10,模块的名字,标识字符串,默认是空指针
    // 由脚本生成ngx_module_names数组,然后在ngx_preinit_modules里填充
    // 动态模块在ngx_load_module里设置名字
    char                 *name;

    // 两个保留字段,1.9之前有4个
    ngx_uint_t            spare0;
    ngx_uint_t            spare1;

    // nginx.h:#define nginx_version      1010000
    // 模块的版本,默认为1
    ngx_uint_t            version;

    // 模块的二进制兼容性签名,即NGX_MODULE_SIGNATURE
    const char           *signature;

    // 模块不同含义不同,通常是函数指针表,是在配置解析的某个阶段调用的函数
    // core模块的ctx
    //typedef struct {
    //    ngx_str_t             name;
    //    void               *(*create_conf)(ngx_cycle_t *cycle);
    //    char               *(*init_conf)(ngx_cycle_t *cycle, void *conf);
    //} ngx_core_module_t;
    //
    //用于指向一类模块的上下文结构体,指向特定类型模块的公共接口
    void                 *ctx;

    // 模块支持的指令,数组形式,最后用空对象表示结束,处理 nginx.conf 中的配置项
    ngx_command_t        *commands;

    // 模块的类型标识,相当于RTTI,如CORE/HTTP/STRM/MAIL等
    // Nginx 官方取值为 NGX_HTTP_MODULE NGX_CORE_MODULE NGX_CONF_MODULE NGX_EVENT_MODULE NGX_MAIL_MODULE
    ngx_uint_t            type;

    // 以下7个函数会在进程(Nginx)的启动或结束阶段被调用
    // 若不需要Nginx在某个阶段调用相应的回调函数则置为NULL

    // init_master目前nginx不会调用,当 master 进程启动时回调
    ngx_int_t           (*init_master)(ngx_log_t *log);

    // 在ngx_init_cycle里被调用
    // 在master进程里,fork出worker子进程之前
    // 做一些基本的初始化工作,数据会被子进程复制
    //
    // 初始化所有模块时回调
    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);

    // 在ngx_single_process_cycle/ngx_worker_process_init里调用
    // 在worker进程进入工作循环之前被调用
    // 初始化每个子进程自己专用的数据
    //
    // 在正常服务前被调用
    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);

    // init_thread目前nginx不会调用
    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);

    // exit_thread目前nginx不会调用
    void                (*exit_thread)(ngx_cycle_t *cycle);

    // 在ngx_worker_process_exit调用,在服务停止前调用
    void                (*exit_process)(ngx_cycle_t *cycle);

    // 在ngx_master_process_exit(os/unix/ngx_process_cycle.c)里调用
    // 在master进程退出前调用
    void                (*exit_master)(ngx_cycle_t *cycle);

    // 下面8个成员通常用用NGX_MODULE_V1_PADDING填充
    // 暂时无任何用处,保留字段
    uintptr_t             spare_hook0;
    uintptr_t             spare_hook1;
    uintptr_t             spare_hook2;
    uintptr_t             spare_hook3;
    uintptr_t             spare_hook4;
    uintptr_t             spare_hook5;
    uintptr_t             spare_hook6;
    uintptr_t             spare_hook7;
};

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

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

【2】Nginx模块开发入门

来源:https://www./content-3-877251.html

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多