Ngixn基础. 认识Nginx事件模块(一)

2024-06-22 19:48

本文主要是介绍Ngixn基础. 认识Nginx事件模块(一),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

对于Nginx的整体框架, 尚且只能了解个大概, 并不能弄清除整个流程. 索性先放着, 先了解其他组件.
事件处理框架所要解决的问题是 如何收集, 管理, 分发事件. 且事件类型主要为网络事件和定时器事件.
既然需要支持跨平台, 那么就肯定要封装不同平台的事件驱动机制. 当然, 我只能看懂select, poll, epoll这几个... 那么Nginx是如何选择的呢?
    1 . 之前在对框架作了解时, 知道了整个Nginx框架为了不那么复杂, 只定义配置模块和核心模块. 而核心模块中又有其他模块的代言模块.
        对于事件模块而言, 其在核心模块中的代言模块就是ngx_events_module模块. 所以可以这样理解, ngx_events_module不是事件模块而是核心模块, 但它定义了事件模块.
        在Nginx启动过程中调用的ngx_cycle_init方法解析配置项时, 一旦在配置文件中找到ngx_events_module感兴趣的"event{}"配置项, 该核心模块就开始工作了. 此模块定义了事件类型的模块, 它的全部工作就是为所有事件模块解析"event{}"中的配置项, 同时管理这些事件模块存储配置项的结构体.
    2 . 只是, 除了ngx_events_module对于事件模块很重要外, 还有一个事件模块 ngx_event_core_module也十分重要. 这个模块会决定使用哪种事件驱动机制, 以及如何管理事件
    3 . 当然, 其他就是一系列不同系统不同内核版本的事件驱动模块的封装而成的模块了.


下面开始分析Nginx的事件模块:
关于事件模块中的事件
在Nginx中, 每个事件都由ngx_event_t结构体来表示. 所以以此作为切入点.
struct ngx_event_s{//事件相关对象, 通常data指向ngx_connection_t连接对象. (开启异步IO后可能指向ngx_event_aio_t结构体)void *data;  ...//为1时表示可以建立连接. 通常在ngx_cycle_t中的listening动态数组中, 每一个监听对象ngx_listening_t对应的读事件中的accept才会是1unsigned accept;//这个标志位用于区分当前事件是否过期. 它仅仅是给事件驱动模块使用的(epoll等模块), 无关事件消费模块(http等模块)//当需要处理一批事件时, 处理这批事件前面的事件可能导致关闭后面的连接, 而这些被关闭的连接可能影响到这批事件中还未处理的事件.  //这时, 可以通过此标志位来处理来避免处理后面已经过期的事件//看不懂很正常, 下面会细谈.unsigned instance;...//标志, 为1表示延迟建立TCP连接, 即经过三次握手后并不建立连接, 而是正真数据包来后才建立TCP连接unsigned deferred_accept;...//在epoll事件驱动机制下表示一次尽可能多的建立TCP连接, 与配置文件中的配置项"multi_accept"对应unsigned available;//这个事件发生时的处理方法, 每个事件消费模块都会重新实现它, 以决定这个事件最终如何消费ngx_event_handler_pt handler;//定时器节点, 用于定时器红黑树中ngx_rbtree_node_t timer;...//post事件将构成一个队列再统一处理, 队列以next和prev作为链表指针, 以此构成一个简易的双向链表ngx_event_t *next;ngx_event_t **prev;
};

以上并没有完整的列出该结构体中的每个元素, 是为了更容易专注当前分析的部分.
对于每个事件, 其最核心的部分就是handler回调方法, 所有的Nginx模块只要处理事件就必然要设置handler回调方法.
既然handler决定如何消费事件, 那么如何触发这个消费事件呢?
这时候就是将事件放到事件驱动机制中去(epoll..), 等待被触发之后调用handler函数. 那么如何将事件添加到epoll中呢?
按照没有被封装过的epoll, 我们立即能想到调用epoll_ctl函数. 所以肯定会有结构体封装这类操作函数:
下面结构体定义了每个事件模块都要实现的接口(ngx_event_core_module和几个事件驱动模块):

typedef struct {ngx_str_t              *name;         //事件模块的名称void                 *(*create_conf)(ngx_cycle_t *cycle);            //创建存储配置项的结构体char                 *(*init_conf)(ngx_cycle_t *cycle, void *conf);    //解析完配置项后, 此函数用于综合处理当前模块感兴趣的全部配置项ngx_event_actions_t     actions;      //每个事件驱动机制都要实现的10个方法. 比如添加事件, 删除事件, 初始化等等
} ngx_events_module_t;
从ngx_event_actions_t这个结构体就可以看出, 这就是封装操作函数的结构体:

typedef struct {//添加和删除事件ngx_int_t  (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);ngx_int_t  (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);...//向事件驱动机制中添加或删除一个连接的读写事件ngx_int_t  (*add_conn)(ngx_connection_t *c);ngx_int_t  (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);...//在正常的工作循环中, 将通过调用此方法来处理事件, 此方法是处理, 分发事件的核心ngx_int_t  (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer,ngx_uint_t flags);//初始化事件驱动模块ngx_int_t  (*init)(ngx_cycle_t *cycle, ngx_msec_t timer);//退出事件驱动模块前调用的方法void       (*done)(ngx_cycle_t *cycle);
} ngx_event_actions_t;


所以, 如果想要往epoll中添加事件的话, 可以调用封装好的add函数.  只是因为平台不同, Nginx提供了两个简单的方法用于在事件驱动模块中添加删除事件
下面是这两个函数的原型:
ngx_int_t ngx_handle_read_event(ngx_event_t *rev, ngx_uint_t flags);
ngx_int_t ngx_handle_write_event(ngx_event_t *wev, size_t lowat);
第一个函数用于将读事件添加到事件驱动模块中, 这样该事件对应的TCP连接一旦出现可读事件, 就会回调该事件的handler方法.
        其第二个参数可以是0或NGX_CLOSE_EVENT(但NGX_CLOSE_EVENT只在epoll的LT模式有效, 但epoll在Nginx中默认使用ET模式, 所以这个标志一般不用)
第二个函数将写事件添加到事件驱动模块中.
        第二个参数lowat表示只有当连接对应的套接字缓冲区中必须有lowat大小的可用空间时, 事件收集器才处理这个可写事件(lowat为0表示不考虑可写缓冲区大小)


关于事件中的连接事件
对于web服务器 , 每个用户请求至少对应一个TCP连接; 为了处理这个事件, 每个连接至少需要一个读事件和一个写事件, 这样才能根据触发事件调度相应模块读取请求或是发送响应.
因此, Nginx定义了结构体 ngx_connection_t来表示连接, 表示被动接收的连接. 要注意的是, 这种连接不能随意创建, 必须从连接池(数组)中获取.
谈到连接池, 那么我们可以想象, 某个连接创建某个连接释放都是不固定的, 所以每次想要向连接池申请连接时, 都需要遍历一遍连接池寻找空闲连接. 这种效率自然很差, 所以Nginx定义了空闲连接池用以提供空闲连接. 空闲连接池的实现是利用了ngx_connection_t中的某个暂时不用的指针来作为连接下一个空闲连接的枢纽的.
struct ngx_connection_s {//因为Nginx采用了事先创建好连接池的策略, 所以当连接未使用时, data成员用于充当连接池中空闲连接链表中的next指针. //当连接被使用时, data的意义由使用的模块而定. 比如在HTTP模块中, data指向ngx_http_request_t请求void               *data;//对应的读写事件ngx_event_t        *read;ngx_event_t        *write;//套接字句柄ngx_socket_t        fd;//直接接收/发送网络字节流的方法ngx_recv_pt         recv;ngx_send_pt         send;//以ngx_chain_t链表为参数来接收/发送网络字符流的方法ngx_recv_chain_pt   recv_chain;ngx_send_chain_pt   send_chain;//这个连接对应的ngx_listening_t对象, 此连接由listening监听端口的事件建立ngx_listening_t    *listening;//这个连接已经发送出去的字节数off_t               sent;ngx_log_t          *log;//内存池, 一般在accept一个新连接时, 会创建一个内存池, 而在连接结束时会销毁内存池. //内存池大小将由listening监听对象中的pool_size来指定ngx_pool_t         *pool;...struct sockaddr    *local_sockaddr;socklen_t           local_socklen;//用于接收, 缓存来自客户发来的字节流. 每个事件消费模块可以自由决定从连接池中分配多大空间给buffer这个字段//比如HTTP模块, 它的大小决定于client_header_buffer_size配置项ngx_buf_t          *buffer;//该字段用来将当前连接以双向链表元素的形式添加到ngx_cycle_t核心结构体的reusable_connections_queue双向链表中//表示可重用的连接ngx_queue_t         queue;//连接使用次数ngx_atomic_uint_t   number;//处理的请求次数ngx_uint_t          requests;...//表示TCP连接被销毁了, 此时此结构体仍可用, 但其套接字, 内存池已不可用了unsigned            destroyed:1;unsigned            idle:1;unsigned            reusable:1;unsigned            close:1;unsigned            sendfile:1;//表示发送缓冲区的阀值unsigned            sndlowat:1;unsigned            tcp_nodelay:2;   /* ngx_connection_tcp_nodelay_e */unsigned            tcp_nopush:2;    /* ngx_connection_tcp_nopush_e */...
};
这其中的4个关于接收, 发送网络字节流的方法以指针的形式出现, 说明每个连接都可以采用不同的接收方法, 每个事件消费模块都可以灵活决定其行为.
不同的事件驱动机制需要使用的发送接收的方法也多是不同的
关于连接池, 上面谈到一点, 这里再做补充.
从下图可以看出, 在ngx_cycle_t中的connections和free_connections两个成员构成了一个连接池. 其中connections指向连接池首部, free_connections则指向第一个空闲连接.
所有空闲连接都以data成员作为next指针串联成一个单链表. 一旦有申请连接, 就返回free_connections指向的空闲连接, free_connections继续指向下一个空闲连接.
归还连接时则只需把连接插入到free_connections链表的表头即可.(有点静态链表的感觉)

除了分配连接, Nginx还认为每个连接至少需要一个读事件和写事件, 于是在ngx_cycle_t结构体中可以发现read_events和write_events这两个数组. 其数组大小和连接池大小相同. 所以根据下标就能将一个连接与两种事件相联系起来
Nginx也定义了两个用于申请回收连接的函数:
ngx_connection_t *ngx_get_connection(ngx_socket_t s, ngx_log_t *log);
void ngx_free_connection(ngx_connection_t *c);

                         


ngx_events_module核心模块
文章一开始也提到了, Nginx框架定义了配置模块与核心模块, 某些核心模块又定义了事件模块, HTTP模块等.
ngx_events_module就是定义了事件模块的核心模块. 此模块定义了上面涉及的事件类型, 每个事件模块都要实现的ngx_events_module_t接口, 管理这些事件模块生成的配置项结构体, 并解析事件类型配置项, 同时, 会调用其在ngx_commands_t数组中定义的回调方法
既然ngx_events_module定义了事件模块, 且有了以上结构体的认识, 那么就从ngx_events_module模块开始对事件模块进行整体的认识
static ngx_command_t  ngx_events_commands[] = {{ ngx_string("events"),NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,ngx_events_block,0,0,NULL },ngx_null_command
};
可见, 在配置文件中遇到"events{}"后, ngx_events_module就会调用ngx_events_block来处理.
作为核心模块, ngx_events_module还要实现核心模块的共同接口:
static ngx_core_module_t  ngx_events_module_ctx = {ngx_string("events"),NULL,ngx_event_init_conf
};
如果还有印象的话, 可以想起在Nginx框架中, 在解析配置文件前, 有这么一段代码:
    for (i = 0; ngx_modules[i]; i++) {if (ngx_modules[i]->type != NGX_CORE_MODULE) {continue;}module = ngx_modules[i]->ctx;if (module->create_conf) {rv = module->create_conf(cycle);if (rv == NULL) {ngx_destroy_pool(pool);return NULL;}cycle->conf_ctx[ngx_modules[i]->index] = rv;}}
不过, 在ngx_events_module中实现的核心模块却没有实现此函数, ngx_event_init_conf函数也只是简单进行正确性判断. 这是因为ngx_events_module模块并不会解析配置项的参数, 只是在出现events配置项后会调用各事件模块去解析events{...}块内的配置项, 所以也就不需要什么存储结构体了.
除了上面两个结构体, 每个模块都必须实现的模块接口:
ngx_module_t  ngx_events_module = {NGX_MODULE_V1,&ngx_events_module_ctx,                /* module context */ngx_events_commands,                   /* module directives */NGX_CORE_MODULE,                       /* module type */NULL,                                  /* init master */NULL,                                  /* init module */NULL,                                  /* init process */NULL,                                  /* init thread */NULL,                                  /* exit thread */NULL,                                  /* exit process */NULL,                                  /* exit master */NGX_MODULE_V1_PADDING
};
可见, 除了对events{...}内的配置项进行解析外, 此模块并没有做其他事情. 所以重点就关注ngx_events_block函数.
但是, 在关注此函数之前, 我们需要理清思路, 每个事件模块都有为其自己创建的存储配置项的结构体(就是利用ngx_events_module_t中的craete_conf函数). 事件模块只需要在被调用时为其结构体分配内存即可, 那么这么多事件模块的配置项结构体的指针是如何被ngx_events_module管理/存储的呢?
        每一个事件模块产生的配置项结构体都会被存放到ngx_events_module模块创建的 指针数组中, 既然如此, 该指针数组又是被存放在了哪里呢?
        该指针数组被存放在ngx_cycle_t结构体的conf_ctx成员中. 这个成员是四级指针, 即为存放((指针数组)的地址)的数组, 粗略的看就是指针数组 (或者说, 它首先指向一个存放指针的数组, 这个数组中的指针成员同时又指向另外一个存放指针的数组). 这个指针数组就依次存放着所有Nginx模块关于配置项方面的指针. 顺序在ngx_modules.c文件中已经被定义好了. 可以看到, ngx_events_module被放在第4个位置, 那么所有进程的conf_ctx数组的第4个指针就保存着ngx_events_module产生的指针数组.
如下, 是Nginx为了方便获取某个事件模块结构体而定义的宏:
#define ngx_event_get_conf(conf_ctx, module)                                  \(*(ngx_get_conf(conf_ctx, ngx_events_module))) [module.ctx_index];
#define ngx_get_conf(conf_ctx, module)  conf_ctx[module.index]
看到这个, 我们就容易理解为什么在ngx_module_t中要定义index和ctx_index这两个索引了.
也可以发现, 没有给该核心模块创建存储配置项的结构体, 此时就被用来存放所有事件模块的结构体了.
如果我们想要获得某事件模块的结构体, 那么只要传入ngx_cycle_t的conf_ctx成员, 与对应模块的名称就可以了.
                      

接下来就回到ngx_events_block函数.
static char *
ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{char                 *rv;void               ***ctx;ngx_uint_t            i;ngx_conf_t            pcf;ngx_events_module_t   *m;//虽然没能联系上下文, 但这里可以猜想就是ngx_cycle_t中的ctx_conf数组中的某一个元素(的指针? )//如果已经出现过"events{...}"了, 就不会再重复处理第二次if (*(void **) conf) {return "is duplicate";}/* count the number of the event modules and set up their indices *///为每个事件模块赋予一个ctx_index, 表示在事件模块中的顺序(index表示在所有模块中的顺序)ngx_event_max_module = 0;for (i = 0; ngx_modules[i]; i++) {if (ngx_modules[i]->type != ngx_events_module) {continue;}ngx_modules[i]->ctx_index = ngx_event_max_module++;}//以下几个步骤就是创建存储所有事件模块的存储配置项的结构体指针的数组//而ctx就是一个指向指针数组的指针. (可以这样理解, 一个指向指针的指针就是二级指针, 那么指向指针数组的指针那自然是三级指针)ctx = ngx_pcalloc(cf->pool, sizeof(void *));if (ctx == NULL) {return NGX_CONF_ERROR;}//分配指针数组所需空间*ctx = ngx_pcalloc(cf->pool, ngx_event_max_module * sizeof(void *));if (*ctx == NULL) {return NGX_CONF_ERROR;}*(void **) conf = ctx;//为每个事件模块创建其所需要的存储配置项的结构体for (i = 0; ngx_modules[i]; i++) {...m = ngx_modules[i]->ctx;if (m->create_conf) {(*ctx)[ngx_modules[i]->ctx_index] = m->create_conf(cf->cycle);...}}pcf = *cf;cf->ctx = ctx;cf->module_type = ngx_events_module;cf->cmd_type = NGX_EVENT_CONF;//解析"events{...}"中的内容rv = ngx_conf_parse(cf, NULL);*cf = pcf;if (rv != NGX_CONF_OK)return rv;//在解析完"events{...}“中的内容后, 为每个模块调用init函数, 最终处理配置项for (i = 0; ngx_modules[i]; i++) {...m = ngx_modules[i]->ctx;if (m->init_conf) {rv = m->init_conf(cf->cycle, (*ctx)[ngx_modules[i]->ctx_index]);...}}return NGX_CONF_OK;
}
可以发现, 这里最主要的工作落在了ngx_conf_parse函数上, 只是我对配置模块的理解不够, 所以这里没有能力继续分析了...
不过, 对于events{...}块来说, 解析的配置项可能就是worker_connection指令设置每个worker的连接数, 可能是accept_mutex指令就是指定是否使用accept锁等等...

参考博客:

      《深入理解NGINX》

      http://blog.csdn.net/lengzijian/article/details/7598996
      http://blog.csdn.net/chosen0ne/article/details/7741482

这篇关于Ngixn基础. 认识Nginx事件模块(一)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1085245

相关文章

windows下安装Nginx全过程

《windows下安装Nginx全过程》文章介绍了HTTP和反向代理服务器的概念,包括正向代理和反向代理的区别,并详细描述了如何安装和配置Nginx作为反向代理服务器... 目录概念代理正向代理反向代理安装基本属性nginx.conf查询结构属性使用运行重启停止总结概念是一个高性能的HTTP和反向代理we

检查 Nginx 是否启动的几种方法

《检查Nginx是否启动的几种方法》本文主要介绍了检查Nginx是否启动的几种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录1. 使用 systemctl 命令(推荐)2. 使用 service 命令3. 检查进程是否存在4

Java利用Spire.Doc for Java实现在模板的基础上创建Word文档

《Java利用Spire.DocforJava实现在模板的基础上创建Word文档》在日常开发中,我们经常需要根据特定数据动态生成Word文档,本文将深入探讨如何利用强大的Java库Spire.Do... 目录1. Spire.Doc for Java 库介绍与安装特点与优势Maven 依赖配置2. 通过替换

Nginx概念、架构、配置与虚拟主机实战操作指南

《Nginx概念、架构、配置与虚拟主机实战操作指南》Nginx是一个高性能的HTTP服务器、反向代理服务器、负载均衡器和IMAP/POP3/SMTP代理服务器,它支持高并发连接,资源占用低,功能全面且... 目录Nginx 深度解析:概念、架构、配置与虚拟主机实战一、Nginx 的概念二、Nginx 的特点

Nginx内置变量应用场景分析

《Nginx内置变量应用场景分析》Nginx内置变量速查表,涵盖请求URI、客户端信息、服务器信息、文件路径、响应与性能等类别,这篇文章给大家介绍Nginx内置变量应用场景分析,感兴趣的朋友跟随小编一... 目录1. Nginx 内置变量速查表2. 核心变量详解与应用场景3. 实际应用举例4. 注意事项Ng

Python AST 模块实战演示

《PythonAST模块实战演示》Python的ast模块提供了一种处理Python代码的强大工具,通过解析代码生成抽象语法树(AST),可以进行代码分析、修改和生成,接下来通过本文给大家介绍Py... 目录 什么是抽象语法树(AST)️ ast 模块的核心用法1. 解析代码生成 AST2. 查看 AST

JavaScript装饰器从基础到实战教程

《JavaScript装饰器从基础到实战教程》装饰器是js中一种声明式语法特性,用于在不修改原始代码的情况下,动态扩展类、方法、属性或参数的行为,本文将从基础概念入手,逐步讲解装饰器的类型、用法、进阶... 目录一、装饰器基础概念1.1 什么是装饰器?1.2 装饰器的语法1.3 装饰器的执行时机二、装饰器的

Java JAR 启动内存参数配置指南(从基础设置到性能优化)

《JavaJAR启动内存参数配置指南(从基础设置到性能优化)》在启动Java可执行JAR文件时,合理配置JVM内存参数是保障应用稳定性和性能的关键,本文将系统讲解如何通过命令行参数、环境变量等方式... 目录一、核心内存参数详解1.1 堆内存配置1.2 元空间配置(MetASPace)1.3 线程栈配置1.

从基础到高级详解Go语言中错误处理的实践指南

《从基础到高级详解Go语言中错误处理的实践指南》Go语言采用了一种独特而明确的错误处理哲学,与其他主流编程语言形成鲜明对比,本文将为大家详细介绍Go语言中错误处理详细方法,希望对大家有所帮助... 目录1 Go 错误处理哲学与核心机制1.1 错误接口设计1.2 错误与异常的区别2 错误创建与检查2.1 基础

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景