探秘 Nginx 的工作原理

发布于:2025-07-28 ⋅ 阅读:(12) ⋅ 点赞:(0)

Nginx 简介

Nginx 是一个高性能的HTTP和反向代理 Web 服务器,处理高并发能力十分强大,能经受高负载的考验。

首先我们理解 Web 服务器是互联网基础设施的核心组件,负责通过 HTTP/HTTPS 协议接收、处理和响应客户端(如浏览器)的请求,向用户提供网页、图像、视频等资源。

Nginx 我们就可以理解为当客户端发送 HTTP 请求以后,需要到 Niginx 服务器,然后由 Niginx 服务器进行处理,分发给对应的处理程序进行处理,然后再发给 Server 端,如下图所示:
在这里插入图片描述
我们可以简单的将 Nginx 理解为这样的一个模型,至于他为什么高效,如何实现负载均衡跟反向代理的,我们后续会介绍到。

Nginx 他的主要用途就是做网关,也就是网络数据的出口,我么可以从网络的七层模型去理解:
在这里插入图片描述

对于物理层来说,是没有网关的,数据链路层就存在网关(交换机),网络层(路由器),应用层可能就不一样,可以是 Nginx ,也可以是其他的服务器。

Nginx 在应用层工作的区域我们可以按如下的方式理解:
在这里插入图片描述
比如说一台路由器或者是交换机,在同一局域网内他会对应多个子网,那么 Nginx 就可以工作在任意一个子网中,我们可以理解同一局域网内会存在多个 IP,但是对于外界我们并不会将这多个 IP 都暴露出去,我们只会暴露 Nginx 对应的这个 IP ,但是他又是与其他的子网相连接的,我们访问一个 IP ,其实对应的是多个 IP 的数据。

当 Nginx 配置完毕以后,首先我们可以使用一下命令启动 Nginx:

sudo ./sbin/nginx -c conf/nginx.conf

打开对应的 Nginx 下的 nginx.conf 文件,我们可以看见如下的配置:


#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       8100;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

这是 Nginx 默认的配置,我们用 8100 这个端口去进行访问,就会发现此时就可以接收到服务端给我们返回的内容,这也是 Nginx 默认配置的一个 html 文件,这其实就是一个静态网页,对应的 html 文件可以随便改,这也就意味着我们并不需要更改客户端的内容,只需要更改这个 html 文件即可。

在这里插入图片描述

Nginx.conf 文件解析

整个 Nginx 运行起来就是依赖于这个 Nginx.conf 文件, Nginx.conf 文件的核心我们可以理解为四个模块:全局配置段、事件模块、HTTP核心模块和虚拟主机配置实例:

全局配置段


## 核心配置模块

### 1. 全局配置段 (main)
```nginx
user nginx;                    # 运行用户和组
worker_processes auto;         # 工作进程数(建议设为CPU核心数)
error_log /var/log/nginx/error.log warn;  # 错误日志路径和级别
pid /var/run/nginx.pid;        # PID文件位置
worker_rlimit_nofile 65535;    # 每个进程最大打开文件数

事件模块

events {
    worker_connections 1024;    # 单个工作进程最大连接数
    use epoll;                 # 事件驱动模型(Linux推荐)
    multi_accept on;           # 同时接受多个连接
}

HTTP核心模块

http {
    include /etc/nginx/mime.types;  # MIME类型映射文件
    default_type application/octet-stream;
    
    # 日志格式定义
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
    
    access_log /var/log/nginx/access.log main;  # 访问日志配置
    
    sendfile on;               # 启用高效文件传输
    tcp_nopush on;             # 优化网络数据包传输
    keepalive_timeout 65;      # 保持连接超时时间(秒)
    
    gzip on;                   # 开启Gzip压缩
    gzip_types text/plain text/css application/json;
    
    # 包含其他配置文件(推荐模块化配置)
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

虚拟主机配置示例

server {
    listen 80;                 # 监听端口
    server_name example.com www.example.com;  # 域名
    
    root /var/www/html;        # 网站根目录
    index index.html index.htm;
    
    location / {
        try_files $uri $uri/ =404;  # 文件查找规则
    }
    
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php-fpm.sock;
        include fastcgi_params;
    }
    
    # 静态资源缓存
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 30d;
        access_log off;
    }
}

当然,我们就可以尝试来配置一个我们自己 conf 文件,来模拟实现 Nginx 下的工作模式,首先在 nginx 目录下创建一个 test.conf 文件,然后我们就创建单个工作进程:
在这里插入图片描述

events {
        worker_connections 1024;
}

拉起 nginx 服务,我们就会发现是可以被拉起来的,但是并没有对应的监听端口,因为我们并没有去创建:
在这里插入图片描述
接下来我们创建端口为 9000 再来看一下:

events {
	worker_connections 1024;
}

http {
	server {
		listen 9000;
	}
}

在这里插入图片描述

我们就会发现,此时已经有端口了,当然,我们也可以设置多个端口,在这儿我们需要理解的是到底是一个协议对应多个端口还是一个端口对应多个协议,很明显更新该是后者,因为前者不容易去区分:

在这里插入图片描述

接下来我们创建多个端口来试一下:

在这里插入图片描述

此时启动的 4 个 server ,只不过是没有资源而已,我们在本地来 telnet 一下,他是可以通的:

在这里插入图片描述

那我们当前就可以理解了,一个 nginx 里面是有多个进程的,而每个进程里面又可以对应的多个 server,构成了这样的一个关系。

那么 nginx 是如何去组织这种关系的呢?

我们接下来可以创建多个进程来试一下,这儿我们去创建 4 个进程:

worker_processes 4;

events {
	worker_connections 1024;
}

http {
	server {
		listen 9000;
	}
	server {
		listen 9001;
	}
	server {
		listen 9002;
	}
	server {
		listen 9003;
	}
}

重新启动 nginx,我们就可以看见如下的场景:

在这里插入图片描述

此时我们就会发现,区域 4 个 server 监听的操作我们是放在主进程当中,当我们连接某一个端口以后,此时对于处理的操作是选择其他进程进行处理的,如下图所示:

在这里插入图片描述

那么 Nginx 当前是怎么进行处理的呢?

它采用的就是一种 fork 子进程的方式,如下图所示,本质还是调用 epoll 的三个接口进行处理:
在这里插入图片描述
这种惊群效应的问题就在于当前如果只有一个连接到来,会将所有的进程都唤醒,但是最终只有一个进程争夺成功。

在 Nginx 的内部解决这个问题方法就是会去加一把锁,谁获取到这把锁就会将对应的 listenfd 添加到 epoll 当中,然后后续触发事件的也是这个添加到 epoll 的线程。

当前我们提供的 conf 文件中是没有配置东西的,接下来我们就可以配置静态网页来看一下对应的操作效果:


worker_processes 4;

events {
	worker_connections 1024;
}

http {
	server {
		listen 9000;
		location / {
			root /usr/local/nginx/html9000/;
		}
	}
	server {
		listen 9001;
		location / {
			root /usr/local/nginx/html9001/;
		}
	}
	server {
		listen 9002;
		location / {
			root /usr/local/nginx/html9002/;
		}
	}
	server {
		listen 9003;
		location / {
			root /usr/local/nginx/html9003/;
		}
	}
}

然后我们对每一端口进行访问,此时就可以看见明显对应的端口号访问的内容是不一样的:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代理服务

我们在最开始就已经介绍到 Nginx 可以去实现反向代理这个功能,接下来我们就来实践一下这个功能:


worker_processes 4;

events {
	worker_connections 1024;
}


http {
	upstream backend {
		server 192.168.1.5:9002;
		server 192.168.1.5:9003;
	}

	server {
		listen 9000;
		location / {
#			root /usr/local/nginx/html9000/;
			proxy_pass http://backend;
		}
	}
	server {
		listen 9001;
		location / {
			root /usr/local/nginx/html9001/;
		}
	}
	server {
		listen 9002;
		location / {
			root /usr/local/nginx/html9002/;
		}
	}
	server {
		listen 9003;
		location / {
			root /usr/local/nginx/html9003/;
		}
	}
}

当前的一个操作就是 9000 端口就被设置为代理服务器,使用 proxy_pass 将请求转发到 9002 和 9003 两个端口,至于他们如何进行处理我们看接下来的表现:

在这里插入图片描述
在这里插入图片描述

我们就可以发现我们访问的 9000 这个端口,但是由于他进行了反向代理的设置,此时对应的请求就被转发给了 9002 和 9003 两个接口,这就是反向代理技术。

反向代理,也是一个位于客户端和目标服务器之间的服务器,对于客户端而言,反向代理服务器就相当于目标服务器,用户不需要知道目标服务器的地址,用户只需要访问反向代理服务器,再由反向代理服务器将客户端的请求转发给真正的目标服务器进行处理,数据处理完毕后反向代理服务器再将数据结果返回给客户端。
在这里插入图片描述

反向代理的好处:

  • 反向代理可以起到负载均衡的作用。比如不设置反向代理服务器,那么用户在访问百度时,就会随机访问到百度内部的某台服务器,此时就可能导致某些服务器压力太大,而某些服务器却处于闲置状态。而设置了反向代理服务器后,我们就能够通过某些方法让用户的数据请求较为平均的落到每台服务器上。
  • 反向代理还能起到安全防护的作用。有了方向代理服务器后,我们不需要直接将提供服务的服务器对应的信息暴露出去,此外,当由非法请求发送到反向代理服务器时,反向代理服务器就相当于一层软件屏障,可以在反向代理服务器当中部署一些防护措施,让这些非法请求在反向代理服务器这里就被过滤掉,而不会影响内部实际提供服务的服务器。

注意,代理服务器不做任何业务的处理,只负责将请求推送到后端的主机。

负载均衡

负载均衡其实就跟上面的反向代理是息息相关的,我们只要修改对 conf 文件的配置即可:

upstream backend {
	server 192.168.1.5:9002 weight=2;
	server 192.168.1.5:9003 weight=1;
}

weight 表示的就是权重的意思,也就是说我们刚开始并没有设置,那么他们对应的权重是一致的,此时通过代理服务器转发过来的请求也是均匀进行分配的,9002 端口一次,9003 端口一次;如果改成向我们上面的设置,就表示 9002 端口跟 9003 的访问比例是 2:1 的关系,这就是通过反向代理去实现负载均衡的思想。

conf如何解析

上面一直在阐述 conf 内部实现的过程,接下来就需要介绍一下 conf 文件的解析, conf 核心的作用其实就是将一些固定的信息更新到 conf 文件当中,便于我们修改,我们不可能每修改一个地方就重新编译一下代码,非常的耗时,那 conf 文件就可以去将这些配置信息组织起来,我们只需要修改 conf 文件即可,就简化了对应的流程,报错 Redis 以及 MySQL 都是这样去进行操作的。

conf 文件内部其实我们定义的变量其实就是以字符串的形式去解析的,比如说worker_processes,在 Nginx 源码中我们就可以看见这一环节,下图所示,我们可以看见,他就是一个字符串,只不过是 Niginx 内部在实现的时候做了一层操作,去解析这个字符串,达到了我们最终需要实现的效果:
在这里插入图片描述

对于 conf 文件中的一些关键字就是存放在 ngx_command_t 当中的,如下图所示:

在这里插入图片描述
我们可以对其中的一些数据进行解析:

  • NGX_MAIN_CONF:表示该指令是主配置块里的指令,主配置块即不在 server、location 等子块中的全局配置部分。
  • NGX_DIRECT_CONF:表明该指令可直接在配置文件里进行设置,而不是通过继承或其他间接方式。
  • NGX_CONF_FLAG:意味着该指令是一个布尔类型的标志,其值可以是 on 或者 off。
  • NGX_CONF_TAKE1:表示该指令需要且仅能接受一个参数,也可以设置为多个。

其实他的一个流程就是在 conf 文件按顺序读取对应的关键字,然后根据解析的关键字去调用对应的函数,进行一些列的操作,比如说上面的 ngx_set_worker_processes 接口就是为了处理 Nginx 配置文件中 worker_processes 指令的配置解析。

Nginx 过滤器模块的实现

Nginx 内部组件的使用

在 Nginx 的内部也是有实现各种各样类型的组件的,包括内存池,红黑树,共享内存等等,我们都是可以拿出来进行使用的,接下来我们就来简单介绍一下如何去使用其中的的一些组件:

ngx_str_t

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

我们可以看见 nginx 内部实现的 string 就是这样的一个结构体,我们是可以直接拿出来进行使用的,包含对应的头文件就可以了。

#include "ngx_config.h"
#include "ngx_conf_file.h"
#include "nginx.h"
#include "ngx_core.h"
#include "ngx_string.h"
#include "ngx_palloc.h"

#define unused(x) x = x
volatile ngx_cycle_t  *ngx_cycle;

void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err,
                        const char *fmt, ...)
{

    unused(level);
    unused(log);
    unused(err);
    unused(fmt);
}

int main()
{

    ngx_str_t name = ngx_string("gtt");

    printf("name --> len: %ld, data: %s\n", name.len, name.data);
}

我们需要注意两个点就是,我们在实现的过程中需要用到定义出 ngx_cycle_t 这个变量以及 ngx_log_error_core这个函数,不然编译过程会存在链接错误,因为在 Nginx 内部使用了 extern C 进行声明,某些头文件就包含这些变量,我们就需要将他实现出来才可以。

接下来是 makefile 文件的编写:

CXX = gcc
CXXFLAGS += -g -Wall -Wextra

NGX_ROOT = /home/gtt/nginx/nginx-1.22.1
PCRE_ROOT = /home/gtt/nginx/pcre-8.45


TARGETS = ngx_str_code
TARGETS_C_FILE = $(TARGETS).c

CLEANUP = rm -f $(TARGETS) *.o

all: $(TARGETS)

clean:
	$(CLEANUP)

CORE_INCS = -I. \
	-I$(NGX_ROOT)/src/core \
	-I$(NGX_ROOT)/src/event \
	-I$(NGX_ROOT)/src/event/modules \
	-I$(NGX_ROOT)/src/os/unix \
	-I$(NGX_ROOT)/objs \
	-I$(PCRE_ROOT) \

NGX_PALLOC = $(NGX_ROOT)/objs/src/core/ngx_palloc.o
NGX_STRING = $(NGX_ROOT)/objs/src/core/ngx_string.o
NGX_ALLOC = $(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o

$(TARGETS): $(TARGETS_C_FILE)
	$(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING) $(NGX_ALLOC) $^ -o $@

makefile 文件当中就是一些依赖的实现,有兴趣可以去了解一下。

我们正常编译运行以后,其实就可以看见我们对应的打印:
在这里插入图片描述

内存池

nginx 当中实现的内存池也是可以直接用的,我们可以自己来试一下:

#include "ngx_config.h"
#include "ngx_conf_file.h"
#include "nginx.h"
#include "ngx_core.h"
#include "ngx_string.h"
#include "ngx_palloc.h"

#define unused(x) x = x

volatile ngx_cycle_t *ngx_cycle;

void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err,
                        const char *fmt, ...)
{

    unused(level);
    unused(log);
    unused(err);
    unused(fmt);
}

void print_pool(ngx_pool_t *pool)
{

    printf("\nlast: %p, end: %p\n", pool->d.last, pool->d.end);
}

// compile:

int main()
{
#if 0

	ngx_str_t name = ngx_string("gtt");

	printf("name --> len: %ld, data: %s\n", name.len, name.data);

#elif 1

    ngx_pool_t *pool = ngx_create_pool(4096, NULL);

    print_pool(pool);

    int *p1 = ngx_palloc(pool, sizeof(int));

    print_pool(pool);

    void *p2 = ngx_palloc(pool, 0x10);

    print_pool(pool);

    void *p3 = ngx_palloc(pool, 0x15);

    print_pool(pool);

    ngx_destroy_pool(pool);

#endif
}

运行可执行程序以后就可以看见其为我们分配的内存,其实就和 malloc 函数是一样的:
在这里插入图片描述

过滤器模块的实现

nginx 内部主要实现了 3 个模块,分别是 upstreamfilterhandler

  • upstream 模块其实就是我们最开始介绍到的用于定义一组后端服务器,这些服务器可以接收来自 Nginx 的代理请求。通过 upstream 配置,Nginx 可以将客户端请求分发到多个后端服务器,实现负载均衡和故障转移;
  • filter 模块 Nginx 中用于处理和修改请求与响应内容的机制,它允许在请求处理流程的不同阶段对数据进行操作和转换,其实也就是解析数据以及回数据模块进行实现的;
  • handler 模块其实就是指的就是请求响应这个过程,通过回调函数来进行实现。

接下来我们首先需要介绍到的就是 filter 模块,我们在浏览浏览器网页的时候会看见很多广告,这其实就是过滤器模块进行实现的,就是在回数据的过程中在 Nginx 中区分为一个一个的块,然后将对应的网页数据填进去,返回回来,最终就是我们所看到的界面。

config

对于 config 文件其实就是一个固定的写法:

ngx_addon_name=ngx_http_filter_module
HTTP_FILTER_MODULES="$HTTP_FILTER_MODULES ngx_http_filter_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_filter_module.c"
  • ngx_addon_name 是 Nginx 模块开发中的一个重要变量,用于定义 Nginx 第三方模块的名称。这个变量会在编译时被 Nginx 的构建系统使用。
# 示例1:HTTP模块
ngx_addon_name=ngx_http_hello_world_module

# 示例2:Stream模块
ngx_addon_name=ngx_stream_custom_module
  • HTTP_FILTER_MODULES(HTTP过滤模块)是Web服务器(如Nginx)中用于处理和修改HTTP请求/响应的可插拔组件。这些模块构成了HTTP处理流水线的重要组成部分,允许开发者在请求处理的不同阶段插入自定义逻辑。
  • NGX_ADDON_SRCS 是 Nginx 模块开发中的一个重要变量,用于定义模块的源代码文件,主要作用是指定当前模块需要编译的源文件列表。

我们需要执行以下命令将我们当前文件夹所在位置的模块添加进去:

./configure --prefix=/usr/local/nginx --with-http_realip_module 
--with-http_addition_module --with-http_ssl_module 
--with-http_gzip_static_module --with-http_secure_link_module 
--with-http_stub_status_module --with-stream 
--with-pcre=/home/gtt/nginx/pcre-8.45 
--with-zlib=/home/gtt/nginx/zlib-1.2.13 
--with-openssl=/home/gtt/nginx/openssl-1.1.1s --add-module=/home/gtt/nginx/ngx_http_filter_mode/

添加成功以后就是这个样子的:

在这里插入图片描述

打开 objs 目录下的 ngx_module.c 文件,我们就可以看见模块已经被添加进来了:

在这里插入图片描述
接下来就是代码部分的实现:

ngx_module_t

ngx_module_t 是 Nginx 模块系统的核心数据结构,它是所有 Nginx 模块的基础结构,无论是官方模块还是第三方模块都需要通过这个结构体来注册和定义。

首先我们就需要来实现这个模块,只有将这个模块实现以后,后续我们编译的过程中出现问题,我们来看ngx_module_t这个结构体所实现的一些内容:

# ngx_module_t 结构体详解

## 基本定义
`ngx_module_t` 是 Nginx 模块系统的核心数据结构,定义在 `src/core/ngx_module.h` 头文件中。它是所有 Nginx 模块的基础结构,无论是官方模块还是第三方模块都需要通过这个结构体来注册和定义。

## 结构体成员分析

```c
typedef struct ngx_module_s ngx_module_t;

struct ngx_module_s {
    ngx_uint_t            ctx_index;    /* 上下文索引,用于区分不同模块类型 */
    ngx_uint_t            index;        /* 模块在同类模块中的序号 */

    ngx_uint_t            spare0;      /* 保留字段 */
    ngx_uint_t            spare1;      /* 保留字段 */
    
    ngx_uint_t            version;     /* Nginx 模块接口版本 */
    void                 *ctx;         /* 模块上下文,特定类型的模块有不同的定义 */
    ngx_command_t        *commands;    /* 模块支持的配置指令 */
    ngx_uint_t            type;        /* 模块类型 */

    ngx_int_t           (*init_master)(ngx_log_t *log);         /* 主进程初始化回调 */
    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);     /* 模块初始化回调 */
    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);   /* 工作进程初始化回调 */
    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);     /* 线程初始化回调 */
    void                (*exit_thread)(ngx_cycle_t *cycle);     /* 线程退出回调 */
    void                (*exit_process)(ngx_cycle_t *cycle);   /* 进程退出回调 */
    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;  /* 保留钩子 */
};

以我们自己实现的代码为例:

// ngx_module_t 相当于所有模块的一个基类
static ngx_module_t ngx_http_filter_mode = {
    // NGX_MODULE_V1 是一个预定义宏,代表 Nginx 模块版本 1 的标准初始化值。此宏能简化模块结构的初始化,
    // 让模块遵循 Nginx 模块 API 的规范
    NGX_MODULE_V1,
    &ngx_http_filter_mode_ctx,
    ngx_http_filter_mode_commands,
    NGX_HTTP_MODULE,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NGX_MODULE_V1_PADDING
};

其中的核心就在于 3 个模块:

  • ngx_http_filter_mode_ctx它的返回值类型是一个ngx_http_module_t类型,用于定义 HTTP 模块的上下文;
  • ngx_http_filter_mode_commands它的返回值类型是一个ngx_command_t类型,用于定义 Nginx 命令行参数以及对应的 conf 文件中的一些指令;
  • NGX_HTTP_MODULE 就表示这是一个 HTTP 模块。

ngx_http_module_t

我们可以来看一下 ngx_http_module_t结构体内部的实现:

typedef struct {
    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);

    void       *(*create_main_conf)(ngx_conf_t *cf);
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);

    void       *(*create_srv_conf)(ngx_conf_t *cf);
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

    void       *(*create_loc_conf)(ngx_conf_t *cf);
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;

ngx_http_module_t 的内部实现是非常巧妙的,他并不是包含的一些请求对应的信息,而是设置了 8 个回调函数,也就是说,我们在调用到这个阶段的时候,当前我们就只需要去调用这些回调函数然后根据 conf 文件中的配置,依次去进行处理就好了,所有的操作都是通过调用回调函数去进行处理,其实就是一层套着一层的一个逻辑。

回调解析:

配置阶段回调函数

  • preconfiguration:在解析配置文件前执行,通常用于注册模块的处理程序到HTTP处理流程中。
    示例:注册HTTP阶段处理器

  • postconfiguration:在解析配置文件后执行,常用于模块的最终初始化工作。
    示例:验证配置参数的有效性

配置存储相关函数

  • create_main_conf:创建main级别的配置存储结构;
    应用场景:存储全局生效的配置参数

  • init_main_conf:初始化main级别的配置;
    典型实现:设置配置参数的默认值

  • create_srv_conf:创建server级别的配置存储结构;
    注意:每个server块都会调用一次

  • merge_srv_conf:合并server级别配置(当继承发生时)
    典型场景:处理父server块和子server块的配置继承;

  • create_loc_conf:创建location级别的配置存储结构;
    注意:每个location块都会调用一次

  • merge_loc_conf:合并location级别配置。
    关键作用:实现配置继承机制

注意,不是所有回调函数都必须实现,未使用的可以设为NULL,配置结构体通常需要定义main/srv/loc三个级别。

接下来看我们自己的一个实现:

ngx_http_output_header_filter_pt ngx_http_next_header_filter;
ngx_http_output_body_filter_pt ngx_http_next_body_filter;

// 用于处理 HTTP 响应头过滤的函数
ngx_int_t ngx_http_next_header_filter(ngx_http_request_t *r)
{

}

// 用于处理 HTTP 响应体过滤的函数
ngx_int_t ngx_http_next_body_filter(ngx_http_request_t *r, ngx_chain_t *chain)
{

}

// 初始化模块
ngx_int_t   ngx_http_filter_init(ngx_conf_t *cf){
	ngx_http_next_header_filter = ngx_http_top_header_filter;
	ngx_http_top_header_filter = ngx_http_next_header_filter;

	ngx_http_next_body_filter = ngx_http_top_body_filter;
	ngx_http_top_body_filter = ngx_http_next_body_filter;
}

// 定义 HTTP 模块的上下文
void* ngx_http_filter_create_loc_conf (ngx_conf_t *cf)
{
    // 分配内存
    ngx_http_filter_mode_conf_t *conf = ngx_palloc(cf->pool, sizeof(ngx_http_filter_mode_conf_t));
    if (conf == NULL) {
        return NULL;
    }
    // 初始化配置项
    conf->enable = NGX_CONF_UNSET;
    return conf;
}

// 合并配置项
void* ngx_http_filter_merge_loc_conf (ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_filter_mode_conf_t *prev = (ngx_http_filter_mode_conf_t*)parent;
    ngx_http_filter_mode_conf_t *conf = (ngx_http_filter_mode_conf_t*)child;

    ngx_conf_merge_value(conf->enable, prev->enable, 0);

    return conf;
}

// ngx_http_module_t 结构体用于定义 HTTP 模块的上下文
static ngx_http_module_t ngx_http_filter_mode_ctx = {
    // conf
    NULL,
    // post conf
    ngx_http_filter_init,

    // main
    NULL,
    NULL,

    // server
    NULL,
    NULL,

    // loc
    ngx_http_filter_create_loc_conf,
    ngx_http_filter_merge_loc_conf,
};

这里着重与介绍一下初始化模块,也就是ngx_http_filter_init函数的实现过程,他是在解析配置文件后执行,常用于模块的最终初始化工作,我们可以将其理解为头插法,过滤器模块上面链接着很多的块,每一系插入都是一个头插法,当 worker_process调用以后,就循环的往里面头插对应的信息。

ngx_command_t

ngx_command_t是 Nginx 模块开发中定义配置指令的核心数据结构,它定义了模块能够识别的配置指令及其处理方式,也就是最开始我们所介绍到的,conf 文件中是对应的是字符串,正是在该模块中进行解析的。

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;
};
  • name:指令名称字符串(如"listen"),支持多指令合并定义;
  • type:通过位掩码组合指定指令属性,常见标志包括:
    NGX_CONF_NOARGS:不接受参数
    NGX_CONF_TAKE1/2/…:指定参数个数
    NGX_CONF_TAKE12:接受1或2个参数
    NGX_CONF_FLAG:布尔值(on/off)
    NGX_CONF_BLOCK:包含子配置块(如server{})
    NGX_DIRECT_CONF:仅允许在main配置层级使用
  • set 函数:指令处理回调函数,也就是对应的指令是如何去进行处理的;
  • conf 和 offset:指定配置存储位置
struct {
    ngx_flag_t enable;
} ngx_http_filter_mode_conf_t;


// ngx_command_t 结构体用于定义 Nginx 命令行参数
// 对应的 conf 文件中的一些指令
static ngx_command_t ngx_http_filter_mode_commands[] = {
    {    
        ngx_string("nginx"),
        NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG,
        // char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
        ngx_conf_set_flag_slot,
        // 计算配置项在 ngx_http_conf_ctx_t 结构体里相对于 location 配置块的偏移量
        NGX_HTTP_LOC_CONF_OFFSET,
        // 计算配置项在 ngx_http_filter_mode_conf_t 结构体里相对于 enable 配置项的偏移量
        offsetof(ngx_http_filter_mode_conf_t, enable),
        NULL,
    },
    ngx_null_command // 命令数组以空命令结尾
};

注意,数组必须以ngx_null_command结束,要不然可能就会出现 Nginx 该模块都不会拉起来的现象。

其实以上三个模块实现以后,过滤器模块大部分内容就已经实现完成,接下来其实就根据自己所需,完成对应内容的填充就可以,上面就是目前博主对于 Nginx 过滤器模块的一个理解。

Nginx handler 模块的实现

Nginx handler 模块上面我们已经解释了,其实就是请求响应的这个过程,接收到对应的 HTPP 请求以后,我们就需要进行处理,然后调用回调函数进行对应的处理工作。

我们接下来实现的是一个统计访问服务器次数的功能,也就是查看当前客户端访问了服务器多少次。

一般而言, Nginx 一个模块的启动可以简述为 3 个流程:Nginx 启动流程conf 文件中模块功能的开启(set函数)当HTTP请求来的流程

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#include <arpa/inet.h>
#include <netinet/in.h>

// 定义一个数组进行存储
typedef struct {
    int count;
    struct in_addr addr; // ip
} ngx_pv_table_t;

ngx_pv_table_t pv_table[256];

// 网页处理
int ngx_http_encode_page(char* html)
{
    html = (char*)html;
    sprintf(html, "<h1>gtt</h1>");
	strcat(html, "<h2>");

    int i = 0;
    for(i = 0; i < 256; i++)
    {
        if (pv_table[i].count != 0) {
            char str[INET_ADDRSTRLEN] = {0};
            char buffer[128] = {0};
            snprintf(buffer, 128, "req from : %s, count : %d <br/>",
                        inet_ntop(AF_INET, &pv_table[i].addr, str, sizeof(str)), pv_table[i].count);
            strcat(html, buffer);
        }
    }

    strcat(html, "<h2/>");
    return 0;
}
// 如何去进行处理
ngx_int_t ngx_http_pagecount_handler(ngx_http_request_t *r)
{
    u_char html[1024] = {0};
    // 获取客户端的IP地址
    /*
        1. array
        2. hashtable
        3. 红黑树
        4. 跳表
        5. B树
        [hey, value] -> [ip, count]
    */
    struct sockaddr_in *client_addr = (struct sockaddr_in *)r->connection->sockaddr;
    int idx = client_addr->sin_addr.s_addr >> 24;
    pv_table[idx].count++;
    memcpy(&pv_table[idx].addr, &client_addr->sin_addr.s_addr, sizeof(client_addr->sin_addr));
    ngx_http_encode_page((char*)html);

    // 事件已经准备就绪,接下来组织发送数据
    // send heaf
    r->headers_out.status = 200;
    ngx_str_set(&r->headers_out.content_type, "text/html");
    ngx_http_send_header(r);

    // send body
    ngx_buf_t* b = ngx_palloc(r->pool, sizeof(ngx_buf_t));
    ngx_chain_t out;
    out.buf = b;
    out.next = NULL;

    b->pos = html;
    b->last = html + sizeof(html);
    b->memory = 1;
    b->last_buf = 1;

    return ngx_http_output_filter(r, &out);
}

// Nginx.conf -> "gtt"
// 当前函数是设置指令的处理函数,当请求来的时候就会执行
// 来一次请求就会去执行一次
char* ngx_http_pagecount_handler_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_core_loc_conf_t *corecf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
    corecf->handler = ngx_http_pagecount_handler;
    
    return NGX_CONF_OK;
}

static ngx_http_module_t ngx_http_pagecount_handler_ctx = {
    NULL,
    NULL,

    NULL,
    NULL,

    NULL,
    NULL,

    NULL,
    NULL,
};

static ngx_command_t ngx_http_pagecount_handler_commands[] = {
    {
        ngx_string("gtt"),
        NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
        ngx_http_pagecount_handler_set,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },
    ngx_null_command
};

// ngx_module_t 相当于所有模块的一个基类
ngx_module_t ngx_http_pagecount_handler_moudle = {
    // NGX_MODULE_V1 是一个预定义宏,代表 Nginx 模块版本 1 的标准初始化值。此宏能简化模块结构的初始化,
    // 让模块遵循 Nginx 模块 API 的规范
    NGX_MODULE_V1,
    &ngx_http_pagecount_handler_ctx,
    ngx_http_pagecount_handler_commands,
    NGX_HTTP_MODULE,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NGX_MODULE_V1_PADDING
};

我们来解析一下这一段代码:

  • 跟前面实现的过滤器模块是一样的,其实核心部分依然会对应的 3 个环节,都需要包含进去,在 handler 模块最重要的一个环节就是 set函数的设置;
  • ngx_http_pagecount_handler_set就是我们自己所实现的一个指令处理函数,我们实现的是一个统计访问服务器的次数,那么就需要记录每一次的访问,也就是说每一次请求都会进入到 ngx_http_pagecount_handler_set这个函数里面,我们可以设置在 conf 文件当中设置一个任意一个标记,每次进来都需要经过这个位置,当前设置的是gtt,然后进行解析。
  • 最重要的一点就是将对应的次数组织起来,很明显我们可以将对应的 IP 地址与 count 组织成一种[key,value]关系的键值对,key对应 IP 地址,value对应 访问次数,每一次访问进行++即可,做种返回对应的访问次数,这种组织方式最优应该是选取红黑树进行实现,当前是采取数据进行实现的,后续会进行优化。

我们可以来看一下对应的实操结果,可以看见统计出来了对应的访问次数:

在这里插入图片描述

使用红黑树进行优化

上面我们使用的是数组结构,这种其实一般情况下是不会用到的,更多的使用到的是红黑树这种结构,而且当前我们只是一个服务器进行访问,如果是多个不同的服务器进行访问,我们就需要考虑如何进行统计了。

在下面的实现中还采用了共享内存的方式,就是将红黑树的结点添加在共享内存当中,这样所有的进程其实都可以看见这一部分内存,然后我们在统计的时候也就可以统计出所有的访问次数。

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#include <arpa/inet.h>
#include <netinet/in.h>

char *ngx_http_pagecount_handler_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
void  *ngx_http_pagecount_create_location_conf(ngx_conf_t *cf);

typedef struct
{
    // 红黑树
    ngx_rbtree_t rbtree;
    // 红黑树结点
    ngx_rbtree_node_t sentinel;
} ngx_http_pagecount_shm_t;

// 关于共享内存的配置
typedef struct
{
    ssize_t shmsize;

    ngx_slab_pool_t *shpool;

    ngx_http_pagecount_shm_t *sh;
} ngx_http_pagecount_conf_t;

static ngx_http_module_t ngx_http_pagecount_handler_ctx = {
    NULL,
    NULL,

    NULL,
    NULL,

    NULL,
    NULL,

    ngx_http_pagecount_create_location_conf,
    NULL,
};

static ngx_command_t ngx_http_pagecount_handler_commands[] = {
    {
        ngx_string("count"),
        NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
        ngx_http_pagecount_handler_set,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
     },
    ngx_null_command
};

// ngx_module_t 相当于所有模块的一个基类
ngx_module_t ngx_http_pagecount_handler_moudle = {
    // NGX_MODULE_V1 是一个预定义宏,代表 Nginx 模块版本 1 的标准初始化值。此宏能简化模块结构的初始化,
    // 让模块遵循 Nginx 模块 API 的规范
    NGX_MODULE_V1,
    &ngx_http_pagecount_handler_ctx,
    ngx_http_pagecount_handler_commands,
    NGX_HTTP_MODULE,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NGX_MODULE_V1_PADDING
    };

void  *ngx_http_pagecount_create_location_conf(ngx_conf_t *cf) {

	ngx_http_pagecount_conf_t *conf;
	
	
	conf = ngx_palloc(cf->pool, sizeof(ngx_http_pagecount_conf_t));
	if (NULL == conf) {
		return NULL;
	}

	conf->shmsize = 0;
	//ngx_log_error(NGX_LOG_EMERG, cf->log, ngx_errno, "ngx_http_pagecount_create_location_conf");

	// init conf data
	// ... 

	return conf;

}
static ngx_int_t ngx_http_pagecount_lookup(ngx_http_request_t *r, ngx_http_pagecount_conf_t *conf, ngx_uint_t key) {

	ngx_rbtree_node_t *node, *sentinel;

	node = conf->sh->rbtree.root;
	sentinel = conf->sh->rbtree.sentinel;
	
	while (node != sentinel) {

		if (key < node->key) {
			node = node->left;
			continue;
		} else if (key > node->key) {
			node = node->right;
			continue;
		} else { // key == node
			node->data ++;
			return NGX_OK;
		}
	}
	
	// insert rbtree
    // 分配共享内存
	node = ngx_slab_alloc_locked(conf->shpool, sizeof(ngx_rbtree_node_t));
	if (NULL == node) {
		return NGX_ERROR;
	}

	node->key = key;
	node->data = 1;

	ngx_rbtree_insert(&conf->sh->rbtree, node);
	
	return NGX_OK;
}

// 网页处理
static int ngx_encode_http_page_rb(ngx_http_pagecount_conf_t *conf, char *html) {

	sprintf(html, "<h1>count</h1>");
	strcat(html, "<h2>");

	ngx_rbtree_node_t *node = ngx_rbtree_min(conf->sh->rbtree.root, conf->sh->rbtree.sentinel);

	do {

		char str[INET_ADDRSTRLEN] = {0};
		char buffer[128] = {0};

		sprintf(buffer, "req from : %s, count: %d <br/>",
			inet_ntop(AF_INET, &node->key, str, sizeof(str)), node->data);

		strcat(html, buffer);

		node = ngx_rbtree_next(&conf->sh->rbtree, node);

	} while (node);

	strcat(html, "</h2>");

	return NGX_OK;
}

// 如何去进行处理
ngx_int_t ngx_http_pagecount_handler(ngx_http_request_t *r)
{
	u_char html[1024] = {0};
	
	ngx_rbtree_key_t key = 0;

	struct sockaddr_in *client_addr =  (struct sockaddr_in*)r->connection->sockaddr;
	
	ngx_http_pagecount_conf_t *conf = ngx_http_get_module_loc_conf(r, ngx_http_pagecount_handler_moudle);
	key = (ngx_rbtree_key_t)client_addr->sin_addr.s_addr;

    // 访问共享内存,需要进行加锁解锁
	ngx_shmtx_lock(&conf->shpool->mutex);
	ngx_http_pagecount_lookup(r, conf, key);	
	ngx_shmtx_unlock(&conf->shpool->mutex);
	
	ngx_encode_http_page_rb(conf, (char*)html);

    // 事件已经准备就绪,接下来组织发送数据
    // send heaf
    r->headers_out.status = 200;
    ngx_str_set(&r->headers_out.content_type, "text/html");
    ngx_http_send_header(r);

    // send body
    ngx_buf_t *b = ngx_palloc(r->pool, sizeof(ngx_buf_t));
    ngx_chain_t out;
    out.buf = b;
    out.next = NULL;

    b->pos = html;
    b->last = html + sizeof(html);
    b->memory = 1;
    b->last_buf = 1;

    return ngx_http_output_filter(r, &out);
}

// B+树结点的插入
void ngx_http_pagecount_rbtree_insert_value(ngx_rbtree_node_t *temp,
                                            ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
{
    ngx_rbtree_node_t **p;
    // ngx_http_testslab_node_t *lrn, *lrnt;

    for (;;)
    {
        if (node->key < temp->key)
        {
            p = &temp->left;
        }
        else if (node->key > temp->key)
        {
            p = &temp->right;
        }
        else
        {
            return;
        }

        if (*p == sentinel)
        {
            break;
        }

        temp = *p;
    }

    *p = node;

    node->parent = temp;
    node->left = sentinel;
    node->right = sentinel;
    ngx_rbt_red(node);
}

// 共享内存的初始化模块
ngx_int_t ngx_http_pagecount_shm_init(ngx_shm_zone_t *zone, void *data)
{
    ngx_http_pagecount_conf_t *conf;
    ngx_http_pagecount_conf_t *oconf = data;

    conf = (ngx_http_pagecount_conf_t *)zone->data;
    if (oconf)
    {
        conf->sh = oconf->sh;
        conf->shpool = oconf->shpool;
        return NGX_OK;
    }

    conf->shpool = (ngx_slab_pool_t *)zone->shm.addr;
    conf->sh = ngx_slab_alloc(conf->shpool, sizeof(ngx_http_pagecount_shm_t));
    if (conf->sh == NULL)
    {
        return NGX_ERROR;
    }

    conf->shpool->data = conf->sh;

    // B+树模块的初始化
    ngx_rbtree_init(&conf->sh->rbtree, &conf->sh->sentinel,
                    ngx_http_pagecount_rbtree_insert_value);

    return NGX_OK;
}
// Nginx.conf -> "gtt"
// 当前函数是设置指令的处理函数,当请求来的时候就会执行
// 来一次请求就会去执行一次
char *ngx_http_pagecount_handler_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    // 关于共享内存的配置
    ngx_shm_zone_t *shm_zone;
    ngx_str_t name = ngx_string("pagecount_slab_shm");
    ngx_http_pagecount_conf_t *mconf = (ngx_http_pagecount_conf_t *)conf;

    mconf->shmsize = 1024 * 1024;
    shm_zone = ngx_shared_memory_add(cf, &name, mconf->shmsize, &ngx_http_pagecount_handler_moudle);
    if (NULL == shm_zone)
    {
        return NGX_CONF_ERROR;
    }

    shm_zone->init = ngx_http_pagecount_shm_init;
    shm_zone->data = mconf;

    ngx_http_core_loc_conf_t *corecf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
    corecf->handler = ngx_http_pagecount_handler;

    return NGX_CONF_OK;
}

代码运行以后,我们访问对应的服务器,可以看见如下的界面:

在这里插入图片描述
以上就是博主在学习 Nginx 时的一些理解,其实 Nginx 源码很庞大,但是我们如果需要自己进行一个模块开发的话,我们可以效仿 Nginx 源码中的实现,因为他很多的东西都是可以拿出来进行使用的,我们可以在已有的基础上进行修改即可。


网站公告

今日签到

点亮在社区的每一天
去签到