专栏名称: OSC开源社区
OSChina 开源中国 官方微信账号
目录
相关文章推荐
OSC开源社区  ·  Linux内核开发者Serge Semin的告别信 ·  4 天前  
OSC开源社区  ·  曾风靡全球的“现象级”开源游戏——「2048 ... ·  3 天前  
OSC开源社区  ·  线上直播 | ... ·  4 天前  
程序员的那些事  ·  趣图:好比成人玩积木还以为自己是天才 ·  1 周前  
OSC开源社区  ·  谷歌“杀死”流行的开源广告屏蔽扩展 ·  1 周前  
51好读  ›  专栏  ›  OSC开源社区

NGINX 开发指南(Part 2)

OSC开源社区  · 公众号  · 程序员  · 2017-06-09 08:32

正文


前文请戳:NGINX 开发指南(Part 1)

nginx提供了 ngx_slab_pool_t 来分配共享内存。对每个zone,slab pool会自动创建用来分配内存。这个池在共享zone的开头,并且通过表达式 (ngx_slab_pool_t *) shm_zone->shm.addr 访问。共享内存的分配通过调用 ngx_slab_alloc(pool, size)/ngx_slab_c alloc(pool, size) 函数完成,内存通过调用 ngx_slab_free(pool, p) 释放。

slab pool 将共享zone分成多个页。每个页被用于分配同样大小的对象。大小推荐为2的次方,并且不小于8。其它值被四舍五入。对每个页,bitmask被用来表示哪些块是已经使用的和哪些是空闲的。对大小超过半页(通常是2048字节),将按完整的页大小分配。

为了保护数据不会并发访问,需要有 ngx_slab_pool_t 的 mutex 字段。mutex 在分配和释放内存里被使用。然后它也可以用来保护其它分配自共享内存的数据。调用 ngx_shmtx_lock(&shpool->mutex) 锁住,调用 ngx_shmtx_unlock(&shpool->mutex) 解锁。


日志


nginx用ngx_log_t对象记录日志。nginx的日志提供以下几种方式:

  • stderr — 记录到标准错误输出

  • file — 记录到文件

  • syslog — 记录到syslog

  • memory — 记录到内部内存用于开发的目的。这块内存可以在debugger时访问。

一个日志实例可以是一个日志对象链接,每个通过next连接起来。每个消息都被写到所有的日志对象。

每个日志对象有错误级别,用于限制消息写到它自己。以下是nginx提供的几种错误级别:

  • NGX_LOG_EMERG

  • NGX_LOG_ALERT

  • NGX_LOG_CRIT

  • NGX_LOG_ERR

  • NGX_LOG_WARN

  • NGX_LOG_NOTICE

  • NGX_LOG_INFO

  • NGX_LOG_DEBUG

对于调试日志,有以下几种选项:

  • NGX_LOG_DEBUG_CORE

  • NGX_LOG_DEBUG_ALLOC

  • NGX_LOG_DEBUG_MUTEX

  • NGX_LOG_DEBUG_EVENT

  • NGX_LOG_DEBUG_HTTP

  • NGX_LOG_DEBUG_MAIL

  • NGX_LOG_DEBUG_STREAM

通常而言,日志是通过error_log指令创建的,并且在各个阶段都有效,cycle, 配置解析, 客户端连接和其它。

nginx提供以下的日志宏:

  • ngx_log_error(level, log, err, fmt, ...) — 记录错误

  • ngx_log_debug0(level, log, err, fmt), ngx_log_debug1(level, log, err, fmt, arg1) etc — 调试日志,提供最多8个可格式化的参数。

一条日志被存放于栈上大小为NGX_MAX_ERROR_STR(当前为2048字节)的缓冲区里。日志消息的前缀由错误等级,进程PID,连接id(存储于log->connection)以及系统错误文本组成。对于非调式日志(non-debug),log->handler也会被调用以向日志消息增加更多的具体信息。HTTP模块将ngx_http_log_error()函数设置为log handler来记录客户端和服务器的IP地址,当前动作(存储于log->action),客户端的请求行以及server name等等。

例如:

/* specify what is currently done */

log->action = "sending mp4 to client”;

/* error and debug log */

ngx_log_error(NGX_LOG_INFO, c->log, 0, "client prematurely

              closed connection”);

ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,

               "mp4 start:%ui, length:%ui”, mp4->start, mp4->length);

将输出日志:

2016/09/16 22:08:52 [info] 17445#0: *1 client prematurely closed connection while

sending mp4 to client, client: 127.0.0.1, server: , request: "GET /file.mp4 HTTP/1.1”

2016/09/16 23:28:33 [debug] 22140#0: *1 mp4 start:0, length:10000


周期


cycle 对象保持了nginx的运行时上文,由指定的配置创建。cycle的类型是 ngx_cycle_t。在配置重新加载后,新的cycle将从新版的配置创建,而旧的cycle通常在新的成功创建之后删除。目前活动的cycle保存在 ngx_cycle 这个全局变量并且继承自新启动的nginx进程。

cycle 是通过ngx_init_cycle()这个函数创建的。这个函数接收老的cycle作为参数。它用于定位配置并且尽可能多的继承旧的cycle以达到平滑过度。当nginx启动时,模拟的cycle被创建,然后被根据配置的正常cycle替换。

以下是cycle的一些字段:

  • pool — cycle内存池。每个新的cycle都会创建一个内存池。

  • log — cycle日志。初始时这个log继承自旧的cycle。当读完配置后,它会指向new_log。

  • new_log - cycle日志。由配置创建。会根据最外层范围的error_log指令的设置而变化。

  • connections, connections_n — 每个工作进程有一个类型为 ngx_connection_t 的数组 connections,由 event 模块在进程初始化时创建。connections的数目由worker_connections指令指定。

  • files, files_n — 将文件描述符映射到nginx连接的数组。这个映射由event模块使用,具有NGX_USE_FD_EVENT标记(当前是poll和devpoll)。

  • conf_ctx — 模块配置数组。这些配置在读nginx配置文件时创建和填充。

  • modules, modules_n — 类型为 ngx_module_t 的模块数组,包括静态和通过当前配置加载的动态模块。

  • listening — 类型为 ngx_listening_t 的监听socket数组。监听对象通常由不同模块的监听指令通过调用ngx_create_listening()函数添加。nginx基于这些监听对象创建监听socket。

  • paths - 类型为 ngx_path_t 的数组。paths由那些想操作指定目录的模块通过调用ngx_add_path()添加。nginx读完配置之后,如果这些目录不存在,nginx会创建它们。些外,两个handler会被加到每个path:

    • path loader — 只会在nginx启动或配置加载60秒后执行一次。通常读取目录并将数据保存在共享内存里,这个handler由名为“nginx cache loader”的进程调用。

    • path manager — 定期执行。通常移走目录中旧的文件并将变化重新反映到共享内存里。这个handler由名为“nginx cache manager”的进程调用。

  • open_files — 类型为ngx_open_file_t的列表。一个open file对象通过调用ngx_conf_open_file()创建。nginx读完配置后会根据open_files列表打开文件,并且将文件描述符保存在各自的open file对象的fd字段。这些文件会以追加模块打开,并且如果不存在时创建。nginx的工作进程在收到reopen信号(通常是USR1)后会重新打开被打开。这种情况下fd会变成新的文件描述符。目前这些打开的文件被用于日志。

  • shared_memory — 共享内存zone的列表,通过调用ngx_shared_memory_add()函数添加。在所有nginx进程里,共享内存zone会映射到同样的地址,以共享所有的数据,比如HTTP缓存的in-memory树。


缓冲



nginx对 input/output 操作提供了类型为 ngx_buf_t 的buffer。它通常用于保存写入到目的的或从源读的数据。buffer可以将数据指向内存或文件。 技术上来讲同时指向这两种也是可能的。缓冲区的内存是单独创建的,并且不会关联到 ngx_buf_t 这个结构体。

ngx_buf_t 结构体有以下字段:

  • start, end — 内存块的边界,分配给这个缓冲区。

  • pos, last — 内存缓冲区边界,一般在 start .. end 以内。

  • file_pos, file_last — 文件缓冲区边界。相对文件开头的偏移量。

  • tag — 唯一值。用于区分buffer,由不同的模块创建,通常是为了buffer复用。

  • file — file对象。

  • temporary — 临时标记。意味着这个buffer指向可写的内存。

  • memory — 内存标记。表示这个buffer指向只读的内存。

  • in_file — 文件村记。表示该当前buffer指向文件的数据。

  • flush — 清空标记。表示应该清空这个buffer之前的所有数据。

  • recycled — 可回收标记。表示该buffer可以被回收,而且应该尽快的使用。

  • sync — 同步标记。表示这个buffer不带任何数据或特殊的像flush, last_buf这样的。一般这样的buffer在nginx里会被认为是错误的,这个标记允许略过错误检查。

  • last_buf — 标记。表示这个buffer是输出的最后一个。

  • last_in_chain — 标记。表示在(子)请求没有更多有数据的buffer了。

  • shadow — 指向另一个buffer,与当前的buffer有关。通常当前buffer使用这个shadow buffer的数据。一量当前buffer使用完了,这个shadow buffer也应该被标记为已使用。

  • last_shadow — 标记。表示当前buffer是最后一个buffer,并且指向特殊的shadow buffer。

  • temp_file — 标记。表示这个buffer是临时文件。

输入输出 buffer 连接在一个链里。链是定义为下的一系列 ngx_chain_t 。

每个链保存着它的buffer,并且指向下一个链。

使用buffer和chain例子:


网络



连接

连接结构体 ngx_connection_t 是socket描述符的封装。有如下字段:

  • fd — socket描述符

  • data — 任意连接上下文。通常指向更高层次的对象,构建在连接的上面,比如HTTP请求或Stream会话。

  • read, write — 连接的读写事件。

  • recv, send, recv_chain, send_chain — 连接的I/O操作。

  • pool — 连接池

  • log — connection日志

  • sockaddr, socklen, addr_text — 客户端的二进制和文格形式的地址。

  • local_sockaddr, local_socklen — 本地二进制形式地址。初始时这些为空,通过函数 ngx_connection_local_sockaddr() 得到本地socket地址。

  • proxy_protocol_addr, proxy_protocol_port - PROXY protocol 客户端地址和端口,如果为连接开启了 PROXY protocol。

  • ssl — nginx 连接 SSL 上下文

  • reusable — 可复用村记。

  • close — 关闭标记。表示连接是可复用的,并且应该被关闭。

nginx connection可以透传SSL层。这种情况下connection ssl字段指向一个ngx_ssl_connection_t结构体,保留着这个连接的SSL相关的数据,包括 SSL_CTX 和 SSL。处理函数 recv, send, recv_chain, send_chain 被设置成对应的SSL函数。

每个进程的connection数量被限制为 worker_connections 的值。所有的connection结构体会提前创建并且保存在cycle的connections这个字段里。通过 ngx_get_connection(s, log) 获得一个connection结构体。该函数接收socket描述符并且会在connection结构体里作封装。

国为每个进程有connection数的限制,nginx提供了一个抢占connection的方式。通过 ngx_reusable_connection(c, reusable) 允许或禁止connection的复用。调用 ngx_reusable_connection(c, 1) 设置reuse标记并且将connection加入 cycle 的 reusable_connections_queue。每当 ngx_get_connection() 发现 cycle 的 free_connections 无可用的 connection 时,它会调用 ngx_drain_connections() 以释放一定数量的可复用connection。对每个这样的 connection,关闭标记被设置并且读handler被调用以便通过调用ngx_close_connection(c)释放connection,然后将它设置为可复用。连接处于可复用状态下,调用ngx_reusable_connection(c, 0)可以取消复用。举个nginx里connection可复用的例子,在接收客户端的数据之前,HTTP客户端的connection会被标记为可复用。


事件



事件

事件对象 ngx_event_t 在nginx里提供了一种特定事件发生时能被通知的方式。

以下是 ngx_event_t 的一些字段:

  • data — 任意的事件上下文,用于事件处理。通常指向 connection,使其绑定到事件。

  • handler — 回调函数。当事件发生时调用。

  • write — 写标记。表示这是一个写事件。用于区分读写事件。

  • active — 活跃标记。表示该事件收到I/O通知后已经注册,一般来自像 epoll, kqueue, poll 这样的通知机制。

  • ready — 就绪标记。表示这个事件接收到I/O通知。

  • delayed — 延迟标记。意味着I/O由于限速而延迟。

  • timer — 红黑树节点。用于添加进超时红黑树。

  • timer_set — 定时器设置标记。意味着这个事件定时器被设置,但还未过期。

  • timedout — 超时标记。意味着这个事件已经过期。

  • eof — 读结束标记。表示读结束。

  • pending_eof — 结束挂起标记。表示结束是在socket上挂起的,虽然可能还有一些数据可用。这个标记通过 epoll EPOLLRDHUP 事件 or kqueue EV_EOF 标记传递。

  • error — 错误标记。意思当读或写时发生了错误。

  • cancelable — 可取消标记。表示当nginx工作进程退出时,即使该事件没过期也能被立即调用。它提供了一种方式用来完成特定动作,比如清空日志文件。

  • posted — 队列加入标记。意味这个事件已经加入了队列。

  • queue — 队列节点。用于加到队列。


I/O 事件

每个通过调用ngx_get_connection()的 connection 有两个事件:c->read 和 c->write。这两事件用于接受可读写socket的通知。所有的这些事件都是边缘触发模式,意味着只有socket的状态变化时它们才会触发。举个例子,假设只读了部份数据,当有更多的数据到达时,nginx不会重新发读通知。即使底层的I/O通知机制本质上是水平触发的(poll, select等等),nginx将会把它们转成边缘触发。为了将不同平台的事件通知机制统一起来,当处理I/O socket通知或任何I/O操作后,必须调用ngx_handle_read_event(rev, flags) and ngx_handle_write_event(wev, lowat) 这两函数。通常这两函数在读或写事件处理结束后调用一次。


定时器事件

事件可以被设置以通知超时过期。ngx_add_timer(ev, timer) 函数设置事件的超时时间,ngx_del_timer(ev) 删除前面设置的超时。当前为所有事件设置的超时都存放在一个全局的超时红黑树 ngx_event_timer_rbtree。这个树key的类型是 ngx_msec_t,值是从1970年1月1日算起的过期时间。这个树结构提供了快速的插入和删除,以及访问那些最小的超时。后者被nginx用于查找等待I/O事件的时间以及之后的过期事件。


已加事件

添加事件意味着它的handler会在某个时间点的事件遍历时调用。加入事件对简化代码和防止栈溢出是一个好的实践。加入的事件放在一个队列里。宏 ngx_post_event(ev, q) 加入事件到 post queue,ngx_delete_posted_event(ev) 从它所加入的队列中删除事件。通常事件加到 ngx_posted_events 这个队 列。 这个事件在稍后事件遍历中被事件(在所有的I/O和定时器事件已经处理后)。 ngx_event_process_posted() 函数用来处理事件队列。这个函数一直处理到列队为空,这意味着在当前的事件遍历过程中可以加更多的事件。

例子:


遍历事件

所有做I/O处理的nginx进程都有一个事件遍历。唯一没有I/O的进程是master进程,因为它花大部份时间在sigsuspend()上面,以等待信号的到达。事件遍历由 ngx_process_events_and_timers 函数实现。只要进程存在,这个函数就会一直重复的调用。它有以下几个阶段:

  • 找出通过调用 ngx_event_find_timer() 的最小超时时间。该函数找到最左边的定时器树节点,并且返回该节点的到期毫秒数。

  • 处理I/O事件。通过nginx配置选出对应的事件通知机制,然后处理。这个handler会一直待待至有I/O事件发生,或者最小的超时时间。对每个发生的读写事件,它的ready标记会被设置,它的handler会被调用。对Linux而言,通常会使用 ngx_epoll_process_events() 来调用 epoll_wait() 以等待I/O发生。

  • 通过调用 ngx_event_expire_timers() 处理过期事件。这个定时器树会从最左侧的节点向右历遍,直到找到没有过期到期的超时。对每个超时的节点,timedout 标记会被设置,timer_set 会被重量置,并且事件handler会被调用。

  • 通过调用 ngx_event_process_posted() 处理已加事件。这个函数一直重复删除和处理队列里的第一个无素,直到队列为空。

所有这些nginx进程也处理信号。信号handler只是设置了在 ngx_process_events_and_timers() 调用之后的全局变量。


进程



nginx有好几种进程类型。当前进程的类型保存在ngx_process这个全局变量。

  • NGX_PROCESS_MASTER — 主进程运行ngx_master_process_cycle()这个函数。主进程不能有任何的I/O,并且只对信号响应。它读取配置,创建cycle,启动和控制子进程。

  • NGX_PROCESS_WORKER — 工作进程运行ngx_worker_process_cycle()函数。工作进程由子进程创建,处理客户端连接。他们同样也响应来自主进程的信号。

  • NGX_PROCESS_SINGLE — 单进程只存在于master_process模式模式的情况下。生命周期函数是ngx_single_process_cycle()。这个进程创建生命周期并且处理客户端连接。

  • NGX_PROCESS_HELPER — 目前只有两种help进程:cache manager 和 cache loader. 它们共用同样的生命周期函数ngx_cache_manager_process_cycle()。

所有的nginx处理如下信号:

  • NGX_SHUTDOWN_SIGNAL (SIGQUIT) — 优雅结束。收到此信号后主进程发送 shutdown 信号给所有的子进程。当没有任何子进程时,主进程释放生命周期内存池然后结束。工作进程收到此信号后,关闭所有的监听端口然后一直等到超时树为空,最后释放生命周期内存池并且结束。cache 管理进程收到这个信号后立马退出。收到信号后 ngx_quit 设置为0,然后在处理完成后立马重置。ngx_exiting 在工作进程处理退出状态时设置为1。

  • NGX_TERMINATE_SIGNAL (SIGTERM) - 终止。. 收到此信号后主进程发送 terminate 信号给所有的子进程。如果子进程1秒内没结束,它们会通过SIGKILL 信号被杀掉。当没有任何子进程时,主进程释放生命周期内存池然后结束。工作进程或cache管理进程释放生命周期内存池并且结束。ngx_terminate 在收到结信号后设置为1.

  • NGX_NOACCEPT_SIGNAL (SIGWINCH) - 优雅结束工作进程。

  • NGX_RECONFIGURE_SIGNAL (SIGHUP) - 配置热加载。 收到此信号后主进程根据配置文件创建新的cycle。如果这个新的cycle被成功的创建了,旧的cycle会被删除并且启动新的子进程。同时旧进程会被到 shutdown 信号。在单进程模式下,nginx 同样创建新的cycle,但是旧的会一直保留到所有跟它关联的连接都结束了。工作进程和helper进程忽略这种信号。

  • NGX_REOPEN_SIGNAL (SIGUSR1) — 重新打开文件。主进程发送这个信号给工作进程。工作进程重新打开来自cycle的open_files。

  • NGX_CHANGEBIN_SIGNAL (SIGUSR2) — 更新可执行程序。主进程启动新的可执行程序,将所有的监听文件描述符传给它。这些列表是通过环境变量“NGINX” 传递的,描述符值以分号分隔。新的nginx实例读这个变量然后将socket描述符添加到自己的初始cycle。其它进程忽略这种信号。

虽然nginx工作进程可以接受和处理POSIX信号,但是主进程却不通过调用标准kill()给工作进程和help进程发送信号。nginx通过内部进程间通道发送消息。即使这样,目前nginx也只是从主进程给工作进程发送消息。这些消息携带同样的信号。这些通过是socketpairs,其对端在不同的进程。

当运行可执行程序,可以通过-s参数指定几种值。分别是 stop, quit, reopen, reload。它们被转化成信号 NGX_TERMINATE_SIGNAL, NGX_SHUTDOWN_SIGNAL, NGX_REOPEN_SIGNAL 和 NGX_RECONFIGURE_SIGNAL 并且被发送给nginx主进程,通过从nginx pid文件获取进程id。


线程



可以将可能阻塞nginx工作进程的任务移到一个独立的线程。举例,nginx可以配置成使用线程来执行文件I/O操作。另一个例子是使用不具有异步接口的库,不能按通常方式用于nginx。请记住,线程接口是现有异步处理客户端连接的一种补充,而不是一种替代。

为了处理异步,可以使用以下原生pthread的封装:

nginx实现了线程池策略,而不是为每个任务创建一个线程。可以配置多个线程池用于不同的目的(举例,在不同的碰盘组上执行I/O)。每个线程池在启动时创建,并且包含一定数目的线程用来处理一个任务队列。当任务完成时,预定的handler就会被调用。

头文件 src/core/ngx_thread_pool.h 包含了对应的定义:

在配置阶段,一个模块通过调用ngx_thread_pool_add(cf, name)获取线程池引用,以便使用线程。这个函数要么创建新的线程池,要么返回name对应存在的创建池引用。

在运行阶段,用ngx_thread_task_post(tp, task)函数将任务添加进tp线程池的队列。结构体ngx_thread_task_t包含了所有信息,用来执行线程里的用户函数,传递参数和建立完成时的处理handler。


模块



添加新模块

标准nginx模块位于独立的目录,至少包含两个文件:config和包含模块源码的文件。config包含需要跟nginx整合的信息,比如:

这是个POSIX shell脚本,它能设置(或访问)以下变量:

  • ngx_module_type — 模块类型。可选值包括 CORE, HTTP, HTTP_FILTER, HTTP_INIT_FILTER, HTTP_AUX_FILTER, MAIL, STREAM, or MISC

  • ngx_module_name — 模块名称。可以用空格分隔并且单个源文件可以构造多个模块。如果是动态模块,第一个名称将作为二制进文件的名称。这些名称必须跟模块里面的能匹配。

  • ngx_addon_name — 该模块在控制台的输出文本。

  • ngx_module_srcs — 编译该模块时用到的源文件列表,用空格分隔。$ngx_addon_dir 变量可用作替代符,表示模块的当前路径。

  • ngx_module_incs — 用于构建该模块的包含路径。

  • ngx_module_deps — 模块依赖头文件列表。

  • ngx_module_libs — 模块用到的链接库列表。 举个例子,libpthread 可以这样被链接 ngx_module_libs=-lpthread。这些宏可以直接在nginx里使用: LIBXSLT, LIBGD, GEOIP, PCRE, OPENSSL, MD5, SHA1, ZLIB, and PERL

  • ngx_module_link — 模块链接形式,DYNAMIC表示动态模块,ADDON表示静态模块,其它根据不同的值会执行不同的操作。

  • ngx_module_order — 模块顺序,设置模块的加载顺序在 HTTP_FILTER 和 HTTP_AUX_FILTER 类型的模块中是很有用的。模块按反序加载。  在列表底部附近的 ngx_http_copy_filter_module 是最先被执行的。它读数据给其它的filter使用。在列表头部附近的ngx_http_write_filter_module 输出数据,并且是最后执行的。

选项格式是这样的:当前模块名称紧接着用空格分隔的模块列表,这些列表位置靠前,但执行是靠后。这个模块将被插入在这个列表最后一个模块的前面。

对filter模块默认是“ngx_http_copy_filter”,这样该模块被插入在copy filter之前,执行也就是copy filter的后面。对其它类型模块默认值为空。

模块通过使用 --add-module=/path/to/module 表示静态编译,--add-dynamic-module=/path/to/module 表示动态编译。


核心模块

模块是nginx的构建方式,nginx的大部份功能也被实现成模块。模块源文件必须包含类型为 ngx_module_t 的全局变量,定义为:

省略私有部分包含模块版本,签名和预定义的宏 NGX_MODULE_V1。

每个模块包含有私有数据的上下文,特定的配置指令,命令数组,还有可能在特写被调用的函数钩子。模块的生命周期由下面这些组成:

  • 配置指令处理函数在master进程解析配置文件时被调用。

  • init_module 在master进程成功解析配置后调用。

  • master进程创建了worker进程,然后调用这些worker进程各自的 init_process。

  • 当一个工作进程收到来自master的shutdown命令后 exit_process 被调用。

  • master进程在退出前调用 exit_master。

init_module 可能会被调用多次,如果master进程做了配置的reload。

init_master, init_thread and exit_thread 目前是没有实现的;线程在nginx里用于补充处理IO功能,而init_master看起来不是必须的。

type定义了模块类型,有以下几种:

  • NGX_CORE_MODULE

  • NGX_EVENT_MODULE

  • NGX_HTTP_MODULE

  • NGX_MAIL_MODULE

  • NGX_STREAM_MODULE

NGX_CORE_MODULE 是最基础和通用的,处于最低层次的类型。其它类型都依赖在它上面,并且提供更方便的方式去处理各自领域的问题,比如事件和http请求。

核心模块有 ngx_core_module, ngx_errlog_module, ngx_regex_module, ngx_thread_pool_module, ngx_openssl_module,当然 http, stream, mail and event 也是。核心模块的上下文定义如下:

name只是用于方便识别的模块字符串名称,create_conf 和 init_conf 指向创建和初始模块对应的配置结构体。对核心模块,create_conf在解析配置之前被调用, init_conf 在配置成功解析后调用。典型的 create_conf 函数分配空间用于配置,并且设置默认值。init_conf 处理已知配置,然后执行合理的校验和完成配置初始化。

举个例子,很简单的模块 ngx_foo_module 是这样的:


配置指令

ngx_command_t 表示一个配置指令。每个模块包含一组指令,每个指令的格式表示了如何处理参数和解析时调用的函数。

指令数组以 “ngx_null_command” 结束。name 是指令名称,体现在配置文件中,比如 “worker_processes” or “listen”。type 是bit组合,表示参数个数,指令类型和其它对应的属性。参数的标记为:

  • NGX_CONF_NOARGS — 没有参数

  • NGX_CONF_1MORE — 至少一个参数

  • NGX_CONF_2MORE — 至少两个参数

  • NGX_CONF_TAKE1..7 — 明确的1..7个参数

  • NGX_CONF_TAKE12, 13, 23, 123, 1234 — 一个或两个参数,一个或参数,依此类推。

指令类型:

  • NGX_CONF_BLOCK — 表示是一个块,比如它可能用 { } 包含其它指令,或自己实现的解析以处理包含的内容,比如 map 指领。

  • NGX_CONF_FLAG — 表示是个boolean的标记,“on” 或者 “off”。

指令的上下文定义了配置的位置,并且关联到对应的存储配置的地方。

  • NGX_MAIN_CONF — 上层配置

  • NGX_HTTP_MAIN_CONF — http 块

  • NGX_HTTP_SRV_CONF — http server 块

  • NGX_HTTP_LOC_CONF — http location 块

  • NGX_HTTP_UPS_CONF — http upstream 块

  • NGX_HTTP_SIF_CONF — http server “if” 块

  • NGX_HTTP_LIF_CONF — http location “if” 块

  • NGX_HTTP_LMT_CONF — http “limit_except” 块

  • NGX_STREAM_MAIN_CONF — stream 块

  • NGX_STREAM_SRV_CONF — stream server 块

  • NGX_STREAM_UPS_CONF — stream upstream 块

  • NGX_MAIL_MAIN_CONF — mail 块

  • NGX_MAIL_SRV_CONF — mail server 块

  • NGX_EVENT_CONF — event 块

  • NGX_DIRECT_CONF — 没有层级的上下文,直接存储在模块的ctx

配置解析时根据这些标记,要么对放错位置的指令抛出错误,要么调用指令handler,这样即使相同的配置在不同的location也能存储到能区分的位置。

set字段定义了解析配置时调用的handler,并且将解析的值存放到对应的配置结构体。Nginx提供了一些方便的公共函数集:

  • ngx_conf_set_flag_slot — 将 “on” or “off” 转化成 ngx_flag_t 类型的值 1 or 0

  • ngx_conf_set_str_slot — 存储类型为 ngx_str_t 的值

  • ngx_conf_set_str_array_slot — 追加元素为ngx_str_t的ngx_array_t一个新的值。array会自动创建,如果不存在的话。

  • ngx_conf_set_keyval_slot — 追加元素为ngx_keyval_t的ngx_array_t一个新的值。第一个作为键,第二个作为值,如果不存在的话。

  • ngx_conf_set_num_slot — 转化参数为 ngx_int_t 类型的值

  • ngx_conf_set_size_slot — 转化参数为 size_t 类型的值

  • ngx_conf_set_off_slot — 转化参数为 off_t 类型的值

  • ngx_conf_set_msec_slot — 转化参数为 ngx_msec_t 类型的值

  • ngx_conf_set_sec_slot — 转化参数为 time_t 类型的值

  • ngx_conf_set_bufs_slot — 转化两个参数为 ngx_bufs_t,包含了 ngx_int_t 类型的 number 和 buffers的size

  • ngx_conf_set_enum_slot — 转化参数为 ngx_uint_t 类型的值。这是个类似枚举的功能,可以传以 null-terminated 结尾的 ngx_conf_enum_t 数组给post字段,以设置对应的值。

  • ngx_conf_set_bitmask_slot — 转化参数为 ngx_uint_t 类型的值。这是个类似枚举的功能,可以传以 null-terminated ngx_conf_bitmask_t 数组给post字段,以设置对应的值。

  • set_path_slot — 转化参数为 ngx_path_t 类型并且做必须的初始化。详情请看 proxy_temp_path 指令

  • set_access_slot — 转化参数为文件权限mask。详情请看 proxy_store_access 指令。

conf字段定义了哪个上下文用来存储指令的值,或者用NULL表示不使用上下文。简单的核心模块不用配置上下文并且设置 NGX_DIRECT_CONF 标识。 在真实场景里,像http或stream的模块往往更复杂,配置可以在pre-server或者pre-location里,还有甚至是在 "if" 里的。这样的模块里,配置结构会更复杂,请到一些模块里看他们是如何管理各自的配置的。

  • NGX_HTTP_MAIN_CONF_OFFSET — http 块配置

  • NGX_HTTP_SRV_CONF_OFFSET — http 块配置

  • NGX_HTTP_LOC_CONF_OFFSET — http 块配置

  • NGX_STREAM_MAIN_CONF_OFFSET — stream 块配置

  • NGX_STREAM_SRV_CONF_OFFSET — stream server 块配置

  • NGX_MAIL_MAIN_CONF_OFFSET — mail 块配置

  • NGX_MAIL_SRV_CONF_OFFSET — mail server 块配置

offset字段定义了存储该指令值的位置在配置结构体的偏移大小。典型的使用是调用 offsetof() 宏。

post字段包含双重意思:它可能在主handler完成后调用,或者传额外的数据给主handler。第一种情况 ngx_conf_post_t 需要初始化handler,举个例子:

post函数参数是:ngx_conf_post_t它自己, data 来自主handler的参数。


HTTP



连接

每个HTTP客户端连接经历以下几个阶段:

  • ngx_event_accept() 接受一个客户端TCP连接。这个函数在监听socket发生读通知时被调用。在这阶段创建新的 ngx_connecton_t 对象。这个对象封装了新接受的客户端socket。每个nginx监听会提供并传递给这个新的connection对象一个handler。比如 HTTP connection 是ngx_http_init_connection(c)。

  • ngx_http_init_connection() 执行了HTTP connection的早期初始化。这个阶段为connection创建了一个 ngx_http_connection_t 对象,并且引用存放在 connection 的 data 字段。稍后会被替换成 HTTP request 对象。PROXY 协议的解析和 SSL 握手也发生在这个阶段。

  • ngx_http_wait_request_handler() 是读事件handler,当客户端socket有数据可读时被调用。在这个阶段会创建 HTTP request 对象 ngx_http_request_t 并且设置到 connection 的 data 字段。

  • ngx_http_process_request_line() 是读事件handler,用来读请求行。这个 handler 在 ngx_http_wait_request_handler() 里设置。数据被读进 connection 的 buffer。 buffer的大小初始值是指令 client_header_buffer_size。整个 client header 应该是适合这个buffer的。如果这个初始值不够时,会分配一个更大的buffer,它的大小为指令large_client_header_buffers的值。

  • ngx_http_process_request_headers() 是读事件handler,在 ngx_http_process_request_line() 之后设置,被用来读请求头。

  • ngx_http_core_run_phases() 当整个http请求头读完和解析后调用。这个函数运行从 NGX_HTTP_POST_READ_PHASE 到 NGX_HTTP_CONTENT_PHASE 的请求阶段。最后阶段产生响应内容并传给整个filter链。响应不一定要在这阶段发给客户端。它可能缓冲起来然后在最后阶段发送。

  • ngx_http_finalize_request() 通常在请求已经产生了所有的输出或发生错误时调用。后者会查找合适的错误页面作为响应。如果响应没有完全的发送给客户端,HTTP写处理 ngx_http_writer() 会被激活以完成数据的发送。

  • ngx_http_finalize_connection() 在响应完全发送给客户端后调用,然后销毁请求。如果客户端连接的keepalive功能启用了,ngx_http_set_keepalive() 会被调用,用来销毁当前请求并等待这个连接的下一个请求。否则,调用 ngx_http_close_request() 同时销毁请求和连接。


请求

对每个客户端HTTP请求创建一个ngx_http_request_t对象。以下是这个对象的一些字段:

  • connection — 指向类型为 ngx_connection_t 的 connection 对象。多个请求可能同时指向同个连接 - 一个主请求和它的多个子请求。一个请求被删除后,新的请求可能会在同样的连接上被创建。

    注意:HTTP连接 ngx_connection_t 的 data 字段会指向这个请求。这种请求被认为是激活的,相反的其它该连接上的请求则不是。激活的请求被用来处理客户端事件,并且允许发送它的响应给客户端。通常每个请求会在某个时间点激活以发送它的数据。

  • ctx — 一组HTTP模块的上下文。每个类型为 NGX_HTTP_MODULE 的模块在这个请求里可以存任意的东西(通常指向一个结构体)。值存放在模块ctx_index位置上对应ctx数组的地方。以下宏提供了获取和设置请求上下文的方便方式。

    • ngx_http_get_module_ctx(r, module) — 返回模块的上下文。

    • ngx_http_set_ctx(r, c, module) — 设置c为模块的上下文。

  • main_conf, srv_conf, loc_conf — 当前请求的配置数组。配置存放在模块的ctx_index对应的位置。

  • read_event_handler, write_event_handler - 请求的读写事件handler。通常,HTTP连接用 ngx_http_request_handler() 作为读写事件 handler。这个函数会调用当前激活请求的 read_event_handler 和 write_event_handler。

  • cache — 用于缓存上游响应的缓存对象。

  • upstream — 用于代理的上游对象。

  • pool — 请求内存池。这个内存池在请求被删除后被销毁。这个请求对象本身也是从该内存池分配的。对需要活动在整个客户端连接生命周期的分配,应该使用 ngx_connection_t 的 内存池。

  • header_in — 从请求头读的buffer。

  • headers_in, headers_out — 输入和输出的 HTTP 头部对象。两个对象都包含类型为 ngx_list_t 的 headers 头部域,用来保存原始的头部列表。此外还有比较特别的单独字段,用来直接获取和设置,比如 content_length_n, status 等等。

  • request_body — 客户端请求体对象。

  • start_sec, start_msec — 请求创建时间点。用于跟踪请求时间。

  • method, method_name — 客户端HTTP请求方法的数字和文本表示方式。方法的数字值定义在 src/http/ngx_http_request.h,有 NGX_HTTP_GET, NGX_HTTP_HEAD, NGX_HTTP_POST 等宏。

  • http_protocol, http_version, http_major, http_minor - 客户端HTTP协议和版本的文本形式 (“HTTP/1.0”, “HTTP/1.1” 等),数字形式 (NGX_HTTP_VERSION_10, NGX_HTTP_VERSION_11 等) 和主次版本号

  • request_line, unparsed_uri — 客户端原始的请求行和URI。

  • uri, args, exten — 当前请求的请求URI, 参数和文件扩展名。URI值可能由于规范跟客户端发送过来的原始URI不同。经过请求处理,这些值可能在内部重定向时发生改变。

  • main — 指向主请求对象。创建这个创建用来处理HTTP请求,而那些子请求被创建用来执行主请求里的特定子任务。

  • parent — 子请求指向的父请求。

  • postponed — 依次要发送和创建的buffer和子请求列表。这个列表被用在 postpone filter 以提供连续的请求输出,它的各部份由子请求创建。

  • post_subrequest — 不用于主请求。指向子请求完成会调用的具有上下文的handler。不用于主请求。

  • posted_requests — 开始要执行或恢复的请求列表。通过调用请求的write_event_handler完成启动或恢复。通常这个handler会保留请求主函数,第一个运行请求阶段并且产生输出的。

    一个请求经常通过调用 ngx_http_post_request(r, NULL)加到posted_requests。这样会加到主请求的 posted_requests 列表里。函数会 ngx_http_run_posted_requests(c) 会运行所有的请求,这些添加在通过连接激活请求对应的主请求。这个函数应该在所有的事件处理中调用,这样能产生新的添加请求。通常在执行了请求的读写处理后调用。

  • phase_handler — 当前请求阶段的索引。

  • ncaptures, captures, captures_data — 请求最后一次正则匹配产生的正则capture。当处理一个请求时,有很多地方可以发生正则匹配:map 查找, server 通过 SNI 或 HTTP Host 查找,rewrite, proxy_redirect 等等。capture 在查找时产生并且保存这些字段里。字段 ncaptures 保存caputure的个数, captures 保存 capture 边界,captures_data 保存字符串,针对这些匹配到的正则和被用于精确的capture。每次正则匹配后,请求capture会重置并且保存新的值。

  • count — 请求引用计数。这个字段只发生在主请求上。通过简单的 r->main->count++ 就可以递增。要通过 ngx_http_finalize_request(r, rc) 递减。创建子请求和运行读请求体处理都会增加这个计数。

  • subrequests — 当前子请求的嵌套级别。每个子请求会让它的父请求的嵌套级别数减1。一旦这个值到达0就会发生错误,主请求的这个值定义为 NGX_HTTP_MAX_SUBREQUESTS 这个常量。

  • uri_changes — 请求的URI剩余可改变数。一个请求可以改变它的URI的总次数限制为 NGX_HTTP_MAX_URI_CHANGES 这个常量。每次变化都会递减直到0。后者会导致错误发生。这些被认为是改变URI的操作是重写和内部重定向到普通或有命名的location。

  • blocked — 请求上的阻塞次数。只要此值为非0,请求不会被终止。目前这个值会由于待处理AIO(POSIX AIO和线程操作)操作和缓存锁增加。

  • buffered — 位,表示一些模块缓冲了请求产生的输出。一些filter都可以缓冲输出,比如 sub_filter 可以缓冲数据用来作部分字符串匹配,copy filter 因为缺少空闲的output_buffers缓冲数据,等等。只要这个值为非0,请求就不会终止,期望继续刷新。

  • header_only — 标记。用于表示不需要输出请求体。举例,这个标记用于 HTTP HEAD 请求。

  • keepalive — 标记。用于表示否支持客户端的持久连接。这个值根据 HTTP 版本和 头部 "Connection" 的值推算出。

  • header_sent — 标记。表示请求的头部信息已经发送(不一定发到客户端)。

  • internal — 标记。表示当前请求是内部的。要进入这种内部的状态,请求必须通过内部重定向或者是一个子请求。内部请求进入内部的location。

  • allow_ranges — 标记。用于表示如果是HTTP Range的请求,可以发送部份响应给客户端。

  • subrequest_ranges — 标记。用于表示处理子请求时,允许发送部分响应给客户端。

  • single_range — 标记。表示只有一个连续的range能被发送给客户端。这个标记通常在发送数据流时设置,比如来自代理服务器,并且整个响应不是一次完成的。

  • main_filter_need_in_memory, filter_need_in_memory — 标记。用于表示输出应该产生自内存,而非文件。这个被copy filter用来从文件buffer读数据,即使开了sendfile。两者的匹别在设置它们的filter模块的location。这些在postpone filter调用之前的filters,设置了filter_need_in_memory 表明当前请求的输出应该来自memory buffer。在之后调用的filter设置 main_filter_need_in_memory 表明主请求和子请求在发送输出时都要从读文件到内存里。

  • filter_need_temporary — 表示请求输出应该产生自 temporary buffer,而且不能是只读的memory buffer或file buffer。这个用于那些可能直接改变要发送buffer输出的filter。


配置

每个HTTP模块都可以有三种类型的配置:

  • Main配置。 此配置作用于整个http{}块,属于全局配置。此配置中存储了模块的全局配置。

  • Server配置. 此配置作用于一个server{}块,用于存储模块server特有的配置。

  • Location配置. 此配置作用于一个location{}块,if{}块或者limit_except()块,用于存储location相关的配置。

上述配置的结构体是在nginx的配置阶段,通过调用一系列函数来创建的。这些函数会为配置结构体分配内存,并进行初始化和合并操作。下面的例子演示了如何创建一个简单的location配置。该配置中只有一个无符号整形的配置项foo。

在例子中可见,ngx_http_foo_create_loc_conf()函数创建了一个新的配置结构,ngx_http_foo_merge_loc_conf()函数则将配置和更高层次的配置进行合并。实际上,server和location的配置并不仅仅存在于server和location这两个配置层次中,而是为相应更高的配置层次全部进行创建。具体来说,server配置也会在main层次进行创建,而location配置同时会在main, server和location三个层次创建。这些配置使得server和location的配置出现在任何层次的nginx配置中成为了可能。最终各级配置会进行合并。为了在合并的时候识别出缺失的配置并进行忽略,nginx提供了一系列类似于NGX_CONF_UNSET和NGX_CONF_UNSET_UINT这样的宏。标准的nginx合并宏,比如ngx_conf_merge_value()和ngx_conf_merge_uint_value(),提供了一种更加方便的方法来对配置选项进行合并,此外如果在配置文件中没有显式的进行配置,上述合并宏还可以设置默认值。完整的合并宏请参考src/core/ngx_conf_file.h文件。

可以使用如下这些宏来再配置阶段访问HTTP模块的配置。它们的第一个参数都是ngx_conf_t类型的指针。

  • ngx_http_conf_get_module_main_conf(cf, module)

  • ngx_http_conf_get_module_srv_conf(cf, module)

  • ngx_http_conf_get_module_loc_conf(cf, module)

下面的例子展示了nginx核心模块ngx_http_core_module的location配置的指针,并修改其content handler内容的过程。

在运行阶段,可以使用下面的这些宏来获取HTTP模块的配置。

  • ngx_http_get_module_main_conf(r, module)

  • ngx_http_get_module_srv_conf(r, module)

  • ngx_http_get_module_loc_conf(r, module)

需要将指向表示HTTP请求的ngx_http_request_t结构体的指针传递给这些宏。对于一个请求,main配置从不会发生变化,server配置会在切换虚拟服务器配置后发生改变。请求的location配置会随着rewrite或者内部重定向而被多次改变。下面的例子展示了如何在运行阶段获取HTTP配置。


阶段

每个HTTP请求都会经过一系列HTTP阶段(phase),其中每个阶段都会负责处理不同的功能。大部分阶段允许注册handler,这些阶段的handler会在请求到达这个阶段的时候被调用。很多标准nginx模块通过注册阶段handler的方式来实现在某个请求处理阶段被调用模块逻辑。下面是nginx HTTP阶段列表:

  • NGX_HTTP_POST_READ_PHASE是最开始的一个阶段。ngx_http_realip_module模块在此注册了handler,这样一来就可以在其他模块被触发之前就替换掉客户端的IP地址。

  • NGX_HTTP_SERVER_REWRITE_PHASE是用来执行server层面rewrite脚本的阶段。ngx_http_rewrite_module模块在这里注册handler。

  • NGX_HTTP_FIND_CONFIG_PHASE — 基于请求URI来查找location的特殊阶段。这个阶段里不允许注册任何handler。该阶段只执行匹配location的动作。在进入到这个阶段之前,请求中的location被设置成了对应server中的默认location,任何模块获取请求的location配置,只会得到默认location的配置。这个阶段之后,请求将会得到新的location配置。

  • NGX_HTTP_REWRITE_PHASE — 和NGX_HTTP_SERVER_REWRITE_PHASE阶段类似,不过是执行上个阶段新选择的location中的rewrite动作。

  • NGX_HTTP_POST_REWRITE_PHASE — 用于将请求重定向到新location的特殊阶段,这种重定向会在URI被rewrite过的情况下发生。重定向是通过重新跳转回NGX_HTTP_FIND_CONFIG_PHASE阶段来实现的。该阶段不允许注册handler。

  • NGX_HTTP_PREACCESS_PHASE — 这是一个可以注册不同类型handler的通用阶段,此时没有进行过访问控制检查。标准nginx模块ngx_http_limit_conn_module和ngx_http_limit_req_module在此阶段注册了handler。

  • NGX_HTTP_ACCESS_PHASE — 对请求进行访问控制权限检查的阶段。ngx_http_access_module和ngx_http_auth_basic_module这些标准nginx模块在此阶段注册handler。如果使用satisfy指令进行相应的配置,则可以实现只要任意一个handler进行了放行,请求就可以继续后续的处理。

  • NGX_HTTP_POST_ACCESS_PHASE — 对于satisfy设置为any时候的特殊阶段。如果某些access阶段的handler阻断了了访问且没有其他handler放行,则请求会被阻断。此阶段不允许注册任何handler。

  • NGX_HTTP_TRY_FILES_PHASE — 实现try_file功能的特殊阶段。此阶段不允许注册任何handler。

  • NGX_HTTP_CONTENT_PHASE — 用于生成HTTP应答的阶段。多个标准nginx模块在此阶段注册handler,例如ngx_http_index_module和ngx_http_static_module模块。所有注册的这些模块handler会被按照顺序调用直到其中的一个生成输出。也可以基于每个location单独设置content handler。如果ngx_http_core_module模块的location配置中的handler成员被设置,则在NGX_HTTP_CONTENT_PHASE阶段此handler会被调用,注册到此阶段的其他handler会被忽略。

  • NGX_HTTP_LOG_PHASE用来对请求记录日志。当前,只有ngx_http_log_module模块在此阶段注册handler以便记录访问日志。Log阶段handler在每个请求结束,但还没被释放的时候被调用。



推荐阅读

NGINX 开发指南(Part 1)

Web 高效开发必备的 PHP 框架 | 码云周刊

倾力推荐,学习 Kotlin 的 20 个实用资源

TIOBE 6 月编程语言排行榜:Kotlin 突围进入 50 强

“放码过来”邀您亮“项”,一不小心就火了!

点击“阅读原文”查看更多精彩内容