【Nginx】开发一个简单的HTTP模块

首先来分析一下HTTP模块是如何介入Nginx的。

当master进程fork出若干个workr子进程后,每个worker子进程都会在自己的for死循环中不断调用事件模块:

for ( ;; ) {
        ....
        ngx_process_events_and_timers(cycle);   /* 调用事件模块 */
        ....
    }

事件模块检测是否有TCP连接请求,当收到一个SYN包后,由事件模块建立一条TCP连接。连接建立成功后,交由HTTP框架处理,HTTP框架负责接收HTTP头部,并根据头部信息将HTTP请求分发到不同的HTTP模块。最常见的分发策略就是根据HTTP头部的URI和配置文件nginx.conf里的location配置项的匹配度来决定如何分发。假设配置文件中有如下配置块:

location /name {
    test;
}

并假设服务器域名为www.nestle.com。那么,当客户端发来的URL请求为“www.nestle.com/name”时,HTTP框架就会根据配置文件调用test模块,将这个HTTP请求交给test模块处理。

HTTP模块在处理完请求后,会自动依次调用HTTP过滤模块对准备返回的响应信息做预处理,比如是否压缩。以下是HTTP模块调用的大致流程图:

下面开始讲解如何定义一个HTTP模块。

凡是模块,都需要一个模块结构体,这个结构体是ngx_module_t,它的定义如下:

struct ngx_module_s {
    ngx_uint_t            ctx_index;    /* 当前模块在同类模块中的序号 */
    ngx_uint_t            index;        /* 当前模块在ngx_modules数组中的序号 */

    ngx_uint_t            spare0;       /* 保留 */
    ngx_uint_t            spare1;       /* 保留 */
    ngx_uint_t            spare2;       /* 保留 */
    ngx_uint_t            spare3;       /* 保留 */

    ngx_uint_t            version;      /* 模块版本,目前为1 */

    void                 *ctx;          /* 指向特定类型模块的公共接口 */
    ngx_command_t        *commands;     /* 用于处理配置文件nginx.conf中的配置项 */
    ngx_uint_t            type;         /* 当前模块类型 */

    /* 以下7个函数指针表示7个执行点,这些执行点将在Nginx启动和退出过程中被调用
     * 如果不需要则设置为NULL
     */
    ngx_int_t           (*init_master)(ngx_log_t *log);     /* 从未被调用,设为NULL */

    ngx_int_t           (*init_module)(ngx_cycle_t *cycle); /* 启动worker子进程前调用 */

    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);/* 启动worker子进程后调用 */
    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle); /* 从未被调用,设为NULL */
    void                (*exit_thread)(ngx_cycle_t *cycle); /* 从未被调用,设为NULL */
    void                (*exit_process)(ngx_cycle_t *cycle);/* worker子进程推出前调用 */

    void                (*exit_master)(ngx_cycle_t *cycle); /* master进程退出前调用 */

    /* 以下全为保留字段 */
    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;
};

HTTP模块的type字段必须设置为NGX_HTTP_MODULE,以表明此模块为HTTP模块。

HTTP模块的ctx指针必须指向ngx_http_module_t结构体,它表示HTTP模块所定义的通用接口,结构体定义如下:

typedef struct {
    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);    // 解析配置文件前调用
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);   // 解析完配置文件后调用

    void       *(*create_main_conf)(ngx_conf_t *cf);    // 创建存储直属于http{}的配置项的结构体
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);  // 初始化main级别配置项

    void       *(*create_srv_conf)(ngx_conf_t *cf);     // 创建存储直属于srv{}的配置项的结构体
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);  // 合并main级别和srv级别的同名配置项

    void       *(*create_loc_conf)(ngx_conf_t *cf);     // 创建存储直属于loc{}的配置项的结构体
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);  // 合并srv级别和loc级别的同名配置项
} ngx_http_module_t;

HTTP框架会在启动过程中调用上述结构体中的各个阶段函数,设为NULL则不会调用它们。

HTTP模块的commands数组由若干ngx_command_t链接而成。当Nginx在解析配置文件的一个配置项时,会遍历所有模块的commands数组,以找到对该配置项感兴趣的ngx_command_t结构体,从而找到相应的模块。ngx_command_t结构体定义如下:

struct ngx_command_s {
    ngx_str_t             name;     // 配置项名称
    ngx_uint_t            type;     // 配置项类型,包括该配置项可以出现的位置和可以携带参数的个数

    // 出现name配置项后,调用此方法解析配置项参数
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
    ngx_uint_t            conf;     // 配置文件中的偏移量,确定将该配置项放入哪个存储结构体中
    ngx_uint_t            offset;   // 将该配置项放在存储结构体的哪个字段
    void                 *post;     // 配置项读取后的处理方法
};

对于一个HTTP请求的处理,多个模块可以在不同阶段介入,关于阶段的定义和介入方法见“HTTP请求的11个处理阶段”。但对于NGX_HTTP_CONTENT_PHASE阶段还有另外一个介入的方法:把希望处理请求的ngx_http_handler_pt方法放入保存location块配置项的ngx_http_core_loc_t结构体的handler指针中。我们的模块就是运用这种方法使得该模块在NGX_HTTP_CONTENT_PHASE介入一个HTTP请求。

现在来写Nginx模块,也就是填充ngx_module_t结构体。

1、定义ngx_module_t结构体中的commands数组:

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,        // 出现mytest配置项时,ngx_http_mytest函数被调用
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL,
    },
    ngx_null_command            // 以一个空的ngx_command_t作为结尾
};

下面是当配置文件中出现“mytest”配置项时会解析该配置项的方法ngx_http_mytest:

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

    // 找到mytest配置项所属的配置块
    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;
}

处理HTTP请求的ngx_http_mytest_handler方法先跳过。

2、定义ngx_module_t结构体中的ctx接口:

static ngx_http_module_t ngx_http_mytest_module_ctx = {
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
};

HTTP框架在初始化时会调用每个HTTP模块的这8个方法,我们没有什么方法需要调用,所以设为NULL。

定义完上述两个最重要的结构体成员,现在模块的定义如下:

ngx_module_t ngx_http_mytest_module = {
    NGX_MODULE_V1,      // 0,0,0,0,0,0,1
    &ngx_http_mytest_module_ctx,
    ngx_http_mytest_commands,
    NGX_HTTP_MODULE,    // 定义模块类型

    /* Nginx在启动和退出时会调用下面7个回调方法 */
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NGX_MODULE_V1_PADDING,  // 0,0,0,0,0,0,0,0,保留字段
};

ngx_module_t结构体中开始的7个字段不需要在定义时赋值,所以由一个宏NGX_MODULE_V1来进行集体初始化。

最后一个任务就是处理HTTP请求,下面就是处理请求并返回响应的函数ngx_http_mytest_handler:

// 请求的所有信息都存入ngx_http_request_t结构体中
static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r)
{
    // 请求的方法必须为GET或者HEAD
    if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD)))
        return NGX_HTTP_NOT_ALLOWED;

    // 丢弃请求中的包体
    ngx_int_t rc = ngx_http_discard_request_body(r);
    if (rc != NGX_OK)
        return rc;

    ngx_str_t type = ngx_string("text/plain");
    ngx_str_t response = ngx_string("Hello World!");    // 包体内容

    // 设置响应的HTTP头部
    r->headers_out.status = NGX_HTTP_OK;           // 返回的响应码
    r->headers_out.content_length_n = response.len;    // 响应包体长度
    r->headers_out.content_type = type;                // Content-Type

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

   // 如果响应不包含包体,则在此处可以直接返回rc

    // 分配响应包体空间,因为是异步发送,所以不要从栈中获得空间
    ngx_buf_t *b = ngx_create_temp_buf(r->pool, response.len);

    if (b == NULL)
        return NGX_HTTP_INTERNAL_SERVER_ERROR;

    ngx_memcpy(b->pos, response.data, response.len);
    b->last = b->pos + response.len;  // 指向数据末尾
    b->last_buf = 1;                    // 声明这是最后一块缓冲区

    ngx_chain_t out;
    out.buf = b;
    out.next = NULL;

    return ngx_http_output_filter(r, &out);   // 向用户发送响应包
}

下面说明如何让写好的模块编译进Nginx。

1、在和源文件相同目录下创建一个名为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"
  • ngx_addon_name:模块名称
  • HTTP_MODULES:保存所有的HTTP模块名称,以空格符分隔
  • NGX_ADDON_SRCS:新增模块的源代码路径,变量ngx_addon_dir稍后说明

路径如下所示:

2、在configure时添加自定义模块的路径:

./configure --add-module=/work/nginx/modules/mytest

其中,路径/work/nginx/modules/mytest就对应了config文件中的ngx_addon_dir变量。现在configure程序便知道了我们定义的模块的绝对路径。

3、make

4、make install

最终会在/usr/local/nginx/sbin目录下生成可执行文件ngxin。

5、在/usr/local/nginx/conf/nginx.conf配置文件中添加需要的配置项:

6、启动nginx,并在客户端浏览器中输入Nginx服务器的主机名:

可以看到,Nginx成功返回了我们设置的字符串。注意URI必须和/nestle完全匹配,因为location配置块后面是一个“=”符号,表示必须完全一致。

参考:

《深入理解Nginx》第三章。

【Nginx】开发一个简单的HTTP模块,布布扣,bubuko.com

时间: 2024-12-11 17:10:46

【Nginx】开发一个简单的HTTP模块的相关文章

nginx 学习五 filter模块简介和实现一个简单的filter模块

1 nginx过滤模块简介 过滤(filter)模块是过滤响应头和内容的模块,可以对回复的头和内容进行处理.它的处理时间在获取回复内容之后, 向用户发送响应之前.它的处理过程分为两个阶段,过滤HTTP回复的头部和主体,在这两个阶段可以分别对头部和主体 进行修改. 2 过滤模块执行顺序 2.1 ngx_http_output_(head, body)_filter_pt 先看一下nginx常用的过滤模块,在ngx_moudles.c中有一下代码: ngx_module_t *ngx_modules

用Verilog语言实现一个简单的MII模块

项目中要求简单地测试一下基于FPGA的模拟平台的RJ45网口,也就是需要实现一个MII或者RMII模块.看了一下官方网口PHY芯片的官方文档,还是感觉上手有点障碍,想在网络上找些参考代码看看,最后只在opencores找到了一些MAC层控制模块,代码庞大且复杂,对于初学者来说阅读起来很困难. 于是在此以一个初学者的角度记录一下我实现一个简单的MII模块的过程,并且指出一些实现过程中要注意的问题.希望可以帮助有需要的朋友. 为了便于测试,我选择了和我们平台使用相同物理芯片的FPGA开发板NEXYS

基于Servlet、JSP、JDBC、MySQL的一个简单的用户注册模块(附完整源码)

最近看老罗视频,做了一个简单的用户注册系统.用户通过网页(JSP)输入用户名.真名和密码,Servlet接收后通过JDBC将信息保存到MySQL中.虽然是个简单的不能再简单的东西,但麻雀虽小,五脏俱全,在此做一归纳和整理.下面先上源码: 一.index.jsp <%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <% String path =

Python开发一个简单的BBS论坛

项目:开发一个简单的BBS论坛 需求: 整体参考“抽屉新热榜” + “虎嗅网” 实现不同论坛版块 帖子列表展示 帖子评论数.点赞数展示 在线用户展示 允许登录用户发贴.评论.点赞 允许上传文件 帖子可被置顶 可进行多级评论 就先这些吧... 知识必备: Django HTML\CSS\JS BootStrap Jquery 设计表结构 1 # -*- coding:utf-8 -*- 2 from django.db import models 3 from django.contrib.aut

作业1开发一个简单的python计算器

开发一个简单的python计算器 实现加减乘除及拓号优先级解析 用户输入 1 - 2 * ( (60-30 +(-40/5) * (9-2*5/3 + 7 /3*99/4*2998 +10 * 568/14 )) - (-4*3)/ (16-3*2) )等类似公式后,必须自己解析里面的(),+,-,*,/符号和公式(不能调用eval等类似功能偷懒实现),运算后得出结果,结果必须与真实的计算器所得出的结果一致 hint: re.search(r'\([^()]+\)',s).group() '(-

开发一个简单实用的android紧急求助软件

之前女朋友一个人住,不怎么放心,想找一个紧急求助的软件,万一有什么突发情况,可以立即知道.用金山手机卫士的手机定位功能可以知道对方的位置状态,但不能主动发送求助信息,在网上了很多的APK,都是鸡肋功能,都需要解锁.并打开软件,真正的紧急情况可能没有时间来完成这一系列操作. 于是我自己做了一个这样的软件,在紧急情况下,连续按电源键5次即可发送求救短信和位置信息给事先指定的用户,这个操作在裤兜里就能完成.原理很简单,就是设置监听器捕获屏幕的开关,在较短的时间内屏幕开关达到一定次数后,触发手机定位,定

vue_cli下开发一个简单的模块权限系统之建立登录页面并且实现在浏览器输入地址出现内容

新建一个Login.vue(登录页面,先把Hello.vue的内容复制过来即可) 然后我们打开router下面的index.js,第一个箭头:(引入vue路由)第二个箭头(引入我们新建的Login.vue页面)第三个箭头(我们要使用这个路由)第四个箭头(配置路由,path表示在浏览器中输入的路由名称) 然后我们在浏览器中输入这个login页面的地址,就会出来内容了,这样一个简单的页面就建成了 因为我们要使用semantic-ui,所以我们安装一下semantic-ui,进入到命令行界面输入npm

HTML5实战教程———开发一个简单漂亮的登录页面

最近看过几个基于HTML5开发的移动一样,已经越来越流畅了,相信随着职能手机的配置越来越高性能越来越好,HTML5技术的使用在移动端应用的会越来越普及,应用越来越广泛,因此作为移动开发者,掌握这门技术自然有着强烈的紧迫感.今天就写一个小小的登录页面的demo,巩固最近的学习,也给有兴趣的朋友学习HTML5技术做个参考. 在这里您可以下载到我最后实现的登录页面的demo源码,地址:http://pan.baidu.com/s/1kU1I50b. 准备工作 1.webStorm或者其他网页开发工具.

手把手教你编写一个简单的PHP模块形态的后门

看到Freebuf 小编发表的用这个隐藏于PHP模块中的rootkit,就能持久接管服务器文章,很感兴趣,苦无作者没留下PoC,自己研究一番,有了此文 0×00. 引言 PHP是一个非常流行的web server端的script语言.目前很多web应用程序都基于php语言实现.由于php是个开源软件并易于扩展,所以我们可以通过编写一个PHP模块(module 或者叫扩展 extension)来实现一个Backdoor. 本文就简单介下如何一步步编写一个简单的php 动态扩展后门. 0×01. p