《深入理解NGINX 模块开发与架构解析》之摘抄学习

本文探讨了基于Nginx框架开发的优势,包括事件驱动封装、跨平台开发、模块化设计、服务器进程管理和操作系统优化,以及如何通过调整Linux内核参数提升Nginx性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.基于Nginx框架开发程序有5个优势:

    (1).Nginx将网络、磁盘及定时器等异步事件的驱动都做了非常好的封装,基于它开发将可以忽略这些事件处理的细节;

    (2).Nginx封装了许多平台无关的接口、容器,适用于跨平台开发。

    (3) 优秀的模块化设计,使得开发者可以轻易地复用各种已有的模块,其中既包括基本的读取配置、记录日志等模块,也包括处理请求的诸如HTTP.mail等高级功能模块;

    (4) Nginx是作为服务器来设计其框架的,因此,它在服务器进程的管理上相当出色,基于它开发服务器程序可以轻松地实现程序的动态升级,子进程的监控、管理,配置项的动态修改生效等;

    (5).Nginx充分考虑到各操作系统所擅长的“绝活”,能够使用特殊的系统调用更高效地完成任务时,绝不会去使用低效的通用接口。尤其对于Linux操作系统,Nginx不遗余力地做了大量优化。

2.由于默认的Linux内核参数考虑的是最通用的场景,这明显不符合用于支持高并发访问的Web服务器的定义,所以需要修改Linux内核参数,使得Nginx可以拥有更高的性能。

   最常用的配置:

fs.file-max = 999999  //这个参数表示进程(比如一个worker进程)可以同时打开的最大句柄数
net.ipv4.tcp_tw_reuse = 1 //这个参数设置为1,表示允许将TIME-WAIT状态的socket重新用于新的TCP连接,这对于服务器来说很有意义,因为服务器上总会有大量TIME-WAIT状态的连接。
net.ipv4.tcp_keepalive_time = 600 //这个参数表示当前keepalive启用时,TCP发送keepalive消息的频度。默认是2小时,若将其设置得小一些,可以更快地清理无效的连接
net.ipv4.tcp_fin_timeout = 30 //这个参数表示当前当服务器主动关闭连接时,socket保持在FIN-WAIT-2状态的最大时间
net.ipv4.tcp_max_tw_buckets = 5000 //这个参数表示操作系统允许TIME_WAIT套接字数量的最大值,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。
net.ipv4.ip_local_port_range = 1024   61000  //这个参数定义了在UDP和TCP连接中本地(不包括连接的远端)端口的取值范围。
net.ipv4.tcp_rmem = 4096 32768 262142  //这个参数定义了TCP接收缓存(用于TCP接受滑动窗口)的最小值、默认值、最大值。
net.ipv4.tcp_wmem = 4096 32768 262142  //这个参数定义了TCP发送缓存(用于TCP发送滑动窗口)的最小值、默认值、最大值。
net.core.netdev_max_backlog = 8096  //当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。这个参数表示该队列的最大值。
net.core.rmem_default = 262144 //这个参数表示内核套接字接收缓存区默认的大小。
net.core.wmem_default = 262144  //这个参数表示内核套接字发送缓存区默认的大小。
net.core.rmem_max = 2097152  //这个参数表示内核套接字接收缓存区的最大大小。
net.core.wmem_max = 2097152  //这个参数表示内核套接字发送缓存区的最大大小。
net.ipv4.tcp_syncookies = 1  //该参数与性能无关,用于解决TCP的SYN攻击。
net.ipv4.tcp_max_syn_backlog = 1024 //这个参数表示TCP三次握手建立阶段SYN请求队列的最大长度,默认为1024,将其设置得大一些可以使出现Nginx繁忙来不及accept新连接的情况时,Linux不至于丢失客户端发起的连接请求。

3.configure脚本的内容如下:

#!bin/sh

# Copyright (C) Igor Sysoev
# Copyright (C) Nginx, Inc.

#auto/options脚本处理configure命令的参数。例如,如果参数是--help,那么显示支持的所有参数格式。options脚本会定义后续工作将要用到的变量,然后根据本次参数以及默认值设置这些变量
. auto/options

#auto/init脚本初始化后续将产生的文件路径。例如,Makefile、ngx_modules.c等文件默认情况下会在<nginx-source>/objs/
. auto/init

#auto/sources脚本将分析Nginx的源码结构,这样才能构造后续的Makefile文件
. auto/sources

# 编译过程中所有目录文件生成的路径由--builddir=DIR参数指定,默认情况下为<nginx-source>/objs,此时这个目录将会被创建
test -d $NGX_OBJS || mkdir $NGX_OBJS

# 开始准备建立ngx_auto_headers.h、autoconf.err等必要的编译文件
echo > $NGX_AUTO_HEADERS_H
echo > $NGX_AUTOCONF_ERR

# 向objs/ngx_auto_config.h写入命令行带的参数
echo "#define NGX_CONFIGURE \"$NGX_CONFIGURE\"" > $NGX_AUTO_CONFIG_H

# 判断DEBUG标志,如果有,那么在objs/ngx_auto_config.h文件中写入DEBUG宏
if [ $NGX_DEBUG = YES ]; then
    have=NGX_DEBUG . auto/have
fi

# 现在开始检查操作系统参数是否支持后续编译
if test -z "$NGX_PLATFORM"; then
    echo "checking for OS"

    NGX_SYSTEM=`uname -s 2>/dev/null`
    NGX_RELEASE=`uname -r 2>/dev/null`
    NGX_MACHINE=`uname -m 2>/dev/null`

#屏幕上输出OS名称、内核版本、32位/64位内核
    echo " + $NGX_SYSTEM $NGX_RELEASE $NGX_MACHINE"
    
    NGX_PLATFORM="$NGX_SYSTEM:$NGX_RELEASE:$NGX_MACHINE";
        
    case "$NGX_SYSTEM" in
        MINGW32_*)
            NGX_PLATFORM=win32
            ;;
        esac
    else
        echo "building for $NGX_PLATFORM"
        NGX_SYSTEM=$NGX_PLATFORM
    fi
    
    #检查并设置编译器,如GCC是否安装、GCC版本是否支持后续编译nginx
    . auto/cc/conf

    # 对非Windows操作系统定义一些必要的头文件,并检查其是否存在,一次决定configure后续步骤是否可以成功
    if [ "$NGX_PLATFORM" != win32 ]; then
        . auto/headers
    fi

    # 对于当前操作系统,定义一些特定的操作系统相关的方法并检查当前环境是否支持。例如,对于Linux,在这里使用sched_sestaffinity设置进程优先级,使用Linux特有的sendfile系统调用来加速向网络中发送文件块
    . auto/os/conf

    # 定义类UNIX操作系统中通用的头文件和系统调用等,并检查当前环境是否支持
    if [ "$NGX_PLATFORM" != win32 ]; then
        . auto/unix
    fi

    #最核心的构造运行期modules的脚本。它将会生成ngx_modules.c文件,这个文件会被编译进Nginx中,其中它所做的唯一的事情就是定义了ngx_modules数组。ngx_modules指明Nginx运行期间有哪些模块会参与到请求的处理中,包括HTTP请求可能会使用哪些模块处理,因此,它对数组元素的顺序非常敏感,也就是说,绝大部分模块在ngx_modules数组中的顺序其实是固定的。例如,一个请求必须先执行ngx_http_gzip_filter_module模块重新修改HTTP响应中的头部后,才能使用ngx_http_header_filter模块按照headers_in结构体里的成员构造出以TCP流形式发送给客户端的HTTP响应头部。注意,我们在--add-module=参数里加入的第三方模块也在此步骤写入到ngx_modules.c文件中了
    . auto/modules
    
    # conf脚本用来检查Nginx在链接期间需要链接的第三方静态库、动态库或者目标文件是否存在
    . auto/lib/conf

    # 处理Nginx安装后的路径
    case ".$NGX_PREFIX" in
        .)
            NGX_PREFIX=${NGX_PREFIX:-/usr/local/nginx}
            have=NGX_PREFIX value="\"$NGX_PREFIX/\"" . auto/define
            ;;
        .!)
            NGX_PREFIX=
            ;;
        *)
            have=NGX_PREFIX value="\"$NGX_PREFIX/\"" . auto/define
            ;;
    esac
    
    # 处理Nginx安装后conf文件的路径
    if [ ".$NGX_CONF_PREFIX" != "." ]; then
        have=NGX_CONF_PREFIX value="\"$NGX_CONF_PREFIX/\"" . auto/define
    fi

    # 处理Nginx安装后,二进制文件、pid、lock等其他文件的路径可参见configure参数中路径类选项的说明
    have=NGX_SBIN_PATH value="\"$NGX_SBIN_PATH\"" . auto/define
    have=NGX_CONF_PATH value="\"$NGX_CONF_PATH\"" . auto/define
    have=NGX_PID_PATH value="\"$NGX_PID_PATH\"" . auto/define
    have=NGX_LOCK_PATH value="\"$NGX_LOCK_PATH\"" . auto/define
    have=NGX_ERROR_LOG_PATH value="\"$NGX_ERROR_LOG_PATH\"" . auto/define

    have=NGX_HTTP_LOG_PATH value="\"$NGX_HTTP_LOG_PATH\"" . auto/define
    have=NGX_HTTP_CLIENT_TEMP_PATH value="\"$NGX_HTTP_CLIENT_TEMP_PATH\"" . auto/define
    have=NGX_HTTP_PROXY_TEMP_PATH value="\"$NGX_HTTP_PROXY_TEMP_PATH\"" . auto/define
    have=NGX_HTTP_FASTCGI_TEMP_PATH value="\"$NGX_HTTP_FASTCGI_TEMP_PATH\"" . auto/define
    have=NGX_HTTP_UWSGI_TEMP_PATH value="\"$NGX_HTTP_UWSGI_TEMP_PATH\"" . auto/define
    have=NGX_HTTP_SCGI_TEMP_PATH value="\"$NGX_HTTP_SCGI_TEMP_PATH\"" . auto/define

    # 创建编译时使用的objs/Makefile文件
    . auto/make

    # 为objs/Makefile加入需要连接的第三方静态库、动态库或者目标文件
    . auto/lib/make

    # 为objs/Makefile加入install功能,当执行make install时将编译生成的必要文件复制到安装路径,建立必要的目录
    . auto/install

    # 在ngx_auto_config.h文件中加入NGX_SUPPERSS_WARN宏、NGX_SMP宏
    . auto/stubs

    # 在ngx_auto_config.h文件中指定NGX_USER和NGX_GROUP宏,如果执行configure时没有参数指定,默认两者皆为nobody(也就是默认以nobody用户运行进程)
    have=NGX_USER value="\"$NGX_USER\"" . auto/define
    have=NGX_GROUP value="\"$NGX_GROUP\"" . auto/define
    
    # 显示configure执行的结果,如果失败,则给出原因
    . auto/summary

4.ngx_moduels.c文件就是用来定义ngx_moduels数组的。它指明了每个模块在Nginx中的优先级,当一个请求同时符合多个模块的处理规则时,将按照它们在ngx_moduels数组中的顺序选择最靠前的模块优先处理。对于HTTP过滤模块而言,在ngx_modules数组中越是靠后的模块反而会首先处理HTTP响应。

5.日志文件回滚

   使用-s reopen参数可以重新打开日志文件,这样可以先把当前日志文件改名或转移到其他目录中进行备份,再重新打开时就会生成新的日志文件。这个功能使得日志文件不至于过大。

6.平滑升级Nginx  

   当Nginx服务升级到新的版本时,必须要将旧的二进制文件Nginx替换掉,通常情况下这是需要重启服务的,但Nginx支持不重启服务来完成新版本的平滑升级。

   升级时包括以下步骤:

    1) 通知正在运行的旧版本Nginx准备升级。通过向master进程发送USR2信号可达到目的。例如:

kill -s SIGUSR2 <nginx master pid>

      这时,运行中的Nginx会将pid文件重命名,如将/usr/local/nginx/los/nginx.pid重命名为/usr/local/nginx/logs/nginx.pid.oldbin,这样新的Nginx才有可能启动成功。

     2) 启动新版本的Nginx,可以使用以上介绍过的任意一种启动方法,这时通过ps命令可以发现新旧版本的Nginx在同时运行。

     3) 通过kill命令向旧版本的master进程发送SIGQUIT信号,以“优雅”的方式关闭旧版本的Nginx。随后将只有新版本的Nginx服务运行,此时平滑升级完毕。

7.部署后Nginx进程间的关系

   

8.系统调用gettimeofday的执行频率

   默认情况下,每次内核的事件调用(如epoll、select、poll、kqueue等)返回时,都会执行一次gettimeofdata,实现用内核的时钟来更新Nginx中的缓存时钟。

9.server_name后可以跟多个主机名称,如server_name www.testweb.com、download.testweb.com;。

  在开始处理一个HTTP请求时,Nginx会取出header头中的Host,与每个server中的server_name进行匹配,以此决定到底由哪一个server块来处理这个请求。有可能一个Host与多个server块中的server_name都匹配,这是就会根据匹配优先级来选择实际处理的server块。server_name与Host的匹配优先级如下:

    1) 首先选择所有字符串完全匹配的server_name,如www.testweb.com.

    2)其次选择通配符在前面的server_name,如 *.testweb.com。

    3)再次选择通配符在后面的server_name,如www.testweb.*。

    4)最后选择使用正则表达式才匹配的server_name,如~^\.testweb\.com$。

10.作为静态Web服务器与反向代理服务器的Nginx

    

11.Nginx作为反向代理服务器时转发请求的流程

    

   当客户端发来HTTP请求时,Nginx并不会立刻转发到上游服务器,而是先把用户的请求(包括HTTP包体)完整地接收到Nginx所在服务器的硬盘或者内存中,然后再向上游服务器发起连接,把缓存的客户端请求转发到上游服务器。而Squid等代理服务器则采用一边接收客户端请求,一边转发到上游服务器的方式。

   Nginx的这种工作方式有什么优缺点呢?很明显,缺点是延长了一个请求的处理时间,并增加了用于缓存请求内容的内存内核磁盘空间。而优点则是降低了上游服务器的负载,尽量把压力放在Nginx服务器上。

12.Nginx HTTP模块调用的简化流程

    

13.在Linux平台下,Nginx对ngx_int_t和ngx_uint_t的定义如下:

typedef intptr_t ngx_int_t;
typedef uintptr_t ngx_uint_t;

14.ngx_str_t的定义如下:

typedef struct {
    size_t len;
    u_char *data;
} ngx_str_t;

   任何视图将ngx_str_t的data成员当做字符串来使用的情况,都可能导致内存越界!

   Nginx使用ngx_str_t可以有效地降低内存使用量。例如,用户请求“GET /test?a=1 http/1.1\r\n”存储到内存地址0x1d0b0110上,这时只需要把r->method_name设置为{len = 3,data = 0x1d0b0110}就可以表示方法名"GET",而不需要单独为method_name再分配内存冗余的存储字符串。

15.ngx_list_t是Nginx封装的链表容器,它在Nginx中使用得很频繁,例如HTTP的头部就是用ngx_list_t来存储的。先看一下ngx_list_t相关成员的定义:

typedef struct ngx_list_part_s ngx_list_part_t;

struct ngx_list_part_s {
    void *elts;  //指向数组的起始地址。
    ngx_uint_t nelts;  //表示数组中已经使用了多少个元素,当然,nelts必须小于ngx_list_t结构体中的nalloc.
    ngx_list_part_t *next; //下一个链表元素ngx_list_part_t的地址。
};

typedef struct {
    ngx_list_part_t *last; //指向链表的最后一个数组元素
    ngx_list_part_t part; //链表的首个数组元素
    size_t size;  //链表中的每个ngx_list_part_t元素都是一个数组。因为数组存储的是某种类型的数据结构,且ngx_list_t是非常灵活的数据结构,所以它不会限制存储什么样的数据,只是通过size限制每一个数组元素的占用的空间大小,也就是用户要存储的一个数据所占用的字节数必须小于或等于size。
    ngx_uint_t nalloc; //链表的数组元素一旦分配后是不可更改的。nalloc表示每个ngx_list_part_t数组的容量,即最多可存储多少个数据。
    ngx_pool_t *pool; //链表中管理内存分配的内存池对象。用户要存放的数据占用的内存都是由pool分配的。
} ngx_list_t;

  ngx_list_t的内存分布情况如下:

   

   上图中是由3个ngx_list_part_t数组元素组成的ngx_list_t链表中可能拥有的一种内存分布结构。这里,pool内存池为其分配了连续的内存,最前端内存存储的是ngx_list_t结构中的成员,紧接着是第一个ngx_list_part_t结构占用的内存,然后是ngx_list_part_t结构指向的数组,它们一共占用size*nalloc字节,表示数组中拥有nalloc个大小为size的元素。其后面是第2个ngx_list_part_t结构以及它所指向的数组,依次类推。

16.ngx_table_elt_t数据结构如下所示:

typedef struct {
    ngx_uint_t hash; //表明ngx_table_elt_t也可以是某个散列表数据结构(ngx_hash_t类型)中的成员
    ngx_str_t key;
    ngx_str_t value;
    u_char *lowcase_key;
} ngx_table_elt_t;

  显而易见,ngx_table_elt_t是为HTTP头部"量身定制"的。

17.缓冲区ngx_buf_t是Nginx处理大数据的关键数据结构,它既应用于内存数据也应用于磁盘数据。下面主要介绍ngx_buf_t结构体本身:

typedef struct ngx_buf_s ngx_buf_t;
typedef void * ngx_buf_tag_t;
struct ngx_buf_s {
    /* pos通常是用来告诉使用者本次应该从pos这个位置开始处理内存中的数据,这样设置是因为同一个ngx_buf_t可能被多次反复处理,当然,pos的含义是由使用它的模块定义的*/
    u_char *pos;
    /* last通常表示有效的内容到此为止,注意,pos与last之间的内存是希望nginx处理的内容*/
    u_char *last;
    /*处理文件时,file_pos与file_last的含义与处理内存时的pos与last相同,file_pos表示将要处理的文件位置,file_last表示截止的文件位置*/
    off_t file_pos;
    off_t file_last;

    //如果ngx_buf_t缓冲区用于内存,那么start指向这段内存的起始地址
    u_char *start;
    //与start成员对应,指向缓冲区内存的末尾
    u_char *end;
    /* 表示当前缓冲区的类型,例如由哪个模块使用就指向这个模块ngx_module_t变量的地址*/
    ngx_buf_tag_t tag;
    //引用的文件
    ngx_file_t *file;
    /* 当前缓冲区的影子缓冲区,该成员很少用到,仅仅在12.8节描述的使用缓冲区转发上游服务器的响应时才使用了shadow成员,这是因为Nginx太节约内存了,分配一块内存并使用ngx_buf_t表示接收到的上游服务器响应后,在向下游客户端转发时可能会把这块内存存储到文件中,也可能直接向下游发送,此时Nginx绝不会重新复制一份内存用于新的目的,而是再次建立一个ngx_buf_t结构体指向原内存,这样多个ngx_buf_t结构体指向了同一块内存,它们之间的关系就通过shadow成员来引用。这种设计过于复杂,通常不建议使用*/
    ngx_buf_t *shadow;

    //临时内存标志位,为1时表示数据在内存中且这段内存可以修改
    unsigned temporary:1;
    //标志位,为1时表示数据在内存中且这段内存不可以被修改
    unsigned memory:1;
    //标志位,为1时表示这段内存使用mmap系统调用映射过来的,不可以被修改
    unsigned mmap:1;
    //标志位,为1时表示可回收
    unsigned recycled:1;
    //标志位,为1时表示这段缓冲区处理的是文件而不是内存
    unsigned in_file:1;
    //标志位,为1时表示需要执行flush操作
    unsigned flush:1;
    /*标志位,对于操作这块缓冲区时是否使用同步方式,需谨慎考虑,这可能会阻塞Nginx进程,Nginx中所有操作几乎都是异步的,这是它支持高并发的关键。有些框架代码在sync为1时可能会有阻塞的方式进行I/O操作,它的意义视使用它的Nginx模块而定*/
    unsigned sync:1;
    /*标志位,表示是否是最后一块缓冲区,因为ngx_buf_t可以由ngx_chain_t链表串联起来,因为,当last_buf为1时,表示当前是最后一块待处理的缓冲区*/
    unsigned last_buf:1;
    //标志位,表示是否是ngx_chain-t中的最后一块缓冲区
    unsigned last_in_chain:1;
    /* 标志位,表示是否是最后一个影子缓冲区,与shadow域配合使用。通常不建议使用它*/
    unsigned last_shadow:1;
    //标志位,表示当前缓冲区是否属于临时文件
    unsigned temp_file:1;
};

18.ngx_chain_t是与ngx_buf_t配合使用的链表数据结构,来看一下定义:

typedef struct ngx_chain_s ngx_chain_t;
struct ngx_chain_s {
    ngx_buf_t *buf;  //指向当前的ngx_buf_t缓冲区
    ngx_chain-t *next; //用来指向下一个ngx_chain_t,如果这是最后一个ngx_chain_t,则需要把next置为NULL。
};

  在向用户发送HTTP包体时,就要传入ngx_chain_t链表对象,注意,如果是最后一个ngx_chain_t,那么必须将next置为NULL,否则永远不会发送成功,而且这个请求将一直不会结束(Nginx框架的要求).

19.ngx_module_t是一个Nginx模块的数据结构,如下所示:

typedef struct ngx_module_s ngx_module_t;
struct ngx_module_s {
    /* 下面的ctx_index、index、spare0、spare1、spare2、spare3、version变量不需要在定义时赋值,可以用Nginx准备好的宏NGX_MODULE_V1来定义,它已经定义好了这7个值 
    #define NGX_MODULE_V1   0,0,0,0,0,0,1

    对于一类模块(由下面的type成员决定类别)而言,ctx_index表示当前模块在这类模块中的序号。这个成员常常是由管理这类模块的一个Nginx核心模块设置的,对于所有的HTTP模块而言,ctx_index是由核心模块ngx_http_module设置的。ctx_index非常重要,Nginx的模块化设计非常依赖于各个模块的顺序,它们既用于表达优先级,也用于表明每个模块的位置,借以帮助Nginx框架快速获得某个模块的数据*/
    ngx_uint_t ctx_index;

    /*index表示当前模块在ngx_modules数组中的序号,注意,ctx_index表示的是当前模块在一类模块中的序号,而index表示当前模块在所有模块中的序号,它同样关键。Nginx启动时会根据ngx_modules数组设置各模块的index值,例如:
    ngx_max_module = 0;
    for (i=0; ngx_modules[i]; i++) {
        ngx_modules[i]->index = ngx_max_module++;
    }
    */
    ngx_uint_t index;
    //spare系列的保留变量,暂未使用
    ngx_uint_t spare0;
    ngx_uint_t spare1;
    ngx_uint_t spare2;
    ngx_uint_t spare3;
    //模块的版本,便于将来的扩展。目前只有一种,默认为1
    ngx_uint_t version;

    /*ctx用于指向一类模块的上下文结构体,为什么需要ctx呢?因为前面说过,Nginx模块有许多种类,不同类模块之间的功能差别很大。例如,事件类型的模块主要处理I/O事件相关的功能,HTTP类型的模块主要处理HTTP应用层的功能。这样,每个模块都有了自己的特性,而ctx将会指向特定类型模块的公共接口。例如,在HTTP模块中,ctx需要指向ngx_http_module_t结构体*/
    void *ctx;

    //commands将处理nginx.conf中的配置项
    ngx_command_t *commands;
    
    /*type表示该模型的类型,它与ctx指针是紧密相关的。在官方Nginx中,它的取值范围是以下5种:NGX_HTTP_MODULE、NGX_CORE_MODULE、NGX_CONF_MODULE、NGX_EVENT_MODULE、NGX_MAIL_MODULE。*/
    ngx_uint_t type;
    
    /*在Nginx的启动、停止过程中,以下7个函数指针表示有7个执行点会分别调用者7种方法。对于任一个方法而言,如果不需要Nginx在某个时刻执行它,那么简单地把它设为NULL空指针即可*/
    /*虽然从字面上理解应当在master进程启动时回调init_master,但到目前为止,框架代码从来不会调用它,因此,可将init_master设为NULL */
    ngx_int_t  (*init_master)(ngx_lot_t *log);
    /* init_module回调方法在初始化所有模块时被调用。在master/worker模式下,这个阶段将在启动worker子进程前完成*/
    ngx_int_t (*init_module)(ngx_cycle_t *cycle);
    /*init_process回调方法在正常服务前被调用。在master/worker模式下,多个worker子进程已经产生,在每个worker进程的初始化过程会调用所有模块的init_process函数 */
    ngx_int_t (*init_process)(ngx_cycle_t *cycle);
    /*由于Nginx暂不支持多线程模式,所以init_thread在框架代码中没有被调用过,设为NULL*/
    ngx_int_t (*init_thread)(ngx_cycle_t *cycle);
    //同上,exit_thread也不支持,设为NULL.
    void (*exit_process)(ngx_cycle_t *cycle);
    //exit_master回调方法将在master进程退出前被调用
    void (*exit_master)(ngx_cycle_t *cycle);

    /*保留字段,目前没有使用*/
    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;
};

20.HTTP框架在读取、重载配置文件时定义了由ngx_http_module_t接口描述的8个阶段,HTTP框架在启动过程中会在每一个阶段中调用ngx_http_module_t中相应的方法。

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

    /*当需要创建数据结构用于存储main级别(直属于http{...}块的配置项)的全局配置项时,可以通过create_main_conf回调方法创建存储全局配置项的结构体 */
    void *(*create_main_conf)(ngx_conf_t *cf);
    //常用于初始化main级别配置项
    char *(*init_main_conf)(ngx_conf_t *cf, void *conf);

    /* 当需要创建数据结构用于存储srv级别(直属于虚拟主机server{...}块的配置项)的配置项时,可以通过实现create_srv_conf回调方法创建存储srv级别配置项的结构体 */
    void *(*create_srv_conf)(ngx_conf_t *cf);
    //merge_srv_conf回调方法主要用于合并main级别和srv级别下的同名配置项
    char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

    /*当需要创建数据结构用于存储loc级别(直属于location{...}块的配置项)的配置项时,可以实现create_loc_conf回调方法*/
    void *(*create_loc_conf)(ngx_conf_t *cf);
    //merge_loc_conf回调方法主要用于合并srv级别和loc级别下的同名配置项
    char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;

21.每一个ngx_command_t结构体定义了自己感兴趣的一个配置项:

typedef struct ngx_command_s ngx_command_t;
struct ngx_command_s {
    //配置项名称,如"gzip"
    ngx_str_t name;
    /*配置项类型,type将指定配置项可以出现的位置。例如,出现在server{}或location{}中,以及它可以携带的参数个数 */
    ngx_uint_t type;
    //出现了name中指定的配置项后,将会调用set方法处理配置项的参数
    char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
    //在配置文件中的偏移量
    ngx_uint_t conf;
    /*通常用于使用预设的解析方法解析配置项,这是配置模块的一个优秀设计。*/
    ngx_uint_t offset;
    //配置项读取后的处理方法,必须是ngx_conf_post_t结构的指针
    void *post;
};

   ngx_null_command只是一个空的ngx_command_t,如下所示:

 #define ngx_null_command { ngx_null_string, 0, NULL, 0, 0, NULL }

22.   

typedef enum {

        //在接收到完整的HTTP头部后处理的HTTP阶段

        NGX_HTTP_POST_READ_PHASE = 0,

        /*在还没有查询到URI匹配的location前,这时rewrite重写URL也作为一个独立的HTTP阶段*/

        NGX_HTTP_SERVER_REWRITE_PHASE,
        /*根据URI寻找匹配的location,这个阶段通常由ngx_http_core_module模块实现,不建议其他HTTP模块重新定义这一阶段的行为*/
        NGX_HTTP_FIND_CONFIG_PHASE,
        /*在NGX_HTTP_FIND_CONFIG_PHASE阶段之后重写URL的意义与NGX_HTTP_SERVER_REWRITE_PHASE阶段显然是不同的,因为这两者会导致查找到不同的location块(location是与URI进行匹配的) */
        NGX_HTTP_REWRITE_PHASE,
        /* 这一阶段是用于在rewrite重写URL后重新跳到NGX_HTTP_FIND_CONFIG_PHASE阶段,找到与心得URI匹配的location。所以,这一阶段是无法由第三方HTTP模块处理的,而仅由ngx_http_core_module模块使用*/
        NGX_HTTP_POST_REWRITE_PHASE,
        //处理NGX_HTTP_ACCESS_PHASE阶段前,HTTP模块可以介入的处理阶段
        NGX_HTTP_PREACCESS_PHASE,
        /*这个阶段用于让HTTP模块判断是否允许这个请求访问Nginx服务器*/
        NGX_HTTP_ACCESS_PHASE,
        /*当NGX_HTTP_ACCESS_PHASE阶段中HTTP模块的handler处理方法返回不允许访问的错误码时(实际是NGX_HTTP_FORBIDDEN或者NGX_HTTP_UNAUTHORIZED),这个阶段将负责构造拒绝服务的用户响应。所以,这个阶段实际上用于给NGX_HTTP_ACCESS_PHASE阶段收尾*/
        NGX_HTTP_POST_ACCESS_PHASE,
        /*这个阶段完全是为了try_files配置项而设计的。当HTTP请求访问静态文件资源时,try_files配置项可以使这个请求顺序地访问多个静态文件资源,如果某一次访问失败,则继续访问try_files中指定的下一个静态资源。另外,这个功能完全是在NGX_HTTP_TRY_FILES_PHASE阶段中实现的 */
        NGX_HTTP_TRY_FILES_PHASE,
        //用于处理HTTP请求内容的阶段,这是大部分HTTP模块最喜欢介入的阶段
        NGX_HTTP_CONTENT_PHASE,
        /* 处理完请求后记录日志的阶段,例如,ngx_http_log_module模块就在这个阶段中加入了一个handler处理方法,使得每个HTTP请求处理完毕后会记录access_log日志 */
        NGX_HTTP_LOG_PHASE
} ngx_http_phases;

23.处理方法的返回值,其中包括了HTTP框架已经在/src/http/ngx_http_request.h文件中定义好的宏,如下所示:

#define NGX_HTTP_OK 200
#define NGX_HTTP_CREATED 201
#define NGX_HTTP_ACCEPTED 202
#define NGX_HTTP_NO_CONTENT 204
#define NGX_HTTP_PARTIAL_CONTENT 206

#define NGX_HTTP_SPECIAL_RESPONSE 300
#define NGX_HTTP_MOVED_PERMANENTLY 301
#define NGX_HTTP_MOVED_TEMPORARILY 302
#define NGX_HTTP_SEE_OTHER 303
#define NGX_HTTP_NOT_MODIFIED 304
#define NGX_HTTP_TEMPORARY_REDIRECT 307

#define NGX_HTTP_BAD_REQUEST 400
#define NGX_HTTP_UNAUTHORIZED 401
#define NGX_HTTP_FORBIDDEN 403
#define NGX_HTTP_NOT_FOUND 404
#define NGX_HTTP_NOT_ALLOWED 405
#define NGX_HTTP_REQUEST_TIME_OUT 408
#define NGX_HTTP_CONFLICT 409
#define NGX_HTTP_LENGTH_REQUIRED 411
#define NGX_HTTP_PRECONDITION_FAILED 412
#define NGX_HTTP_REQUEST_ENTITY_TOO_LARGE 414
#define NGX_HTTP_REQUEST_URI_TOO_LARGE 414
#define NGX_HTTP_UNSUPPORTED_MEDIA_TYPE 415
#define NGX_HTTP_RANGE_NOT_SATISFIABLE 416

/* The special code to close connection without any response */
#define NGX_HTTP_CLOSE 444
#define NGX_HTTP_NGINX_CODES 494
#define NGX_HTTP_REQUEST_HEADER_TOO_LARGE 494
#define NGX_HTTPS_CERT_ERROR 495
#define NGX_HTTPS_NO_CERT 496

#define NGX_HTTP_TO_HTTPS 497
#define NGX_HTTP_CLIENT_CLOSED_REQUEST 499

#define NGX_HTTP_INTERNAL_SERVER_ERROR 500
#define NGX_HTTP_NOT_IMPLEMENTED 501
#define NGX_HTTP_BAD_GATEWAY 502
#define NGX_HTTP_SERVICE_UNAVAILABLE 503
#define NGX_HTTP_GATEWAY_TIME_OUT 504
#define NGX_HTTP_INSUFFICIENT_STORAGE 507

24.请求的所有信息(如方法、URI、协议版本号和头部等)都可以在传入的ngx_http_request_t类型参数r中取得。

typedef struct ngx_http_request_s ngx_http_request_t;
struct ngx_http_request_s {
    ...
    ngx_uint_t method;
    ngx_uint_t http_version;

    ngx_str_t request_line;
    ngx_str_t uri;
    ngx_str_t args;
    ngx_str_t exten;
    ngx_str_t unparsed_uri; //表示没有进行URL解码的原始请求。

    ngx_str_t method_name;
    ngx_str_t http_protocol;

    u_char *uri_start;
    u_char *uri_end;
    u_char *uri_ext;
    u_char *args_start;
    u_char *request_start;
    u_char *request_end;
    u_char *method_end;
    u_char *schema_start;
    u_char *schema_end;
    ...
};
    

25.ngx_http_headers_in_t类型的headers_in则存储已经解析过的HTTP头部。

typedef struct {
    /* 所有解析过的HTTP头部都在headers链表中,可以使用遍历链表的方法来获取所有的HTTP头部。注意:这里headers链表的每一个元素都是ngx_table_elt_t成员*/
    ngx_list_t headers;

    /*以下每个ngx_table_elt_t成员都是RFC1616规范中定义的HTTP头部,它们实际都指向headers链表中的响应成员。注意,当它们为NULL空指针时,表示没有解析到响应的HTTP头部*/
    ngx_table_elt_t *host;
    ngx_table_elt_t *connection;
    ngx_table_elt_t *if_modified_since;
    ngx_table_elt_t *if_unmodified_since;
    ngx_table_elt_t *user_agent;
    ngx_table_elt_t *referer;
    ngx_table_elt_t *content_length;
    ngx_table_elt_t *content_type;

    ngx_table_elt_t *range;
    ngx_table_elt_t *if_range;
        
    ngx_table_elt_t *transfer_encoding;
    ngx_table_elt_t *expect;

#if (NGX_HTTP_GZIP)
    ngx_table_elt_t *accept_encoding;
    ngx_table_elt_t *via;
#endif

    ngx_table_elt_t *authorization;
    ngx_table_elt_t *keep_alive;
#if (NGX_HTTP_PROXY || NGX_HTTP_REALIP || NGX_HTTP_GEO)
    ngx_table_elt_t *x_forwarded_for;
#endif

#if (NGX_HTTP_REALIP)
    ngx_table_elt_t *x_real_ip;
#endif

#if (NGX-HTTP_HEADERS)
    ngx_table_elt_t *accept;
    ngx_table_elt_t *accept_language;
#endif

#if (NGX_HTTP_DAV)
    ngx_table_elt_t *depth;
    ngx_table_elt_t *destination;
    ngx_table_elt_t *overwrite;
    ngx_table_elt_t *date;
#endif

    /* user和passwd是只有ngx_http_auth_basic_module才会用到的成员,这里可以忽略*/
    ngx_str_t user;
    ngx_str_t passwd;

    /* cookies是以ngx_array_t数组存储的。*/
    ngx_array_t cookies;
    //server名称
    ngx_str_t server;
    //根据ngx_table_elt_t *content_length计算出的HTTP包体代销
    off_t content_length_n;
    time_t keep_alive_n;

    /* HTTP连接类型,它的取值范围是0、NGX_http_CONNECTION_CLOSE或者NGX_HTTP_CONNECTION_KEEP_ALIVE */
    unsigned connection_type:2;
    /*以下7个标志位是HTTP框架根据浏览器传来的"useragent"头部,它们可用来判断浏览器的类型,值为1时表示是相应的浏览器发来的请求,值为0时则相反 */
    unsigned msie:1;
    unsigned msie6:1;
    unsigned opera:1;
    unsigned gecko:1;
    unsigned chrome:1;
    unsigned safari:1;
    unsigned konqueror:1;
} ngx_http_headers_in_t;

26.HTTP包体的长度有可能非常大,如果视图一次性调用并读取完所有的包体,那么多半会阻塞Nginx进程,HTTP框架提供了一种方法来异步地接受包体:

ngx_int_t ngx_http_read_client_request_body(ngx_http_request_t *r, ngx_http_client_body_handler_pt post_handler);

  ngx_http_read_client_request_body是一个异步方法,调用它只是说明要求Nginx开始接收请求的包体,并不表示是否已经接收完,当接收完所有的包体内容后,post_handler指向的回调方法会被调用。因此,即使在调用了ngx_http-read_client_request_body方法后它已经返回,也无法确定这时是否已经调用过post_handler指向的方法。换句话说,ngx_http_read_client_request_body返回时既有可能已经接收完请求中所有的包体(假设包体的长度很小),也有可能还没开始接受包体。

27.HTTP框架提供的发送HTTP头部的方法如下所示:

ngx_int_t ngx_http_send_header(ngx_http_request_t *r);

28.headers_out的结构类型ngx_http_headers_out_t:

typedef struct {
    //待发送的HTTP头部链表,与headers_in中的headers成员类似
    ngx_list_t headers;

    /*响应中的状态值,如200表示成功。*/
    ngx_str_t status;
    //响应的状态行,如"HTTP/1.1 201 CREATED"
    ngx_str_t status_line;

    /*以下成员(包括ngx_table_elt_t)都是RFC1616规范中定义的HTTP头部,设置后,ngx_http_header_filter_module过滤模块可以把它们加到待发送的网络包中*/
    ngx_table_elt_t *server;
    ngx_table_elt_t *date;
    ngx_table_elt_t *content_length;
    ngx_table_elt_t *content_encoding;
    ngx_table_elt_t *location;
    ngx_table_elt_t *refresh;
    ngx_table_elt_t *last_modified;
    ngx_table_elt_t *content_range;
    ngx_table_elt_t *accept_ranges;
    ngx_table_elt_t *www_authenticate;
    ngx_table_elt_t *expires;
    ngx_table_elt_t *etag;

    ngx_str_t *override_charset;

    /* 可以调用ngx_http_set_content_type(r)方法帮助我们设置Content-Type头部,这个方法会根据URI中的文件扩展名饼对应着mime.type来设置Content-Type值*/
    size_t content_type_len;    
    ngx_str_t content_type;
    ngx_str_t charset;
    u_char *content_type_lowcase;
    ngx_uint_t content_type_hash;

    ngx_array_t cache_control;

    /*在这里指定过content_length_n后,不用再次到ngx_table_elt_t *content_length中设置响应长度*/
    off_t content_length_n;
    time_t date_time;
    time_t last_modified_time;
} ngx_http_headers_out_t;

   ngx_http_send_header方法会首先调用所有的HTTP过滤模块共同处理headers_out中定义的HTTP响应头部,全部处理完毕后才会序列化为TCP字符流发送到客户端。

28.注意:在向用户发送响应包体时,必须牢记Nginx是全异步的服务器,也就是说,不可以在进程的栈里分配内存并将其作为包体发送。当ngx_http_output_filter方法返回时,可能由于TCP连接上的缓冲区还不可写,所以导致ngx_buf_t缓冲区指向的内存还没有发送,可这时方法返回已把控制权交给Nginx了,又会导致栈里的内存被释放,最后就会造成内存越界错误。因此,在发送响应包体时,尽量将ngx_buf_t中的pos指针指向从内存池里分配的内存。

29.经典的"Hello World"示例

static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r)
{
    //必须是GET或者HEAD方法,否则返回405 Not Allowed
    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;
    }

    /* 设置返回的Content-Type.注意,ngx_str_t有一个很方便的初始化宏ngx_string,它可以把ngx_str_t的data和len成员都设置好 */
    ngx_str_t type = ngx_string("text/plain");
    //返回的包体内容
    ngx_str_t type = ngx_string("Hello World!");
    //设置返回状态码
    r->headers_out.status = NGX_HTTP_OK;
    //响应包是有包体内容的,需要设置Content-Lenght的长度
    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;
    }
    //构造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;
    }
    //将Hello World复制到ngx_buf_t指向的内存中
    ngx_memcpy(b->pos, response.data, response.len);
    //注意,一定要设置好last指针
    b->last = b->pos + response.len;
    //声明这是最后一块缓冲区
    b->last_buf = 1;

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

    /*最后一步为发送包体,发送结束后HTTP框架会调用ngx_http_finalize_request方法结束请求 */
    return ngx_http_output_filter(r, &out);
}

30.ngx_file_t的结构如下:

typedef struct ngx_file_s ngx_file_t;
struct ngx_file_s {
    //文件句柄描述符
    ngx_fd_t fd;
    //文件名称
    ngx_str_t name;
    //文件大小等资源信息,实际就是Linux系统定义的stat结构
    ngx_file_info_t info;
    
    /*该偏移量告诉Nginx现在处理到文件何处了,一般不用设置它,Nginx框架会根据当前发送状态设置它*/
    off_t offset;
    //当前文件系统偏移量,一般不用设置它
    off_t sys_offset;

    //日志对象,相关的日志会输出到log指定的日志文件中
    ngx_log_t *log;
    //目前未使用
    unsigned valid_info:1;
    //与配置文件中的directio配置项相对应,在发送大文件时可以设为1
    unsigned directio:1;
};

  Nginx不只对stat数据结构做了封装,对于由操作系统中获取文件信息的stat方法,Nginx也使用一个宏进行了简单的封装,如下:

#define  ngx_file_info(file, sb) stat((const char *) file, sb)

   之后必须要设置Content-Length头部:

r->headers_out.content_length_n = b->file->info.st_size;

   还需要设置ngx_buf_t缓冲区的file_pod和file_last:

b->file_pos = 0;
b->file_last = b->file->info.st_size;

   这里告诉Nginx从文件的file_pos偏移量开始发送文件,一直到达file_last偏移量处截止。

31. HTTP框架定义了3个级别的配置main、srv、loc,分别表示直接出现在http{}、server{}、location{}块内的配置项。当nginx.conf中出现http{}时,HTTP框架会接管配置文件中http{}块内的配置项解析。当遇到http{...}配置块时,HTTP框架会调用所有HTTP模块可能实现的create_main_conf、create_srv_conf、create_loc_conf方法生成存储main级别配置参数的结构体;在遇到server{...}块时会再次调用所有HTTP模块的create_srv_conf、create_loc_conf回调方法生成存储srv级别配置参数的结构体;在遇到location{...}时则会再次调用create_loc_conf回调方法生成存储loc级别配置参数的结构体。因此,实现这3个回调方法的意义是不同的。

32.设定配置项的解析方式

    ngx_command_t结构详解

struct ngx_command_s {
    ngx_str_t name;
    ngx_uint_t type;
    char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
    ngx_uint_t conf;
    ngx_uint_t offset;
    void *post;
};

    (1) ngx_str_t name

        其中,name是配置项名称。

    (2) ngx_uint_t type

        其中,type决定这个配置项可以在哪些块(如http、server、location、if、upstream块等)中出现,以及可以携带的参数类型和个数等。

          ngx_command_t结构体中type成员的取值及其意义

type类型type取值意义
处理配置项时获取当前配置块的方式NGX_DIRECT_CONF一般由NGX_CORE_MODULE类型的核心模块使用,仅与下面的NGX_MAIN_CONF同时设置,表示模块需要解析不属于任何{}内的全局配置项。它实际上会指定set方法里的第3个参数conf的值,使之指向每个模块解析全局配置项的配置结构体(表下面解释)
NGX_ANY_CONF目前未使用,设置与否均无意义
配置项可以在哪些{}配置块中出现NGX_MAIN_CONF配置项可以出现在全局配置中,即不属于任何{}配置块
NGX_EVENT_CONF配置项可以出现在events{}块内
NGX_MAIL_MAIN_CONF配置项可以出现在mail{}块或者imap{}块内
NGX_MAIL_SRV_CONF配置项可以出现在server{}块内,然而该server{}块必须属于mail{}块或者imap{}块
NGX_HTTP_MAIN_CONF配置项可以出现在http{}块内
NGX_HTTP_SRV_CONF配置项可以出现在server{}块内,然而该server块必须属于http{}内
NGX_HTTP_LOC_CONF

配置项可以出现在location{}块内,然而该location块必须属于http{}内

NGX_HTTP_UPS_CONF配置项可以出现在upstream{}块内,然而该upstream块必须属于http{}块
NGX_HTTP_SIF_CONF配置项可以出现在server块内的if{}块中。目前仅有rewrite模块会使用,该if块必须属于http{}块
NGX_HTTP_LIF_CONF配置项可以出现在location块内的if{}块中。目前仅有rewrite模块会使用,该if块必须属于http{}块
NGX_HTTP_LMT_CONF配置项可以出现在limit_except{}块内,然而该limit_except块必须属于http{}块
限制配置项的参数个数NGX_CONF_NOARGS配置项不携带任何参数
NGX_CONF_TAKE1配置项必须携带1个参数
NGX_CONF_TAKE2配置项必须携带2个参数
NGX_CONF_TAKE3配置项必须携带3个参数
NGX_CONF_TAKE4配置项必须携带4个参数
NGX_CONF_TAKE5配置项必须携带5个参数
NGX_CONF_TAKE6配置项必须携带6个参数
NGX_CONF_TAKE7配置项必须携带7个参数
NGX_CONF_TAKE12配置项可以携带1个参数或2个参数
NGX_CONF_TAKE13配置项可以携带1个参数或3个参数
NGX_CONF_TAKE23配置项可以携带2个参数或3个参数
NGX_CONF_TAKE123配置项可以携带1~3个参数
NGX_CONF_TAKE1234配置项可以携带1~4个参数
限制配置项后的参数出现的形式NGX_CONF_ARGS_NUMBERS目前未使用,无意义
NGX_CONF_BLOCK配置项定义了一种新的{}块。例如,http、server、location等配置,它们的ttype都必须定义为NGX_CONF_BLOCK
NGX_CONF_ANY不验证配置项携带的参数个数
NGX_CONF_FLAG配置项携带的参数只能是1个,并且参数的值只能是on或者off
NGX_CONF_1MORE配置项携带的参数个数必须超过1个
NGX_CONF_2MORE配置项携带的参数个数必须超过2个
NGX_CONF_MULTI表示当前配置项可以出现在任意块中(包括不属于任何块的全局配置),它仅用于配合其他配置项使用。type中未加入NGX_CONF_MULTI时,如果一个配置项出现在type成员未标明的配置块中,那么Nginx会认为该配置项非法,最后将导致Nginx启动失败。但如果type中加入了NGX_CONF_MULTI,则认为该配置项一定是合法的,然而又会有两种不同的结果: a.如果配置项出现在type指示的块中,则会调用set方法解析配置项;b.如果配置项没有出现在type指示的块中,则不对该配置项做任何处理。因此,NGX_CONF_MULTI会使得配置项出现在未知块中时不会出错。目前,还没有官方模块使用过NGX_CONF_MULTI.

解释:每个进程总都有一个唯一的ngx_cycle_t核心结构体,它有一个成员conf_ctx维护着所有模块的配置结构体,其类型是void ****。conf_ctx意义为首先指向一个成员皆为指针的数组,其中每个成员指针又指向另外一个成员皆为指针的数组,第2个子数组中的成员指针才会指向各模块生成的配置结构体。这正是为了事件模块、http模块、mail模块而设计的,这有利于不同于NGX_CORE_MODULE类型的特定模块解析配置项。然而,NGX_CORE_MODULE类型的核心模块解析配置项时,配置项一定是全局的,不会从属于任何{}配置块的,它不需要上述这种双数组设计。解析标识为NGX_DIRECT_CONF类型的配置项时,会把void ****类型的conf_ctx强制转换为void **,也就是说,此时,在conf_ctx指向的指针数组中,每个成员指针不再指向其他数组,直接指向核心模块生成的配置结构体。因此,NGX_DIRECT_CONF仅由NGX_CORE_MODULE类型的核心模块使用,而且配置项只应该出现在全局配置中。

   c. char*(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)

    预设的14个配置项解析方法

预设方法名行为
ngx_conf_set_flag_slot如果ngxin.conf文件中某个配置项的参数是on或者off(即希望配置项表达打开或者关闭某个功能的意思),而且在Nginx模块的代码中使用ngx_flag_t变量来保存这个配置项的参数,就可以将set回调方法设为ngx_conf_set_flag_slot。当nginx.conf文件中参数是on时,代码中的ngx_flag_t类型变量将设为1,参数为off时则设为0
ngx_conf_set_str_slot如果配置项后只有1个参数,同时在代码中我们希望用ngx_str_t类型的变量来保存这个配置项的参数,则可以使用ngx_conf_set_str_slot方法
ngx_conf_set_str_array_slot如果这个配置项会出现多次,每个配置项后面都跟着1个参数,而在程序中我们希望仅用一个ngx_array_t动态数组来存储所有的参数,且数组中的每个参数都以ngx_str_t来存储,那么预设的ngx_conf_set_str_array_slot方法可以帮我们做到
ngx_conf_set_keyval_slot与ngx_conf_set_str_array_slot类似,也使用用一个ngx_array_t数组来存储所有同名配置项的参数。只是每个配置项的参数不再只有1个,而必须是两个,且以“配置项名 关键字 值;”的形式出现在nginx.conf文件中,同时,ngx_conf_set_keyval_slot将把这些配置项转化为数组,其中每个元素都存储着key/value键值对。
ngx_conf_set_num_slot

配置项后必须携带1个参数,且只能是数字。存储这个参数的变量必须是整形

ngx_conf_set_size_slot

配置项后必须携带1个参数,表示空间大小,可以是一个数字,这时表示字节数(Byte)。如果数字后跟着k或者K,就表示Kilobyt,1KB=1024B;如果数字后跟着m或者M,就表示Megabyte,1MB=1024KB。ngx_conf_set_size_slot解析后将把配置项后的参数转化成以字节数为单位的数字

ngx_conf_set_off_slot配置项后必须携带1个参数,表示空间上的偏移量。它与设置的参数非常类似,其参数是一个数字时表示Byte,也可以在后面加单位,但与ngx_conf_set_size_slot不同的是,数字后面的单位不仅可以是k或者K、m或者M,还可以是g或者G,这时表示Gigabyte,1GB=1024MB,ngx_conf_set_off_slot解析后将把配置项后的参数转化成以字节数为单位的数字
ngx_conf_set_msec_slot配置项后必须携带1个参数,表示时间。这个参数可以在数字后面加单位,如果单位为s或者没有任何单位,那么这个数字表示秒;如果单位为m,则表示分钟,1m=60s;如果单位为h,则表示小时,1h=60m;如果单位为d,则表示天,1d=24h;如果单位为w,则表示周,1w=7d;如果单位为M,则表示月,1M=30d;如果单位为y,则表示年,1y=265d。ngx_conf_set_msec_slot解析后将把配置项后的参数转化成以毫秒为单位的数字
ngx_conf_set_sec_slot与ngx_conf_set_msec_slot非常类似,唯一的区别是ngx_conf_set_msec_slot解析后将把配置项后的参数转化成以毫秒为单位的数字,而ngx_conf_set_sec_slot解析后会把配置项后的参数转化成以秒为单位的数字
ngx_conf_set_bufs_slot配置项后必须携带一两个参数,第1个参数是数字,第2个参数表示空间大小。例如:"gzip_buffers 4 8k;"(通常用来表示有多少个ngx_buf_t缓冲区),其中第1个参数不可以携带任何单位,第2个参数不带任何单位时表示Byte,如果yik或者K作为单位,则表示Kilobyte,如果以m或者M作为单位,则表示Megabyte。ngx_conf_set_bufs_slot解析后会把配置项后的两个参数转化成ngx_bufs_t结构体下的两个成员,这个配置项对应于Nginx最喜欢用的多缓冲区的解决方案(如接受连接对端发来的TCP流)
ngx_conf_set_enum_slot配置项后必须携带1个参数,其取值范围必须是我们设定好的字符串之一(就像C语言中的枚举一样),首先,我们要用ngx_conf_enum_t结构定义配置项的取值范围,并设定每个值对应的序列号,然后,ngx_conf_set_enum_slot将会把配置项参数转化为对应的序列号。
ngx_conf_set_bitmask_slot与ngx_conf_set_bitmask_slot类似,配置项后必须携带1个参数,其取值范围必须是设定好的字符串只注意。首先,我们要用ngx_conf_bitmask_t结构定义配置项的取值范围,并设定每个值对应的比特位。注意,每个值所对应的比特位都要不同,然后ngx_conf_set_bitmask_slot将会把配置项参数转化为对应的比特位
ngx_conf_set_access_slot这个方法用于设置目录或者文件的读写权限。配置项后可以携带1~3个参数,可以是如下形式:user:rw group:rw all:rw。注意,它的意义与Linux上文件或者目录的权限意义视一致的,但是user/group/all后面的权限只可以设为rw(读/写)或者r(只读),不可以有其他任何形式,如w或者rx等。ngx_conf_set_access_slot将会把这些参数转化为一个整形。
ngx_conf_set_path_slot这个方法用于设置路径,配置项后必须携带1个参数,表示1个有意义的路径。ngx_conf_set_path_slot将会把参数转化为ngx_path_t结构。

    d.ngx_uint_t conf

       conf用于指示配置项所处内存的相对偏移位置,仅在type中没有设置NGX_DIRECT_CONF和NGX_MAIN_CONF时才会生效。对于HTTP模块,conf是必须要设置的,它的取值范围如下:

conf在HTTP模块中的取值意义
NGX_HTTP_MAIN_CONF_OFFSET使用create_main_conf方法产生的结构体来存储解析出的配置项参数
NGX_HTTP_SRV_CONF_OFFSET使用create_srv_conf方法产生的结构体来存储解析出的配置项参数
NGX_HTTP_LOC_CONF_OFFSET使用create_loc_conf方法产生的结构体来存储解析出的配置项参数

    e.ngx_uint_t offset

      offset表示当前配置项在整个存储配置项啊的结构体中的偏移位置(以字节(Byte)为单位)。

    f.void *post

     一般置为NULL。

33.解析HTTP配置的流程

    

   (1) 主循环是指Nginx进程的主循环,主循环只有调用配置文件解析器才能解析nginx.conf文件(这里的"主循环"是指解析全部配置文件的循环代码)。

   (2) 当发现配置文件中含有http{}关键字时,HTTP框架开始启动,这一过程见ngx_http_block方法。

   (3) HTTP框架会初始化所有HTTP模块的序列号,并创建3个数组用于存储所有HTTP模块的create_main_conf、create_srv_conf、create_loc_conf方法返回的指针地址,并把这3个数组的地址保存到ngx_http_conf_ctx_t结构中。

   (4) 调用每个HTTP模块(当然也包括例子中的mytest模块)的create_main_conf、create_srv_conf、create_loc_conf(如果实现的话)方法。

   (5) 把各HTTP模块上述3个方法的地址依次保存到ngx_http_conf_ctx_t结构体的3个数组中。

   (6) 调用每个HTTP模块的preconfiguration方法(如果实现的话).

   (7) 注意,如果preconfiguration返回失败,那么Nginx进程将会停止。

   (8) HTTP框架开始循环解析nginx.conf文件中http{...}里面的所有配置项,注意,这个过程到第19步才会返回。

   (9) 配置文件解析器在检测到1个配置项后,会遍历所有的HTTP模块,检查它们的ngx_command_t数组中的name项是否与配置项名相同。

    (10) 如果找到有1个HTTP模块对这个配置项感兴趣,就调用ngx_command_t结构中的set方法来处理。

    (11) set方法返回是否处理成功。如果处理失败,那么Nginx进程会停止。

    (12) 配置文件解析器继续监测配置项。如果发现server{...}配置项,就会调用ngx_http_core_module模块来处理。因为ngx_http_core_module模块明确表示希望处理server{}块下的配置项。注意,这次调用到第18步才会返回。

    (13) ngx_http_core_module模块在解析server{...}之前,也会如第3步一样建立ngx_http_conf_ctx_t结构,并建立数组保存所有HTTP模块返回的指针地址。然后,它会调用每个HTTP模块的create_srv_conf、create_loc_conf方法(如果实现的话).

    (14) 将上一步各HTTP模块返回的指针地址保存到ngx_http_conf_ctx_t对应的数组中。

    (15) 开始调用配置文件解析器来处理server{...}里面的配置项,注意,这个过程在第17步返回。

    (16) 继续重复第9步的过程,遍历nginx.conf中当前server{...}内的所有配置项。

    (17)配置文件解析器继续解析配置项,发现当前server块已经遍历到尾部,说明server块内的配置项处理完毕,返回ngx_http_core_module模块。

    (18) http core模块也处理完server配置项了,返回至配置文件解析器继续解析后面的配置项。

    (19) 配置文件解析器继续解析配置项,这时发现处理到了http{...}的尾部,返回给HTTP框架继续处理。

    (20) 在第3步和第13步,以及我们没有列出来的某些步骤中(如发现其他server块或者location块),都创建了ngx_http_conf_ctx_t结构体,这时将开始调用merge_srv_conf、merge_loc_conf等方法合并这些不同块(http、server、location)中每个HTTP模块分配的数据结构。

    (21) HTTP框架处理完毕http配置项(也就是ngx_command_t结构中的set回调方法处理完毕),返回给配置文件解析器继续处理其他http{...}外的配置项。

    (22) 配置文件解析器处理完所有配置项后会告诉Nginx主循环配置项解析完毕,这时Nginx才会启动Web服务器。

34.http块与server块下的ngx_http_conf_ctx_t所指向的内存间的关系

   

35.合并配置项过程的活动图,主要包含四大部分内容:

   (1) 如果HTTP模块实现了merge_srv_conf方法,就将http{...}块下create_srv_conf生成的结构体与遍历每一个server{...}配置块下的结构体做merge_srv_conf操作;

   (2) 如果HTTP模块实现了merge_loc_conf方法,就将http{...}块下create_loc_conf生成的结构体与嵌套的每一个server{...}配置块下生成的结构体做merge_loc_conf操作;

   (3) 如果HTTP模块实现了merge_loc_conf方法,就将server{...}块下create_loc_conf生成的结构体与嵌套的每一个location{...}配置块下create_loc_conf生成的数据结构做merge_loc_conf操作;

   (4) 如果HTTP模块实现了merge_loc_conf方法,就将location{...}块下create_loc_conf生成的结构体与继续嵌套的每一个location{...}配置块下create_loc_conf生成的数据结构做merge_loc_conf操作。

   

  上图中包括4重循环,第1层(最外层)遍历所有的HTTP模块,第2层遍历所有的server{...}配置块,第3层是遍历某个server{}块中嵌套的所有location{...}块,第4层遍历某个location{}块中继续嵌套的所有location块(实际上,它会一直递归下去以解析可能被层层嵌套的location块).

36.请求的上下文

    在Nginx中,上下文有很多种含义。HTTP框架定义的这个上下文是针对于HTTP请求的,而且一个HTTP请求对应于每一个HTTP模块都可以有一个独立的上下文结构体(并不是一个请求的上下文由所有HTTP模块共用)。

37.ngx_http_get_module_ctx和ngx_http_set_ctx这两个宏可以完成HTTP上下文的设置和使用。

#define ngx_http_get_module_ctx(r, module) (r)->ctx[module.ctx_index]
#define ngx_http_set_ctx(r, c, module) r->ctx[module.ctx_index] = c;

   当请求第1次进入mytest模块处理时,创建ngx_http_mytest_ctx_t结构体,并设置到这个请求的上下文中。

static ngx_int_t 
ngx_http_mytest_handler(ngx_http_request_t *r)
{
    //首先调用ngx_http_get_module_ctx宏来获取上下文结构体
    ngx_http_mytest_ctx_t *myctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    //如果之前没有设置过上下文,那么应当返回NULL。
    if (myctx == NULL) {
        /*必须在当前请求的内存池r->pool中分配上下文结构体,这样请求结束时结构体占用的内存才会释放*/
        myctx = ngx_palloc(r->pool, sizeof(ngx_http_mytest_ctx_t));
        if (myctx == NULL) {
            return NGX_ERROR;
        }
        //将刚分配的结构体设置到当前请求的上下文中
        ngx_http_set_ctx(r, myctx, ngx_http_mytest_module);
    }
    //之后可以任意使用myctx这个上下文结构体
    ...
}

38.模块在处理任何一个请求时都有ngx_http_request_t结构的对象r,而请求r中又有一个ngx_http_upstream_t类型的成员upstream.

typedef struct ngx_http_request_s ngx_http_request_t;
struct ngx_http_request_s {
    ...
    ngx_http_upstream_t *upstream;
    ...
};

39.启动upstream的流程图

   

40.upstream执行的一般流程

  

41.ngx_http_upstream_t结构体

typedef struct ngx_http_upstream_s ngx_http_upstream_t;
struct ngx_http_upstream_s {
    ...
    /* request_bufs决定发送什么样的请求给上游服务器,在实现create_request方法是需要设置它 */
    ngx_chain_t   *request_bufs;

    //upstream访问时的所有限制性参数
    ngx_http_upstream_conf_t *conf;
    
    //通过resolved可以直接指定上游服务器地址
    ngx_http_upstream_resolved_t *resolved;

    /* buffer成员存储接收自上游服务器发来的响应内容,由于它会被复用,所以具有下例多种意义:
     * a) 在使用process_header方法解析上游响应的包头时,buffer中将会保存完整的响应包头;
     * b) 当下面的buffering成员为1,而且此时upstream是向下游转发上游的包体时,buffer没有意义;
     * c) 当buffering标志位位0时,buffer缓冲区会被用于反复地接收上游的包体,进而向下游转发;
     * d) 当upstream并不用于转发上游包体时,buffer会被用于反复接收上游的包体,HTTP模块实现的input_filter方法需要关注它
    */
    ngx_buf_t   buffer;

    //构造发往上游服务器的请求内容
    ngx_int_t (*create_request)(ngx_http_request_t *r);

    /* 销毁upstream请求时调用 */
    void (*finalize_request)(ngx_http_request_t *r, ngx_int_t rc);

    //5个可选的回调方法
    ngx_int_t (*input_filter_init)(void *data);
    ngx_int_t (*input_filter)(void *data, ssize_t bytes);
    ngx_int_t (*reinit_request)(ngx_http_request_t *r);
    void (*abort_request)(ngx_http_request_t *r);
    ngx_int_t (*rewrite_redirect)(ngx_http_request_t *r, ngx_table_elt_t *h, size_t prefix);

    //是否基于SSL协议访问上游服务器
    unsigned ssl:1;

    /* 在向客户端转发上游服务器的包体时才有用。当buffering为1时,表示使用多个缓冲区以及磁盘文件来转发上有的响应包体。当Nginx与上游间的网速远大于Nginx与下游客户端见的网速时,让Nginx开辟更多的内存甚至使用磁盘文件来缓存上游的响应包体,这是有意义的,它可以减轻上游服务器的并发压力。当buffering为0时,表示只使用上面的这一个buffer缓冲区来向下游转发响应包体*/
    unsigned buffering:1;
    ...
};

42.设置upstream的限制性参数

    ngx_http_upstream_t中的conf成员,它用于设置upstream模块处理请求时的参数,包括连接、发送、接收的超时时间等。

typedef struct {
    ...
    //连接上游服务器的超时时间,单位为毫秒
    ngx_msec_t connect_timeout;
    //发送TCP包到上游服务器的超时时间,单位为毫秒
    ngx_msec_t send_timeout;
    //接收TCP包到上游服务器的超时时间,单位为毫秒
    ngx_msec_t read_timeout;
    ...
} ngx_http_upstream_conf_t;

43.ngx_http_upstream_t结构中的resolved成员可以直接设置上游服务器的地址,首先介绍一下resolved的类型:

typedef struct {
    ...
    //地址个数
    ngx_uint_t naddrs;
    
    //上游服务器的地址
    struct sockaddr *sockaddr;
    socklen_t socklen;
    ...
} ngx_http_upstream_resolved_t;

44.直接执行ngx_http_upstream_init方法即可启动upstream机制。

45.create_request回调方法

    

    如上图,步骤分别如下:

    1) 在Nginx主循环(之类的主循环是指ngx_worker_process_cycle方法)中,会定期地调用事件模块,以检查是否有网络事件发生。

    2)事件模块在接收到HTTP请求后会调用HTTP框架来处理。假设接收、解析完HTTP头部后发现应该由mytest模块处理,这时会调用mytest模块的ngx_http_mytest_handler来处理。

    3)设置回调函数和第三方地址;

    4)调用ngx_http_upstream_init方法启动upstream;

    5)upstream模块会去检查文件缓存,如果缓存中已经有合适的响应包,则会直接返回缓存(当然必须是在使用反向代理文件缓存的前提下)。

    6)回调mytest模块已经实现的create_request回调方法;

    7)mytest模块通过设置r->upstream->request_bufs已经决定好发送什么样的请求到上有服务器。

    8)upstream模块会检查已经介绍过的resolved成员,如果有resolved成员的话,就根据它设置好上游服务器的地址r->upstream->peer成员。

    9)用无阻塞的TCP套接字建立连接;

    10)无论连接是否建立成功,负责建立连接的connect方法都会立刻返回。

    11)ngx_http_upstream_init返回;

    12)mytest模块的ngx_http_mytest_handler方法返回NGX_DONE。

    13)当事件模块处理完这批网络事件后,将控制权交还给Nginx主循环.

46.reinit_request可能会被多次回调。它被调用的原因只有一个,就是在第一次试图向上游服务器建立连接时,如果连接由于各种异常原因失败,那么会根据upstream中conf参数的策略要求再次重连上游服务器,而这时就会调用reinit_request方法了。

    

      上图中的流程的步骤描述如下:

      1) Nginx主循环中会定期地调用事件模块,检查是否有网络事件发生。

      2) 事件模块在确定与上游服务器的TCP连接建立成功后,会回调upstream模块的相关方法处理。

      3) upstream模块这时会把r->upstream->request_sent标志位置为1,表示连接已经建立成功了,现在开始向上游服务器发送请求内容。

      4) 发送请求到上游服务器。

      5) 发送方法当然是无阻塞的(使用了无阻塞的套接字),会立刻返回。

      6) upstream模块处理第2步中的TCP连接建立成功事件。

      7) 事件模块处理完本轮网络事件后,将控制权交还给Nginx主循环。

      8) Nginx主循环重复第1步,调用事件模块检查网络事件。

      9) 这时,如果发现与上游服务器建立的TCP连接已经异常断开,那么事件模块会通知upstream模块处理它。

      10) 在符合重试次数的前提下,upstream模块会毫不犹豫地再次用无阻塞的套接字试图建立连接。

      11) 无论连接是否建立成功都立刻返回。

      12) 这时检查r->upstream->request_sent标志位,会发现它已经被置为1了。

      13) 如果mytest模块没有实现reinit_request方法,那么是不会调用它的。而如果reinit_request不为NULL空指针,就会回调它。

       14) mytest模块在reinit_request中处理完自己的事情。

       15) 处理完第9步中的TCP连接断开事件,将控制权交还给事件模块。

       16) 事件模块处理完本轮网络事件后,交还控制权给Nginx主循环。

47.finalize_request回调方法

    当调用ngx_http_upstream_init启动upstream机制后,在各种原因(无论成功还是失败)导致该请求被销毁前都会调用finalize_request方法。

    在finalize_request方法中可以不做任何事情,但必须实现finalize_request方法,否则Nginx会出现空指针调用的严重错误。

48.process_header回调方法

    process_header是用于解析上游服务器返回的基于TCP的响应头部的,因此,process_header可能会被多次调用,它的调用次数与process_header的返回值有关。如果process_header返回NGX_AGAIN,这意味着还没有接收到完整的响应头部,如果再次接收到上游服务器发来的TCP流,还会把它当做头部,仍然调用process_header处理。如果process_header返回NGX_OK(或者其他非NGX_AGAIN的值),那么在这次连接的后续处理中将不会再次调用process_header。

   

    上图中的步骤解释:
   1) Nginx主循环中会定期地调用事件模块,检查是否有网络事件发生。

   2) 事件模块接收到上游服务器发来的响应时,会回调upstream模块处理。

   3) upstream模块这时可以从套接字缓冲区中读取到来自上游的TCP流。

   4) 读取的响应会存放到r->upstream->buffer指向的内存中。注意:在未解析完响应头部前,若多次接收到字符流,所有接收自上游的响应头回完整地存放到r->upstream->buffer缓冲区中。因此,在解析上游响应包头时,如果buffer缓冲区全满却还没有解析到完整的响应头部(也就是说,process_header一直在返回NGX_AGAIN),那么请求就会出错。

   5) 调用mytest模块实现的process_header方法。

   6) process_header方法实际上就是在解析r->upstream->buffer缓冲区,试图从中取到完整的响应头部(当然,如果上游服务器与Nginx通过HTTP通信,就是接收到完整的HTTP头部)。

   7) 如果process_header返回NGX_AGAIN,那么表示还没有解析到完整的响应头部,下次还会调用process_header处理接收到的上游响应。

   8) 调用无阻塞的读取套接字接口。

   9) 这时有可能返回套接字缓冲区已经为空。

   10) 当第2步中的读取上游响应时间处理完毕后,控制权交还给事件模块。

   11) 事件模块处理完本轮网络事件后,交还控制权给Nginx主循环。

49.举例说明upstream机制。实现的功能很简单:"即以访问mytest模块的URL参数作为搜索引擎的关键字,用upstream方式访问google,查询URL里的参数,然后把google的结果返回给用户。"

   每一个HTTP请求都会有独立的ngx_http_upstream_conf_t结构体,在mytest模块的例子中,所有的请求都将共享同一个ngx_http_upstream_conf_t结构体,因此,这里把它放到ngx_http_mytest_conf_t配置结构体中,如下所示:

typedef struct {
    ngx_http_upstream_conf_t upstream;
} ngx_http_mytest_conf_t;

   (1) 在启动upstream前,先将ngx_http_mytest_conf_t下的upstream成员赋给r->upstream->conf成员。

static void *ngx_http_mytest_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_mytest_conf_t *mycf;

    mycf = (ngx_http_mytest_conf_t *)ngx_pcalloc(cf->pool, sizeof(ngx_http_mytest_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:

    retrurn mycf;
}

static char *ngx_http_mytest_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_mytest_conf_t *prev = (ngx_http_mytest_conf_t *)parent;
    ngx_http_mytest_conf_t *conf = (ngx_http_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, ngx_http_proxy_hide_headers, &hash) != NGX_OK) {
        return NGX_CONF_ERROR;
    }
    
    return NGX_CONF_OK;
}

    

  本例必须要使用上下文才能正确地解析upstream上游服务器的响应包,因为upstream模块每次接收到一段TCP流时都会回调mytest模块实现的process_header方法解析,这样就需要有一个上下文保存解析状态。

      (2) 在create_request方法中构造请求

         下面方法用于创建发送给上游服务器的HTTP请求,upstream模块将会回调它,实现如下:

static ngx_int_t
mytest_upstream_create_request(ngx_http_request_t *r)
{
    /* 在发往google上游服务器的请求很简单,就是模仿正常的搜索请求,以/search?q=...的URL来发起搜索请求。backendQueryLine中的%V等转化格式的用法*/
    static ngx_str_t backendQueryLine = 
        ngx_string("GET /search?q=%V HTTP/1.1\r\nHost: www.google.com\r\nConnection: close\r\n\r\n");
    ngx_int_t queryLineLen = backendQueryLine.len + r->args.len - 2;
    /* 必须在内存池中申请内存,这有以下两点好处:一个好处是,在网络情况不佳的情况下,向上游服务器发送请求时,可能需要epoll多次调度send才能发送完成,这时必须保证这段内存不会被释放;另一个好处是,在请求结束时,这段内存会被自动释放,减低内存泄露的可能性*/
    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;
}

          (3) 在process_header方法中解析包头

             process_header负责解析上游服务器发来的基于TCP的包头,在本例中,就是解析HTTP响应行和HTTP头部,因此,这里使用mytest_process_status_line方法解析HTTP响应行,使用mytest_upstream_process_header方法解析http响应头部。

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_mytest_ctx_t *ctx = ngx_http_get_module_ctx(r, ngx_http_mytest_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);
}

  mytest_upstream_process_header方法可以解析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_haeder_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, "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;
    }
}


    

   当mytest_upstream_process_header返回NGX_OK后,upstream模块开始把上游的包体(如果有的话)直接转发到下游客户端。

          (4) 在finalize_request方法中释放资源

                当请求结束时,将会回调finalize_request方法,如果我们希望此时释放资源,如打开的句柄等,那么可以把这样的代码添加到finalize_request方法中。本例中定义了mytest_upstream_finalize_request方法,由于我们没有任何需要释放的资源,所以该方法没有完成任何实际工作,只是因为upstream模块要求必须实现finalize_request回调方法。

          (5) 在ngx_http_mytest_handler方法中启动upstream

                 在开始介入处理客户端请求的ngx_http_mytest_handler方法中启动upstream机制,而何时会结束,则视Nginx与上游的google服务器间的通信而定。

static ngx_int_t 
ngx_http_mytest_handler(ngx_http_request_r *r)
{
    //首先建立HTTP上下文结构体ngx_http_mytest_ctx_t
    ngx_http_mytest_ctx_t *myctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    if (myctx == NULL) {
        myctx = ngx_palloc(r->pool, sizeof(ngx_http_mytest_ctx_t));
        if (myctx == NULL) {
            return NGX_ERROR;
        }
        
        //得到配置结构体ngx_http_mytest_conf_t
        ngx_http_mytest_conf_t *mycf = (ngx_http_mytest_conf_t *)ngx_http_get_module_loc_conf(r, ngx_http_mytest_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_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.google.com
        static struct sockaddr_in backendSockAddr;
        struct hostent *pHost = gethostbyname((char *) "www.google.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;

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

        //这里必须将count成员加1
        r->main->count++;
        //启动upstream
        ngx_http_upstream_init(r);
        //必须返回NGX_DONE
        return NGX_DONE;
    }

           到此为止,高性能地访问第三方服务的upstream例子就介绍完了。在本例中,可以完全异步地访问第三方服务,并发访问数也只会受制于物理内存的大小,完全可以轻松达到几十万的并发TCP连接。

50.使用subrequest的方式只需要完成以下4步操作即可:

    1) 在nginx.conf文件中配置好子请求的处理方式;

    2) 启动subrequest子请求;

    3) 实现子请求执行结束时的回调方法;

    4) 实现父请求被激活时的回调方法。

51.ngx_http_subrequest的定义:

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

     1) ngx_http_request_t *r

          ngx_http_request_t *r 是当前的请求,也就是父请求。

      2) ngx_str_t *uri

          ngx_str_t *uri是子请求的URI,它对究竟选用nginx.conf配置文件中的哪个模块来处理子请求起决定性作用。

      3) ngx_str_t *args

          ngx_str_t *args是子请求的URI参数,如果没有参数,可以传送NULL空指针。

       4) ngx_http_request_t **psr

          psr是输出参数而不是输入参数,它将把ngx_http_subrequest生成的子请求传出来。

        5) ngx_http_post_subrequest_t *ps

          这里传入创建的ngx_http_post_subrequest_t结构体地址,它指出子请求结束时必须回调的处理方法。

         6) ngx_uint_t flags

           flag的取值范围包括: (1) 0.在没有特殊需求的情况下都应该填写它; (2) NGX_HTTP_SUBREQUEST_IN_MEMORY。这个宏会将子请求的subrequest_in_memory标志位置为1,这意味着如果子请求使用upstream访问上游服务器,那么上游服务器的响应都将会在内存中处理;(3) NGX_HTTP_SUBREQUEST_WAITED。这个宏会将子请求的waited标志位置为1,当子请求提前结束时,有个done标志位置为1,但目前HTTP框架并没有针对这两个标志位做任何实质性处理。注意,flag是按比特位操作的,这样可以同时包含上述3个值。

         7) 返回值

            返回NGX_OK表示成功建立子请求;返回NGX_ERROR表示建立子请求失败。

52.如何启动subrequest

     处理父请求的过程中会创建子请求,在父请求的处理方法返回NGX_DONE后,HTTP框架会开始执行子请求

      

       上图中的步骤如下:

       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主循环。

53.如何转发多个子请求的响应包体

     每个请求的ngx_http_request_t结构体中都有一个postponed成员:

struct ngx_http_request_s {
    ...
    ngx_http_postponed_request_t *postponed;
    ...
}
它实际上是一个链表:
typedef struct ngx_http_postponed_request_s ngx_http_postponed_request_t;
struct ngx_http_postponed_request_s {
    ngx_http_request_t *request;
    ngx_chain_t *out;
    ngx_http_postponed_request_t *next;
};

        多个ngx_http_postponed_request_t之间使用next指针连接成一个单向链表。ngx_http_postponed_request_t中的out成员是ngx_chain_t结构,它指向的是来自上游的、将要转发给下游的响应包体。

        每当使用ngx_http_output_filter方法(反向代理模块也使用该方法转发响应)向下游的客户端发送响应包体时,都会调用到ngx_http_postpone_filter_module过滤模块处理这段要发送的包体。

//这里的参数in就是将要发送给客户端的一段包体
static ngx_int_t
ngx_http_postpone_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
    ngx_connection_t *c;
    ngx_http_postponed_request_t *pr;

    //c是Nginx与下游客户端间的连接,c->data保存的是原始请求
    c = r->connection;

    //如果当前请求r是一个子请求(因为c->data指向原始请求)
    if (r != c->data) {
        /* 如果待发送的in包体不为空,则把in加到postponed链表中属于当前请求的ngx_http_postponed_request_t结构体的out链表中,同时返回NGX_OK,这意味着本次不会把in包体发送客户端*/
        if (in) {
            ngx_http_postpone_filter_add(r, in);
            return NGX_OK;
        }

        //如果当前请求是子请求,而in包体又为空,那么直接返回即可
        return NGX_OK;
    }

    //如果postponed为空,表示请求r没有子请求产生的响应需要转发
    if (r->postponed == NULL) {
        /* 直接调用下一个HTTP过滤模块继续处理in包体即可。如果没有错误的话,就会向下游客户端发送响应 */
        if (in || c->buffered) {
            return ngx_http_next_filter(r->main, in);
        }
        return NGX_OK;
    }

    /* 至此,说明postponed链表中是有子请求产生的响应需要转发的,可以先把in包体加到待转发响应的末尾 */
    if (in) {
        ngx_http_postpone_filter_add(r, in);
    }

    //循环处理postponed链表中所有子请求待转发的包体
    do {
        pr = r->postponed;

        /* 如果pr->request是子请求,则加入到原始请求的posted_requests队列中,等待HTTP框架下次调用这个请求时再来处理 */
        if (pr->request) {
            r->postponed = pr->next;
            c->data = pr->request;
            return ngx_http_post_request(pr->request, NULL);
        }

        //调用下一个HTTP过滤模块转发out链表中保存的待转发的包体
        if (pr->out == NULL) {
        } else {
            if (ngx_http_next_filter(r->main, pr->out) == NGX_ERROR) {
                return NGX_ERROR;
            }
        }

        //遍历完postponed链表
        r->postponed = pr->next;

    } while (r->postponed);
    return NGX_OK;
}

         

54.子请求在结束前会回调在ngx_http_post_subrequest_t中实现的handler方法,在这个handler方法中,又设置了父请求被激活后的执行方法mytest_post_handler,流程如下:

                

   上图中的步骤如下:

    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主循环。

55.subrequest是分解复杂请求的设计方法,派生出的子请求使用某些HTTP模块基于upstream访问第三方服务是最常见的用法,通过subrequest可以使Nginx在保持高并发的前提下处理复杂的业务。

56.

57.默认即编译进Nginx的HTTP过滤模块

默认即编译进Nginx的HTTP过滤模块功能
ngx_http_not_modified_filter_module仅对HTTP头部进行处理。在返回200成功时,根据请求中If-Modified-Since或者If-Unmodified-Since头部取得浏览器缓存文件的时间,再分析返回用户文件的最后修改时间,以此决定是否直接发送304 Not Modified响应给用户
ngx_http_range_body_filter_module处理请求中的Range信息,根据Range中的要求返回文件的一部分给用户
ngx_http_copy_filter_module仅对HTTP包体做处理。将用户发送的ngx_chain_t结构的HTTP包体复制到新的ngx_chain-t结构中(都是各种指针的复制,不包括实际HTTP响应内容),后续的HTTP过滤模块处理的ngx_chain-t类型的成员都是ngx_http_copy_filter_module模块处理后的变量
ngx_http_headers_filter_module仅对HTTP头部做处理。允许通过修改nginx.conf配置文件,在返回给用户的响应中添加任意的HTTP头部
ngx_http_userid_filter_module仅对HTTP头部做处理。这就是执行configure命令时提到的http_userid_module模块,它基于cookie提供了简单的认证管理功能
ngx_http_charset_filter_module可以将文本类型返回给用户的响应包,按照nginx.conf中的配置重新进行编码,再返回给用户
ngx_http_ssi_filter_module支持SSI(Server Side Include,服务器端嵌入)功能,将文件内容包含到网页中并返回给用户
ngx_http_postpone_filter_module仅对HTTP包体做处理。它仅应用于subrequest产生的子请求。它使得多个子请求同时向客户端发送响应时能够有序,所谓的“有序”是指按照子请求的顺序发送响应
ngx_http_gzip_filter_module对特定的HTTP响应包体(如网页或者文本文件)进行gzip压缩,再把压缩后的内容返回给用户
ngx_http_range_header_filter_module

支持range协议

ngx_http_chunked_filter_module支持chunk编码
ngx_http_header_filter_module仅对HTTP头部做处理。该过滤模块将会把r->headers_out结构体中的成员序列化为返回给用户的HTTP响应字符流,包括响应行(如HTTP/1.1 200 OK)和响应头部,并通过调用ngx_http_write_filter_module过滤模块中的过滤方法直接将HTTP包头发送给客户端
ngx_http_write_filter_module仅对HTTP包体做处理。该模块负责向客户端发送HTTP响应

58.过滤模块例子中,HTTP头部处理方法的执行活动图

 

59.过滤模块例子中,HTTP包体处理方法的执行活动图

   

60.参考另一篇:https://blog.youkuaiyun.com/zhangge3663/article/details/83180659

61.ngx_module_t接口及其对核心、事件、HTTP、mail等4类模块ctx上下文成员的具体化

    

62.Nginx常用模块及其之间的关系

    

63.传统Web服务器和Nginx间的重要差别:前者是每个事件消费者独占一个进程资源,后者的事件消费者只是被事件分发者进程短期调用而已。

64.在阻塞代码段上按照下面4种方式来划分阶段:

   (1) 将阻塞进程的方法按照相关的触发事件分解为两个阶段

   (2) 将阻塞方法调用按照时间分解为多个阶段的方法调用

   (3) 在“无所事事”且必须等待系统的响应,从而导致进程空转时,使用定时器划分阶段

   (4) 如果阻塞方法完全无法继续划分,则必须使用独立的进程执行这个阻塞方法

65.内存池的设计

    为了避免出现内存碎片、减少向操作系统申请内存的次数、降低各个模块的开发复杂度,Nginx设计了简单的内存池。这个内存池没有很复杂的功能:通常它不负责回收内存池中已经分配出的内存。

    通常每一个请求都有一个这种简易的独立内存池(Nginx为每一个TCP连接都分配一个内存池,HTTP框架为每一个HTTP请求又分配了1个内存池),而在请求结束时则会销毁整个内存池,把曾经分配的内存一次性归还给操作系统。

66.Nginx核心的框架代码一直围绕着一个结构体展开,它就是ngx_cycle_t。无论是master管理进程、worker工作进程还是cache manager(loader)进程,每一个进程都毫无例外地拥有唯一一个ngx_cycle_t结构体。

    作为一个Web服务器,Nginx首先需要监听端口并处理其中的网络事件。ngx_cycle_t对象中有一个动态数组成员叫做listening,它的每一个数组元素都是ngx_listening_t结构体,而每个ngx_listen_t结构体又代表着Nginx服务器监听的一个端口。

typedef struct ngx_listening_s ngx_listening_t;
struct ngx_listening_s {
    //socket套接字句柄
    ngx_socket_t fd;
    //监听sockaddr地址
    struct sockaddr *sockaddr;
    //sockaddr地址长度
    socklen_t socklen;
    /* 存储IP地址的字符串addr_text最大长度,即它指定了addr_text所分配的内存大小 */
    size_t addr_text_max_len;
    //以字符串形式存储IP地址
    ngx_str_t addr_text;
    //套接字地址。例如,当type是SOCK_STREAM时,表示TCP
    int type;
    /* TCP实现监听时的backlog队列,它表示允许正在通过三次握手建立TCP连接但还没有任何进程开始处理的连接最大个数 */
    int backlog;
    //内核中对于这个套接字的接收缓冲区大小
    int rcvbuf;
    //内核中对于这个套接字的发送缓冲区大小
    int sndbuf;
    
    //当新的TCP连接成功建立后的处理方法
    ngx_connection_handler_pt handler;

    /* 实际上框架并不使用servers指针,它更多的是作为一个保留指针,目前主要用于HTTP或者mail等模块,用于保存当前监听端口对应着的所有主机名 */
    void *servers;
    //log和logp都是可用的日志对象的指针
    ngx_log_t log;
    ngx_lot_t *logp;

    //如果为新的TCP连接创建内存池,则内存池的初始大小应该是pool_size
    size_t pool_size;
    /* TCP_DEFER_ACCEPT选项将在建立TCP连接成功且接收到用户的请求数据后,才向对监听套接字感兴趣的进程发送事件通知,而连接建立成功后,如果post_accept_timeout秒后仍然没有收到的用户数据,则内核直接丢弃连接 */
    ngx_msec_t post_accept_timeout;

    /* 前一个ngx_listening_t 结构,多个ngx_listening_t结构体之间由previous指针组成单链表 */
    ngx_listening_t *previous;
    //当前监听句柄对应着的ngx_connection_t结构体
    ngx_connection_t *connection;

    /* 标志位,为1则表示在当前监听句柄有效,且执行ngx_init_cycle时不关闭监听端口,为0时则正常关闭。该标志位框架代码会自动设置 */
    unsigned open:1;
    /* 标志位,为1表示使用已有的ngx_cycle_t来初始化新的ngx_cycle_t结构体时,不关闭原来打开的监听端口,这对运行中升级程序很有用,remain为0时,表示正常关闭曾经打开的监听端口。该标志位框架代码会自动设置,参见ngx_init_cycle方法 */
    unsigned remain:1;
    /* 标志位,为1时表示跳过设置当前ngx_listening_t结构体中的套接字,为0时正常初始化套接字。该标志位框架代码会自动设置 */
    unsigned ignore:1;
    //表示是否已经绑定。实际上目前该标志位没有使用
    unsigned bound:1; /* 已经绑定 */
    /* 表示当前监听句柄是否来自前一个进程(如升级Nginx程序),如果为1,则表示来自前一个进程。一般会保留之前已经设置好的套接字,不做改变 */
    unsigned inherited:1; /* 来自前一个进程 */
    //目前未使用
    unsigned nonblocking_accept:1;
    //标志位,为1时表示当前结构体对应的套接字已经监听
    unsigned listen:1;
    //表示套接字是否阻塞,目前该标志位没有意义
    unsigned nonblocking:1;
    //目前该标志位没有意义
    unsigned shared:1;
    //标志位,为1时表示Nginx会将网络地址转变为字符串形式的地址
    unsigned addr_ntop:1;
};

67.Nginx框架是围绕着ngx_cycle_t结构体来控制进程运行的。

typedef struct ngx_cycle_s ngx_cycle_t;
struct ngx_cycle_s {
    /* 保存着所有模块存储配置项的结构体的指针,它首先是一个数组,每个数组成员又是一个指针,这个指针指向另一个存储着指针的数组,因此会看到void **** */
    void ****conf_ctx;
    //内存池
    ngx_pool_t *pool;

    /* 日志模块中提供了生成基本ngx_log_t日志对象的功能,这里的log实际上是在还没有执行ngx_init_cycle方法前,也就是还没有解析配置前,如果有信息需要输出到日志,就会暂时使用log对象,它会输出到屏幕。在ngx_init_cycle方法执行后,将会根据nginx.conf配置文件中的配置项,构造出正确的日志文件,此时会对log重新赋值 */
    ngx_lot_t *log;
    /*由nginx.conf配置文件读取到日志文件路径后,将开始初始化error_log日志文件,由于log对象还在用于输出日志到屏幕,这时会用new_log对象暂时性地替代log日志,待初始化成功后,会用new_log的地址覆盖上面的log指针 */
    ngx_lot_t new_log;
    
    //与下面的files成员配合使用,指出files数组里元素的总数
    ngx_uint_t files_n;
    /* 对于poll、rtsig这样的事件模块,会以有效文件句柄数来预先建立这些ngx_connection_t结构体,以加速事件的收集、分发。这时files就会保存所有ngx_connection_t的指针组成的数组,files_n就是指针的总数,而文件句柄的值用来访问files数组成员 */
    ngx_connection_t **files;

    //可用连接池,与free_connection_n配合使用
    ngx_connection_t *free_connections;
    //可用连接池中连接的总数
    ngx_uint_t free_connection_n;

    /* 双向链表容器,元素类型是ngx_connection_t结构体,表示可重复使用连接队列 */
    ngx_queue_t reusable_connections_queue;

    /*动态数组,每个数组元素存储着ngx_listening_t成员,表示监听端口及相关的参数 */
    ngx_array_t listening;

    /*动态数组容器,它保存着Nginx所有要操作的目录。如果有目录不存在,则会视图创建,而创建目录失败将会导致Nginx启动失败。例如,上传文件的临时目录也在pathes中,如果没有权限创建,则会导致Nginx无法启动 */
    ngx_array_t pathes;

    /* 单链表容器,元素类型是ngx_open_file_t结构体,它表示Nginx已经打开的所有文件。事实上,Nginx框架不会向open_files链表中添加文件,而是由对此感兴趣的模块向其中添加文件路径名,Nginx框架会有ngx_init_cycle方法中打开这些文件 */
    ngx_list_t open_files;

    /* 单链表容器,元素的类型是ngx_shm_zone_t结构体,每个元素表示一块共享内存*/
    ngx_list_t shared_memory;

    //当前进程中所有连接对象的总数,与下面的connections成员配合使用
    ngx_uint_t connection_n;

    //指向当前进程中的所有连接对象,与connection_n配合使用
    ngx_connection_t *connections;

    //指向当前进程中的所有读事件对象,connection_n同时表示所有读事件的总数
    ngx_event_t *read_events;
    //指向当前进程中的所有写事件对象,connection_n同时表示所有写事件的总数
    ngx_event_t *write_events;

    /* 旧的ngx_cycle_t对象用于引用上一个ngx_cycle_t对象中的成员。例如ngx_init_cycle方法,在启动初期,需要建立一个临时的ngx_cycle_t对象保存一些变量,再调用ngx_init_cycle方法时就可以把旧的ngx_cycle_t对象传进去,而这时old_cycle对象就会保存这个前期的ngx_cycle_t对象 */
    ngx_cycle_t *old_cycle;

    //配置文件相对于安装目录的路径名称
    ngx_str_t conf_file;
    /* Nginx处理配置文件时需要特殊处理的在命令行携带的参数,一般是-g选项携带的参数 */
    ngx_str_t conf_param;
    //Nginx配置文件所在目录的路径
    ngx_str_t conf_prefix;
    //Nginx安装目录的路径
    ngx_str_t prefix;
    //用于进程间同步的文件锁名称
    ngx_str_t lock_file;
    //使用gethostname系统调用得到的主机名
    ngx_str_t hostname;
};

68.Nginx启动过程的流程图

   

69.worker进程正常工作、退出时的流程图

    

70.master进程不需要处理网络事件,它不负责业务的执行,只会通过管理worker等子进程来实现重启服务、平滑升级、更换日志文件、配置文件实时生效等功能。

71.epoll是目前Linux操作系统上最强大的事件管理机制。

72.首先,Nginx定义了一个核心模块ngx_events_module,这样在Nginx启动时会调用ngx_init_cycle方法解析配置项,一旦在nginx.conf配置文件中找到ngx_events_module感兴趣的“events{}”配置项,ngx_events_module模块就开始工作了。ngx_events_module模块定义了事件类型的模块,它的全部工作就是为所有的事件模块解析"events{}"中的配置项,同时管理这些事件模块存储配置项的结构体。

    其次,Nginx定义了一个非常重要的事件模块ngx_event_core_module,这个模块会决定使用哪种事件驱动机制,以及如何管理事件。

    最后,Nginx定义了一系列运行在不同操作系统、不同内核版本上的事件驱动模块,包括:ngx_epoll_module、ngx_kqueue_module、ngx_poll_module、ngx_select_module、ngx_devpoll_module、ngx_eventport_module、ngx_aio_module、ngx_rtsig_module和基于Windows的ngx_select_module模块。在ngx_event_core_module模块的初始化过程中,将会从以上模块中选取一个作为Nginx进程的事件驱动模块。

73.ngx_connection_t连接池示意图

    

74.所有事件模块配置项结构体的指针是如何管理的。

  

75.ngx_event_core_module事件模块启动时的工作流程

     

  76.epoll在Linux内核中申请了一个简易的文件系统,把原来的一个select或者poll调用分成了3个部分:调用epoll_create建立1个epoll对象(在epoll文件系统中给这个句柄分配资源)、调用epoll_ctx向epoll对象中添加这100万个连接的套接字、调用epoll_wait收集发生事件的连接。

77.ngx_event_accept方法建立新连接的流程

   

78.ngx_process_events_and_times方法中的事件框架处理流程

   

79.解析server{}块内配置项的流程

   

80.解析location{}配置块的流程

    

81.HTTP框架的初始化流程

    

82.接收、解析HTTP请求行的流程图

    

83.ngx_http_process_request_headers方法接收HTTP头部的流程图

    

84.ngx_http_process_request处理HTTP请求的流程图

    

85.ngx_http_core_run_phases方法的执行流程

    

86.ngx_http_request_handler方法的执行流程

    

87.ngx_http_read_client_request_body方法的流程图

    

88.upstream机制的场景示意图

    

89.ngx_http_upstream_connect方法的流程图

    

90.ngx_http_upstream_send_request方法的流程图

     

91.邮件代理功能的示意序列图

    

   从网络通信的角度来看,Nginx实现邮件代理功能时会把一个请求分为以下4个阶段。

   1) 接收并解析客户端初始请求的阶段;

   2) 向认证服务器验证请求合法性,并获取上游邮件服务器地址的阶段;

   3) Nginx根据用户信息多次与上游邮件服务器交互验证合法性的阶段;

   4) Nginx在客户端与上游邮件服务器间纯粹透传TCP流的阶段。

92.初始化邮件请求的流程

    

93.启动邮件认证、向认证服务器发起连接的流程

   

94.Nginx框架使用了3种传递消息传递方式:共享内存、套接字、信号。Nginx各进程间共享数据的主要方式就是使用共享内存.

nginx一个高性能的Web服务器,同时也是一个反向代理服务器和电子邮件(IMAP/POP3)代理服务器。它最显著的特点是具有高并发处理能力和低内存占用。为了满足不同用户的需求,nginx提供了模块化的架构,可以通过开发模块来扩展其功能。 深入理解nginx模块开发,首先需要了解nginx架构nginx的主要部分包括master进程和worker进程。master进程负责管理worker进程,而worker进程负责处理实际的客户端请求。nginx模块系统允许开发者向master进程或worker进程添加自定义的功能。 在nginx模块开发中,主要涉及到以下几个方面的内容: 1. 配置文件解析nginx的配置文件是使用类似于C语言的语法进行解析的。模块开发者需要了解nginx的配置文件语法,并且能够解析和处理自定义的配置项。 2. HTTP请求处理:开发基于HTTP协议的模块时,需要能够处理和解析HTTP请求。模块可以拦截特定的URL,处理请求,并返回相应的响应。 3. 事件处理:nginx使用事件驱动的模型来处理并发请求。模块开发者需要了解事件驱动的机制,实现自己的事件处理逻辑,并nginx的事件处理系统进行交互。 4. 内存管理:nginx以低内存占用著称,这是因为它使用了自己的内存管理机制。模块开发者需要了解nginx的内存管理方式,并遵循相应的规则。 5. 日志记录:nginx提供了灵活的日志记录功能。模块开发者可以通过定制日志记录方式,将特定的信息记录到指定的日志文件中。 总的来说,深入理解nginx模块开发架构解析需要对nginx的整体架构有深入了解,并具备一定的系统编程和网络编程经验。通过开发和调试模块,可以进一步理解nginx的原理和内部实现,掌握更多高性能Web服务器开发的知识和技巧。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值