Nginx源码入门指南

liminjun

引言:

​ Nginx作为我们常用的反向代理服务,其优秀的性能和扩展性获得了众多大厂的青睐,Nginx的源码在设计上有很多值得我们学习的地方,本文将介绍一些Nginx源码的一些基础概念,为读者阅读源码扫除一部分障碍。

​ 通过阅读本文,你可以了解到Nginx源码目录层级的安排,Nginx中实现的一些巧妙的数据结构,如何通过模块为Nginx扩展新的功能,以及如何单步调试Nginx源码。另外,本文编写内容均假定Nginx运行在linux系统上。

1 窥探Nginx源码结构

​ 虽然Nginx的源码包很小,以nginx 1.12.5版本为例,在压缩的情况下只会占用1M左右的存储空间,但是完整阅读Nginx源码仍然是一个非常具有挑战性的工作,所以需要提前了解每个文件夹具体负责哪些功能,然后根据我们感兴趣的点进行选择性的阅读。

以下简略地展示了nginx源码的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
nginx-1.21.5
├── ...
├── conf
│   ├── ...
│   ├── nginx.conf
│   ├── ...
├── configure
├── ...
└── src
├── core # nginx日志,数据结构,线程池,锁,配置文件,模块加载实现等
│   ├── nginx.c
│   ├── ...
├── event # nginx事件循环,主要是select,poll,epoll等的封装
│   ├── modules
│   │   ├── ngx_devpoll_module.c
│   │   ├── ...
│   ├── ngx_event_accept.c
│   ├── ...
├── http # nginx http协议相关
│   ├── modules # http协议的处理模块,这里属于nginx的官方模块,提供了非常多的实用功能
│   │   ├── ngx_http_access_module.c
│   │   ├── ...
│   ├── ngx_http.c
│   ├── ...
│   └── v2 # http2.0支持
│   ├── ngx_http_v2.c
│   ├── ...
├── mail # 邮件协议支持,imap和smtp等
│   ├── ngx_mail_auth_http_module.c
│   ├── ...
├── misc
│   ├── ngx_cpp_test_module.cpp
│   └── ...
├── os # nginx系统调用相关封装
│   └── unix
│   ├── ngx_alloc.c
│   ├── ...
└── stream # tcp代理相关
├── ngx_stream_access_module.c
├── ...

37 directories, 403 files

​ 这里推荐大家可以简单地阅读一下core/ngx_cycle.c、os/linux/ngx_process.c和os/linux/ngx_process_cycle.c这三个文件,主要负责nginx的进程管理,这里相当于是进程启动后的一条主线,可以帮助理解nginx的多进程模型。

2 Nginx数据结构一览

​ 数据结构是软件运行的基石,如果抛开数据结构直接去学习软件的逻辑和算法设计无疑是非常困难的,所以我们有必要先简单了解下nginx中一些常用的数据结构。

2.1 基础数据结构

​ nginx支持linux,windows等多个平台,nginx源码为了支持多平台,对很多基础的数据结构都进行了封装,这里简单列举几个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(1) ngx_int_t和ngx_uint_t: nginx中对整型和无符号整型的封装
(2) ngx_str_t: nginx中对字符串类型的封装,相比C原生的以\0表示字符串结束的方式,ngx_str_t中增加len大小字段表示字符串长度
(3) ngx_table_elt_t: nginx中存储key/value中的一种数据结构,ngx_table_elt_t是nginx为处理HTTP头“量身定制”的数据结构,其value同时保存了大小写敏感和纯小写的值,这样nginx处理HTTP请求时就可以更快地以忽略大小写的方式匹配HTTP头了
(4) ngx_buf_t: nginx中处理缓冲区的数据结构,缓冲对象可以是内存或者磁盘上的文件
(5) ngx_chain_t: ngx_chain_t与ngx_buf_t一起使用,ngx_chain_t通过单链表的方式将多个ngx_buf_t串起来
(6) ngx_cycle_t: 包含nginx中的执行各个阶段的生命周期
(7) ngx_event_t: 封装epoll等网络事件的封装
(8) ngx_ssl_t: nginx中对openssl的封装
(9) ngx_atomic_t: 实现acs的自旋锁
(10) ngx_thread_pool_t: nginx中对线程池的封装
(11) ngx_time_t: nginx中对时间类型的封装
(12) ngx_regex_t: nginx中对pcre正则表达式库的封装
(13) ngx_sockaddr_t: nginx中对套接字的封装
(14) ngx_flag_t: 通常用在配置文件的解析中,可以处理开关类型的配置
(15) ngx_err_t: nginx中定义的错误码

2.2 容器数据结构

​ 我们在编写代码时免不了需要使用容器,nginx在实现常用容器数据结构的过程中,一方面贴合了实际的功能需求,另一方面着重考虑的节约内存和高性能,其对数据结构的定制非常值得我们学习。以下列举了几个在nginx源码中常出现的几个数据结构,并简单说明其特点。

1
2
3
4
5
6
(1) ngx_array_t: nginx中动态数组的实现,由于nginx使用纯C开发的,所以并不能直接使用C++ STL中的verctor容器,ngx_array_t可以在数组大小达到分配容量上限时自动扩容
(2) ngx_hash_t: nginx中哈希表的实现,nginx中使用开放地址法解决哈希冲突,比较特殊的是这里的哈希表实现了前后带通配符的匹配,一些带通配符的url匹配使用的就是这个数据结构
(3) ngx_list_t: nginx中链表的实现,这里的链表是由多个ngx_list_part_t链起来,而每个ngx_list_part_t又是一个数组,这样设计是为了在分配小块内存时可以通过数组的偏移量直接访问元素,相当于同时结合了数组和链表的优势
(4) ngx_queue: nginx中一个基础顺序容器的实现,它以双向链表的方式将数据组织在一起,该容器实现了排序功能,并且支持两个链表之间的合并
(5) ngx_rbtree_t: nginx中红黑树的实现,红黑树是一种自平衡的二叉树,在ngix的定时器管理和文件缓存模块均有使用
(6) ngx_radix_tree_t: nginx中基数树的实现,相比红黑树要求key必须是32位的整数,但插入和删除速度要比红黑树快很多,在geo模块有使用

2.3 内存池和连接池

​ nginx中有两个内存池的实现,一个是位于core/ngx_palloc.h文件里定义的ngx_pool_t数据结构,另一个是位于core/ngx_slab.h文件里定义的ngx_slab_pool_t数据结构,它们之间的区别如下:

1
2
(1) ngx_pool_t:进程内的内存池,nginx中分配内存时基本上都是通过此数据结构,所以在nginx源码中被大量地使用
(2) ngx_slab_pool_t:构建在共享内存之上的内存池,由于nginx使用的是master管理worker的多进程架构,多个worker进程之间有时候需要做数据上的交换(如在使用nginx的限流功能时,限流状态需要在多个worker进程之间共享),nginx使用系统的共享内存机制解决多个进程之间的数据共享问题,另外带有zooe关键字的数据结构或者模块一般都和共享内存有关

​ 除了内存池,nginx中为了提高性能,还有连接池ngx_connection_t,连接池内封装了每个连接的读写事件,该连接池主要使用在ngx_cycle_t结构体中,ngx_cycle_t中有connections和free_connections,分别代表当前正在使用的连接和空闲连接。

2.4 模块相关数据结构

​ 在学习nginx模块代码或者编写自定义模块时,经常会看到以下几个数据结构

1
2
3
4
5
(1) ngx_module_t: 这个数据结构定义了模块执行时的几个生命周期,通过传入回调函数的方式,可以执行模块内的自定义逻辑
(2) ngx_command_t: 定义模块中使用的配置项
(3) ngx_http_request_t: 在自定义http模块时,可以通过这个数据结构获取到http的request及发送response消息
(4) ngx_conf_t: nginx中对配置文件的封装,在编写模块代码时,经常会用到该配置文件定义的内存池
(5) ngx_log_t: 在编写模块时,可以直接从ngx_connection_t连接池中取到该数据结构对象,通过ngx_log_t可以直接调用nginx中写日志的功能

3 Nginx模块之旅

​ 总的来说nginx的源码其实就是一个框架加上一堆模块实现的,模块化的设计无疑是nginx的一大亮点之一,其中nginx模块可分为官方模块和第三方模块,官方模块即随nginx源码一起发布的模块,而第三方模块通常需要用户额外下载,然后通过--add-module选项添加到nginx源码中编译的;下面将介绍一些常用的Nginx模块,在了解这些模块后,你一定会惊讶于nginx还能做这些事。

3.1 Nginx官方模块

​ 以下模块代码直接包含在nginx的源码中,默认编译配置下部分功能可能未开启,此时只需要通过--with-XXX选项开启即可使用。

模块 功能
ngx_http_auth_basic_module 该模块可以为代理服务增加简单的用户名和密码的验证
ngx_http_autoindex_module 使用该模块后可以获取到服务器上某个文件夹下的文件列表
ngx_http_geoip_module 这个模块通过连接MaxMind 数据库获取ip的所在地
ngx_http_memcached_module 可以通过调用http接口的方式操作memcached
ngx_http_secure_link_module 为nginx增加防盗链功能
ngx_http_realip_module 获取反向代理过程中客户端的真实ip
ngx_http_mp4_module 提供mp4流媒体功能
ngx_http_access_module 实现ip黑名单和白名单的功能
ngx_http_core_module 处理http请求的核心模块
ngx_http_mirror_module http请求镜像,可以将生产环境的流量镜像一份到测试环境
ngx_http_rewrite_module http url路径重写功能
ngx_stream_core_module 处理tcp请求的核心模块

更多nginx官方模块可参考: https://nginx.org/en/docs/

​ nginx官方模块为处理http请求过程中增加了很多实用的功能,除此以外,nginx还可以处理邮件相关协议,也可以直接代理tcp协议请求,通过灵活使用这个功能可以减少很多我们日常的开发工作。

3.2 Nginx第三方模块

​ nginx除了官方模块,更是有很多优秀的第三方模块,很多优秀的开源项目诸如openresty和kong网关都是基于nginx的第三方模块扩展开发的。

模块 功能
nginx-clojure/nginx-clojure 让nginx可以内嵌java编程语言
openresty/lua-nginx-module 让nginx可以内嵌lua编程语言,基于该模块nginx可以直接实现简单的web接口
youzee/nginx-unzip-module 该模块可以让nginx直接从zip压缩包中获取文件
arut/nginx-rtmp-module 让nginx支持rtmp推流协议
ngx_http_redis 可以通过http请求的方式操作redis
mdirolf/nginx-gridfs 可以让nginx直接访问mongodb的gridfs
evanmiller/mod_zip 动态压缩文件
nginx-goodies/nginx-sticky-module-ng 基于cookie实现的会话保持功能
openresty/echo-nginx-module 可以将nginx的变量直接通过http响应返回

更多nginx第三方模块可参考: https://www.nginx.com/resources/wiki/modules/

nginx中lua的写法可参考: https://github.com/openresty/lua-nginx-module#content_by_lua

3.3 Nginx模块源码分析

​ 本小结首先简单分析sticky会话保持模块的代码,然后引出如何编写自己的nginx模块。

3.3.1 sticky模块代码分析

​ sticky模块的主要作用是对请求进行会话保持,可以让同一个客户端的多次请求都连接到同一个上游服务,如果部署或调用过事业部内的AI+能力平台对该模块应该会有一定的了解。该模块属于nginx的第三方模块,在编译nginx时需要额外下载。

sticky模块的源码下载地址: https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng/get/master.tar.gz

相比nginx官方源码而言,sticky模块的注释比较多,阅读起来会更加轻松一些,这也是这里选择sticky模块作为例子的原因

​ 以下梳理了sticky模块的主要流程图,方便后面对代码的学习:

1
2
3
4
5
6
7
8
9
10
11
12
flowchart LR
A(请求) --> B{cookies中是否存在'route'?}
B --> |yes| C[从cookies中解析'route']
C --> D{是否能找到和'route' cookie关联的上游服务?}
B --> |no| E[使用轮询算法查找查找上游服务器]
D --> |no| E
D --> H[尝试通过找到的上游服务处理请求]
H --> I{出错?}
I --> |yes| E
I --> |no| G
E --> F[向响应头中增加Set-Cookie: route=md5处理后的$upstream_addr]
F --> G(响应)

​ sticky模块的源码非常简洁,核心处理逻辑就是以下几个函数:

1
2
3
4
5
6
7
8
9
10
//执行一些初始化工作
ngx_int_t ngx_http_init_upstream_sticky(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
//解析cookies中的会话保持字段,并查找对应的上游服务
ngx_int_t ngx_http_init_sticky_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us);
//获得一个可以使用的连接
ngx_int_t ngx_http_get_sticky_peer(ngx_peer_connection_t *pc, void *data);
//解析sticky模块的配置
char *ngx_http_sticky_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
//创建sticky模块的配置
void *ngx_http_sticky_create_conf(ngx_conf_t *cf);

3.3.2 如何自定义Nginx模块

​ 当我们需要一些功能,而nginx官方模块或第三方模块都没有提供时,就可以通过自定义模块实现了,编写一个nginx模块其实是非常简单的,比如上面的sticky模块源码中,实际有用的文件就只有4个。

​ 编写一个nginx的步骤如下:

1
2
flowchart LR
A[编写config文件] --> B[通过ngx_command_t设置配置] --> C[向ngx_module_t中注册回调函数] --> D[实现回调函数中的业务逻辑]

config文件其实是一个shell脚本,sticky模块的config文件内容如下:

1
2
3
4
5
6
ngx_addon_name=ngx_http_sticky_module
HTTP_MODULES="$HTTP_MODULES ngx_http_sticky_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_sticky_module.c $ngx_addon_dir/ngx_http_sticky_misc.c"
NGX_ADDON_DEPS="$NGX_ADDON_DEPS $ngx_addon_dir/ngx_http_sticky_misc.h"
USE_MD5=YES
USE_SHA1=YES

如果只想开发一个HTTP模块,那么config文件中至少需要定义以下三个变量:

  • ngx_addon_name: 仅在configure执行时使用,一般设置为模块名称
  • HTTP_MODULES: 保持所有HTTP模块的名称,每个HTTP模块之间由空格符相连
  • NGX_ADDON_SRCS: 用于指定新增模块的源代码,多个待编译的源代码之间用空格隔开

除了HTTP_MODULES表示HTTP模块,还包含CORE_MODULES(核心模块),EVENT_MODULES(事件模块),HTTP_FILTER_MODULES(HTTP过滤模块),HTTP_HEADERS_FILTER_MODULES(HTTP头部过滤模块)

在编写完config文件后,需要在模块源码中设置ngx_command_t和ngx_module_t结构体,为理解ngx_command_t和ngx_module_t的使用方法,下面先列出了这几个结构体的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
struct ngx_command_s {
//配置项名称
ngx_str_t name;
//配置项可以出现的位置。例如,出现在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;
//配置项读取后的处理方法
void *post;
};
typedef struct ngx_command_s ngx_command_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回调方法存储loc级别配置项的结构体
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;

#define NGX_MODULE_V1 \
NGX_MODULE_UNSET_INDEX, NGX_MODULE_UNSET_INDEX, \
NULL, 0, 0, nginx_version, NGX_MODULE_SIGNATURE

#define NGX_MODULE_V1_PADDING 0, 0, 0, 0, 0, 0, 0, 0

struct ngx_module_s {
ngx_uint_t ctx_index;
ngx_uint_t index;

char *name;

ngx_uint_t spare0;
ngx_uint_t spare1;

ngx_uint_t version;
const char *signature;

//ctx用于指向一类模块的上下文结构体,在http模块中,ctx需要指向ngx_http_module_t结构体
void *ctx;
//上面定义的模块配置结构体
ngx_command_t *commands;
//表示该模块的类型,它与ctx紧密相关,在官方nginx中,它的取值范围是以下5种: NGX_HTTP_MODULE、NGX_CORE_MODULE、NGX_CONF_MODULE、NGX_EVENT_MODULE、NGX_MAIL_MODULE
ngx_uint_t type;

//master进程启动时会回调init_master
ngx_int_t (*init_master)(ngx_log_t *log);
//在初始化所有模块时会回调init_module,在启动worker进程前执行
ngx_int_t (*init_module)(ngx_cycle_t *cycle);
//在正常服务前调用,在每个worker进程初始化时会调用
ngx_int_t (*init_process)(ngx_cycle_t *cycle);
//保留,目前nginx暂不支持多线程模式,所以暂未使用
ngx_int_t (*init_thread)(ngx_cycle_t *cycle);
//同上
void (*exit_thread)(ngx_cycle_t *cycle);
//在服务停止前会调用,worker进程会在退出前调用它
void (*exit_process)(ngx_cycle_t *cycle);
//在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;
};
typedef struct ngx_module_s ngx_module_t;

以下为对应sticky模块中对这几个结构体的设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static ngx_command_t  ngx_http_sticky_commands[] = {

{ ngx_string("sticky"),
NGX_HTTP_UPS_CONF|NGX_CONF_ANY,
ngx_http_sticky_set,
0,
0,
NULL },

ngx_null_command
};

static ngx_http_module_t ngx_http_sticky_module_ctx = {
NULL, /* preconfiguration */
NULL, /* postconfiguration */

NULL, /* create main configuration */
NULL, /* init main configuration */

ngx_http_sticky_create_conf, /* create server configuration */
NULL, /* merge server configuration */

NULL, /* create location configuration */
NULL /* merge location configuration */
};


ngx_module_t ngx_http_sticky_module = {
NGX_MODULE_V1,
&ngx_http_sticky_module_ctx, /* module context */
ngx_http_sticky_commands, /* module directives */
NGX_HTTP_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
};

通过在以上结构体中加入自己编写的回调函数,执行configure时使用--add-module选项指定模块的路径,即可将业务逻辑代码嵌入到nginx中执行了。

4 浅谈Nginx源码调试

​ 通常在学习开源组件的源码过程中,如果可以将程序运行并单步执行将会大大降低我们理解代码的难度,为此这里介绍通过vscode调试nginx的方法。

(1) 首先我们需要基于源码编译出nginx的可执行文件,执行configure生成Makefile (nginx默认是依赖opensslpcrezlib这三个库的其中openssl用来支持https协议,pcre用于正则表达式匹配,zlib主要影响gzip压缩功能)。

1
2
#执行./configure --help可以查询configure支持的所有选项
./configure --prefix=`pwd`/dist --with-openssl=OpenSSL_1_1_1l --with-pcre=pcre-8.45 --with-zlib=zlib-1.2.11

(2) 修改objs/Makefile,对CFLAGS增加-O0-g选项,其中-O0是禁用编译优化而-g是为编译的二进制文件增加debug信息,修改完成后执行make编译nginx可执行文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CC =	cc
CFLAGS = -pipe -O0 -W -Wall -Wpointer-arith -Wno-unused-parameter -Werror -g
CPP = cc -E
LINK = $(CC)

ALL_INCS = -I src/core \
-I src/event \
-I src/event/modules \
-I src/os/unix \
-I pcre-8.45 \
-I zlib-1.2.11 \
-I objs \
-I src/http \
-I src/http/modules
...略

(3) vscode安装C/C++插件。

(4) vscode的调试器launch.json配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/objs/nginx",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/dist/sbin",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}

(5) 修改nginx.conf配置文件,增加以下配置项,禁止nginx daemon模式和多进程模式。

1
2
daemon off;
master_process off;

​ 经过以上配置,通过nginx上的调试按钮启动后,就可以进行常用的调试操作了。

5 总结

​ 在阅读过以上内容后,相信大家对nginx的源码已经有了一个比较基础的了解了,本文仅介绍了nginx源码的冰山一角,更多的内容需要大家自己去探索了,另外由于nginx的源码缺乏注释,建议学习时结合一些书籍,可以帮助更快的理解。

​ 由于编者自己水平也有限,以上如有错误的地方还烦请指正,感谢!

参考资料:
nginx源码下载地址: https://nginx.org/en/download.html
nginx官方模块: https://nginx.org/en/docs/
nginx第三方模块: https://www.nginx.com/resources/wiki/modules/
nginx http核心模块: https://nginx.org/en/docs/http/ngx_http_core_module.html
书籍推荐: 《深入理解Nginx:模块开发与架构解析(第2版)》

  • Title: Nginx源码入门指南
  • Author: liminjun
  • Created at: 2022-03-13 14:45:54
  • Updated at: 2023-05-15 10:39:06
  • Link: https://olldbg.github.io/2022/03/13/Nginx源码入门指南/
  • License: This work is licensed under CC BY-NC-SA 4.0.