目录

内容简介

前言

第0章 导读

0.1 关于本书

0.2 读者对象

0.3 读者要求

0.4 运行环境

0.5 本书的结构

0.6 如何阅读本书

0.7 本书的源码

第1章 Nginx入门

1.1 关于Nginx

1.1.1 历史

1.1.2 特点

1.1.3 进程模型

1.1.4 版本

1.2 安装Nginx

1.2.1 准备工作

1.2.2 快速安装

1.2.3 运行命令

1.2.4 验证安装

1.2.5 定制安装

1.3 配置Nginx

1.3.1 配置文件格式

1.3.2 进程配置

1.3.3 运行日志配置

1.3.4 http配置

1.3.5 server配置

1.3.6 location配置

1.3.7 文件访问配置

1.3.8 upstream配置

1.3.9 变量

1.4 总结

第2章 Nginx开发准备

2.1 开发环境

2.1.1 C++标准

2.1.2 Boost程序库

2.2 Nginx的目录结构

2.3 Nginx源码的特点

2.3.1 代码风格

2.3.2 代码优化

2.3.3 面向对象思想

2.4 在Nginx里使用C++

2.4.1 实现原则

2.4.2 代码风格

2.4.3 编译脚本

2.5 基本的C++包装类

2.5.1 类定义

2.5.2 构造和析构

2.5.3 成员函数

2.6 总结

第3章 Nginx基础设施

3.1 头文件

3.2 整数类型

3.2.1 标准整数类型

3.2.2 自定义整数类型

3.2.3 无效值

3.2.4 C++封装

3.3 错误处理

3.3.1 错误码定义

3.3.2 C++异常

3.4 内存池

3.4.1 结构定义

3.4.2 操作函数

3.4.3 C++封装

3.4.4 清理机制

3.4.5 C++内存分配器

3.5 字符串

3.5.1 结构定义

3.5.2 操作函数

3.5.3 C++封装

3.6 时间与日期

3.6.1 时间结构定义

3.6.2 时间操作函数

3.6.3 日期结构定义

3.6.4 日期操作函数

3.6.5 C++封装时间

3.6.6 C++封装日期

3.7 运行日志

3.7.1 结构定义

3.7.2 操作函数

3.7.3 C++封装

3.8 总结

第4章 Nginx高级数据结构

4.1 动态数组

4.1.1 结构定义

4.1.2 操作函数

4.1.3 C++封装

4.2 单向链表

4.2.1 结构定义

4.2.2 操作函数

4.2.3 C++迭代器

4.2.4 C++封装链表

4.3 双端队列

4.3.1 结构定义

4.3.2 操作函数

4.3.3 C++节点

4.3.4 C++迭代器

4.3.5 C++封装队列

4.4 缓冲区

4.4.1 结构定义

4.4.2 操作函数

4.4.3 C++封装

4.5 数据块链

4.5.1 结构定义

4.5.2 操作函数

4.5.3 C++节点

4.5.4 C++迭代器

4.5.5 C++封装数据块链

4.6 键值对

4.6.1 简单键值对

4.6.2 散列表键值对

4.7 总结

第5章 Nginx开发综述

5.1 最简单的Nginx模块

5.1.1 模块设计

5.1.2 配置解析

5.1.3 处理函数

5.1.4 模块集成

5.1.5 编译脚本和命令

5.1.6 测试验证

5.2 Nginx开发基本流程

5.2.1 设计

5.2.2 开发

5.2.3 编译

5.2.4 测试验证

5.2.5 调优

5.3 Nginx的模块

5.3.1 模块的数据结构

5.3.2 模块的种类

5.3.3 http模块

5.3.4 模块的类图

5.3.5 模块的组织形式

5.4 Nginx的配置

5.4.1 结构定义

5.4.2 配置解析的基本流程

5.4.3 配置数据的存储模型

5.4.4 访问配置数据

5.4.5 确定配置数据的位置

5.4.6 配置解析函数

5.4.7 配置数据的合并

5.4.8 配置指令的类型

5.5 Nginx模块源码分析

5.5.1 ngx_core_module

5.5.2 ngx_errlog_module

5.6 Nginx的编译脚本

5.6.1 运行机制

5.6.2 使用的变量

5.7 C++封装模块信息

5.7.1 NgxModuleConfig

5.7.2 NgxModule

5.8 C++封装配置解析

5.8.1 NgxCommand

5.8.2 NgxTake

5.8.3 NGX_MODULE_NULL

5.9 C++开发Nginx模块

5.9.1 C++模块的基本组成

5.9.2 配置信息类

5.9.3 业务逻辑类

5.9.4 模块集成类

5.9.5 实现源文件

5.9.6 增加更多功能

5.10 总结

第6章 Nginx HTTP框架综述

6.1 HTTP框架简介

6.1.1 模块分类

6.1.2 处理流程

6.1.3 请求结构体

6.1.4 请求的处理阶段

6.1.5 请求的环境数据

6.2 HTTP处理引擎

6.2.1 处理函数原型

6.2.2 处理函数的存储方式

6.2.3 内容处理函数

6.2.4 引擎的数据结构

6.2.5 引擎的初始化

6.2.6 引擎的运行机制

6.2.7 日志阶段的处理

6.3 HTTP过滤引擎

6.3.1 过滤函数原型

6.3.2 过滤函数链表

6.3.3 过滤函数的顺序

6.3.4 过滤链表的运行机制

6.3.5 请求体过滤

6.4 Nginx模块源码分析

6.4.1 ngx_http_static_module

6.4.2 ngx_http_not_modified_filter_module

6.5 C++封装

6.5.1 NgxModuleCtx

6.5.2 NgxHttpCoreModule

6.5.3 NgxFilter

6.6 总结

第7章 Nginx HTTP请求处理

7.1 HTTP状态码

7.2 请求结构体

7.3 请求行

7.3.1 请求方法

7.3.2 协议版本号

7.3.3 资源标识符

7.4 请求头

7.5 请求体

7.5.1 结构定义

7.5.2 操作函数

7.6 响应头

7.6.1 结构定义

7.6.2 操作函数

7.7 响应体

7.8 C++封装

7.8.1 NgxHeaders

7.8.2 NgxRequestBody

7.8.3 NgxRequest

7.8.4 NgxResponse

7.9 开发HTTP处理模块

7.9.1 模块设计

7.9.2 配置信息类

7.9.3 业务逻辑类

7.9.4 模块集成类

7.9.5 实现源文件

7.9.6 编译脚本

7.9.7 测试验证

7.10 开发HTTP过滤模块

7.10.1 模块设计

7.10.2 配置信息类

7.10.3 环境数据类

7.10.4 业务逻辑类

7.10.5 模块集成类

7.10.6 实现源文件

7.10.7 编译脚本

7.10.8 测试验证

7.11 总结

第8章 Nginx HTTP请求转发

8.1 upstream框架简介

8.1.1 工作原理

8.1.2 请求结构体

8.1.3 上游结构体

8.1.4 上游配置参数

8.2 upstream运行机制

8.2.1 回调函数

8.2.2 初始化upstream

8.2.3 设置upstream

8.2.4 启动upstream

8.2.5 处理upstream数据

8.3 load-balance运行机制

8.3.1 结构定义

8.3.2 初始化模块入口

8.3.3 初始化IP地址列表

8.3.4 初始化算法

8.3.5 执行算法

8.4 Nginx模块源码分析

8.4.1 ngx_http_memcached_module

8.4.2 ngx_http_upstream_ip_hash_module

8.5 C++封装

8.5.1 NgxUpstream

8.5.2 NgxUpstreamHelper

8.5.3 NgxHttpUpstreamModule

8.5.4 NgxLoadBalance

8.6 开发upstream模块

8.6.1 模块设计

8.6.2 配置信息类

8.6.3 业务逻辑类

8.6.4 模块集成类

8.6.5 实现源文件

8.6.6 编译脚本

8.6.7 测试验证

8.7 开发load-balance模块

8.7.1 模块设计

8.7.2 配置信息类

8.7.3 业务逻辑类

8.7.4 模块集成类

8.7.5 实现源文件

8.7.6 编译脚本

8.7.7 测试验证

8.8 总结

第9章 Nginx HTTP子请求

9.1 子请求简介

9.1.1 工作原理

9.1.2 请求结构体

9.1.3 回调函数

9.1.4 待处理请求链表

9.1.5 子请求存储结构

9.2 子请求运行机制

9.2.1 创建子请求

9.2.2 处理引擎

9.2.3 数据整理

9.3 C++封装

9.3.1 NgxSubRequestHandler

9.3.2 NgxSubRequest

9.4 数据回传模块

9.4.1 模块设计

9.4.2 环境数据类

9.4.3 业务逻辑类

9.4.4 模块集成类

9.4.5 编译脚本

9.5 在模块里使用子请求

9.5.1 模块设计

9.5.2 配置信息类

9.5.3 业务逻辑类

9.5.4 测试验证

9.6 总结

第10章 Nginx变量

10.1 结构定义

10.1.1 变量值

10.1.2 变量访问对象

10.1.3 变量的存储

10.1.4 请求结构体

10.2 运行机制

10.2.1 注册变量

10.2.2 获取变量

10.3 C++封装

10.3.1 NgxVariableValue

10.3.2 NgxVariable

10.3.3 NgxVariables

10.3.4 NgxVarManager

10.3.5 NgxVariableValueProxy

10.4 在模块里使用变量

10.4.1 添加变量

10.4.2 读写变量

10.5 总结

第11章 Nginx辅助设施

11.1 摘要算法

11.1.1 MD5

11.1.2 SHA-1

11.1.3 MurmurHash

11.1.4 C++封装

11.2 编码和解码

11.2.1 CRC校验

11.2.2 Base64编码解码

11.2.3 URI编码解码

11.2.4 HTML和JSON编码

11.3 正则表达式

11.4 共享内存

11.4.1 结构定义

11.4.2 操作函数

11.4.3 C++共享内存

11.5 定时器

11.5.1 结构定义

11.5.2 操作函数

11.5.3 C++定时器事件

11.5.4 C++定时器工厂

11.6 总结

第12章 Nginx与设计模式

12.1 设计模式简介

12.2 框架级别的模式

12.3 业务级别的模式

12.4 代码级别的模式

12.5 总结

第13章 结束语

13.1 本书的遗憾

13.2 下一步

13.3 临别赠言

附录A 推荐书目

附录B GDB调试简介

附录C Nginx的字符串格式化

附录D Nginx里的void*变量

附录E Nginx C++模块简介

附录F Nginx Lua模块简介

未经许可，不得以任何方式复制或抄袭本书之部分或全部内容。

版权所有，侵权必究。

图书在版编目（CIP）数据

Nginx模块开发指南：使用C++11和Boost程序库/罗剑锋著．—北京：电子工业出版社，2015.11

ISBN 978-7-121-27294-3

Ⅰ．①N…　Ⅱ．①罗…　Ⅲ．①互联网络－网络服务器－程序设计－指南②C语言－程序设计－指南　Ⅳ．①TP368.5-62②TP312-62

中国版本图书馆CIP数据核字（2015）第227682号

策划编辑：孙学瑛

责任编辑：安娜

出版发行：电子工业出版社

北京市海淀区万寿路173信箱　邮编：100036

开　　本：787×980　1/16　印张：23.25　字数：438千字

版　　次：2015年11月第1版

印　　次：2015年11月第1次印刷

印　　数：3000册　　定价：79.00元

凡所购买电子工业出版社图书有缺损问题，请向购买书店调换。若书店售缺，请与本社发行部联系，联系及邮购电话：（010）88254888。

质量投诉请发邮件至zlts@phei.com.cn，盗版侵权举报请发邮件至dbqq@phei.com.cn。

服务热线：（010）88258888。

内容简介

Nginx是由俄罗斯工程师Igor Sysoev开发的一个高性能Web服务器，运行效率远超传统的Apache、Tomcat，是世界第二大Web服务器，被国内外诸多顶级互联网公司采用。

Nginx的一个突出特点是其灵活优秀的模块化架构，可以在不修改核心的前提下增加任意功能，自2004年发布至今，已经拥有百余个官方及非官方的功能模块（如fastcgi、memcached、mysql等），使得Nginx成长为了一个近乎“全能”的服务器软件。

Nginx以纯C语言实现，开发扩展功能模块也大多使用C语言，但由于C语言固有的过程式特性，编写、调试代码都较麻烦——特别是对于Nginx的初学者。本书深入源码，详细解析了模块体系、配置指令、HTTP框架等Nginx核心运行机制，并在此基础上讲解如何使用C++和Boost程序库来开发Nginx模块，充分利用现代C++里的大量新特性和库组件，让Nginx的模块开发变得更加便捷、轻松和愉快。

本书结构严谨、脉络清晰、论述精确、详略得当，值得广大软件开发工程师、系统运维工程师和编程爱好者拥有。

前言

缘起

最早接触Nginx大概是在2011年，面对着一个全新的Web服务器，和大多数人一样最初我也是一片茫然，能找到的参考资料十分有限，安装、配置、运行几乎都是“摸着石头过河”，犯过许多低级错误。

随着对Nginx逐渐熟悉，它的高并发处理能力给我留下了深刻的印象，作为一个开源软件的爱好者，很自然地想要探究一下它的内部工作原理。我由此开始了对Nginx源码的钻研之路，中间经过了很多的艰辛曲折，走过了不少的弯路。

我最常用的工作语言是C++，所以在阅读Nginx源码时也总以C++的面向对象方式来思考和理解，以对象作为切入点记笔记、画UML：从最简单的ngx_str_t、ngx_array_t入手，然后到ngx_request_t、ngx_upstream_t等复杂的结构，再围绕着这些对象研究相关的功能函数和处理流程，梳理代码逻辑的同时也摸索着使用C++编写Nginx模块的方法，逐渐积累了一些用起来颇为顺手的小工具——当然还是比较初级的形式。

去年年中的某个时刻，我被调到了新的工作岗位，需要重度使用Nginx开发，这让我以前的零散积累终于有了用武之地，那段时间里使用C++陆续做了很多东西，也借着机会重新优化了原有的工具代码。

繁忙的工作之余，我有了种进一步整理经验的迫切感，因为只有系统完整地分享这些知识，才能让更多的人使用C++来基于Nginx二次开发，让Nginx更好地为网络世界服务，于是今年年初终于动起了写作的念头。

经过大半年的努力，现在这本书终于呈现在了读者面前，结构上基本反映了我学习研究Nginx时的心路历程，从最初的“一无所知”起步，慢慢地深入到定制开发模块的层次，希望能与读者“心有戚戚焉”。

Nginx随感

毫无疑问，Nginx是目前这个星球上所能获得的最强劲的Web服务器（没有之一），同时也是目前最成熟、最优秀的TCP/HTTP服务器开发框架。

Nginx资源消耗低，并发处理性能高，配置灵活，能够连接CGI、PHP、MySQL、Memcached等多种后端，还有着出色的负载均衡能力，可以整合封装各种service，构建稳定高效的服务。如今Nginx已经成为了网站架构里不可或缺的关键组件，广泛应用于国内外许多大型IT企业内部。每一个繁忙的网站背后，可能都有Nginx默默工作的身影。

在Nginx出现之前，使用C/C++开发Web服务器是项比较“痛苦”的工作，虽然有很多网络程序库可以使用（例如ACE、asio、libevent、thrift等），但它们通常只关注较底层的基础功能实现，离成熟的“框架”相距甚远，不仅开发过程烦琐低效，而且程序员还必须要处理配置管理、进程间通信、协议解析等许多Web服务之外的其他事情，才能开发出一个较为完善的服务器程序。但即使开发出了这样的服务器，通常性能上也很难得到保证，会受到程序库和开发者水平等因素的限制——很长一段时间里，C/C++在Web服务器领域都没有大展拳脚的机会。

Nginx的横空出世为Web服务器开辟了一个崭新的天地，它搭建了一个高性能的服务器开发框架，而且是一个完整的、全功能的服务器。模块化的架构设计很好地分离了底层支撑模块和上层逻辑模块，底层模块处理了配置、并发等服务器的外围功能，核心支撑模块定义了主体的TCP/HTTP处理框架。开发者可以把大部分精力集中在上层的业务功能实现上，再也不必去为其他杂事而分心，提高了软件的开发效率。

在Nginx框架里C/C++程序员可以尽情发挥自己的专长，充分利用Nginx无阻塞处理的优势，打造出高质量的Web应用服务器，与其他系统一较高下。

Nginx和C/C++

Igor Sysoev选择用C语言（准确地说是ANSI C）来实现Nginx肯定是经过了认真的考虑。

作为与UNIX一同诞生的编程语言，C语言一直是系统级编程的首选。和其他高级语言相比，它简单可靠，更接近计算机底层硬件，运行效率更高。指针更是C语言的一大特色，善用指针能够完成许多其他语言无法完成的工作。

以C语言实现的Nginx没有“虚拟机”的成本，省略了不必要的中间环节，直接操纵计算机硬件，从根本上提高了Web服务器的处理能力。虽然C语言不直接支持面向对象，但Nginx灵活运用了指针，采用结构体+函数指针的形式，达到了同样的效果，从而使软件拥有了良好的结构。

C++是仅次于C的系统级编程语言，在兼容C的同时又增加了类、异常、模板等新特性，还支持面向对象、泛型、函数式、模板元等多种编程范式，可以说是计算机语言里的一个“庞然大物”。C++的特性很多，有的也很好用，但总体上的确比较复杂，易学难精，容易被误用和滥用，导致低效、难维护的代码，我想这可能是Igor Sysoev放弃使用C++的一个重要原因。

另一个可能的原因是C语言本身已经非常稳定，几十年来没有太大的变动，各个系统里都支持得非常好。而C++在1998年才有了第一个标准，而且现在还在发展之中，语言特性还不够稳定（例如export、register等曾经的关键字在C++11里就已经被废弃），许多编译器对C++的支持程度也有差异，这与Nginx的高可移植性目标明显不符。

但C++毕竟还是有很多的优点，类可以更好地封装信息、异常简化了错误处理、模板能够在编译期执行类型计算。在C++11标准颁布之后C++更是几乎变成了一门“全新”的语言，auto/decltype/nullptr/noexcept等新关键字增强了语言的描述能力，标准库也扩充了相当多的组件，易用性和稳定性都大大提升。

在Nginx里使用C++时要对C++的长处和不足有清醒的认识，避免多层次继承、虚函数等影响效率的编程范式，只使用经过充分验证的、能够切实提高开发效率和性能的语言特性和库，避免华而不实的技术炫耀，尽量做到像Nginx源码那样质朴踏实。只有这样，才能够发挥出1+1>2的作用，让Nginx从C++中得到更进一步的发展动力。

致谢

首先当然要感谢Nginx的作者Igor Sysoev，没有他就不会有如此优秀的Web服务器，也就不会有本书的诞生。

亲情永远是人生命中最值得珍惜的部分，我要感谢父母多年来的养育之恩和“后勤”工作，感谢妻子在生活中的陪伴（但因为频繁的加班常常缺少相处的时间），感谢即将上小学的女儿，愿你们能够永远幸福快乐。

我还要感谢工作中共同奋斗拼搏的诸多同事，从他们那里我获取到了很多的经验和知识，度过了很多难忘的岁月，和他们在一起让工作超越了谋生的手段变成了一种享受。

最后感谢读者选择本书，希望读者能够在阅读过程中有所收获，在Nginx开发过程中获得乐趣。

您的朋友　罗剑锋
2015年8月18日　于　北京　酒仙桥

第0章　导读

0.1　关于本书

Nginx[1]是由俄罗斯工程师Igor Sysoev开发的一个高性能Web服务器，各方面的表现均远超传统的Apache，已经应用于诸多顶级互联网公司，为全世界数以亿计的网民提供着出色的服务。根据某权威公司分析统计，现在它已是市场份额第二大的Web服务器，并且仍在快速增长。

除了最引人注目的高性能和高稳定性外，Nginx的另一个突出特点是高扩展性，其灵活优秀的模块化架构允许在不修改核心的前提下增加任意功能。自2004年正式发布以来，Nginx已经拥有了百余个官方及非官方的功能模块（如fastcgi、memcache、mysql、lua等），这使得Nginx超越了一般意义上的Web服务器，成为了一个近乎“全能”的应用服务器。

Nginx以纯C语言实现，故开发扩展功能模块也大多使用C语言。但由于C语言固有的“中级语言”特性，难以实现良好的软件结构，编写、调试代码都比较麻烦——特别是对于初学者。本书将在解析Nginx源码的基础上详细讲解使用C++和Boost程序库来开发Nginx模块，充分利用现代C++的新特性和标准库/Boost库，让Nginx的模块开发变得更加便捷和轻松愉快。

0.2　读者对象

本书适合以下五类读者：

- 基于Nginx进行二次开发的软件工程师；

- Nginx运维工程师；

- Web服务器开发者；

- 对Nginx架构、内部实现感兴趣的程序员；

- C/C++、计算机编程爱好者和在校学生。

Nginx采用进程池、事件驱动等方式来支持海量并发连接的处理，搭建了一个高性能高稳定性的服务器框架，在这个框架之内可以利用Nginx内部的各种构件编写模块，自由实现所需的业务功能。因此，国内外都有很多个人和企业以Nginx为平台进行二次开发，编写模块甚至直接定制Nginx，进而提高网站的整体服务能力。本书由浅入深地详细介绍了Nginx模块开发的全过程，并且分析了Nginx的内部运行机制，能够帮助软件工程师较快地熟悉Nginx原理，迅速地投入到实际开发工作中。

Nginx项目十分活跃，版本更新较快，新的功能不断增加，这使得市面上的相关书籍和网络资料常常会变得“过时”——只言片语、语焉不详或者答非所问，Nginx运维工程师很可能会遇到运行、配置出现问题却无法解决的尴尬局面。然而Nginx是开源的，一切问题都可以在源码中找到答案。但十余万行的Nginx源码又有如汪洋大海，如果没有经验丰富的引航员指路，工程师难免会迷失方向，上下求索而不得。本书正是担当了这样的引航员角色，细致地分析了Nginx的配置解析功能和HTTP处理流程，让运维工程师可以快速定位配置指令和它的实现逻辑，深层次地做好Nginx的运维工作。

对于那些自行开发Web服务器的程序员来说，Nginx也是一个非常有意义的借鉴品。它采用epoll/kqueue等异步调用削减了系统成本，充分挖掘了计算机硬件的性能，让CPU、磁盘、网卡等设备并发运行，比传统的多进程、多线程方式的服务器运行得更快，服务能力更强。本书深入剖析了Nginx源码，解析了进程模型、模块架构等机制，学习借鉴这些大师级的代码，无疑会让自己的开发功力更上一层楼，写出更好的软件产品。

Nginx虽然功能如此强大，但它的实现却非常的清晰易读：文件组织良好，代码格式优美，内部隐藏的大量编程技巧、设计思想和架构更是值得仔细研究的无价之宝。对于每一位喜好编程、准备或者正在投身于软件/互联网行业的人来说，本书都是一个很好的起点，可以从零起步窥探到Nginx内部的真正精髓，学习到各种实用的技术和知识，增加自身的“含金量”。

本书还大量应用了C++11标准和Boost程序库，使用了lambda表达式、模板元编程等许多高级特性，可以看作是现代C++编程范式的一次成功实践，读者可以接触到当前C++最新、最前沿的技术。

0.3　读者要求

本书要求读者基本了解C/C++和网络编程知识。

Nginx本身是用C实现的，而本书使用C++作为编程语言，所以读者应该具备足够的C/C++知识，例如宏、指针、类型转换、封装、继承、异常等C/C++特性，如果已有实际编程经验则更好。

在基本的C++之上，本书还使用了C++11标准和Boost程序库来提高开发效率，要求读者对模板、泛型编程和C++标准库有一定的了解，能够运用vector、list等常见的泛型容器和迭代器。

Nginx是一个Web服务器，所以熟悉网络编程对于学习本书是非常有益的。但由于Nginx封装了很多网络通信的底层细节，所以本书不要求读者对网络编程技术有很深刻的认识，但至少应该对TCP和HTTP通信协议有所了解。

此外，因为Nginx大多运行在Linux服务器上，读者还应该再学习一些UNIX/Linux和Shell编程的相关知识，才能理解Nginx的编译脚本和运行维护。

附录A列出了一些技术书籍，涵盖了大部分本书要求掌握的C/C++语言和网络编程技术知识，建议读者阅读本书时参考。

0.4　运行环境

Nginx可以跨平台编译和运行，支持Linux、FreeBSD、OS X、Windows等多种操作系统。但就目前市场来看，Linux是应用最普及的服务器操作系统，故本书的开发环境选用Linux。

Linux有很多的发行版本，企业中使用较多的是偏重于稳定性的CentOS和偏重于易用性的Ubuntu，出于个人喜好的原因本书选择了后者（请CentOS支持者见谅）。

下面是本书使用的具体环境：

- 操作系统：Ubuntu 14.04（Linux 3.13.0）；

- Shell：系统自带的dash（注意不是bash）；

- 编译器：系统自带的GCC 4.8.2（支持C++11标准）；

0.5　本书的结构

Nginx是一个非常复杂的系统，简单的循序渐进方式难以透彻地讲解清楚，也不利于学习和掌握模块开发知识，所以本书结合“知其所以然”再“知其然”的方式组织全书的章节：首先介绍Nginx的基本知识作为入门，然后由浅入深地解析Nginx的源码和架构，理解了内部运行机制后再介绍如何开发Nginx模块。

学习Nginx开发不研究其内部工作原理是不行的，但解析得太深会导致源码太多，文字过于晦涩难懂；介绍得太浅又不能达到“知其所以然”的效果。所以本书只以中等深度研究Nginx源码和框架，讲解关键的流程、原理和函数调用，但不涉及过于底层的实现细节。每个章节首先学习结构定义等静态模型，然后研究工作原理、运行机制等动态模型，最后结合Nginx官方源码，通过模块实例讲解开发要点，这种方式能够较好地覆盖Nginx开发的各个方面。

全书共分14章，各章的内容简介如下。

- 第0章：导读

也就是读者正在阅读的这一章，介绍本书的基本内容和一些注意事项。

- 第1章：Nginx入门

本章简要介绍Nginx的历史、特点，以及如何编译、安装和配置Nginx，可以当作是一本简明Nginx使用手册。

- 第2章：Nginx开发准备

本章是开发Nginx模块前的准备工作，介绍本书使用的C++11标准和Boost程序库，还有Nginx源码的目录结构、基本的代码风格和特点。针对Nginx源码的特点提出了C++的解决方案，实现了一个对Nginx数据结构的C++封装类。

- 第3章：Nginx基础设施

剖析Nginx这样复杂的系统必须从最底层的基础设施开始，本章首先介绍了Nginx框架里基本的整数类型和错误码，然后再研究内存池、字符串、时间日期和运行日志，同时使用C++11标准进行了面向对象的封装，打造出方便易用的基础工具类。

- 第4章：Nginx高级数据结构

本章研究ngx_array_t、ngx_list_t、ngx_queue_t、ngx_buf_t等各种高级Nginx数据结构，它们类似C++的标准容器，在Nginx框架里经常出现，必须很好地理解并掌握它们的特性和用法，C++封装能够让这些数据结构更容易使用。

- 第5章：Nginx开发综述

本章首先使用一个小例子介绍模块开发的基本流程，然后详细剖析Nginx的模块架构、配置解析原理和编译集成方法等Nginx开发必需的重要技术，最后实现了C++封装类并开发出现代C++风格的Nginx模块。

- 第6章：Nginx HTTP框架综述

本章深入研究Nginx的HTTP处理框架，解析http模块的核心工作原理和流程，包括处理阶段、处理引擎、过滤链表等重要概念。

- 第7章：Nginx HTTP请求处理

本章详细解析ngx_request_t结构，讲解请求头、请求体、响应体等数据成员和它们的操作方法，实现Nginx里最常见的内容处理（handler）模块和过滤（filter）模块。

- 第8章：Nginx HTTP请求转发

本章解析Nginx的请求转发机制，它是Nginx反向代理的基础，实现转发请求的upstream模块和负载均衡的load-balance模块，访问外部的服务器资源。

- 第9章：Nginx HTTP子请求

本章讲解Nginx独具匠心的子请求机制，可以让Nginx像调用函数那样调用location里的功能，发起多个子请求实现复杂的业务逻辑，让Nginx变成一台强大的应用服务器。

- 第10章：Nginx变量

本章介绍Nginx里灵活的变量机制，是配置指令之外另一种与模块通信的方式，增强了静态配置文件与动态模块之间的互操作性。

- 第11章：Nginx辅助设施

本章介绍Nginx提供的一些实用工具，包括MD5/SHA-1摘要、CRC/Base64/URI编解码、正则表达式、共享内存和定时器，它们是核心的异步IO和HTTP解析处理之外实现一个完善的Web服务器所必需的辅助功能。

- 第12章：Nginx与设计模式

本章简要总结了在Nginx里应用的一些设计模式，帮助读者从设计模式的角度来理解Nginx架构。

- 第13章：结束语

本章给出了在阅读完本书后进一步学习研究Nginx的方向。

- 附录

书末的附录是对本书正文的补充，列出了一些参考书籍，还有就是一些不宜放在正文里的比较琐碎的内容，比如gdb调试方法、ngx_sprintf()格式列表、void*成员总结等。值得一提的是特别介绍了ngx_lua模块，可以使用Lua语言快捷实现业务逻辑。

0.6　如何阅读本书

编程初学者或者初次接触Nginx的读者应当首先阅读第1章，了解什么是Nginx，并在本书的指导下安装配置Nginx，搭建自己的开发环境，然后学习后续章节。

对Nginx比较熟悉的读者可以跳过第1章，从第2章开始顺序阅读，以Nginx源码片段为出发点，学习如何使用C++和Boost程序库来开发Nginx模块。其中第2章至第4章是C++开发的基础，第5章才能开始正式编写Nginx模块。

如果读者已经是一个比较熟练的Nginx开发者，那么可以直接阅读第5、6章及之后的章节，钻研Nginx的内部架构和实现原理，深入理解Nginx的运行机制。不过第2章至第4章也并非可以完全忽略，其中的C++泛型、元编程等崭新的编程范式也许会让你眼前一亮，能够从一个全新的角度来审视Nginx。

0.7　本书的源码

为方便读者利用本书学习研究Nginx模块开发，作者在GitHub网站上发布了本书内所有示例程序和Shell脚本的源代码，地址是：

https://github.com/chronolaw/ngx_cpp_dev.git

本书的附录E对此有简要介绍。

[1] 似乎很多人（包括Nginx官网）都会孜孜不倦地提醒Nginx的正确发音，所以作者在这里也不能“免俗”：Nginx的正确发音应该是“engine eks”。不过作者（还有周围的许多同事）却更愿意像UNIX/Linux那样称它为“engine ks”——虽然这是一个“错误”的发音，但却简洁明快。

第1章　Nginx入门

Nginx是一个高性能、高稳定性和高扩展性的轻量级HTTP、反向代理和邮件代理服务器。它运行效率高，系统消耗低，使用操作系统提供的异步I/O调用可以无阻塞地处理上万的并发请求，是当今众多Web服务器中的佼佼者，被Dropbox、GitHub、Hulu、WordPress等许多知名网站所采用。

本章简要介绍Nginx的历史和特点，讲解如何在Linux上安装和配置Nginx，帮助读者尽快熟悉Nginx。

1.1　关于Nginx

自20世纪90年代以来，Internet和World Wide Web逐渐在全世界普及。早期并没有Web服务器的概念，Internet上的资源只是一些简单的静态文件。慢慢地，Web服务器出现了，它在用户和文件之间加入了一个中间层，响应用户的HTTP请求，从本地或者其他地方获取对应的资源，再返回给用户。

1995年，著名的Apache诞生了。最开始它只是一个NCSA HTTPd服务器的修改版，但许多志愿者不停地为它添加补丁新增功能，使它迅速成为了那个年代最流行的Web服务器，甚至直到二十年后的今天也是如此。

由于时代的限制，Apache被设计为“重量级”的服务器，使用fork进程的方式响应HTTP请求。虽然近年来Apache增加了多线程、多核心等新特性，但基本架构无法改变，在动辄成千上万并发的情况下显得有些力不从心，性能严重下降。

本世纪初，随着C10K问题的提出[1]，计算机界开始认真地研究Web服务器的开发模型。Linux、FreeBSD等操作系统引入了epoll、kqueue等高效异步I/O接口，奠定了解决海量并发的C10K甚至C100K问题的基础，最终导致Nginx这个Web服务器新秀的出现。

1.1.1　历史

2002年，在仔细研究了已有的Web服务器之后，当时任职于俄罗斯门户网站Rambler的工程师Igor Sysoev开始编写一个新的Web服务器，目标是解决高并发的C10K问题，并设计了完全不同于以往服务器的全新架构——这就是Nginx。

2004年，Nginx正式开源，版本号为0.1.0，由于其优异的性能、绝佳的稳定性和良好的扩展性等鲜明特点迅速吸引了大量关注。在开源大旗的鼓舞之下，无数开发者加入贡献行列，为它修复错误、编写功能模块，Nginx从此踏上了迅猛发展的道路，从当初默默无闻的小卒成长为了如今Web服务器界的巨人。

2011年，Nginx之父Igor Sysoev在旧金山正式成立公司，为Nginx提供商业服务，并发布了商业版本的Nginx Plus。

1.1.2　特点

作为Web服务器的后起之秀，Nginx能够战胜Apache、Lighttpd、Jetty、Tomcat等众多对手，获得顶级互联网公司的青睐，必然有它的独到之处。以开发者的视角来看，Nginx的特点是：

- 高性能

卓越的性能是Nginx最突出的特点。Nginx完全使用C语言编写，采用事件驱动模型，并且有针对性地对操作系统进行了特别优化，可以无阻塞地处理海量并发连接，经过仔细调整配置参数还能够进一步释放潜力，帮助网站应对日益增长的巨大访问压力。

- 高稳定性

高度的稳定性是Nginx的又一大特点。内存池避免了C程序常见的资源泄漏问题，模块化的架构使得各个功能模块完全解耦，消除了相互间可能造成的不良影响，而独特的one master/multi workers进程池设计则实现了自我监控和管理，保证即使worker进程发生严重错误也可以快速恢复。在实际应用中，Nginx服务器一经启动，就可以稳定地运行数天甚至数月之久。

- 低资源消耗

Nginx的代码质量极高，不使用传统的进程或线程服务器模型，没有进程或线程切换时的系统成本，而且它还使用了很多节约系统资源的编程技巧，例如使用字符串引用而不是拷贝、使用Linux的sendfile调用来零拷贝发送数据，可以把宝贵的CPU和内存资源更多地用于对外提供服务，提高并发支持能力。

- 高扩展性

Nginx的模块化架构是一个非凡的设计。Nginx本身就是由各种位于不同层次的功能模块组合而成的，它也允许/鼓励任何人依据模块开发规范，开发任意功能的模块——同时充分利用Nginx框架提供的各种高效机制，然后再完美地融合到Nginx之中。

基于高扩展性，Nginx以模块的形式实现了丰富的功能，例如access、gzip、proxy、ssl、flv等，而广大开发者也编写了大量的第三方模块，实现了更多更有用的功能[2]。这些模块都遵循一致的开发原则，可以在配置文件里灵活配置，让Nginx提供更好的网络服务。

以上的三高一低四个方面是从开发者的角度总结的Nginx的主要特点。当然Nginx的优势远不止这些，它还能够运行在多种操作系统上，安装和配置都很容易，支持定制日志、平滑升级、策略限速、热部署等许多重要的运维功能。

高性能是品质，高稳定性是保障，低资源消耗是基石，而高扩展性则是Nginx生命力的源泉，使Nginx拥有了近乎“无限”的能力——如果现有的Nginx模块不能满足我们的需求，最佳的解决方案无疑是开发自己的模块，集成到Nginx里，在享受Nginx高性能、高稳定性的同时实现自己的业务逻辑。这正是作者撰写本书的目的。

1.1.3　进程模型

Nginx采用了独特的one master/multi workers进程池机制，它是Nginx能够稳定运行、灵活配置的保证。

通常情况下，Nginx会启动一个master进程和多个worker进程对外提供服务[3]。master进程又称监控进程，它并不处理具体的HTTP请求，只接收UNIX信号，管理和监控worker进程，所以工作比较“清闲”。worker进程平等地竞争客户端的连接，执行Nginx主要的业务逻辑，使用epoll、kqueue等机制高效率地处理HTTP请求。

master进程和worker进程使用操作系统提供的进程间通信机制，如信号、UNIX域套接字、共享内存等互相通信，彼此独立又保持一定的联系，形成一个有机的整体。当某个worker进程发生错误意外中止时，master进程会快速重新fork出新的worker进程，保持进程池的稳定。

Nginx的进程模型如图1-1所示：

图1-1　Nginx的进程模型

在多核心的服务器上，Nginx可以配置为每个进程运行在单独的核心，最大限度地减少CPU进程切换的成本，提高系统运行效率。

Nginx也可以配置为不使用master进程，只有一个worker进程提供服务，这种方式不适用于真正的生产环境，但对于开发测试却很有用。

1.1.4　版本

本书不讨论商业版本的Nginx Plus[4]，只讨论开源版本的Nginx，它的官网是：http://www.nginx.org/。

Nginx官网提供三种类型的版本供用户选择：

- Mainline：主线开发版本，汇集了最新的功能和错误修复，但稳定性可能略差。

- Stable：当前的稳定版本，经过了完全测试，建议在正式生产环境中使用。

- Legacy：历史稳定版本，有特殊需求的话可以采用。

本书采用的Nginx是2015年4月发布的Stable 1.8.0版，不过由于Nginx的核心架构和代码一直都很稳定，所以本书中的讨论也适用于早期的各种版本。

1.2　安装Nginx

本节介绍在Linux操作系统上使用源码包的方式安装Nginx，但并不是一个完全的安装部署手册，仅供读者参考。[5]

1.2.1　准备工作

只要系统里有GCC（或其他编译器），Nginx就可以编译安装，但几个基本且重要的功能依赖于第三方库：

- zlib：实现gzip压缩解压缩功能。

- pcre：实现配置文件里的正则表达式解析功能。

- openssl：实现SSL功能。

本书不讨论SSL协议，不需要openssl库，在Ubuntu里只安装前两者即可：

sudo apt-get install libz-dev libpcre3-dev        #安装zlib和pcre库

使用其他操作系统的读者请参考网络相关资料安装zlib库和pcre库，例如对于CentOS，安装的命令可以是：

yum install zlib zlib-devel               #安装zlib库
yum install pcre pcre-devel               #安装pcre库

1.2.2　快速安装

Nginx主要以源码的方式发布，可以从官网上直接下载源码形式的安装包：

wget http://nginx.org/download/nginx-1.8.0.tar.gz   #使用wget下载
tar xvfz nginx-1.8.0.tar.gz               #解压缩

本书假定把Nginx解压缩到~/nginx目录。

在~/nginx目录下执行下面的命令即可快速安装Nginx：

./configure                          #编译前的配置工作
make                                 #编译
sudo make install                    #安装

这是最简单的Nginx安装方式，没有任何的定制参数。Nginx将会安装到默认的/usr/local/nginx/目录，可执行文件是/usr/local/nginx/sbin/nginx，默认配置文件是/usr/local/nginx/conf/nginx.conf。

1.2.3　运行命令

启动和停止Nginx需要以root身份，或者使用sudo。

下面介绍一些常用但不是全部的Nginx命令，更详细的命令说明可以使用-h/-?参数查看，或者查阅网络资料。

不带参数简单地执行程序就可以启动Nginx服务，这将使用默认的配置文件：

/usr/local/nginx/sbin/nginx         #使用默认的配置文件启动Nginx

也可以使用-c参数指定配置文件来启动Nginx，这种方式允许我们启动多个Nginx实例：

/usr/local/nginx/sbin/nginx -c x.conf    #指定配置文件x.conf启动Nginx

-p path是-c的增强版，用来设置工作目录，可以指定完整的Nginx环境：

/usr/local/nginx/sbin/nginx -p /var/nginx  #设置Nginx的工作目录为/var/nginx

-s signal参数可以快速地停止或者重启Nginx，signal值可以是stop、quit、reload或reopen：

/usr/local/nginx/sbin/nginx -s stop      #强制立即停止Nginx服务
/usr/local/nginx/sbin/nginx -s quit      #处理完当前所有连接后再停止Nginx
/usr/local/nginx/sbin/nginx -s reload    #重启Nginx，重新加载配置文件
/usr/local/nginx/sbin/nginx -s reopen    #重新打开日志文件

需要注意的是，如果使用-c/-p参数启动了Nginx，那么在使用-s时也必须使用-c/-p参数，告诉Nginx使用的是哪个配置文件，否则会运行失败。例如：

#在使用配置文件x.conf启动Nginx后再重启Nginx，必须使用-c指定配置文件
/usr/local/nginx/sbin/nginx -s reload -c x.conf
#在使用-p启动Nginx后停止Nginx，仍然要使用-p参数
/usr/local/nginx/sbin/nginx -s stop -p /var/nginx

-t参数可以测试配置文件是否正确：

/usr/local/nginx/sbin/nginx -t         #检查默认的配置文件
/usr/local/nginx/sbin/nginx -t -c x.conf    #检查指定的配置文件x.conf

-v或-V参数可以显示Nginx的版本信息，它不需要root权限，两者的区别是-V可以显示更多的信息，包括GCC版本、操作系统版本、configure参数等定制信息：

/usr/local/nginx/sbin/nginx -v          #显示简要版本信息
nginx version: nginx/1.8.0            #Nginx版本是1.8.0

/usr/local/nginx/sbin/nginx -V          #显示完全的版本信息
nginx version: nginx/1.8.0            #Nginx版本是1.8.0
built by gcc 4.8.2 (Ubuntu 4.8.2-19ubuntu1)   #使用Ubuntu GCC 4.8.2编译
configure arguments:               #没有特别的定制编译项

-V参数很多时候特别有用，由于它给出了编译Nginx时的全部相关信息，所以我们可以使用其他版本的源码（例如升级或者修复Bug）加上这些参数重新编译出一个功能完全相同的新程序。

1.2.4　验证安装

如果已经成功启动了Nginx服务，那么就可以使用wget或者curl这样的工具来验证Nginx是否正常工作。

Nginx的默认配置文件开启了localhost:80服务，在/usr/local/nginx/html下存放有一些示例静态html文件，curl测试命令是：

curl -v -o /dev/null http://localhost/index.html

如果Nginx正在运行，那么curl的部分输出可能如下：

* Connected to localhost (127.0.0.1) port 80 (#0)  #连接到 localhost:80

> GET /index.html HTTP/1.1        #获取文件 index.html
> User-Agent: curl/7.35.0       #curl 的版本号

< HTTP/1.1 200 OK         #响应码 200，正常
< Server: nginx/1.8.0        #服务器是 Nginx 1.8.0
< Content-Type: text/html       #响应内容是普通文本
< Content-Length: 612        #HTTP 正文长度是 612字节

使用Linux的ps命令配合grep可以看到所有Nginx进程，也可以验证Nginx是否正常运行：

ps aux|grep nginx         #ps查看进程
root   8261 ... nginx: master process  /usr/local/nginx/sbin/nginx
nobody    8262 ... nginx: worker process

从ps的输出我们可以看到当前共有两个Nginx进程，其中进程号为8261的是master进程，而8262号进程则是worker进程。

如果Nginx没有正确运行，我们可以查看它的错误日志以排除故障，默认的位置是/usr/local/nginx/logs/error.log。

1.2.5　定制安装

make前执行的configure是Nginx的重要组成部件，它检查各种系统参数、命令行参数和依赖库，根据这些参数生成定制的Makefile和一些C源码文件，没有它就无法正确编译安装Nginx。

虽然configure只是一个标准的Shell脚本，但其内部逻辑十分复杂，为了支持各种操作系统、编译器和CPU做了大量工作，复杂程度甚至不逊于Nginx自身的C程序，故在这里暂不解析它的实现细节。

由于configure的选项很多，本书不能也没有必要完全罗列，仅列出一些个人认为比较重要的选项，其他可参考help或者网络资源。

--prefix=PATH :

配置Nginx安装部署的根目录，也就是工作目录。默认值是/usr/local/nginx/，可以把它改为其他路径，这样就可以在一个系统里安装多个不同用途的Nginx[6]，类似的选项还有--sbin-path、--conf-path等。例如：

./configure --prefix=/var/nginx     #把 Nginx 安装到/var/nginx
--with-xxx_module，--without-xxx_module：

这是一系列近50个模块配置选项，用来决定在Nginx里启用或者禁用哪些自带的功能模块，我们可以根据自身的实际情况来定制Nginx的功能，例如：

./configure       \     #定制Nginx 的模块
--with-http_flv_module    \     #支持flv 视频文件
--with-http_mp4_module    \     #支持mp4 视频文件
--without-http_gzip_module      #不使用 gzip 压缩解压缩功能
--build=NAME：

这个选项是Nginx 1.7版之后增加的新功能，可以在Nginx的版本信息里加入自定义的字符串信息，比如公司名称、构建日期、源码版本号等，让nginx-v/-V更加可读。例如，下面的配置命令为Nginx增加了git版本号和构建日期：

./configure        \     #定制构建信息
--build="version: `git describe` , chrono build at `date +%Y%m%d`"

这条命令在字符串里使用Shell直接执行了git和date命令，并把结果输出到了--build参数里，编译后用Nginx-v查看版本信息是：

nginx version: nginx/1.8.0 (version: xxx, chrono build at 2015xxxx)
--with-debug：

这是一个对于我们研究Nginx非常有用的编译选项，启用了Nginx的调试模式，可以让Nginx在运行日志里打印出更多的调试信息。

--with-ld-opt=OPTIONS：

这个选项用来指定编译链接时的额外选项，可以链接其他第三方库，例如使用jemalloc来替代GCC的内存分配函数：

./configure --with-ld-opt="-ljemalloc"    #链接jemalloc 库
--add-module=PATH：

这是Nginx高扩展性的起点，是configure最重要的选项，指定第三方模块的源码路径。这样Nginx就可以把第三方模块像自带模块一样编译进可执行文件，从而扩展Nginx的能力。我们会在之后的5.1节看到它的详细用法。

需要注意的是，--add-module与其他选项不同，在configure命令行里可以出现任意次，为Nginx添加任意数量的第三方模块。

1.3　配置Nginx

configure是对Nginx的静态配置，确定Nginx的基本运行环境和功能模块，使用简单的命令行参数就可以完成。但Nginx的配置绝不只有configure这么简单，实际上，配置文件才是Nginx的核心，它与Nginx的模块化架构是一个互相依存的整体，决定了Nginx的进程数量、运行日志、虚拟主机、反向代理和邮件代理、各种请求处理逻辑、优化调整等方方面面，众多的模块都要依赖配置文件里的指令才能正常工作。Nginx在启动时将会读取配置文件，根据配置指令调用不同的模块处理，设置它们的运行参数。

由于本书作者并非运维方面的专家，故略去与运维调优关系密切的events、user、worker_cpu_affinity等配置指令，删繁就简，只介绍作者认为与开发有关、比较常用的指令，有进一步需要的读者可参考官网Wiki或者其他相关资料。[7]

1.3.1　配置文件格式

Nginx的配置文件是一个普通的纯文本文件，使用了Nginx自定义的一套配置语法，更接近于脚本语言，混合了Shell、Perl和C的部分特性，要点叙述如下：

- 与Shell/Perl相同，使用#开始一个注释行；

- 配置指令以分号结束，可以接受多个参数，用空白字符分隔；

- 可以使用单引号或者双引号来定义字符串，允许用“\”转义字符；

- 配置指令和参数也可以用引号来指定，特别是当它含有空格的时候；

- 配置块是特殊的配置指令，它有一个{...}参数且无须分号结束，{...}里面可以包含多个配置指令，相当于C语言里的复合语句；

- 有的配置指令只能出现在特定的配置块里（即语境Context）；

- 配置块里可以再包含配置块，嵌套层次没有限制，但需符合配置块的语义；

- 可以使用include指令包含其他配置文件，支持*通配符，类似C语言；

- 使用$var可以引用预定义的一些变量，增加配置的灵活性；

- 不能识别或错误的配置指令会导致Nginx解析失败，无法启动。

下面列出Nginx自带的配置文件片段，部分较重要的配置指令用黑体表示：

worker_processes  1;        #设置worker进程的数量为 1

events {            #events配置块
worker_connections  1024;       #worker的最大连接数
}             #events配置块结束

http {           #配置HTTP 服务，是 Nginx 的重点
include       mime.types;        #包含mime.types 文件
default_type  application/octet-stream;    #指定默认的 MIME 类型
server {          #server配置块，虚拟主机
listen       80;         #监听80端口
server_name  localhost;       #主机名称

location / {          #location 配置块
root   html;         #设置http 请求的根目录
index  index.html index.htm;    #设置默认的 index文件
}            #location 配置块结束

error_page  500 502 503 504  /50x.html;   #设置错误返回页

location = /50x.html {       #错误返回页 location 配置块
root   html;         #设置http 请求的根目录
}            #location 配置块结束
}             #server配置块结束
}             #http 配置块结束

这个配置文件片段在全局域里出现了一个配置指令worker_processes，然后是两个大配置块：events和http，分别配置了event模块和http模块的行为。

events配置块很简单，里面只有一个worker_connections指令，确定每个worker进程可以处理的最大连接数。

http配置块比较复杂，它先用include包含了另一个配置文件mime.types，设置了默认的MIME类型，然后使用server块定义了一个端口为80的虚拟主机。server块里又有两个location块，定义了访问虚拟主机上不同路径时的具体行为，在这里只指定了文档根目录和设置index文件。

接下来我们就来了解一些常用的Nginx配置指令。

1.3.2　进程配置

以下三个进程配置指令不属于任何配置块，只能在全局域（main）里配置。

worker_processes number | auto;

设置Nginx能够启动的worker进程的数量，它直接影响Nginx的性能。通常当worker数与服务器的CPU核心数相等时可以获得最佳的性能，这时每一个worker都会工作在一个独立的CPU核心上，完全消除CPU调度的成本。

worker_processes的默认值是1。如果不清楚服务器的CPU核心数量，那么可以设置为auto参数，Nginx会尝试探测数量并设置。

master_process on | off;

决定是否启用Nginx的进程池机制，默认值是on。如果设置为off，那么Nginx不会建立master进程，只会用一个worker进程处理请求，worker_processes指令也会失效，并发处理能力大大下降。在生产环境中不建议设置为off，但对于研究来说它很有用，可以简化我们的调试工作。

daemon on | off;

决定是否以守护进程的方式运行Nginx，默认值是on。大多数情况下Nginx应该是一个守护进程，运行在后台，不与终端有任何的交互，也减少了无谓的资源消耗。但禁用守护进程同样有利于我们的研究调试，可以直接用cout或printf输出调试信息，而不必去查看日志。

本书中配置master_process为off，偶尔会配置daemon off。

1.3.3　运行日志配置

日志是Web服务器非常重要的数字资产，它记录了服务器运行期间的各种信息，可以用来排查故障或者数据分析。

在Nginx里运行日志分为两种：记录HTTP访问请求的access_log和记录服务器错误信息的error_log。本书只使用error_log，它可以在任意域里配置。

error_log file|stderr level;

指定Nginx的运行错误日志，默认是安装目录下的logs/error.log。我们也可以自由设置其他路径，或者使用标准错误输出stderr。第二个参数level是日志的允许输出级别，取值是debug|info|notice|warn|error|crit|alert|emerg，只有高于这个级别的日志才会记录下来，默认值是error。

如果配置为最低的debug日志级别，那么在configure时必须使用--with-debug选项。

1.3.4　http配置

Nginx近90%的功能都是提供HTTP服务，所以HTTP的配置也是最复杂的。从1.3.1节就可以看到，http配置块几乎占据了整个配置文件，而实际运行环境里的配置还会更加庞大。

Nginx使用http块配置HTTP相关的所有功能，包括cache、fastcgi、gzip、server/location、proxy、upstream等，通常的形式是：

http {         #http 配置块开始，所有的 HTTP相关功能
upstream {        #upstream 配置块，配置上游服务器
...
}           #upstream 配置块结束

server {        #server配置块，第一个虚拟主机
listen 80;        #监听80端口

location / {       #location 配置块
...
}          #location 配置块结束
}           #server配置块结束

server {        #server配置块，第二个虚拟主机
...
}           #server配置块结束
}           #http 配置块结束

由于http块内容太多，如果都在一个文件里配置会造成配置文件过度庞大，难以维护。在实践中我们通常把server、location等配置分离到单独的文件，再利用include指令包含进来，这样就可以很好地降低配置文件的复杂度。

使用include后http块就简化成了：

http {         #http 配置块开始，所有的 HTTP相关功能

include common.conf     #基本的 HTTP 配置文件，配置通用参数

include upstream.conf      #包含upstream 配置文件，配置上游服务器

include vhost/*.conf      #包含vhost 下所有虚拟主机配置文件
}           #http 配置块结束

1.3.5　server配置

在http块内使用server指令定义一个虚拟主机，它必须是一个配置块，在块内部再使其他用指令来确定虚拟主机的端口、域名等参数，然后Nginx就可以对外提供Web服务。

listen port;

listen指令设置虚拟主机监听的端口，默认是80。实际上listen指令还有很多参数，可以设置IP地址、UNIX Domain Socket、SSL、backlog、rcvbuf/sndbuf等，因与本书关系不大故从略。

server_name name ...;

server_name指令设置虚拟主机对外提供服务的主机名称，允许使用*通配符和“~”开头的正则表达式。例如“www.nginx.org”、“*.image.nginx.org”，默认值是空字符串""。当Nginx处理请求时将会检查HTTP头部的Host域，选择与server_name匹配的server块提供服务，从而达到在一个Nginx里实现多个虚拟主机的目的。

对于我们自己的开发研究来说，可以直接使用localhost或者简单的通配符*.*，用类似“curl-v http://localhost/...”这样的命令就能够访问Nginx服务。

keepalive_timeout timeout;[8]

设置keepalive的超时时间，默认是75s。它通常有利于客户端复用HTTP长连接，提高服务器的性能。如果希望服务器发送完数据后能够主动断连，就可以把它设置为0。

1.3.6　location配置

location相当于虚拟主机上的虚拟目录，Nginx在成功匹配虚拟主机进入server块后，会继续查找匹配URI的location块，它是Nginx处理的终点站，决定了请求应该如何处理。

location是一个配置块，但语法稍多一些，除{...}外还有其他的参数：

location [ = | ~ | ~* | ^~ | @ ] uri { ... }

location使用配置文件里的uri参数匹配HTTP请求行里的URI，默认是前缀匹配，也支持正则表达式。例如“location/image/{...}”会匹配“/image/001.jpg”。

location使用几个前缀来做进一步的匹配限定：

- =：URI必须完全匹配；

- ~：大小写敏感匹配；

- ~*：大小写不敏感匹配；

- ^~：匹配前半部分即可；

- @：用于内部子请求，外部无法访问。

在server块里可以配置任意数量的location块，location也可以嵌套。Nginx对location的顺序没有特殊要求，并不是按照配置文件里的顺序逐个查找匹配，而是对所有可能的匹配进行排序，查找最佳匹配的location。

不同的location里可以有不同的处理方式，灵活设置location能够让Nginx配置清晰明了，易于维护。比如，我们可以在一个location里存放静态html文件，在另一个location里存放图片文件，还有一个location则调用fastcgi处理PHP请求，这些location互不干扰，修改其中的一个不会影响其他的正常运行。例如：

location   /doc/    {...}    #匹配/doc/*.*
location ~   \.(php)$   {...}    #大小写敏感处理php请求
location ~*   \.(png)$   {...}    #忽略大小写，匹配所有的 png文件
location ^~   /image/  {...}    #匹配/image/*.*，优先级比上一个低
location = /50x.html   {...}    #只处理/50x.html 这一个文件
location   /     {...}    #匹配任意的 URI

需要注意最后一个“location/”，根据Nginx的前缀匹配规则，它能够匹配任意的URI，所以可以把它作为一个“黑洞”，处理所有其他location不能处理的请求。

如果location配置很多，我们同样可以用include的方式来简化配置。

1.3.7　文件访问配置

在经过了虚拟主机server和虚拟目录location后，我们需要确定URI的处理方式。如果把Nginx用作静态Web服务器，那么文件访问配置就很简单，只需指定存放路径和文件名即可，这里仅介绍三个最基本的指令，它们可以出现在http块里的任何位置。

root path;

设置请求文档的根目录，将以path作为起始路径查找文件。如果有：

location /image/ {       #匹配/image/*.*
root /var/data/;      #文档根目录实际上是/var/data/
}

那么请求/image/001.jpg将会返回文件/var/data/image/001.jpg。

对于1.3.1节来说，两个location文档根目录都是安装目录下的html目录。

alias path;

alias指令同样设置文档的访问目录，但与root略有不同，它会把location的路径替换为path，即location是path的别名。

把刚才的location配置改为alias：

location /image/ {       #匹配/image/*.*
alias /var/data/;      #image 路径会映射到/var/data/
}

请求/image/001.jpg将会返回文件/var/data/001.jpg。

index file ...;

设置index文件，即没有指定明确的文件名时的默认文件。

1.3.8　upstream配置

upstream块不属于虚拟主机，只能在http块里配置，它定义了在反向代理时Nginx需要访问的上游服务器集群和负载均衡策略。

upstream块的基本形式是：

upstream back_end {        #upstream 需要有一个名字
ip_hash;          #负载均衡策略
server 127.0.0.1:80;      #一台上游服务器
server ... weight=3;      #可以指定多台上游服务器
server ... backup;        #备份用的上游服务器
}

upstream块的配置比较简单，server指令指定上游的服务器域名或IP地址，还可以用weight/max_fails/down/backup等附加参数来进一步描述服务器的状态。ip_hash指令确定了这些服务器的负载均衡策略，如果不给出明确的策略，Nginx就使用简单的加权轮询（round robin）。

upstream块通常配合proxy_pass、fastcgi_pass等反向代理指令使用，把客户端的请求转发到后端的服务器集群处理，例如：

location /passto {        #一个转发的 location
proxy_set_header Host $host;    #转发原始请求的 host 头部
proxy_pass http://back_end;     #转发到 upstream 块定义的服务器集群
}

本书并不涉及Nginx的反向代理服务搭建，故不对它做详细介绍，但之后的章节会开发与upstream相关的反向代理和负载均衡模块。

1.3.9　变量

如果Nginx的配置文件仅有以上的功能，那么它还称不上是强大。Nginx的真正神奇之处是可以在配置文件里（以及Nginx运行的任何时刻）使用内置的HTTP请求相关变量，并且能够在运行时根据变量值动态变化配置，使编写配置文件更像是编写Shell或者Perl程序。

在配置文件里使用Nginx变量需要以“$”开头，例如$request_method、$args、$uri、$content_length，这与Shell和Perl是一样的。变量的值都是字符串，可以用在access_log里记录访问日志，用在fastcgi里设置参数，或者做一些简单的条件判断，不过变量最大的作用还是供Nginx模块获取各种运行时信息。

下面列出一些开发中常用的Nginx变量：

- $uri：当前请求的URI，但不含“？”后的参数；

- $is_args：当前请求是否带参数，如有则参数值为“？”，否则是空字符串；

- $args：当前请求的参数，即“？”后的字符串；

- $request_uri：当前请求的完整URI，包含参数，相当于$uri$is_args$args；

- $arg_xxx：当前请求里的某个参数，arg_后是参数的名字；

- $http_xxx：当前请求里的xxx头部对应的值；

- $sent_http_xxx：返回给客户端的响应头部对应的值。

如果执行下面的curl命令：

curl -v -o /dev/null 'http://localhost/index.html?a=1&b=2' -H 'hello: world'

那么在Nginx里这些变量的值就是：

$uri      = /index.html
$is_arg      = ?
$args     = a=1&b=2
$request_uri    = /index.html?a=1&b=2
$arg_a      = 1
$arg_b      = 2
$http_hello     = world
$sent_http_server   = nginx/1.8.0

在内置的变量以外，Nginx也允许我们自定义变量，并提供了两个指令。

set $variable value;

set指令相当于$variable=value，即令$variable的值为value，是一条基本的赋值语句，但需要注意变量名不要和已有的内置变量名冲突。

set的用法比较简单，结合内置变量可以组合出很多新的变量，例如：

set $max_size  10000;       #定义变量$max_size="10000"
set $new_uri  /v2$request_uri   #给$request_uri 增加了一个前缀
set $log_tag  "extra action"    #给日志用的一个特别标记
map string $variable { ... }

map指令只能出现在http块内，可以使用简单的条件逻辑把string值转换后赋值给$variable，在{...}里用default设置默认值，用正则表达式匹配做分支处理，有些类似C语言的switch语句。

我们使用一个简单的例子来理解map的用法：

map $is_args  $my_flag {      #取$is_args 的值，定义$my_flag
default  0;        #默认值是 0
"?"   1;        #如果是“？”，值为 1
}

这样，我们使用map指令就把$is_args变量由字符串转换成了数字。

变量属于Nginx里比较高级的部分，用法灵活丰富，可以让Nginx在不使用外部编程语言的情况下实现很多高级功能，限于本书的范围，更多的用法不再讨论。

1.4　总结

本章首先介绍了Nginx的历史和特点，然后讲解了如何在Linux上以源码的方式安装Nginx，最后简要地阐述了Nginx的配置文件格式和里面的各个组成要素。

如果把Nginx比作是一座辉煌的宫殿，那么本章就是从远处眺望这座宫殿，看到了它的大致轮廓和外貌，还没有走进去细致地观察。

通过学习本章，读者应该对Nginx有一个基本的了解，能够凭自己的力量搭建Nginx服务器，实际动手操作无疑能够加强学习的效果。

再提醒读者一下，本章仅仅是“Nginx入门”，并没有也不可能完整介绍Nginx的编译、安装和配置选项，很多重要的功能例如缓存、访问控制、重定向、反向代理等都没有涉及，要想更好地管理维护Nginx必须要参考Nginx官网或者其他资料。

[1] 参见http://www.kegel.com/c10k.html。

[2] 详细的功能模块资料可以参考看官网Wiki的列表，这里不再一一列举。

[3] 如果开启了缓存功能，可能还会有cache进程，本书暂不讨论。

[4] 实际上商业版本的Nginx Plus是基于开源版Nginx的，同时添加了很多对于商业网站非常有用的实用功能，例如负载均衡、健康度检查、状态监控等。

[5] 有的Linux发行版也提供编译好的Nginx包，可以使用apt-get或者yum安装，但这种方式不够灵活，不能定制模块，本书不推荐。

[6] --prefix参数的作用类似运行时的参数-p，但它是在编译时确定的，而-p可以在运行时改变。

[7] 除Nginx配置文件外，要想更好地发挥Nginx的实力可能还需要调整Linux的内核参数，例如net.core.somaxconn等。

[8] keepalive_timeout指令也可以出现在http和location作用域里。

第2章　Nginx开发准备

本章是正式编码开发Nginx前的准备工作，将简要介绍Nginx的源码目录结构、代码风格和特点，以及使用C++开发模块的初步技术，有了这些基本知识才能进行后续的Nginx模块开发。

2.1　开发环境

第0章的0.4节介绍了本书的运行环境，但使用C++开发Nginx模块还需要明确两个要素：C++标准和Boost程序库。

2.1.1　C++标准

C++是C的不完全超集，自1979年诞生以来一直是系统级开发的主要语言。目前C++有三个国际标准，分别是C++98、C++11和C++14，其中C++98过于陈旧，而C++14又太新，故本书采用目前支持比较完善的C++11。

C++11包含很多友好的新特性，本书使用的有：

- final：显式禁止类被继承；

- default：显式默认实现构造函数和析构函数；

- nullptr：强类型的空指针，用来代替C语言里的宏NULL；

- auto/decltype：自动类型推导，可以写出类似Lua/Python的简洁代码；

- lambda表达式：就地编写匿名函数（闭包），非常灵活易用；

- 可变模板参数：可以在模板参数列表里书写任意数量的类型名。

为了使用C++11标准，读者需要采用较新版本的GCC，至少要高于4.6。可能有的操作系统自带的GCC未能达到这个要求，请参考网络上相关文章升级GCC，作者也编写了一个简单的CentOS升级Shell脚本并发布在GitHub上，见0.7节。

有了C++11标准还不够，我们还需要另外一个强大的工具——Boost程序库。

2.1.2　Boost程序库

Boost程序库是一个功能强大、构造精巧、跨平台、开源并且完全免费的C++程序库。它由C++标准委员会部分成员所设立的Boost社区开发并维护，使用了许多现代C++编程技术，涵盖字符串处理、容器与数据结构、泛型编程等许多领域，是对C++标准极好的补充和完善，有着“C++准标准库”的美誉。

本书使用的Boost程序库是2015年4月发布的1.58版，当然读者也可以采用更新的版本。

Linux等操作系统通常不会自带Boost程序库（或者版本较旧），需要我们在官网http://www.boost.org下载源码包自行编译安装。例如下载boost_1_58_0.tar.gz，解压缩后进入目录再执行命令：

./bootstrap.sh                   #配置编译选项
sudo ./b2 link=static install    #编译静态链接库并安装

这样即可把Boost程序库安装到/usr/local/目录下。[1]

2.2　Nginx的目录结构

Nginx的目录结构简单清晰，功能和层次都很明确，非常利于研究和学习。

Nginx的主目录结构如下：

对于我们模块开发者来说，最需要关心的是auto目录和src目录。

auto目录存放了很多Shell脚本，被configure调用，它的结构是：

src目录是我们需要重点关注的目录，所有的Nginx实现源码都存放在这里，并且依据模块类型被进一步分门别类：[2]

在src里比较重要的目录是core、http和os/unix，它们分别存放了Nginx的核心代码、HTTP功能代码和操作系统相关的代码。

此外，objs目录里还有一些自动生成的源码文件，定义了Nginx模块和各种常量宏，虽然简单但也是Nginx的重要组成部分。

在熟悉了Nginx的源码目录结构后，我们就可以有针对性地去阅读源码研究它的工作原理，比如想修改编译配置选项就看auto目录，想学习Nginx整体架构就看src/core目录，想了解具体的模块功能就看src/http目录。

2.3　Nginx源码的特点

本节简要分析Nginx的代码风格和内在的编程思想，有助于读者从总体上把握Nginx源码，在阅读程序时留意这些特点可以更快地理解代码。

2.3.1　代码风格

Nginx完全以纯C语言实现，基本上遵循了传统的K&R风格，与Linux等很相似。

在显而易见的单词对齐、空白字符的使用之外，Nginx源码的特点还有：

- 变量名、函数名全小写，宏全大写（但也有例外），使用下画线连接；

- 类型名、函数名使用前缀“ngx_”，宏使用前缀“NGX_”；

- 结构体（struct）使用后缀“_s”，同名的“_t”后缀是它的等价形式；[3]

- 使用C的“/**/”注释，不使用新的“//”风格注释。

2.3.2　代码优化

Nginx使用了很多优化技巧来达到节约系统资源、提高运行效率的目的，例如：

- 自定义一些整数类型实现跨平台编译运行，如ngx_uint_t；

- 标志位变量使用了不太常用的“位域”特性（bit field），减少内存占用；

- 使用“哨兵”变量（即空对象）表示数组的结束，简化了循环的范围检查，例如ngx_null_command；

- 使用宏封装了一些简单的操作，消除函数调用的成本。注意：因为这些宏相当于函数，所以采用了小写的形式，本书之后称它们为“函数宏”。

2.3.3　面向对象思想

面向对象是现代软件设计的共识，Nginx当然也不例外，模块化架构就是最明显的证据。但受限于C的语法，Nginx虽然使用了面向对象的思想但表现方式上却显得比较“委婉”，采用了许多“不得已而为之”的变通之举：

- 没有继承概念，只能使用void*指针成员，然后每个“子类”再具体化自己的数据结构，相当于自行实现了C++的对象内存模型；

- 没有RTTI（运行时类型信息），只能用一个tag成员来标记类型；

- 没有成员函数，只能用原始的函数指针作为结构的成员；

- 没有模板和泛型，解决方案同样是void*指针，因为它可以转型为任意类型的数据，但这也造成了理解和使用上的困难；

- 没有C++里的引用概念，因此要修改值时只能使用函数形式的宏；

- 不支持函数重载，所以有很多实现相同功能、名称近似的函数族；

- 大量使用宏（小写形式）来实现类似成员函数调用的“API”，存在容易误用的隐患。

这些语法上的“局限”使有的Nginx的源码片段比较晦涩难懂，增加了使用C语言开发Nginx模块的难度，有必要使用更好的C++语言来简化。

2.4　在Nginx里使用C++

C++是C的“精神继承者”，它高度（但不是百分之百）兼容C，同时又增加了面向对象、泛型等很多现代编程范式，使用C++可以更容易、更方便地实现良好的软件结构和代码，提高开发效率。

Nginx受C语言限制所产生的所有“代码缺点”都可以在C++中找到对应的解决方案。

2.4.1　实现原则

从设计模式的视角，我们可以把Nginx看作一个底层系统，使用包装外观模式（Wrapper Facade）把Nginx的数据结构和接口函数重新封装整理，在不影响效率的前提下给出一个更易用的面向对象的接口。

本书编写C++代码的一些基本原则是：

- 使用类封装Nginx数据结构和相应的操作，增强内聚性；

- 使用inline和static成员函数消除函数调用的成本，用来替代宏；

- 尽量使用变量的引用形式而不是指针形式；

- 使用函数重载，简化相同功能的函数调用；

- 使用模板和泛型技术消灭void*的使用；

- 尽量在编译期把类型信息保存起来，避免运行时的类型转换；

- 使用异常简化错误处理流程；

- 常量宏保留，不做封装（因为改为enum并不会带来好处）。

还需要说明的是，出于代码简洁考虑，本书并没有使用名字空间特性，所有的C++类均位于全局名字空间。

2.4.2　代码风格

使用C语言开发Nginx模块应该尽量遵守Nginx的编码风格，但使用C++则不一定要如此，毕竟C++不是C，采用不同的编码风格可以更好地区分这两者，也利于代码的维护。

本书使用的代码风格是：

- 变量名和函数名延续Nginx风格，全小写，使用下画线连接；

- 类名使用camel case，即单词的首字母大写；

- 类名均使用前缀“Ngx”，例如NgxPool；

- 类内部的typedef类型名全小写，使用下画线连接；

- 对外的模块变量名和配置指令使用前缀“ndg_”（Nginx Development Guide）；

- 在一个.hpp源文件里实现该类的所有功能代码；[4]

- 使用C++风格的“//”注释。

2.4.3　编译脚本

Nginx本身只支持使用C语言开发模块，如果我们要使用C++语言，那么就必须修改源码目录里的编译脚本，也就是auto目录里的文件。[5]

分析

首先要明确，我们不能直接使用g++编译Nginx自身的C代码，这是因为C++的语法比C严格很多，而Nginx代码用到了很多C语言的“边角”特性，直接用g++编译的后果就是报出大量的错误最后编译失败。

正确的做法是使用gcc来编译Nginx源码，而使用g++来编译我们自己的C++代码，两者可以使用文件扩展名区分（*.c和*.cpp），最后使用g++链接所有的目标文件。如果使用其他第三方C++库（例如Boost），则可以在configure时用--with-ld-opt参数。

configure生成的Makefile决定了源码的编译链接方式，以此为出发点进行分析梳理就可以找到编译脚本修改的方法。在这里我们略去对configure的分析过程，直达结论：产生Makefile的Shell脚本是auto/make。

修改脚本代码

make脚本代码并不复杂，有400多行，只要了解一些Shell编程知识就可以很容易地为它增添C++编译链接功能，要点是：

- 链接器LINK改用g++；

- 增加C++编译器和编译选项，启用C++11标准；

- 生成编译命令时用扩展名分支处理C源码和C++源码。

修改时还需要注意Shell的版本问题，configure使用系统默认Shell（/bin/sh），而不同的系统默认Shell可能是不同的，为了保证最大的兼容性，最好不要使用某些Shell的特殊语法。

在make脚本里添加新的编译器和链接器如下：

#LINK = $LINK                   #原 25行注释掉原链接器

CXX = g++                       #新增C++编译器
CXXFLAGS = -std=c++0x -Wall     #新增C++编译选项，启用 C++11
LINK = g++                      #新增C++链接器

这里可以给CXXFLAGS增加更多的参数，例如-g、-O0等，读者可根据需要自行增减。

有了编译器和链接器后，我们接着修改340行附近的编译第三方模块部分，黑体字标明了修改的部分：

# the addons sources

if test -n "$NGX_ADDON_SRCS"; then

ngx_cc="\$(CC) $ngx_compile_opt \$(CFLAGS) $ngx_use_pch \$(ALL_INCS)"

#定义C++编译命令
ngx_cxx="\$(CXX) $ngx_compile_opt \$(CXXFLAGS) $ngx_use_pch \$(ALL_INCS)"

for ngx_src in $NGX_ADDON_SRCS
do
...
ngx_src=`echo $ngx_src | sed -e "s/\//$ngx_regex_dirsep/g"`

#获取源码文件的后缀，决定使用 gcc 还是g++
ext=`echo ${ngx_src}|cut -d . -f 2`
ngx_gcc=$ngx_cc
if [ $ext = "cpp" ]; then
ngx_gcc=$ngx_cxx
fi

cat << END                                            >> $NGX_MAKEFILE

$ngx_obj:   \$(ADDON_DEPS)$ngx_cont$ngx_src
#$ngx_cc$ngx_tab$ngx_objout$ngx_obj$ngx_tab$ngx_src$NGX_AUX
$ngx_gcc$ngx_tab$ngx_objout$ngx_obj$ngx_tab$ngx_src$NGX_AUX
END
done

这段脚本里使用了UNIX小工具cut来获取文件的扩展名，比较简陋[6]。这是因为Ubuntu系统的dash Shell没有很方便地处理字符串的功能，如果我们使用bash，那么就可以比较简单地得到扩展名：

ext=`echo ${ngx_src:(-4)}`     #bash 支持直接截取末尾字符串

其他方法

本节介绍的修改auto/make脚本是实现Nginx支持C++最简单的方法，但它还不够完善，因为不能定制C++编译参数。读者可以在熟悉Nginx之后改进它，修改auto目录里的其他脚本，让它工作得更好。[7]

2.5　基本的C++包装类

依据上一节的C++实现原则和编码风格，我们来实现Nginx的基本包装类NgxWrapper，它是本书后续很多类的基类。

NgxWrapper是一个模板类，所以不需要知道Nginx的任何信息，完全是独立的。

2.5.1　类定义

NgxWrapper应用了代理模式，可以包装代理一个Nginx的数据对象，例如ngx_str_t、ngx_array_t、ngx_list_t等[8]，控制它访问方式，实现代码很像boost::reference_wrapper。

NgxWrapper的定义如下：

#include <boost/type_traits.hpp>   //使用type_traits 元函数计算类型

template<typename T>            //包装类型 T
class NgxWrapper
{
public:
typedef typename boost::remove_pointer<T>::type  wrapped_type;

typedef wrapped_type*                              pointer_type;
typedef wrapped_type&                              reference_type;
private:
pointer_type m_ptr = nullptr;     //使用指针保存对象，默认是空指针
...                               //成员函数见后
};

NgxWrapper是对类型T的一层薄薄的封装，使用一个指针成员m_ptr来持有对象，成本很低。考虑到类型T可能也是指针，而我们实际并不想要保存T**，所以使用了boost. type_traits库的元函数移除了类型里的指针修饰，获得原始的待包装类型。[9]

2.5.2　构造和析构

NgxWrapper只能被继承使用，是一个抽象类，所以它的构造函数和析构函数应该是被保护的：

protected:                                  //构造析构只能被子类访问
NgxWrapper(pointer_type p):m_ptr(p)     //参数是指针类型
{}

NgxWrapper(reference_type x):m_ptr(&x)  //参数是引用类型
{}

~NgxWrapper() = default;                //析构函数不做任何事

为了方便使用，NgxWrapper的构造函数有两种重载形式，分别接受指针类型和引用类型，这样用户可以随意传入变量，它可以自动选择恰当的方式获取变量指针。

NgxWrapper不负责对象的生命周期管理，所以析构函数不删除指针。[10]

2.5.3　成员函数

由于NgxWrapper只有一个指针成员，所以它的访问接口非常简单：

public:
pointer_type get() const     //访问指针成员
{
return m_ptr;            //返回内部保存的指针
}

同样，为了方便使用，NgxWrapper还重载了转型操作符和指针操作符，可以在需要的时候自动转化为bool或者被包装的类型T，好像是这层包装不存在一样：

operator bool () const            //转型为bool
{
return get();                 //调用get()
}

operator pointer_type () const    //转型为指针类型
{
return get();                 //调用get()
}

pointer_type operator->() const   //指针操作符重载
{
return get();                 //调用get()
}

需要注意的是这些成员函数都是const的，因为它们不会变动成员变量。

2.6　总结

本章是Nginx模块开发前的预备，我们已经站到了这座宏伟的宫殿的大门口，即将登堂入室。

本书主要使用C++语言，所以我们首先了解了开发必备的C++11标准和Boost程序库，然后学习了Nginx的源码目录结构、代码特点和内在的编程思想。针对Nginx源码的特点，本章介绍了C++开发的原则和如何修改编译脚本以支持C++编写模块，最后使用C++泛型技术实现了一个基本的包装类NgxWrapper。

读者可以在接下来的章节里继续体会这些编程风格、实现原则和包装类的应用。

[1] 这里介绍的是安装Boost程序库最简单的方式，更详细的知识可参见附录A推荐书目[3][4]。

[2] Nginx的1.9.x版新增了一个src/stream目录。

[3] 在C语言中，struct是“二等公民”，类型前必须用struct关键字，所以Nginx的解决方案是定义一个xxx_s的struct，再typedef为xxx_t来简化使用方式。

[4] 本书的代码大都是泛型的，C++技术限制模板类的声明和实现最好在一个文件里。

[5] 另一种方式是修改运行configure后生成的objs/Makefile，但这样做有很大的缺点——每次configure之后都要手动修改，无法做到自动化编译。

[6] Shell编程并非作者强项，也许有更好的实现方法，请读者见谅。

[7] 提示：可以在auto/options文件里增加--with-cxx-opt选项，定义变量NGX_CXX_OPT，然后在auto/cc/conf或者auto/make里赋值给CXXFLAGS。

[8] 实际上因为NgxWrapper是泛型的，可以代理任意对象。

[9] 这里我们也可以使用C++11标准<type_traits>里的元函数std::remove_pointer。

[10] 在Nginx里，对象都是在内存池里统一分配的，销毁也由内存池负责，参见第3章。

第3章　Nginx基础设施

从这里起我们将正式研究Nginx源码，学习如何使用C++编写Nginx模块。

本章从最基本的数据结构和功能入手，学习Nginx里的整数类型、内存池、字符串、时间日期和错误日志等Nginx基础设施。这些内容并不直接与HTTP处理相关，但却是Nginx开发的必备基础。

本章的代码部分依赖于Nginx，所以不能简单地在main()函数里编译运行，心急的读者可以交叉参考第5章，了解如何编写一个Nginx测试模块。

3.1　头文件

因为我们主要是开发HTTP功能模块，所以只需关心core目录和http目录。Nginx在http目录里提供了总括性的头文件<ngx_http.h>，包含了所有必需的宏、类型定义和函数声明。[1]

由于C++与C的编译机制不同，我们不能直接包含<ngx_http.h>。要想在C++程序里使用Nginx的C代码必须使用extern"C"指令，这样C++编译器才能正确链接。

Nginx.hpp是本书的基准头文件，它包含了开发Nginx模块所必需的基本功能代码：

// Nginx.hpp
extern "C" {         //需使用extern "C"才能正确链接
#include <ngx_http.h>             //Nginx头文件，内含大量定义
}                                        //extern "C"结束

#include <cassert>                       //标准断言
#include <boost/core/ignore_unused.hpp>  //用于忽略某些不使用的变量

除<ngx_http.h>外，Nginx.hpp里还包含了另外两个工具头文件：<cassert>提供标准断言宏assert，而<boost/core/ignore_unused.hpp>则提供一个有用的模板函数ignore_unused()，可以忽略暂不使用的变量，用来显式消除编译警告。[2]

本节之后的所有代码均使用Nginx.hpp，为节约篇幅不再特别列出。

3.2　整数类型

整数是计算机里最基本的类型，也是处理速度最快的类型。C/C++内置了short、int、long等标准整数类型，但这些类型是平台相关的，在不同的系统里精度可能不一样。

为了保证跨平台的兼容性，Nginx里整数类型的用法很有特点，我们来一起看一下。

3.2.1　标准整数类型

Nginx主要使用了三个标准整数类型：size_t、u_char和off_t。

size_t和u_char通常同时出现，表示Nginx里的数据块，例如字符串和缓冲区；off_t则表示文件偏移量。

size_t用于计算数据的长度，是C/C++标准里定义的一个类型，是sizeof操作符的结果类型，相当于unsigned int。在64位的系统里宽度是8字节：

assert(sizeof(size_t) == 8);     //检查size_t的宽度

u_char表示一个字节，虽然它也是一个标准整数类型，但并不是C/C++标准，而是“系统标准”，是在<sys/types.h>里的一个typedef：

typedef unsigned char u_char;      //u_char 的类型定义

3.2.2　自定义整数类型

Nginx依据C/C++标准重新定义了三个整数类型来代替标准里的int/long等类型，保证在任何系统上编译都能获得一致的结果。这些类型是：[3]

// 定义在 core/ngx_config.h
typedef intptr_t          ngx_int_t;     //有符号整数
typedef uintptr_t         ngx_uint_t;    //无符号整数
typedef intptr_t          ngx_flag_t;    //标志整数类型

使用这三个基本整数，Nginx又typedef了一些等价类型，用于其他用途：

// 定义在 core/ngx_rbtree.h，红黑树的键类型
typedef ngx_uint_t              ngx_rbtree_key_t;
typedef ngx_int_t               ngx_rbtree_key_int_t;

// 定义在 os/unix/ngx_time.h，毫秒的整数类型
typedef ngx_rbtree_key_t        ngx_msec_t;
typedef ngx_rbtree_key_int_t    ngx_msec_int_t;

我们在开发Nginx模块编写代码时应该尽量使用这些Nginx专有的整数类型，不仅是为了与Nginx保持风格的一致，更重要的是保证代码的跨平台兼容。

3.2.3　无效值

变量的初始化是编程语言里一个重要但又常常会被忽略的问题。在C/C++中，整数类型属于POD类型，初始值是“未定义”的，也就是说初始值可能是任意数值，存在安全隐患。

Lua/Python等语言有Nil/None的概念，一个变量如果未初始化，那么它的值就是Nil或者None。Nginx采用了类似思路的“UNSET”值，使用“-1”表示未初始化。

由于C/C++是强类型语言，单纯的整数-1不能直接与其他类型比较，需要做类型转换，所以Nginx为-1定义了不同类型转换的宏：

// 定义在 core/ngx_conf_file.h
#define NGX_CONF_UNSET                       -1
#define NGX_CONF_UNSET_UINT   (ngx_uint_t)   -1
#define NGX_CONF_UNSET_PTR    (void *)       -1
#define NGX_CONF_UNSET_SIZE   (size_t)       -1
#define NGX_CONF_UNSET_MSEC   (ngx_msec_t)   -1

有了UNSET概念，Nginx以宏的形式提供了初始化函数和条件赋值函数[4]，基本的形式是：

// 定义在 core/ngx_conf_file.h
#define ngx_conf_init_value(conf, default)                 \
if (conf == NGX_CONF_UNSET) {                          \
conf = default;                                    \
}

#define ngx_conf_merge_value(conf, prev, default)           \
if (conf == NGX_CONF_UNSET) {                           \
conf = (prev == NGX_CONF_UNSET) ? default : prev;   \
}

同样的，这两种函数还有若干个其他形式，用来操作ngx_uint_t、size_t、指针等类型，但其内部的逻辑是相同的，代码基本是机械重复，只有少量名称的不同。

3.2.4　C++封装

在3.2.3节我们看到，由于C语言的限制，要操作一个简单的“-1”，居然要用数十行的代码去实现，而且使用也存在很大的不方便——我们必须明确地知道操作的类型，再选择正确的操作函数，否则就会导致无谓的编译错误。

C++语言里的泛型技术可以很好地解决这个问题，它把类型识别的重担由程序员转移给了编译器，让编译器自动去选择合适的类型和函数，而且绝对不会出现失误。

无效值的封装

我们使用类NgxUnsetValue封装Nginx里的无效值“-1”，使用模板转型操作函数来自动确定类型，并且用标准转型操作符static_cast和reinterpret_cast来增加易读性：

class NgxUnsetValue final                      //final禁止被继承
{
public:
template<typename T>                   //模板函数
operator T () const                    //转型到类型 T
{
return static_cast<T>(-1);         //使用static_cast 转型-1
}

template<typename T>                   //模板函数
operator T* () const                   //转型到类型 T*，即指针类型
{
return reinterpret_cast<T*>(-1);   //使用reinterpret_cast
}
};

NgxUnsetValue相当于Nginx里的NGX_CONF_UNSET_XXX宏，但它使用了模板技术支持任意类型的转型，比只做字符串替换的宏更加安全。

NgxUnsetValue应该是个单件，可以再实现一个静态成员函数，提供一个全局访问点：

public:
static const NgxUnsetValue& get()      //获取全局唯一对象
{
static NgxUnsetValue const v = {};     //静态变量，空类
return v;
}

这样，NgxUnsetValue就完全封装了Nginx的UNSET概念，无须烦恼，直接使用NgxUnsetValue::get()就能够获得这个类型安全的“-1”值。

操作函数的封装

类NgxValue封装了对整数类型的基本操作，除了初始化和条件赋值，还增加了判断是否无效的操作：

class NgxValue final                                       //final禁止被继承
{
public:
NgxValue()  = default;                                    //良好的编程习惯
~NgxValue() = default;                                    //为构造和析构提供默认实现
public:
template<typename T>
static bool invalid(const T& v)                       //无效值判断
{
return v ==                                       //与 NgxUnsetValue比较
static_cast<T>(NgxUnsetValue::get());               //静态转型
}

template<typename T, typename U>
static void init(T& x, const U& v)
{
if (invalid(x))                                        //无效值判断
{
x = v;                                             //如果无效则初始化
}
}

template<typename T, typename U, typename V>
static void merge(T& c, const U& p, const V& d) //条件赋值
{
if (invalid(c))                                         //如果无效则赋值
{
c = invalid(p) ? d : p;                             //检查p，无效则赋值为 d
}
}
};

成员函数invalid()简化了变量与NgxUnsetValue的比较，init()和merge()完全复制了Nginx源码的逻辑，但更清晰易读。

注意，这些成员函数都是静态的，需要以NgxValue::xxx()的形式调用。

批量未初始化[5]

有时候我们需要把多个值置为明确的未初始化状态，虽然有了NgxUnsetValue，但仍要编写多条赋值语句，逐一赋值为NgxUnsetValue::get()，比较麻烦。

C++11的可变模板参数列表新特性提供了简化批量赋值语句的可能性。它可以像printf()那样接受任意多个参数，采用（编译期）递归的方式解包模板参数列表，自动生成多条语句，与手写的效果完全相同。

unset()函数的实现代码如下：

class NgxValue final
{
...                                               //invalid()等函数
public:
template<typename T, typename ... Args>     //可变模板参数列表
static void unset(T& v, Args& ... args)   //注意“...”的用法
{
v = NgxUnsetValue::get();                     //置为未初始化状态
unset(args...);                               //递归处理剩余的模板参数
}

static void unset() {}                            //递归终结函数
};

unset()函数可以这样使用：

ngx_int_t  x = 10;                       //一些Nginx变量
ngx_uint_t   y = 20;
void*   p ;                               //指针值未定义

NgxValue::unset(x, y, p);                 //批量未初始化操作
assert(NgxValue::invalid(p));             //验证指针值无效

利用C++的重载函数特性，我们还可以在NgxValue里针对其他类型实现特别版本的init()、merge()函数，例如3.5节的ngx_str_t。

3.3　错误处理

Nginx使用传统的错误码处理错误（注意与HTTP的响应状态码200、404等是不同的），但在C++里使用异常（exception）的方式会更好。因为异常机制可以分离代码逻辑里的正常部分与异常部分，使代码的结构更加清晰。[6]

3.3.1　错误码定义

Nginx使用宏定义了七个常用的错误码，类型是ngx_int_t：

// 定义在 core/ngx_core.h
#define  NGX_OK            0      //执行成功，无错误
#define  NGX_ERROR        -1      //执行失败，最常见的错误码
#define  NGX_AGAIN        -2      //未准备好，需要重试
#define  NGX_BUSY         -3      //后端服务正忙
#define  NGX_DONE         -4      //执行成功，但还需要有后续操作
#define  NGX_DECLINED     -5      //执行成功，但未做处理
#define  NGX_ABORT        -6      //发生了严重的错误

在编写自己的功能函数时通常要使用以上的这七个错误码，如果有必要也可以自定义一些错误码，但必须是负值。

3.3.2　C++异常

C++对异常规定得比较宽松，任何类型都可以作为异常抛出，但最好使用class。这里我们使用C++标准里的std::exception和Boost程序库里的boost::exception来实现异常类NgxException，需要包含下面的头文件：

#include <string>                        //标准字符串
#include <exception>                     //标准异常
#include <boost/exception/all.hpp>       //boost::exception
#include <boost/utility/string_ref.hpp>  //轻量级字符串引用

类定义

NgxException的类定义如下：

class NgxException final :   public virtual std::exception,
public virtual boost::exception
{
public:
typedef boost::string_ref string_ref_type;
private:
ngx_int_t  m_code = NGX_ERROR;      //错误代码
std::string   m_msg;                //错误信息
public:
NgxException(                       //完整的构造函数
ngx_int_t x, string_ref_type msg):
m_code(x), m_msg(msg)
{}

NgxException(ngx_int_t x = NGX_ERROR):
NgxException(x, "")             //委托构造
{}

NgxException(string_ref_type msg):
NgxException(NGX_ERROR, msg)    //委托构造
{}

virtual ~NgxException() noexcept    //虚析构函数
{}
public:
ngx_int_t code() const              //获取错误码
{
return m_code;
}
virtual const char* what() const noexcept override
{
return m_msg.c_str();
}
...                                 //其他成员函数，见后
};

依据C++最佳实践准则，异常应该从std::exception虚继承，我们还使用了boost::exception，可以让异常容纳更多的信息。[7]

NgxException有两个成员变量，存储了错误码和对应的描述信息。构造函数有三种形式，用来简化调用，其中后两个构造函数使用了C++11的委托构造新特性。

为了避免字符串拷贝的成本，NgxException的函数参数使用了boost::string_ref，这是一个轻量级的字符串表示，没有拷贝代价，类似3.5节的ngx_str_t。

成员函数

异常的一般使用方式是直接用throw抛出，但这里我们把它封装为一个raise()函数，表现形式更好：

public:
static void raise(ngx_int_t rc = NGX_ERROR, string_ref_type msg = "")
{
throw NgxException(rc, msg);   //抛出异常
}

检查错误码和空指针是Nginx开发中经常要做的工作。反复出现的if语句很麻烦，由于异常处理流程与正常流程是分离的，所以可以轻松写出封装函数require()：

public:
//检查条件是否满足
static void require(bool cond, ngx_int_t e = NGX_ERROR)
{
if(!cond)                    //如果不符合预期则抛出异常
{   raise(e);}
}

//检查错误码
static void require(ngx_int_t rc, ngx_int_t x = NGX_OK)
{
require(rc == x, rc);        //如果不是 OK则抛出异常
}

//检查空指针
template<typename T>
static void require(T* p, ngx_int_t e = NGX_ERROR)
{
require(p != nullptr, e);     //如果是空指针则抛出异常
}

有的时候当判断条件成立时的逻辑更容易书写，所以require()的“反函数”fail()也很有用：

static void fail(bool cond, ngx_int_t e = NGX_ERROR)
{
if(cond)          //如果符合预期则抛出异常
{   raise(e);}
}

3.4　内存池

内存池是大型软件里常用的一种技术，它是对象池模式的具体应用：一次性向系统申请大块的内存，内部自行切割分配使用，最后再一次性归还系统。这种方式减少了系统调用的次数，而且能够很好地避免内存碎片和泄漏。

Nginx的内存池有它自己的特色，为了提高效率，使用了一些巧妙的设计和技巧，机制比较复杂。本节仅介绍它的使用方法，其详细实现原理的讲解已经超出了本书的范围。

3.4.1　结构定义

Nginx的内存池数据结构是ngx_pool_t，代码摘要如下：

// 定义在 ngx_core.h
typedef struct ngx_pool_s  ngx_pool_t;   //简化定义

// 定义在 ngx_palloc.h
struct ngx_pool_s {                      //结构体定义
...                                  //暂无须关心的内部成员
ngx_pool_cleanup_t    *cleanup;      //“析构”时的清理动作，见后
ngx_log_t              *log;         //关联的日志对象
};

ngx_pool_t代表了Nginx里的内存池概念，每一个HTTP请求Nginx都会为之创建一个独立的内存池——也就是ngx_pool_t对象，在整个请求的处理过程中我们可以在里面任意申请内存使用，不必担心内存的释放问题。当请求结束时Nginx会自动销毁ngx_pool_t对象，释放内存池和它拥有的所有内存。

在Nginx开发时我们需要改变以往的思路，尽量避免用malloc或者new来动态分配内存，而是在Nginx的框架里使用ngx_pool_t，从而获得高性能。

3.4.2　操作函数

ngx_pool_t相关的操作函数有很多，由于Nginx框架负责内存池的创建与销毁，故本节只讲解内存的分配与释放，介绍几个最常用的函数：

void* ngx_palloc(ngx_pool_t *pool, size_t size);
void* ngx_pnalloc(ngx_pool_t *pool, size_t size);
void* ngx_pcalloc(ngx_pool_t *pool, size_t size);

这三个函数相当于标准的malloc()，都是从内存池pool里获取size字节大小的内存，返回一个void*指针，区别是：

- ngx_palloc()使用了内存对齐，速度快，但可能会有少量的内存浪费；

- ngx_pnalloc()没有使用内存对齐；

- ngx_pcalloc()内部调用了ngx_palloc，并且把内存块清零。

大多数情况下我们应该使用ngx_pcalloc()，它更加安全。

与ngx_palloc()对应，可以使用ngx_pfree()函数释放申请的内存，相当于C标准函数free()：

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);

3.4.3　C++封装

ngx_pool_t存在明显的面向对象特征——有数据结构和应用在它上的一些操作，所以可以封装为C++类。

类定义

我们使用2.5节的NgxWrapper来实现封装类NgxPool，定义如下：

#include "NgxWrapper.hpp"                              //基本包装类
#include "NgxException.hpp"                            //异常类

class NgxPool final : public NgxWrapper<ngx_pool_t>
{
public:
typedef NgxWrapper<ngx_pool_t>   super_type;  //简化类型定义
typedef NgxPool       this_type;
...                                           //成员函数见后
};

NgxPool的基类是NgxWrapper<ngx_pool_t>，包装了ngx_pool_t结构，因为复用了NgxWrapper的代码，所以直接获得了操作符重载和bool转型的能力。

构造与析构

NgxPool使用ngx_pool_t*指针构造，并委托给NgxWrapper：

public:
NgxPool(ngx_pool_t* p): super_type(p)     //构造
{}

~NgxPool() = default;                      //析构

由于ngx_pool_t在很多Nginx的数据结构里以成员变量pool的形式出现，所以可以增加一个模板构造函数自动使用这个成员，方便用户调用：

template<typename T>
NgxPool(T* x) : NgxPool(x->pool)     //使用类型 T 的 pool 成员
{}

这里的模板参数T可以是ngx_http_request_t、ngx_conf_t等含有pool成员的结构。

基本的内存分配函数

我们主要使用ngx_pcalloc()分配内存，首先实现一个基本的内存分配函数palloc()，它可以依据模板参数决定是否抛出异常：

public:
template<typename T, bool no_exception = false>
T* palloc() const
{
auto p = ngx_pcalloc(get(), sizeof(T));  //分配内存，注意 auto 的使用

if(!p)                                   //检查空指针
{
if(no_exception)                     //是否允许抛出异常
{   return nullptr; }                //返回空指针

NgxException::raise();               //抛出异常
}

assert(p);                               //断言增强安全性
return new (p) T();                      //构造T对象
}

palloc()使用sizeof计算类型T的长度再分配内存，如果分配成功那么就在这块内存上调用new，执行T的默认构造函数，返回的是一个T*指针，而不是无类型的void*。

bool模板参数no_exception用来控制palloc()是否抛出异常，这是因为有的内存分配操作可能允许返回空指针，执行别的处理逻辑。

成员函数alloc()和alloc_noexcept()包装了palloc()，简化了调用方式：

public:
template<typename T>
T* alloc() const                    //抛异常版本
{
return palloc<T, false>();      //参数是false
}

template<typename T>
T* alloc_noexcept() const            //不抛异常版本
{
return palloc<T, true>();       //参数是true
}

扩展的内存分配函数

NgxPool还提供两个函数，用来分配指定字节数的内存和多参数构造对象：

public:
template<typename T>
T* nalloc(std::size_t n) const                 //分配n个字节内存
{
auto p = ngx_pnalloc(get(), n);            //使用ngx_pnalloc

NgxException::require(p);                  //检查空指针

return reinterpret_cast<T*>(p);      //转型为T*
}

template<typename T, typename ... Args>
T* construct(const Args& ... args) const    //任意数量参数构造对象
{
auto p = ngx_pcalloc(get(), sizeof(T));     //使用ngx_pcalloc

NgxException::require(p);                   //检查空指针

return new (p) T(args ...);                 //转发参数构造
}

更多的成员函数

NgxPool封装了Nginx的内存池对象，所以还可以集成更多的内存池相关操作，例如拷贝字符串、创建ngx_array_t/ngx_list_t等，本书之后会继续完善它。

3.4.4　清理机制

Nginx框架自动管理内存池的生命周期，当一个请求结束时内存池里的内存会完全归还给系统。但这里还存在问题：内存只是系统资源的一个方面，其他的系统资源（如文件句柄）并不会随着内存池的销毁而一并释放，如果不做特殊的操作就有可能造成资源泄漏。

这种清理机制就是C++里的析构函数思想，在对象销毁时自动调用析构函数，执行对应的资源销毁动作。但Nginx使用的是C语言，而C语言并没有析构的概念，所以Nginx实现了自己的清理机制。

实现原理

Nginx定义了一个保存清理信息的结构ngx_pool_cleanup_t，用来在内存销毁时执行清理动作：

// 定义在 ngx_palloc.h
typedef void (*ngx_pool_cleanup_pt)(void *data);   //清理函数指针原型
typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;

struct ngx_pool_cleanup_s {                        //清理信息结构体
ngx_pool_cleanup_pt    handler;                //清理动作，函数指针
void                    *data;                 //清理所需数据
ngx_pool_cleanup_t     *next;                  //后续链表指针
};

ngx_pool_cleanup_t的成员handler保存了清理函数，它是一个函数指针，成员data是一个void*指针，通常指向需释放的资源。Nginx会在销毁内存池时把data传递给handler，即调用handler(data)完成清理动作。注意，因为data的类型不做要求，所以我们可以利用handler执行任意操作，不一定必须是“释放资源”。[8]

ngx_pool_cleanup_t还有一个next指针成员，多个对象可以使用这个指针连接成一个单向链表。当内存池销毁时Nginx会逐个执行链表里的清理函数，释放所有的资源。

ngx_pool_t结构里的成员cleanup是这个链表的头节点：

struct ngx_pool_s {                     //ngx_pool_t 结构体定义
...                                  //暂无须关心的内部成员
ngx_pool_cleanup_t    *cleanup;      //所有的清理动作在此
};

管理清理链表

我们不需要直接操作ngx_pool_t::cleanup，Nginx提供了专门的函数ngx_pool_cleanup_add()，它会自动维护清理链表，声明是：

// 定义在 ngx_palloc.h
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);

这个函数使用size为ngx_pool_cleanup_t::data分配内存，返回清理信息ngx_pool_cleanup_t对象，设置它的handler和data，就可以达到向内存池“注册”清理函数的目的。

C++封装

NgxPool对ngx_pool_cleanup_add()的封装如下：

public:
template<typename F, typename T>   //模板参数支持任意类型
ngx_pool_cleanup_t* cleanup(F func, T* data, std::size_t size = 0) const
{
auto p =
ngx_pool_cleanup_add(get(), size); //调用Nginx函数
NgxException::require(p);            //检查空指针

p->handler = func;                //设置清理函数

if(data)                             //允许直接传入待释放的资源
{   p->data = data;}              //设置data 成员

return p;                            //返回清理信息对象
}

cleanup()函数描述了Nginx里释放资源的基本流程：首先获取一个ngx_pool_cleanup_t对象，然后设置它的handler和data。如果不提供data对象（nullptr），那么用户就需要手工设置。

使用析构函数

对于C++来说析构函数是才释放资源的正确方法，但在Nginx里释放内存池是C调用，并不会触发析构行为。如果我们自己的类用到了其他资源，就必须把类的析构函数适配到Nginx的清理机制里：

public:
template<typename T>
static void destory(void* p)                //适配析构函数符合 Nginx 要求
{
(reinterpret_cast<T*>(p))->~T();        //转换void*，再调用析构函数
}

template<typename T>
void cleanup(T* data) const                  //重载cleanup()函数
{
cleanup(&this_type::destory<T>, data);  //注意函数指针的用法
}

静态成员函数destroy()把void*类型转换为T，然后显式调用它的析构函数，这样就符合了Nginx对handler的要求。由于类自身已经包含了资源的所有信息，所以我们无须额外创建data，直接向Nginx的清理链表里添加handler和data即可。

注意destroy()是一个模板函数，所以在写函数指针时必须带上模板参数列表。

3.4.5　C++内存分配器

C++标准库和Boost程序库提供了大量的泛型容器，例如vector、list、unordered_map等，在开发Nginx模块时我们完全可以自由使用。但有一点需要留意，它们并不知道Nginx内存池的存在，直接向操作系统申请内存，所以可能会有一些效率的损失。

不过C++的内存分配机制非常灵活，允许用户自定义内存分配器。通过这个机制，我们就可以让这些泛型容器与Nginx内存池完美结合，在高效使用内存的同时获得C++的便捷。

类定义

NgxAlloctor与NgxPool基本实现比较相似，都使用NgxWrapper来包装ngx_pool_t对象：

template<typename T>
class NgxAlloctor : public NgxWrapper<ngx_pool_t>
{
public:
typedef NgxWrapper<ngx_pool_t>   super_type;  //简化类型定义
typedef NgxAlloctor              this_type;
public:
typedef std::size_t         size_type;              //内部类型定义
typedef T*                  pointer;
typedef T                   value_type;
public:
NgxAlloctor(ngx_pool_t* p) : super_type(p)           //构造
{}
~NgxAlloctor() = default;                            //析构

...                                                  //成员函数见后
};

内存分配函数

NgxAlloctor同样也需要实现内存分配函数，但它的接口需要遵守C++对内存分配器的约定，这是与NgxPool不同的地方。

在C++11标准中自定义内存分配器的工作较以前已经有了很大的简化，我们只需实现allocate()和deallocate()两个基本的函数：

public:
pointer allocate(size_type n)                      //分配n个元素所需的内存
{
return reinterpret_cast<pointer>(               //转型
ngx_pnalloc(get(), n * sizeof(T)));  //使用Nginx内存池
}

void deallocate(pointer ptr, size_type n)          //“释放”内存
{                                                  //由 Nginx 负责内存回收
boost::ignore_unused(n);                        //忽略入口参数 n
ngx_pfree(get(), ptr);                          //释放内存
}

用法

以最常用的std::vector为例，我们来演示一下自定义内存分配器的用法。

std::vector有两个模板参数，使用std::allocator作为默认的内存分配器的类声明是：

template< class T,           //容纳的元素类型
class Allocator = std::allocator<T>   //内存分配器
> class vector;

所以，只要改写它的第二个模板参数，替换为我们自己的NgxAlloctor，然后在构造函数里传入分配器对象，std::vector就能够使用Nginx的内存池，例如：

//假设已经有内存池对象 pool
std::vector<char, NgxAlloctor<char> > v(pool);  //注意模板参数

使用C++11的模板别名特性可以简化容器的类型声明：

template<typename T>
using NgxStdVector =                               //C++11新特性
std::vector<T, NgxAlloctor<T>>;   //模板别名

NgxStdVector<char>  v(pool);                 //使用别名创建容器

3.5　字符串

HTTP协议是基于纯文本的协议，所以Nginx必须能够正确、高效地处理字符串。Nginx为此设计了ngx_str_t结构，它表示Nginx里的字符串对象。

3.5.1　结构定义

ngx_str_t并不是一个真正意义上的“字符串”，准确地说，它应该是一个“内存块引用”，定义如下：

// 定义在 ngx_string.h
typedef struct {            //一个匿名结构体
size_t      len;        //字符串长度
u_char     *data;       //字符串所在的地址
} ngx_str_t;                //类型定义

从代码里可以看到，ngx_str_t的结构非常简单，只是用成员变量len和data标记了一块内存区域，并不实际持有一个字符串内容，非常的轻量级。data成员的类型是u_char而不是char，意味着它不一定必须以'\0'结尾，任何数据都可以当作字符串来使用。

这样的设计与boost.string_ref或者C++17的std::string_view非常相似。好处是字符串的操作非常廉价——只有两个整数的成本，不需要复制大量的数据，所以对它的拷贝、修改都非常高效，也节约了内存的使用。

当然这种设计也有缺点。因为ngx_str_t只是引用内存，所以我们应尽量以“只读”的方式去使用它。在多个ngx_str_t共享一块内存时要小心，如果擅自修改字符串内容很有可能影响其他的ngx_str_t引用，导致错误发生。另外一种危险是引用的内存地址可能会失效，访问到错误的内存区域。

在实际的HTTP请求处理过程中，我们通常都是以只读的方式去检查HTTP头部和正文，很少修改字符串。如果确实要修改字符串或者获取完全拥有权，那么可以向Nginx内存池单独申请一块内存，把字符串内容复制过去，但这样就增加了内存拷贝的代价。

3.5.2　操作函数

Nginx定义了很多字符串操作函数，本书不可能全部讲解（也无必要），只列出一些常用的函数，读者可阅读Nginx源码（core/ngx_string.*）了解更多的用法。

为了提高运行效率，这些“函数”很多都是宏。

初始化和赋值

Nginx提供两个初始化函数宏：ngx_string()使用字符串初始化，ngx_null_string()初始化为一个空字符串，因为它们使用了{...}的形式，所以只能用于赋值初始化：

#define ngx_string(str)     { sizeof(str) - 1, (u_char *) str }

#define ngx_null_string     { 0, NULL }

运行时设置字符串内容可以使用下面两个函数宏，功能是相同的，注意参数str必须是指针：

#define ngx_str_set(str, text)           \
(str)->len = sizeof(text) - 1; (str)->data = (u_char *) text
#define ngx_str_null(str)   (str)->len = 0; (str)->data = NULL

ngx_string()和ngx_str_set()内部使用了sizeof计算字符串长度，所以参数必须是一个编译期的字符串“字面值”，而不能是一个字符串指针，否则sizeof会计算得到指针的长度（8字节），而不是字符串的实际长度。

这四个函数宏的示例用法如下：

ngx_str_t s1 = ngx_null_string;       //初始化为空字符串
ngx_str_t s2 = ngx_string("matrix");  //初始化为"matrix"

ngx_str_set(&s1, "reloaded");     //运行时赋值，注意取地址操作符
ngx_str_null(&s2);                //运行时置为空字符串

下面的代码则是错误的：

s1 = ngx_null_string;        //错误！不是赋值初始化
ngx_str_set(s1, "wrong");     //错误！没有使用取地址操作符

基本操作函数

ngx_str_t只是个普通的字符串，所以标准的C字符串函数都能够使用（需要转型为const char*），处理它的data成员就可以了，但Nginx也实现了一些特有的操作函数。[9]

下面是一些常用的字符串操作函数，但需要注意有的参数类型不是ngx_str_t，而是u_char*：

#define ngx_strcmp(s1, s2)       //大小写敏感比较，参数是 u_char*
#define ngx_strncmp(s1, s2, n)   //大小写敏感比较，有长度参数

#define ngx_strstr(s1, s2)       //查找子串
#define ngx_strlen(s)            //使用'\0'计算字符串长度

//大小写不敏感字符串比较，参数是 u_char*
ngx_int_t ngx_strcasecmp(u_char *s1, u_char *s2);
ngx_int_t ngx_strncasecmp(u_char *s1, u_char *s2, size_t n);

//字符串转整数类型，参数是 u_char*
ngx_int_t ngx_atoi(u_char *line, size_t n);

//内存池复制字符串，参数是 ngx_str_t*
u_char *ngx_pstrdup(ngx_pool_t *pool, ngx_str_t *src);

格式化函数

Nginx实现了自己的类printf()格式化函数，同样参数是u_char*：

u_char * ngx_sprintf(u_char *buf, const char *fmt, ...);
u_char * ngx_snprintf(u_char *buf, size_t max, const char *fmt, ...);
u_char * ngx_slprintf(u_char *buf, u_char *last, const char *fmt,...);

ngx_sprintf()直接向buf输出格式化内容，不检查缓冲区的有效性，存在缓冲区溢出危险，通常不建议使用。后两个函数比较安全，参数max和last指明了缓冲区的结束位置，所以格式化的结果只会填满缓冲区为止。

函数执行后会返回一个u_char*指针，指示格式化输出后在buf里的结束位置，可以用这个返回值来判断结果的长度。

Nginx还定义了很多自己的格式化标志，详细信息请参看附录C。需要当心的是格式化输出ngx_str_t对象必须使用专有的“%V”，而不能是“%s”。

3.5.3　C++封装

我们仍然利用NgxWrapper来封装ngx_str_t结构，实现类NgxString。读者可以在此基础上参考Nginx字符串函数实现更多的功能，本书不再赘述。

类定义

NgxString与NgxPool的实现相近，定义如下：

class NgxString final : public NgxWrapper<ngx_str_t>
{
public:
typedef NgxWrapper<ngx_str_t>   super_type;  //基本包装类
typedef NgxString               this_type;    //简化类型定义

typedef boost::string_ref   string_ref_type;       //char*字符串引用类型
public:
NgxString(ngx_str_t& str): super_type(str)     //构造
{}

~NgxString() = default;                            //析构

...                                                //成员函数见后
};

基本操作函数

NgxString的接口模仿了标准字符串std::string：

public:
const char* data() const       //获取字符串
{
return reinterpret_cast<const char*>(get()->data);
}

std::size_t size() const       //获取长度
{
return get()->len;
}

bool empty() const             //是否是空字符串
{
return !get()->data || !get()->len;
}

这些成员函数都非常简单，无须过多说明。

为了让NgxString用起来更像std::string，我们实现了一个函数str()，它返回boost::string_ref类型，相当于一个常量字符串：

string_ref_type str() const       //转换为char*字符串形式
{
return string_ref_type(data(), size());
}

我们也可以编写begin()、end()、front()、back()等接口，这样它就可以直接搭配标准算法使用。限于篇幅这里不再列出详细实现源码，读者可以参考0.7节的GitHub资源。

特殊操作函数

基于Nginx特有的字符串操作函数，还可以给NgxString添加其他有用的接口：

public:
operator ngx_int_t () const                   //字符串转整数类型
{
return ngx_atoi(get()->data, get()->len);
}

//重载比较操作符，大小写敏感比较两个 ngx_str_t 对象
friend bool operator==(const this_type& l, const this_type& r)
{
return l.size() == r.size() &&
ngx_strncmp(l.data(), r.data(), l.size()) == 0;
}

template<typename ... Args>             //可变模板参数列表
void printf(const Args& ... args) const   //安全格式化
{
auto p = ngx_snprintf(get()->data, get()->len, args ...);

get()->len =                           //计算实际长度
static_cast<std::size_t>(p - get()->data);
}

注意：printf()没有调用成员函数data()，这是因为data()使用了reinterpret_cast强制转型为了const char*，而Nginx的字符串函数需要的是u_char*。

出于方便调试的目的，本书还为NgxString重载了C++流输出操作符：

public:
template<typename T>
friend T& operator<<(T& o, const this_type& s)  //重载流输出操作符
{
o.write(s.data(), s.size());                //把字符串写入流
return o;
}

条件赋值

类NgxValue可以重载merge()函数，实现ngx_str_t的条件赋值操作，代码逻辑与整数类型的条件赋值类似：

class NgxValue final
{
...                                          //NgxValue 的其他成员函数

static void merge(ngx_str_t& c, const ngx_str_t& p, const ngx_str_t& d)
{
if(!c.data)                               //检查字符串是否为空
{
c = p.data ? p : d;                   //条件赋值
}
}
};

内存池复制字符串

ngx_str_t的ngx_pstrdup()函数使用了内存池，所以应该在类NgxPool里封装：

class NgxPool final : public NgxWrapper<ngx_pool_t>
{
public:
...                                            //NgxPool 的其他成员函数
ngx_str_t dup(ngx_str_t& str) const        //复制字符串
{
ngx_str_t tmp;                             //准备返回的字符串

tmp.len = str.len;                         //设置字符串长度
tmp.data = ngx_pstrdup(get(), &str);   //内存池复制字符串

NgxException::require(tmp.data);           //检查空指针
return tmp;                                //返回复制的字符串
}

ngx_str_t dup(boost::string_ref str) const   //复制普通字符串
{
ngx_str_t tmp{                                  //转换为ngx_str_t 对象
str.size(), (u_char*)str.data()};           //注意转型
return dup(tmp);         //调用dup()
}
};

3.6　时间与日期

与字符串一样，时间与日期是程序经常要处理的对象，在Web服务器里更是如此，Nginx基于C API实现了比较全面的时间与日期功能。[10]

3.6.1　时间结构定义

Nginx定义了专用的时间数据结构ngx_time_t：

// 定义在 core/ngx_times.h
typedef struct {
time_t        sec;       //自 epoch 以来的秒数，即时间戳
ngx_uint_t    msec;      //秒数后的小数部分，单位是毫秒
ngx_int_t     gmtoff;    //GMT 时区偏移量
} ngx_time_t;

ngx_time_t在Nginx里用来表示时间，成员变量sec和msec分别表示秒数（时间戳）和小数部分的毫秒数。gmtoff是本地时区相对于UTC/GMT的偏移量，以分钟为单位，例如北京时区是GMT+8，那么gmtoff就是480=8*60。

3.6.2　时间操作函数

为了节约资源，避免频繁的系统调用，Nginx内部使用了比较巧妙的cache机制来存放时间值，使用一个全局指针ngx_cached_time指示当前缓存的时间：

extern volatile ngx_time_t  *ngx_cached_time;

两个函数宏ngx_time()和ngx_timeofday()可以分别获取当前时间的秒数（时间戳）和完整的时间数据结构：

#define ngx_time()             ngx_cached_time->sec
#define ngx_timeofday()       (ngx_time_t *) ngx_cached_time

基于ngx_cached_time，Nginx还定义了另外一个全局变量ngx_current_msec，表示自epoch以来的毫秒数，即sec*1000+msec：

ngx_msec_t  ngx_current_msec;      //毫秒精度的时间戳

由于ngx_cached_time是一个缓存的时间，可能存在延迟，有的时候我们必须要获取当前的精确时间，这个时候可以调用ngx_time_update()强制更新缓存，然后再获取时间：

void ngx_time_update(void);      //要求Nginx更新缓存的时间

但ngx_time_update()需要使用锁，成本较高，所以不应该频繁调用。

Nginx还使用宏包装了系统调用sleep()，但它会阻塞整个进程，应当慎用：

// 定义在 os/unix/ngx_time.h
#define ngx_msleep(ms)        (void) usleep(ms * 1000)
#define ngx_sleep(s)          (void) sleep(s)

3.6.3　日期结构定义

ngx_time_t只表示计算机世界里的时间，如果要使用现实生活里的年月日还需要另外一个结构ngx_tm_t：

// 定义在 os/unix/ngx_time.h
typedef struct tm             ngx_tm_t;

ngx_tm_t实际上是一个简单的typedef，它是标准C结构tm的同义词，tm的定义是：

// 定义在<ctime>
struct tm
{
int tm_sec;    // Seconds. [0-60] (1 leap second)
int tm_min;    // Minutes. [0-59]
int tm_hour;   // Hours.[0-23]
int tm_mday;   // Day. [1-31]
int tm_mon;    // Month.[0-11]
int tm_year;   // Year 1900.
int tm_wday;   // Day of week. [0-6]
int tm_yday;   // Days in year.[0-365]
int tm_isdst;  // DST.[-1/0/1]
};

为了让struct tm更“像”是Nginx自己的数据结构，Nginx使用宏重命名了tm的成员变量，例如：

#define ngx_tm_mday           tm_mday  //天
#define ngx_tm_mon            tm_mon   //月
#define ngx_tm_year           tm_year  //年
#define ngx_tm_wday           tm_wday  //星期

3.6.4　日期操作函数

Nginx提供两个函数，把time_t分别转换为格林尼治标准时间（GMT）和本地时间，time_t值可以用ngx_time()得到：

// 定义在 core/ngx_times.h
void ngx_gmtime(time_t t, ngx_tm_t *tp);

// 定义在 os/unix/ngx_time.h
void ngx_localtime(time_t s, ngx_tm_t *tm);

Nginx里还有几个函数可以在字符串和time_t之间互相转换处理，可以在处理HTTP请求时使用。

下面两个函数内部调用了ngx_gmtime()和ngx_sprintf()，把time_t转换为日期字符串：

// 定义在 core/ngx_times.h
u_char *ngx_http_time(u_char *buf, time_t t);
u_char *ngx_http_cookie_time(u_char *buf, time_t t);

下面的函数解析字符串形式的日期时间，转换为time_t：

// 定义在 http/ngx_http.h
time_t ngx_http_parse_time(u_char *value, size_t len);

同样的，Nginx使用全局变量提供缓存好的日期字符串，减少频繁调用的成本：

ngx_str_t   ngx_cached_err_log_time;      //错误日志的日期字符串
ngx_str_t   ngx_cached_http_time;         //HTTP 格式的日期字符串
ngx_str_t   ngx_cached_http_log_time;     //HTTP 日志的日期字符串
ngx_str_t   ngx_cached_http_log_iso8601;  //ISO8601 格式的日期字符串
ngx_str_t   ngx_cached_syslog_time;       //系统日志格式的日期字符串

读者可以根据实际情况选择合适的日期字符串应用在自己的程序里。

3.6.5　C++封装时间

我们使用类NgxClock来封装Nginx里与ngx_time_t相关的操作[11]，实现一个简单的计时器。

类定义

NgxClock的定义如下：

class NgxClock final        //Nginx的时间功能封装
{
public:
NgxClock() = default;
~NgxClock() = default;
public:
...                                 //成员函数见后
private:
ngx_time_t m_time = now();   //初始化为当前时间
};

NgxClock使用一个成员变量m_time来保存时间值，在构造时调用成员函数now()获取当前时间，表示计时的起点。

成员函数

静态成员函数now()首先调用ngx_time_update()，然后获取更新后的缓存时间：

public:
static const ngx_time_t& now()     //获取当前时间
{
ngx_time_update();            //更新缓存时间

return *ngx_timeofday();     //获取缓存时间
}

在创建了NgxClock对象后，可以再次获取时间，与保存的m_time之差就是流逝的时间：

public:
ngx_time_t delta() const      //计算流逝的时间
{
auto t = now();            //获取当前时间

t.sec -= m_time.sec;       //计算差值
t.msec -= m_time.msec;
return t;
}

double elapsed() const       //返回浮点数格式的时间
{
auto t = delta();             //计算流逝的时间

return t.sec + t.msec * 1.0 / 1000;   //转换为double
}

复位计时器操作也很有用，它能够重置计时起点：

public:
void reset()          //复位计时器，非 const
{
m_time = now();        //重新获取当前时间
}

NgxClock也可以对ngx_sleep()等进行封装，这里不再列出代码，读者可参考GitHub资源。

用法

可以随时调用NgxClock的静态成员函数now()获取当前时间：

auto t = NgxClock::now();      //获取当前时间

当作计时器来使用时需要构造一个NgxClock对象开始计时，然后当操作结束时调用elapsed()得到计时结果：

NgxClock clock;          //构造对象，开始计时
...                           //执行某些操作
cout << clock.elapsed() << "s"<< endl;    //输出计时结果

3.6.6　C++封装日期

类NgxDatetime用来封装Nginx里日期相关的操作。

类定义

NgxDatetime的定义如下：

class NgxDatetime final        //Nginx的日期功能封装
{
public:
NgxDatetime() = default;
~NgxDatetime() = default;
public:
...             //成员函数见后
};

获取当天日期

获取当天日期是一个很常见的功能，实现也不难：调用ngx_time()得到时间戳，再转换为ngx_tm_t结构，就可以从它的成员得到年月日等值。

简单起见，我们只使用ngx_snprintf()生成“yyyy-mm-dd”格式的字符串，以ngx_str_t的形式输出：

public:
static std::time_t since()      //封装ngx_time()
{
return ngx_time();      //返回时间戳
}

static ngx_str_t today()      //获取当天日期
{
ngx_tm_t tm;         //ngx_tm_t 对象

ngx_localtime(since(), &tm);    //转换为本地时间

static u_char buf[20] = {};     //静态字符数组用于输出

auto p = ngx_snprintf(buf, 20,     //安全格式化，获取字符串末尾
"%d-%02d-%02d",    //格式是“yyyy-mm-dd”
tm.ngx_tm_year, tm.ngx_tm_mon, tm.ngx_tm_mday);

return ngx_str_t{       //返回ngx_str_t 结构
static_cast<std::size_t>(p - buf), buf};
}

因为日期字符串很短，所以我们用一个很小的静态数组来保存格式化的字符串。ngx_snprintf()的返回值指示了格式化结果的结束位置，可以计算得到字符串的长度生成ngx_str_t。[12]

如果需要其他格式的日期字符串，那么只要修改ngx_snprintf()里的格式字符串就可以了，但必须注意字符数组的大小，避免缓冲区溢出错误。

转换http字符串

NgxDatetime使用两个重载的http()函数实现字符串与时间戳的转换，代码比较简单，基本是直接调用Nginx的接口函数：

public:
static ngx_str_t http(std::time_t t = since()) //时间戳转日期字符串
{
static u_char buf[50] = {};      //静态字符数组用于输出

auto p = ngx_http_time(buf, t);     //转换为字符串

return ngx_str_t{static_cast<std::size_t>(p - buf), buf};
}

static std::time_t http(ngx_str_t& str)   //日期字符串转时间戳
{
return ngx_http_parse_time(str.data, str.len);
}

用法

NgxString里已经重载了C++流输出操作符，所以可以直接流输出today()和http()函数：

cout << NgxDatetime::today() << endl;      //输出今天日期
cout << NgxDatetime::http() << endl;      //输出http 格式gmt字符串

输出的结果是：

2015-04-24
Fri, 24 Apr 2015 04:05:22 GMT

3.7　运行日志

我们在1.3.3节初步了解了Nginx运行日志的配置[13]，现在来学习如何在我们自己的代码里记录运行日志。

3.7.1　结构定义

Nginx使用结构体ngx_log_t表示运行日志，定义如下：

// 定义在 core/ngx_core.h
typedef struct ngx_log_s         ngx_log_t;    //简化的类型定义

// 定义在 core/ngx_log.h
struct ngx_log_s {                                   //结构体定义
...                                              //暂不需要关心的内部成员
ngx_uint_t  log_level;                           //日志级别
ngx_log_t*  next;                                //日志对象链表指针
};

ngx_log_t有很多内部成员，可以实现非常灵活的日志功能，但就我们目前开发的HTTP功能来说并不需要额外的定制工作，直接使用ngx_log_t对象就可以了。

3.7.2　操作函数

函数ngx_log_error_core()用来记录日志，它支持可变参数：[14]

// 定义在 core/ngx_log.h
void ngx_log_error_core(
ngx_uint_t level, ngx_log_t *log, ngx_err_t err,
const char *fmt, ...);

ngx_log_error_core()使用ngx_log_t对象记录level级别的日志，字符串消息的语法与ngx_sprintf()相同。

日志参数

日志级别参数level取值为下列的宏，它们对应配置文件里的“debug|info|notice|warn|error|crit|alert|emerg”级别：

#define NGX_LOG_STDERR           0     //最高级别
#define NGX_LOG_EMERG            1
#define NGX_LOG_ALERT            2
#define NGX_LOG_CRIT             3
#define NGX_LOG_ERR              4     //常用级别
#define NGX_LOG_WARN             5     //常用级别
#define NGX_LOG_NOTICE           6
#define NGX_LOG_INFO             7
#define NGX_LOG_DEBUG            8     //最低级别

NGX_LOG_STDERR是比emerg还要高的错误级别，如果使用这个级别来记录日志，那么Nginx将直接输出日志信息到标准错误输出（通常是终端屏幕），而不写入日志文件。

我们常用的日志级别是NGX_LOG_ERR和NGX_LOG_WARN。

err参数表示系统调用失败返回的错误码，即errno，它定义在os/unix/ngx_errno.h中：

// 定义在 os/unix/ngx_errno.h
typedef int               ngx_err_t;

因为我们只调用Nginx接口，所以通常不会用到err参数，可以把它简单地置为0。

日志宏

函数ngx_log_error_core()只是记录日志，它并不检查日志级别决定是否记录日志，Nginx以宏的方式提供这个功能：

#define ngx_log_error(level, log, ...)      \
if ((log)->log_level >= level)        \
ngx_log_error_core(level, log, __VA_ARGS__)

可以看到，只有当消息的日志级别高于log对象级别（即消息的level值小）时才会调用ngx_log_error_core()记录日志。

实际开发过程中应该使用宏ngx_log_error来记录日志，尽量避免直接调用函数ngx_log_error_core()。

3.7.3　C++封装

我们使用模板类NgxLog来封装ngx_log_t和日志操作。

类定义

NgxLog的定义如下：

template<ngx_uint_t level = NGX_LOG_DEBUG>
class NgxLog final : public NgxWrapper<ngx_log_t>
{
public:
typedef NgxWrapper<ngx_log_t>   super_type;  //基本包装类
typedef NgxLog                  this_type;   //简化类型定义
...                                          //成员函数见后
};

NgxLog的模板参数比较特殊，使用了日志级别level，这样做是为了简化调用接口，可以用typedef定义出不同日志级别的log类型：

typedef NgxLog<NGX_LOG_DEBUG>   NgxLogDebug;    //debug级别
typedef NgxLog<NGX_LOG_INFO>    NgxLogInfo;     //info 级别
typedef NgxLog<NGX_LOG_WARN>    NgxLogWarning;  //warn 级别
typedef NgxLog<NGX_LOG_ERR>     NgxLogError;    //err 级别

构造函数

很多Nginx数据结构都有名字为log的成员用于记录日志，所以NgxLog实现了模板构造函数：

public:
NgxLog(ngx_log_t* l): super_type(l)    //直接使用 log 指针构造
{}

template<typename T>
NgxLog(T* x) : super_type(x->log)      //使用log成员构造
{}

但在处理HTTP请求时经常使用的ngx_http_request_t（见第7章）结构里却并没有直接的log成员，而是间接地通过connection成员持有，所以为了方便使用需要再实现一个重载构造函数：

NgxLog(ngx_http_request_t* r):       //使用connection 成员构造
NgxLog(r->connection)        //委托构造
{}

记录日志

成员函数print()封装了日志宏ngx_log_error：

public:
template<typename ... Args>       //可变模板参数列表
void print(const char* fmt, const Args& ... args) const
{
ngx_log_error(level, get(), 0, fmt, args...);
}

template<typename ... Args>       //可变模板参数列表
void print(ngx_err_t err, const Args& ... args) const
{
ngx_log_error(level, get(), err, args...);
}

print()有两种形式，第一种形式置err为0，第二种形式允许用户传入特定的errno。

用法

只要Nginx结构直接或者间接拥有ngx_log_t成员，我们就可以构造出NgxLog对象，调用它的print()记录运行日志，例如：

//假设已经有 ngx_http_request_t 对象r
NgxLogError log(r);         //使用别名类创建 NgxLog 对象
log.print("hello c++");        //记录一条运行日志

这种方式适合需要记录大量日志的场合，log对象可以重复使用。如果只是临时性地记录日志，那么也可以使用临时对象，在一条语句里完成对象的创建和日志记录操作：

NgxLogError(r).print("hello c++");     //构造临时对象同时写入运行日志

在error.log里的记录是：

2015/xx/xx 12:05:22 [error] 4745#0: *1 hello c++, ...

3.8　总结

“勿在浮沙筑高塔”，在Nginx上做二次开发，必须要从底层开始学习，所以本章涉及的都是最基本、最常用的数据结构，包括整数类型、内存池、字符串、时间日期和错误日志，它们是Nginx宫殿的基石。

我们先研究了Nginx使用的整数类型。Nginx定义了自己的一套整数类型，保证了跨平台的编译和运行。为了解决值未初始化的问题，Nginx“发明”了UNSET概念，用来确定值是否有效。

内存池是Nginx里非常重要的数据结构，它消灭了内存碎片和泄漏，提高了程序的运行效率，几乎所有的Nginx数据结构都会用到内存池，我们会在之后的章节里多次与它打交道，必须很好地掌握它的设计思想和用法。

字符串是HTTP处理时常用的对象，Nginx使用引用的方式降低了内存复制的成本，可以高效地操作字符串。但使用时需要时刻注意ngx_str_t不同于我们之前的字符串概念，它是“只读”的，不能随意更改它的内容，如果确有必要可以使用内存池做一份拷贝再修改。

时间和日期也是HTTP处理时常见的对象，Nginx为此提供了比较全面的操作，可以获取当前时间和日期，也可以转换成各种格式的字符串。

最后本章介绍了如何在Nginx里记录日志，它是检查运行状况和排查错误的必备工具。

Nginx使用C语言实现了整个系统，但其中却蕴含着面向对象的思想，所以我们使用包装外观模式以C++实现了这些数据结构对应的包装类，集成了数据结构和对应的操作，增强了类型的内聚性，同时没有效率的损失。我们还充分利用了C++11和Boost程序库的特性，例如异常、类型自动推导、委托构造、可变模板参数列表等，让类的实现和接口更干净清楚，易用性和可维护性都比原始的C接口更好。读者可以从这些代码里仔细体会C++的现代编程风格和思想。

[1] 实际上<ngx_http.h>内部还包含了core目录里的<ngx_config.h>和<ngx_core.h>，它们定义了大部分的基本数据结构。

[2] ignore_unused的用法可参考附录A推荐书目[3]。

[3] intptr_t和uintptr_t是C/C++标准里的两个特殊整数类型，它们是大小足够容纳指针的整数类型，可以简单地理解为指针的整数形式。

[4] 实际上这些“函数”都是宏，但作用上是函数，故作者有时称它们为函数，请读者不要误解。

[5] 实际上这也是一种“初始化”，只不过是初始化为一个表示无效含义的值。

[6] 有的读者可能会顾虑异常的性能，事实上现在的C++编译器的异常机制已经有了很大的优化，虽然还不能达到零成本，但可以说是足够小，完全不需要担心。

[7] boost::exception和boost::string_ref的详细用法可参考附录A推荐书目[3]。

[8] 这种用法很像boost::shared_ptr<void>。

[9] 有些简单操作如ngx_strcmp、ngx_strstr、ngx_strlen等直接调用了C标准函数。

[10] 如果觉得Nginx自身的时间日期功能不够，那么还可以使用boost.chrono和boost.date_time库。

[11] 这里之所以使用Clock是因为Nginx内部还有定时器功能，Timer的名字给定时器更合适。

[12] today()函数可以进一步优化，采用与Nginx类似的缓存机制，在一天的时间里只执行一次ngx_snprintf()，读者可以试试。

[13] 再强调一下，本书讨论的日志是Nginx内部的运行错误日志，实现在ngx_errlog_module中，而不是记录HTTP请求的访问日志。

[14] Nginx还提供其他日志工具，例如类似printf()的ngx_log_stderr()、在配置时记录日志的ngx_conf_log_error()和专门记录调试日志的ngx_log_debug()，本书暂不讨论，读者可自行研究。

第4章　Nginx高级数据结构

C语言是一种小而简单的计算机语言，不仅语法简单，而且标准库也很简单，缺少实际开发工作中常用的数据结构和算法。这既是缺点也是优点，缺点是程序员必须自己实现所需的数据结构，优点则是给予了程序员最大的自由，允许他们充分发挥聪明才智，创造出各种各样令人惊讶不已的艺术品。

Nginx就是C语言这一优点的杰出体现，它灵活运用了内存池和指针，针对Web服务器应用场景设计了数个高效的数据结构，包括动态数组、单向链表、双端队列等。Nginx使用它们高效地处理各种数据，在源码里随处可见这些数据结构的身影。

本章将详细剖析ngx_array_t、ngx_list_t、ngx_queue_t等高级数据结构，并封装为更容易使用的C++标准容器风格。

4.1　动态数组

C语言的内置数组十分简陋，只为原始内存数据增加了一层薄薄的抽象，无法动态增长，使用上存在着诸多限制。

Nginx借鉴了C++标准容器std::vector，以C语言实现了“泛型的”、可以在运行时随意变换大小的动态数组ngx_array_t，而且由于使用ngx_pool_t分配内存保证没有内存泄漏。

4.1.1　结构定义

ngx_array_t表示一块连续的内存，其中顺序存放着数组元素，概念上和原始数组很接近，它的定义是：

// 定义在 core/ngx_array.h
typedef struct {
void*         elts;       //数组的内存位置，即数组首地址
ngx_uint_t    nelts;      //数组当前的元素数量
size_t        size;       //数组元素的大小
ngx_uint_t    nalloc;     //数组可容纳的最多元素数量
ngx_pool_t*   pool;       //数组使用的内存池
} ngx_array_t;         //动态数组类型定义

ngx_array_t里的成员elts就是C语言的原始数组，定义为void*指针，所以是“泛型”的，在实际使用时需要转换为真正的类型。

elts之外的四个成员是动态数组的“元信息”，Nginx利用这些信息来管理数组，可以对比标准容器std::vector来理解：

- nelts：数组内元素的数量，相当于vector.size()；

- size：元素的大小，相当于sizeof<T>；

- nalloc：数组的总容量，相当于vector.capacity();

- pool:数组使用的内存池，相当于vector的allocator。

ngx_array_t的内存布局如图4-1所示。

图4-1　ngx_array_t的内存布局

ngx_array_t的nalloc虽然表示了总容量，但并不意味着只能容纳nalloc个元素。如果数组内的元素不断增加，当nelts>nalloc时将会引发数组扩容，ngx_array_t会向内存池pool申请一块两倍原大小的空间——这个策略是和std::vector一样的。

但ngx_array_t的扩容成本较高，不仅要重新分配内存，而且要拷贝原有元素到新的位置，所以我们在使用ngx_array_t时最好一次性分配足够的空间，尽量避免动态扩容。

4.1.2　操作函数

ngx_array_t的操作比较简单，使用ngx_array_t.elts就可以访问数组里的元素，不过之前必须要转换为实际的元素类型，例如：

auto p = reinterpret_cast<T*>(arr.elts);    //类型转换
cout << p[0] << endl;         //使用operator[]访问元素

在访问数组元素时需要注意越界问题，ngx_array_t没有越界检查，用户必须自行确保数组索引的有效性，也就是说i<nelts，否则会导致未定义错误。

ngx_array_t还提供创建、销毁和添加元素等常用的操作函数。

// 定义在 core/ngx_array.h
ngx_array_t* ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size);
void         ngx_array_destroy(ngx_array_t *a);

ngx_array_create()函数使用内存池创建一个可容纳n个大小为size元素的数组，即分配了一块n*size大小的内存块，ngx_array_destroy()函数则“销毁”动态数组，归还分配的内存。

// 定义在 core/ngx_array.h
void*   ngx_array_push(ngx_array_t *a);
void*   ngx_array_push_n(ngx_array_t *a, ngx_uint_t n);

这两个函数用来向数组添加元素，用法比较特别，它们返回的是一个void*指针，用户必须把它转换为真正的元素类型再操作。不直接使用ngx_array_t.elts操作的原因是防止数组越界，函数内部会检查当前数组容量自动扩容。

4.1.3　C++封装

在NgxPool里可以封装ngx_array_create()函数，创建动态数组：[1]

class NgxPool final : public NgxWrapper<ngx_pool_t>
{
public:
...                                              //其他成员函数
template<typename T>
ngx_array_t* array(ngx_uint_t n = 10) const       //默认容纳 10个元素
{
auto p = ngx_array_create(get(), n, sizeof(T)); //使用内存池创建数组
NgxException::require(p);        //检查空指针

return p;           //返回数组
}
};

然后我们实现ngx_array_t的C++封装类NgxArray。

类定义

NgxArray使用2.5节的NgxWrapper代理ngx_array_t，定义如下：

template<typename T>         //T是数组的元素类型
class NgxArray final : public NgxWrapper<ngx_array_t>
{
public:
typedef NgxWrapper<ngx_array_t>  super_type;  //简化类型定义
typedef NgxArray                 this_type;
public:
...                                      //成员函数见后
};

字符串数组是Nginx里常见的数据结构，为了方便使用，可以定义一个特化类型：

typedef NgxArray<ngx_str_t> NgxStrArray;     //字符串数组类型

构造与析构

NgxArray可以从内存池构造，或者从一个已经存在的ngx_array_t结构构造：

public:
NgxArray(const NgxPool& p, ngx_uint_t n = 10):
super_type(p.array<T>(n))       //调用内存池创建数组
{}

NgxArray(ngx_array_t* arr):super_type(arr)   //指针参数
{}

NgxArray(ngx_array_t& arr):super_type(arr)   //引用参数
{}

~NgxArray() = default;         //默认析构函数

访问元素

成员函数elts()获取ngx_array_t.elts指针，并转换为元素类型，注意它是私有的：

private:
T* elts() const          //获取元素数组指针
{
return reinterpret_cast<T*>(get()->elts);   //类型转换
}

成员函数size()获取数组的大小，重载操作符[]来提供与原始数组一致的访问接口，并且还加入了越界检查，更加安全：

public:
ngx_uint_t size() const        //获得数组大小
{
return get()?get()->nelts:0;     //防止空指针
}

T& operator[](ngx_uint_t i) const     //重载操作符[]
{
NgxException::require(i < size()&& get());  //越界、空指针检查

return elts()[i];        //调用elts()访问元素
}

添加元素

我们使用一个辅助函数prepare()封装ngx_array_push()，它返回新数组元素的引用，然后push()就可以真正添加元素：[2]

public:
T& prepare() const
{
auto tmp = ngx_array_push(get());   //添加元素

NgxException::require(tmp);      //检查空指针

return *reinterpret_cast<T*>(tmp);     //返回元素的左引用
}

void push(const T& x) const
{
prepare() = x;          //左引用可以直接赋值
}

其他操作

遍历数组元素是很常见的操作，使用for可以很容易实现，但一个泛型的遍历操作可能更有用，可以避免反复手写for循环。

成员函数visit()接受一个函数对象，对数组里的每一个元素执行该函数，类似标准算法for_each：

public:
template<typename V>
void visit(V v) const       //访问数组里的所有元素
{
auto p = elts();         //获得数组首地址

for(ngx_uint_t i = 0;i < size(); ++i)    //遍历数组
{
v(p[i]);           //执行函数对象
}
}

NgxArray还可以有更多的功能，比如实现迭代器、符合标准容器定义、支持范围for等，读者可以根据需要参考后续章节自行实现。

用法

NgxArray的接口很小，也很容易使用，如果读者熟悉std::vector或者boost::array，那么就能很快掌握。

示范NgxArray用法的代码如下：

NgxArray<ngx_int_t> arr(pool);     //假设已有内存池，创建数组
assert(arr.size() == 0);      //初始数组为空

arr.push(42);         //添加元素
arr.push(253);
assert(arr[0] == 42);        //访问元素

arr.visit(         //遍历元素
[](ngx_int_t x){        //使用lambda表达式
std::cout << x << ",";     //打印元素值，输出 42,253,
});

4.2　单向链表

链表也是一种常用的数据结构，与数组的连续内存不同，它使用指针来连接各个节点，所以会有额外的存储成本，但理论上来说可以存储任意数量的元素。

Nginx的单向链表ngx_list_t设计融合了一些ngx_array_t的特点，在一个节点里存储多个元素，有效地降低了链表的存储成本。

4.2.1　结构定义

ngx_list_part_t结构定义了链表的节点：

// 定义在 core/ngx_list.h
typedef struct ngx_list_part_s  ngx_list_part_t;

struct ngx_list_part_s {
void*              elts;      //数组元素指针
ngx_uint_t         nelts;     //数组里的元素数量
ngx_list_part_t*   next;      //下一个节点的指针
};

它类似ngx_array_t，是一个简单的数组，也可以“泛型”存储数据，但少了一些成员，还有一个next指针指向链表里的下一个节点。

ngx_list_t结构定义了链表（实际上是头节点+元信息）：

// 定义在 core/ngx_list.h
typedef struct {
ngx_list_part_t*   last;      //链表的尾节点
ngx_list_part_t    part;      //链表的头节点
size_t             size;      //链表存储元素的大小
ngx_uint_t         nalloc;    //每个节点能够存储元素的数量
ngx_pool_t*        pool;      //链表使用的内存池
} ngx_list_t;

对比ngx_array_t可以看到，ngx_list_t的成员size、nalloc和pool的含义是相同的，确定了节点里数组的元信息。可以这么说，链表里的每个节点就是一个简化的ngx_array_t数组结构。

ngx_list_t的part成员是链表的头节点（不是指针），part.next指向链表里的第二个节点，而last则指向链表的尾节点。

ngx_list_t的内存布局如图4-2所示。

图4-2　ngx_list_t的内存布局

4.2.2　操作函数

函数ngx_list_create()使用内存池创建链表，接口与ngx_array_create()类似，每个节点可容纳n个大小为size的元素：

// 定义在 core/ngx_list.h
ngx_list_t *ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size);

向链表里添加元素的操作ngx_list_push()也很类似，同样返回一个void*指针，需要转型操作：

// 定义在 core/ngx_list.h
void *ngx_list_push(ngx_list_t *list);

由于ngx_list_t是单向链表，所以我们不能使用索引来访问元素，必须从头节点开始用next指针逐个访问节点，这样在每个节点里才能用整数索引获取节点数组里的元素。

Nginx在core/ngx_list.h里以注释的形式给出了访问列表元素的示例代码，如下：

part = &list.part;         //获取链表的头节点
data = part->elts;         //获得节点内数组地址

for (i = 0 ;; i++) {       //开始遍历链表

if (i >= part->nelts) {       //检查是否节点数组越界
if (part->next == NULL) {     //下一个节点指针
break;          //指针为空表示链表结束
}

part = part->next;       //跳到下一个节点
data = part->elts;       //下一个节点的数组地址
i = 0;                  //数组索引初始化
}
...  data[i] ...       //在本节点内访问元素
}            //for 循环结束

4.2.3　C++迭代器

对于ngx_list_t这样的单向链表来说访问元素是非常重要的操作，在C++里可以用迭代器的概念来表示，所以我们为ngx_list_t实现一个迭代器类NgxListIterator。

早期实现完整的、符合标准的迭代器要花费很多力气，不过在boost.iterator库的boost::iterator_facade工具类帮助下，这个工作就变得轻而易举了。[3]

类定义

NgxListIterator使用boost::iterator_facade简化迭代器的定义：

#include <boost/iterator/iterator_facade.hpp>    //Boost库头文件
#include <boost/core/explicit_operator_bool.hpp>

template<typename T>
class NgxListIterator final :
public boost::iterator_facade<      //Boost工具类
NgxListIterator<T>, T,      //迭代器的值类型
boost::single_pass_traversal_tag>  //单向遍历
{
public:
typedef boost::iterator_facade<...>    super_type;   //简化类型定义
typedef typename super_type::reference  reference;
public:
NgxListIterator(ngx_list_t* l):        //构造函数
m_part(&l->part),           //初始化头节点
m_data(static_cast<T*>(m_part->elts))    //初始化数组指针
{}

NgxListIterator()  = default;        //默认构造函数
~NgxListIterator() = default;        //默认析构函数
private:
ngx_list_part_t*   m_part = nullptr;      //节点指针
T*                 m_data = nullptr;      //节点内数组指针
ngx_uint_t         m_count = 0;        //节点内数组索引
public:
...              //成员函数见后
};

NgxListIterator使用三个成员变量m_part、m_data和m_count来遍历链表，它们分别对应Nginx示例代码里的part、data和i，构造函数里m_part和m_data指向头节点。

NgxListIterator的boost::iterator_facade用法比较复杂，它使用模板参数在编译期帮助我们实现了迭代器的大部分操作和类型定义。如果不是特别熟悉迭代器概念和模板元编程可能会难以理解，不过这并不影响使用。

迭代器核心操作

boost::iterator_facade要求单向迭代器必须实现dereference()、increment()和equal()三个操作，然后利用这些函数实现operator++等迭代器接口：

private:
friend class boost::iterator_core_access;   //必需的友元声明

reference dereference() const      //解引用操作
{
NgxException::require(m_data);     //检查空指针
return m_data[m_count];        //访问节点内数组元素
}

void increment()          //前进迭代器
{
if(!m_part || !m_data)       //检查空指针
{   return; }

++m_count;           //仿造Nginx示例代码

if(m_count >= m_part->nelts)       //检查是否节点数组越界
{
m_count = 0;          //数组索引初始化
m_part = m_part->next;       //跳到下一个节点

m_data = m_part?         //是否是尾节点？
static_cast<T*>(m_part->elts):  //下一个节点的数组地址
nullptr;         //否则数组地址是空指针
}
}

bool equal(NgxListIterator const& o) const  //比较两个迭代器
{
return m_part == o.m_part &&      //必须是三个因素全相等
m_data == o.m_data &&
m_count == o.m_count;
}

这里我们需要稍微注意increment()函数，它基本仿造了Nginx示例代码的for循环部分，但逻辑有小变化：因为迭代器遍历不会break，所以当到达尾节点时迭代器的m_part等指针都会变成0表示结束，如果在这个已经结束的迭代器上继续前进可能发生未定义错误。

迭代器的有效性

为了防止迭代器结束时操作导致的错误，NgxListIterator增加了bool转型操作符，用来检查空指针的情况：

public:
BOOST_EXPLICIT_OPERATOR_BOOL()      //Boost显式bool 转型

bool operator!() const        //检查指针是否失效
{
return !m_part || !m_data || !m_part->nelts;   //指针为空等情况则失效
}

宏BOOST_EXPLICIT_OPERATOR_BOOL是Boost库提供的一个小工具，它利用operator!()实现安全的bool转型操作。[4]

4.2.4　C++封装链表

NgxList利用NgxWrapper封装了ngx_list_t，创建链表的ngx_list_create()函数同样可以在NgxPool里封装，这里从略。

类定义

NgxList的实现与NgxArray类似，定义如下：

template<typename T>         //T是数组的元素类型
class NgxList final : public NgxWrapper<ngx_list_t>
{
public:
typedef NgxWrapper<ngx_list_t>  super_type;  //简化类型定义
typedef NgxList      this_type;
public:
NgxList(ngx_list_t* l):super_type(l)     //指针参数
{}

NgxList(ngx_list_t& l):super_type(&l)    //引用参数
{}

~NgxList() = default;         //默认析构函数
public:
...             //成员函数见后
};

添加元素

NgxList同样使用函数prepare()和push()添加元素：

public:
T& prepare() const
{
auto tmp = ngx_list_push(get());    //添加元素

NgxException::require(tmp);      //检查空指针

return *reinterpret_cast<T*>(tmp);     //返回元素的左引用
}

void push(const T& x) const
{
prepare() = x;          //左引用可以直接赋值
}

迭代器操作

NgxList使用容器标准的begin()和end()函数产生迭代器：

public:
typedef NgxListIterator<T>   iterator;     //迭代器类型定义
typedef const iterator    const_iterator;

iterator begin() const        //迭代器起点
{
return iterator(get());       //使用链表指针初始化
}

iterator end() const        //迭代器终点
{
return iterator();       //指针为空的迭代器，逾尾
}

使用迭代器可以很方便地遍历链表，例如实现元素的查找操作：

template<typename Predicate>
iterator find(Predicate p) const    //使用谓词函数对象查找
{
auto iter = begin();       //迭代器开始
for(; iter; ++iter)         //结束条件是迭代器无效
{
if(p(*iter))         //谓词检查元素
{
return iter;         //满足条件则返回迭代器
}
}

return end();         //未找到则返回逾尾迭代器
}

用法

经过NgxList的封装后，Nginx的单向链表变得非常容易使用，很像标准容器std::forward_list：

NgxList<ngx_int_t> l(r, 1);       //节点里的数组只保存一个元素

l.push(2000);          //添加三个元素
l.push(2008);
l.push(2015);

assert(*l.begin() = 2000);        //取第一个元素

auto p = l.find(         //查找元素，返回迭代器
[](ngx_int_t x){         //lambda 表达式
return x == 2015;       //查找元素 2015
});
assert(p != l.end());       //必定查找到
assert(*p == 2015);

for(auto p = l.begin();p;++p)       //遍历链表，也可以用范围 for
{
std::cout << *p << ",";       //输出2000,2008,2015,
}

4.3　双端队列

双端队列是在两端都可以插入或删除元素的数据结构，在Nginx里它被实现为双向循环链表ngx_queue_t，类似标准容器std::list。

借用Boost程序库的概念，前面的ngx_array_t和ngx_list_t属于非侵入式容器，元素无须改动即可加入容器，而ngx_queue_t则不同，它是侵入式容器，必须把ngx_queue_t作为元素的一个成员，然后才能放入队列，与boost.intrusive库很接近。

4.3.1　结构定义

ngx_queue_t的定义非常简单，不包含任何元素信息，只有链表的前后两个指针：

typedef struct ngx_queue_s  ngx_queue_t;

struct ngx_queue_s {
ngx_queue_t  *prev;        //前驱指针
ngx_queue_t  *next;        //后继指针
};

ngx_queue_t的使用方式类似boost.intrusive库的成员挂钩（member hook），结构体里需要添加它作为成员，例如：

struct Xinfo         //一个可放入队列的数据结构
{
int x = 0;          //数据结构携带的信息
ngx_queue_t queue;        //ngx_queue_t 成员，名字任意
};

通过这种方式，ngx_queue_t“侵入”了数据结构的内部，为数据结构增加了双向链表所需的两个链接指针。

ngx_queue_t的内存布局如图4-3所示.

图4-3　ngx_queue_t的内存布局

数据结构里可以有不止一个ngx_queue_t成员，这意味着它可以同时属于多个不同的双向链表。

4.3.2　操作函数

ngx_queue_t使用一个头节点来表示队列，这个头节点可以是单纯的ngx_queue_t结构，不存储任何信息，但单从ngx_queue_t结构来看，它与普通的数据节点并无区别，我们只能根据实际情况区分。

Nginx提供数个函数宏来调整ngx_queue_t指针操作队列，有的只对头节点有意义，参数的名字是h（head），但语法层面无法做到强制要求，这是它们的一个缺点。

头节点操作

函数宏ngx_queue_init()初始化头节点，把两个指针都指向自身：

#define ngx_queue_init(q)   \      //初始化队列
(q)->prev = q;         \
(q)->next = q

函数宏ngx_queue_sentinel()返回节点自身，对于头节点来说相当于“哨兵”的作用：

#define ngx_queue_sentinel(h) (h)      //队列操作的“哨兵”

ngx_queue_empty()检查头节点的前驱指针，判断是否是空队列：

#define ngx_queue_empty(h)  \      //队列是否为空
(h == (h)->prev)

函数宏ngx_queue_insert_head()和ngx_queue_insert_tail()用来向队列的头尾插入数据节点：

#define ngx_queue_insert_head(h, x)    //插入队首
#define ngx_queue_insert_tail(h, x)    //插入队尾

函数宏ngx_queue_head()和ngx_queue_last()获取队列的头尾指针，可以用它们来实现队列的正向或反向遍历，直到遇到头节点（ngx_queue_sentinel）停止：

#define ngx_queue_head(h) (h)->next    //获得队首指针
#define ngx_queue_last(h) (h)->prev    //获得队尾指针

函数ngx_queue_sort()使用一个比较函数指针对队列元素排序，但效率不是很高：

void ngx_queue_sort(ngx_queue_t *queue,
ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *));

此外，ngx_queue_add(h, n)可以合并两个队列，ngx_queue_split(h, q, n)可以拆分队列。

数据节点操作

数据节点本质上与头节点并无区别，所以很多操作代码是相同的。

函数宏ngx_queue_insert_after()在节点的后面插入数据，它其实就是ngx_queue_insert_head：

#define ngx_queue_insert_after   ngx_queue_insert_head

在节点前插入数据可以用宏ngx_queue_insert_tail()，很可惜，它没有被重新定义为ngx_queue_insert_before。

节点的前驱指针和后继指针可以使用函数宏ngx_queue_next()和ngx_queue_prev()获得，它们与ngx_queue_head()和ngx_queue_last()是一样的：

#define ngx_queue_next(q) (q)->next    //获得下一个节点的指针
#define ngx_queue_prev(q) (q)->prev    //获得前一个节点的指针

函数宏ngx_queue_remove()可以“删除”当前节点，实际上它只是调整了节点的指针，把节点从队列里摘除，并没有真正从内存里删除数据：

#define ngx_queue_remove(x)      \     //从队列里“删除”节点
(x)->next->prev = (x)->prev; \
(x)->prev->next = (x)->next

Nginx采用了一种巧妙（或者说有些hack）的方法，可以从作为数据成员的ngx_queue_t结构访问到完整的数据节点：

#define ngx_queue_data(q, type, link) \    //获取节点数据
(type *) ((u_char *) q - offsetof(type, link)) //返回结构体指针

函数宏ngx_queue_data()的参数分别是ngx_queue_t指针、节点类型和ngx_queue_t成员名，它利用C标准宏offsetof计算成员在结构体里的偏移量[5]，然后再倒着减去偏移量，这样就得到了结构体的真正地址。

这种获取数据的方法在C语言里是毫无问题的，因为C结构的内存布局是平坦的（Plain），但在C++里则需要小心，只有符合POD（Plain Old Data）的结构体才能这样操作，否则offsetof计算得到的偏移量可能有误，无法获取正确的结构体地址。

结构分析

Nginx的队列ngx_queue_t虽然结构很简单，但应用是比较复杂的，很多概念都混合在了一起，如果理解不透彻很容易误用。

通过上面的分析，我们可以把它们分解为节点、迭代器和队列容器三个概念：节点保存数据，迭代器遍历数据，而队列容器则是头节点。这三个概念可以使用C++封装为不同的类，达到解耦的目的。

4.3.3　C++节点

封装队列节点必须要解决两个问题：

- 已知结构体T，如何获取T内的ngx_queue_t成员；

- 已知结构体T和ngx_queue_t指针，如何获取T内的ngx_queue_t成员偏移量，进而得到对象地址。

Nginx源码把这两个问题完全抛给了用户，要求用户自己手写结构体内的成员。不能说它“不负责”，这完全是因为C语言自身的能力限制。但在C++里，使用强大的模板技术，再加上仔细的设计，我们完全可以抛开宏，在编译期就得到这些信息，从而简化客户代码。

类定义

NgxQueueNode封装了ngx_queue_t的数据节点操作，它在模板参数列表里解决了刚才的两个问题，定义如下：

template<typename T,
ngx_queue_t T::* q = &T::queue,      //成员变量指针类型
std::size_t offset =                 //成员偏移量
(std::size_t)&(((T*)0)->*q)     //模仿offsetof 宏
>
class NgxQueueNode final : public NgxWrapper<ngx_queue_t>
{
public:
typedef NgxWrapper<ngx_queue_t>  super_type;  //简化类型定义
typedef NgxQueueNode    this_type;
public:
NgxQueueNode(ngx_queue_t* ptr): super_type(ptr) //直接指针构造
{}

NgxQueueNode(T& x): super_type(x.*q)     //获取成员变量地址
{}

NgxQueueNode(T* x): super_type(x->*q)     //获取成员变量地址
{}

~NgxQueueNode() = default;        //默认析构函数
public:
...             //成员函数见后
};

NgxQueueNode使用了C++里比较少用的特性——成员变量指针，形式是“type T::*”，如果有类型T的实例x，那么可以用x.*ptr方式访问x的成员变量。所以，只要用户在模板参数里以“&T::member_name”的形式给出成员变量指针，那么就可以使用这个指针“进入”结构体内部得到ngx_queue_t的位置。

有了成员变量指针，我们再参考offsetof宏的实现原理，把0强制转换为T*类型，访问其成员，就能够计算出成员的偏移量（同样要求T是POD类型）。

操作函数

NgxQueueNode获取节点数据的操作和宏ngx_queue_data()很相似，但它已经通过模板参数在编译期得到了数据节点T里与ngx_queue_t相关的信息，所以完全不需要用户的参与：

public:
T& data() const          //获取节点数据
{
return *(T*)((u_char*)(get()) - offset);   //返回引用
}

其他数据节点操作都很简单，直接调用Nginx的函数宏：

public:
NgxQueueNode next() const      //下一个节点
{
return ngx_queue_next(get());
}

NgxQueueNode prev() const      //前一个节点
{
return ngx_queue_prev(get());
}
public:
void append(T& v) const        //插入后继节点
{
ngx_queue_insert_after(get(), &(v.*q));
}

void remove() const         //删除当前节点
{
ngx_queue_remove(get());
}

需要注意ngx_queue_insert_after()里成员变量指针的调用方式，因为&操作符的优先级高，所以获取成员变量的表达式“v.*q”必须要用圆括号括起来。

4.3.4　C++迭代器

ngx_queue_t是双向链表，用来遍历它的迭代器也必然是双向的，我们仍然使用boost::iterator_facade来辅助定义迭代器，但迭代器的tag需要改用boost::bidirectional_traversal_tag。

类定义

NgxQueueIterator封装了ngx_queue_t的数据节点操作，定义如下：

template<typename T>          //T是 NgxQueueNode
class NgxQueueIterator final :
public boost::iterator_facade<
NgxQueueIterator<T>, T,
boost::bidirectional_traversal_tag>  //双向迭代器
{
public:
typedef boost::iterator_facade<...> super_type;  //简化类型定义
typedef NgxQueueIterator    this_type;

typedef typename super_type::reference reference;  //引用类型
public:
NgxQueueIterator(ngx_queue_t* p): m_cur(p)    //初始化指针
{}

NgxQueueIterator() = default;        //默认构造函数
~NgxQueueIterator() = default;
public:
...              //成员函数见后
private:
ngx_queue_t*  m_cur = nullptr;        //迭代用的指针
mutable T  m_proxy{m_cur};      //代理节点对象
};

为了方便使用，NgxQueueIterator模仿了C++的std::vector<bool>，内部构造了一个NgxQueueNode对象作为节点的代理，在解引用时用户可以用这个代理对象直接访问真正的节点数据。

迭代器核心操作

双向迭代器除dereference()、increment()和equal()以外还需要提供decrement()，实现反向迭代功能：

private:
friend class boost::iterator_core_access;    //必需的友元声明

reference dereference() const     //解引用操作，返回代理对象
{
m_proxy = m_cur;         //拷贝赋值数据节点
return m_proxy;          //返回节点对象的引用
}

void increment()         //前进迭代器
{
m_cur = ngx_queue_next(m_cur);
}

void decrement()         //后退迭代器
{
m_cur = ngx_queue_prev(m_cur);
}

bool equal(this_type const& o) const   //比较两个迭代器
{
return m_cur == o.m_cur;
}

NgxQueueIterator的解引用操作比较特殊，它并不直接返回迭代器内部数据（因为m_cur其实是ngx_queue_t，毫无意义），而是返回NgxQueueNode对象作为代理。

4.3.5　C++封装队列

NgxQueue封装了ngx_queue_t的队列操作，它实际上代理了队列的头节点。

类定义

NgxQueue的模板参数与NgxQueueNode相同，也需要计算类型的成员变量指针和偏移量：

template<typename T,
ngx_queue_t T::* q = &T::queue,    //成员变量指针类型
std::size_t offset =          //成员偏移量
(std::size_t)&(((T*)0)->*q)     //模仿offsetof 宏
>
class NgxQueue final : public NgxWrapper<ngx_queue_t>
{
public:
typedef NgxWrapper<ngx_queue_t>   super_type; //简化类型定义
typedef NgxQueueNode<T, q, offset>  node_type;  //节点类型定义
typedef NgxQueue       this_type;
public:
NgxQueue(ngx_queue_t& v): super_type(&v)   //引用构造
{}

NgxQueue(ngx_queue_t* ptr): super_type(ptr)  //指针构造
{}

NgxQueue(T& x): super_type(x.*q)     //获取成员变量地址
{}

NgxQueue(T* x): super_type(x->*q)      //获取成员变量地址
{}

~NgxQueue() = default;         //默认析构函数
public:
...             //成员函数见后
};

NgxQueue的模板参数、构造函数与NgxQueueNode基本相同，这并不奇怪，因为它们代理的是ngx_queue_t，都表示队列里的节点，只不过两者的作用不同：NgxQueue是头节点，而NgxQueueNode是数据节点。

基本操作

NgxQueue的基本操作有初始化、判断是否空、取首尾节点等：

public:
void init() const       //初始化队列
{
ngx_queue_init(get());
}

bool empty() const        //队列是否为空
{
return ngx_queue_empty(get());
}
public:
node_type front() const       //获得队首节点
{
return ngx_queue_head(get());
}

node_type back() const       //获得队尾节点
{
return ngx_queue_last(get());
}

当队列为空时，front()和back()会返回无效的NgxQueueNode对象，可以用operator bool来检查。

迭代器操作

NgxQueue支持双向遍历，所以除begin()和end()函数外还有rbegin()和rend()函数，后两个函数返回逆向迭代器：

public:
//迭代器和逆向迭代器类型定义
typedef NgxQueueIterator<node_type>   iterator;
typedef boost::reverse_iterator<iterator>   reverse_iterator;

typedef const iterator        const_iterator;
typedef const reverse_iterator      const_reverse_iterator;

iterator begin() const        //正向迭代器起点
{
return iterator(ngx_queue_head(get()));
}

iterator end() const        //正向迭代器终点
{
return iterator(ngx_queue_sentinel(get())); //终点即头节点
}

reverse_iterator rbegin() const     //逆向迭代器起点
{
return reverse_iterator(end());     //适配正向迭代器
}

reverse_iterator rend() const     //逆向迭代器终点
{
return reverse_iterator(begin());   //适配正向迭代器
}

这里我们使用了Boost程序库的另一个迭代器工具boost::reverse_iterator，它可以把一个符合规范的正向迭代器适配成逆向迭代器。

添加元素

NgxQueue的成员函数insert()和append()分别向队列两端添加节点：

public:
void insert(T& v) const        //插入队首
{
ngx_queue_insert_head(get(), &(v.*q));
}

void append(T& v) const        //插入队尾
{
ngx_queue_insert_tail(get(), &(v.*q));
}

使用迭代器表示的位置可以在队列中间插入节点：

void insert(const iterator& iter, T& v) const   //插入迭代器位置之后
{
iter->append(v);
}

其他操作

NgxQueue还可以封装ngx_queue_add()、ngx_queue_sort()等函数，限于篇幅不再罗列，读者可参考GitHub资源。

用法

示范NgxQueue用法的代码如下：

typedef NgxQueue<Xinfo> queue_type;   //队列类型定义，成员变量指针默认
ngx_queue_t h;          //队列的头节点，无数据

queue_type q(h);        //代理队列头节点
q.init();          //初始化队列
assert(q.empty());        //此时队列为空

Xinfo arr[3];         //创建三个数据节点
arr[0].x = 100;
arr[1].x = 200;
arr[2].x = 300;

q.insert(arr[0]);         //添加到队列头
q.insert(arr[1]);         //添加到队列头
q.append(arr[2]);         //添加到队列尾
assert(q.front().data().x == 200);    //访问队首元素

for(auto& x : q)        //范围for遍历队列
{
cout << x.data().x << "-";     //输出200-100-300-
}

auto last = q.back();      //获取队尾元素
last.remove();         //删除队尾元素

for(auto& x : q)        //再次范围 for 遍历队列
{
cout << x.data().x << "-";     //输出200-100-
}

BOOST_REVERSE_FOREACH(auto& x, q)     //用 boost.foreach 反向遍历
{
cout << x.data().x << ",";     //输出100,200,
}

使用NgxQueue必须定义好它的模板参数，最重要的是ngx_queue_t成员变量指针，如果成员的名字是默认的queue就可以省略，否则必须以&T::member的形式给出。例如对于结构ngx_resolver_t：

typedef struct {
...            //其他成员
ngx_queue_t      name_resend_queue;   //ngx_queue_t 成员
} ngx_resolver_t;

NgxQueue的模板参数就是：

typedef NgxQueue<
ngx_resolver_t,&ngx_resolver_t::name_resend_queue> queue_type;

4.4　缓冲区

作为Web服务器，Nginx要频繁地收发处理大量的数据，这些数据有时是连续的内存块，有时是多个分散内存块，甚至有时数据过大，内存无法存放，只能保存成磁盘文件。

ngx_str_t结构可以表示内存块，但它过于简单，不能应对这些复杂的场景，所以Nginx实现了ngx_buf_t和ngx_chain_t结构，专门描述数据缓冲区。

本节介绍ngx_buf_t，稍后的4.5节介绍ngx_chain_t。

4.4.1　结构定义

ngx_buf_t表示一个单块的缓冲区，既可以是内存也可以是文件。它的结构比较复杂，可以分成两个部分：缓冲区信息和标志位信息，下面的介绍省略了一些不太常用的数据成员（如shadow、recycled等）。

缓冲区信息

ngx_buf_t的缓冲区信息定义如下：

// 定义在 core/ngx_buf.h
typedef void *             ngx_buf_tag_t;  //void*类型定义
typedef struct ngx_buf_s   ngx_buf_t;      //struct 类型定义

struct ngx_buf_s {
u_char*           pos;            //内存数据的起始位置
u_char*           last;           //内存数据的结束位置
off_t             file_pos;       //文件数据的起始偏移量
off_t             file_last;      //文件数据的结束偏移量

u_char*           start;          //内存数据的上界
u_char*           end;            //内存数据的下界
ngx_buf_tag_t     tag;            //void*指针，可以是任意数据
ngx_file_t*       file;           //存储数据的文件对象

...                               //标志位信息
};

因为Nginx里的缓冲数据可能在内存或者磁盘文件中，所以ngx_buf_t使用pos/last和file_pos/file_last来指定数据在内存或者文件中的具体位置，究竟数据是在哪里则要由后面的标志位信息来确定。[6]

start和end两个成员变量标记了数据所在内存块的边界，如果内存块是可以修改的，那么在操作时必须参考这两个成员防止越界。

tag是一个比较特殊的成员，它的类型是void*，用户可以关联任意数据，在代码中任意解释。

ngx_buf_t的内存布局如图4-4所示。

图4-4　ngx_buf_t的内存布局

标志位信息

ngx_buf_t的标志位信息都是bool值，使用“位域”的方式以节约内存：

struct ngx_buf_s {
...            //缓冲区信息

unsigned          temporary:1;      //内存块临时数据，可以修改
unsigned          memory:1;         //内存块数据，不允许修改
unsigned          mmap:1;           //内存映射数据，不允许修改

unsigned          in_file:1;        //缓冲区在文件里
unsigned          flush:1;          //要求Nginx立即输出本缓冲区
unsigned          sync:1;           //要求Nginx同步操作本缓冲区
unsigned          last_buf:1;       //最后一块缓冲区
unsigned          last_in_chain:1;  //链里的最后一块缓冲区
unsigned          temp_file:1;      //缓冲区在临时文件里
};

这些标志位的含义都比较好理解，但last_buf和last_in_chain存在一点小差异：前者是整个处理过程中的最后一块缓冲区，标志着HTTP请求处理的结束；而后者是当前数据块链（ngx_chain_t）里的最后一块，之后可能还会有数据需要处理。

从ngx_buf_t的定义可以看到，一个有数据的缓冲区不是在内存里，就是在文件里，所以内存标志位成员变量（temporary/memory/mmap）和文件标志成员变量（in_file/temp_file）不能全为0，否则Nginx会认为这是个特殊（special）或无效的缓冲区。

如果缓冲区既不在内存也不在文件里，那么它就不含有有效数据，只起到控制作用，例如刷新（flush）或者同步（sync）。

4.4.2　操作函数

ngx_create_temp_buf()函数可以从内存池里分配一块size大小的缓冲区，它的声明是：

ngx_buf_t *ngx_create_temp_buf(ngx_pool_t *pool, size_t size);

函数返回的ngx_buf_t结构内的成员都已经初始化好了，pos和last都指向内存块的首位置，表示空缓冲区，而temporary标志位是1。

我们也可以直接从内存池创建一个ngx_buf_t结构，然后手工指定它的成员，关联到已经存在的内存，Nginx为此提供了两个便捷宏：

#define ngx_alloc_buf(pool)  ngx_palloc(pool, sizeof(ngx_buf_t))
#define ngx_calloc_buf(pool) ngx_pcalloc(pool, sizeof(ngx_buf_t))

两个函数宏可以检查多个标志位，确定缓冲区是否在内存里：

#define ngx_buf_in_memory(b)        (b->temporary || b->memory || b->mmap)
#define ngx_buf_in_memory_only(b)   (ngx_buf_in_memory(b) && !b->in_file)

起控制作用的特殊缓冲区可以用ngx_buf_special()判断：

#define ngx_buf_special(b)                     \
((b->flush || b->last_buf || b->sync)                    \
&& !ngx_buf_in_memory(b) && !b->in_file)

函数宏ngx_buf_size()计算缓冲区的大小，会根据是否在内存里使用恰当的指针：

#define ngx_buf_size(b)                                     \
(ngx_buf_in_memory(b) ?  (off_t) (b->last - b->pos):   \
(b->file_last - b->file_pos))

拷贝内存数据时我们可以直接使用标准C函数memcpy()，但Nginx自定义了一个函数宏ngx_cpy，接口与memcpy()相同，不过它返回的是拷贝数据后的终点位置，在连续复制多段数据时很方便：

#define ngx_cpy(dst, src, n)      (((u_char *) memcpy(dst, src, n)) + (n))

Nginx也用函数宏封装了设置内存的memset()函数：

#define ngx_memzero(buf, n)         (void) memset(buf, 0, n)
#define ngx_memset(buf, c, n)       (void) memset(buf, c, n)

4.4.3　C++封装

我们使用NgxBuf类封装ngx_buf_t结构，创建缓冲区的ngx_create_temp_buf()函数则在NgxPool里封装，这里从略。

类定义

NgxBuf的定义如下：

class NgxBuf final : public NgxWrapper<ngx_buf_t>
{
public:
typedef NgxWrapper<ngx_buf_t>   super_type;  //简化类型定义
typedef NgxBuf       this_type;
public:
NgxBuf(const NgxPool& p, std::size_t n):    //从内存池创建缓冲区
super_type(p.buffer(n))
{}

NgxBuf(ngx_buf_t* buf):super_type(buf)    //代理一个缓冲区
{}

~NgxBuf() = default;        //默认析构函数
...              //成员函数见后
};

基本操作

NgxBuf简单封装了4.4.2节里操作ngx_buf_t的几个函数宏：

public:
bool memory() const         //缓冲区在内存
{
return ngx_buf_in_memory(get());
}

bool memoryonly() const        //缓冲区不在文件
{
return ngx_buf_in_memory_only(get());
}

bool special() const        //是特殊的缓冲区
{
return ngx_buf_special(get());
}
public:
std::size_t size() const       //缓冲区的大小
{
return ngx_buf_size(get());
}

要检查或设置其他的标志位可以直接用重载的operator->访问ngx_buf_t的成员。

缓冲区操作

如果缓冲区在内存里，可以直接使用ngx_buf_t的pos/last等成员操作缓冲区，但把操作封装为函数会更好：

public:
void range(u_char* a, u_char* b) const   //使用两个指针设置缓冲区
{
get()->pos = a;
get()->last = b;
get()->memory = true;      //设置内存标志
}

void boundary(u_char* a, u_char* b) const  //设置缓冲区的边界
{
get()->start = a;
get()->end = b;
}

void range(ngx_str_t* s) const     //使用ngx_str_t 设置缓冲区
{
range(s->data, s->data + s->len);
boundary(s->data, s->data + s->len);
}

无参的range()和boundary()函数以ngx_str_t形式返回内存块的可用区域和边界，但使用时需要小心，它们并不是真正的字符串，只是一块内存区域：

ngx_str_t range() const       //返回缓冲区内的有效数据
{
return ngx_str_t{get()->last - get()->pos, get()->pos};
}
ngx_str_t boundary() const      //返回缓冲区的边界
{
return ngx_str_t{get()->end - get()->start, get()->start};
}

把ngx_buf_t置为最后一块缓冲区是一个很常用的操作，所以我们封装为成员函数last()和finish()：

bool last() const       //是否是最后一块缓冲区
{
return get()->last_buf || get()->last_in_chain;
}

void finish(bool flag = true) const   //置为最后一块缓冲区
{
get()->last_buf = flag;      //设置标志位
get()->last_in_chain = flag;
}

NgxBuf的用法的示范代码可参见4.5.5节。

4.5　数据块链

在处理HTTP请求时会经常创建多个缓冲区来存放数据，Nginx把缓冲区块简单地组织为一个单向链表，使用ngx_chain_t结构描述。

4.5.1　结构定义

ngx_chain_t把多个分散的ngx_buf_t连接为一个顺序的数据块链，定义如下：

// 定义在 core/ngx_core.h
typedef struct ngx_chain_s  ngx_chain_t;    //struct 类型定义

// 定义在 core/ngx_buf.h
struct ngx_chain_s {
ngx_buf_t    *buf;         //缓冲区指针
ngx_chain_t  *next;         //下一个链表节点
};

ngx_chain_t的结构很简单，就是经典的链表节点。如果这个节点是链表的尾节点，就必须要把next置为nullptr，表示链表结束，否则会导致未定义错误。

4.5.2　操作函数

与ngx_queue_t类似，虽然ngx_chain_t的结构简单，但它表示了复杂的链表结构，可以对它执行很多操作，本节只介绍最基本的几个。

ngx_alloc_chain_link()和ngx_free_chain()用来从内存池里获取释放ngx_chain_t对象，声明是：

ngx_chain_t *ngx_alloc_chain_link(ngx_pool_t *pool);
#define ngx_free_chain(pool, cl)

由于ngx_chain_t在Nginx里应用得很频繁，所以Nginx对此进行了优化。在内存池里保存了一个空闲ngx_chain_t链表，分配时从这个链表里摘取，释放时再挂上去。[7]

函数ngx_create_chain_of_bufs()可以一次创建多个缓冲区，返回一个连接好的链表：

typedef struct {
ngx_int_t    num;       //缓冲区的数量，即节点数量
size_t       size;      //缓冲区的大小
} ngx_bufs_t;               //创建链表的参数结构

ngx_chain_t *ngx_create_chain_of_bufs(ngx_pool_t *pool, ngx_bufs_t *bufs);

ngx_bufs_t是函数ngx_create_chain_of_bufs()的参数，确定了创建的缓冲区数量和大小。

4.5.3　C++节点

我们仍然把ngx_chain_t分解为节点、迭代器和容器三个概念，不同的C++类封装不同种类的操作。

类定义

NgxChainNode封装了ngx_chain_t的节点操作，由于ngx_chain_t结构简单，所以比NgxQueueNode要少很多代码：

class NgxChainNode final : public NgxWrapper<ngx_chain_t>
{
public:
typedef NgxWrapper<ngx_chain_t>  super_type;  //简化类型定义
typedef NgxChainNode    this_type;
public:
NgxChainNode(ngx_chain_t* c):super_type(c)
{}

~NgxChainNode() = default;
public:
...              //成员函数见后
};

操作函数

NgxChainNode把对next指针的操作分解为三个函数，明确地表示检查尾节点、链接节点和结束链表（置尾节点）：

public:
bool last() const        //是否是最后一个节点
{
return !get()->next;        //检查空指针
}

void link(ngx_chain_t* c) const     //链接一个节点
{
get()->next = c;         //next 指针赋值
}

void finish() const         //结束链表
{
link(nullptr);          //链接到空指针，即结束
}

成员函数data()和set()操作ngx_chain_t的buf成员，data()返回NgxBuf对象，set()设置buf指针：

public:
NgxBuf data() const         //获取节点的缓冲区
{
get()->buf;           //隐式转换为 NgxBuf 对象
}

void set(ngx_buf_t* ptr) const       //设置缓冲区指针
{
get()->buf = ptr;
}

内存池创建节点

在NgxPool里可以封装ngx_chain_t的创建工作：

class NgxPool final : public NgxWrapper<ngx_pool_t>
{
public:
...                                 //其他成员函数
ngx_chain_t* chain() const        //创建一个空节点
{
auto p = ngx_alloc_chain_link(get());    //内存池获取节点

NgxException::require(p);        //检查空指针
p->next = nullptr;          //设置next 为空指针

return p;           //返回节点指针
}

ngx_chain_t* chain(const ngx_bufs_t& bufs) const  //创建多个节点
{
auto p = ngx_create_chain_of_bufs(get(),
const_cast<ngx_bufs_t*>(&bufs));

NgxException::require(p);        //检查空指针

return p;           //返回节点指针
}
};

chain()函数以重载的方式同时支持了创建单个和多个链表节点的情形，为了避免next指针不确定的隐患，它总把next置为nullptr。

4.5.4　C++迭代器

ngx_chain_t的迭代器实现与ngx_list_t的迭代器NgxListIterator类似，因为它们都是单向链表，但ngx_chain_t的遍历操作显然要容易得多，只需指针后移即可。

类定义

NgxChainIterator使用boost::single_pass_traversal_tag实现单向迭代器：

class NgxChainIterator final:
public boost::iterator_facade<
NgxChainIterator, NgxChainNode,     //返回代理对象
boost::single_pass_traversal_tag>
{
public:
typedef boost::iterator_facade<...>  super_type;  //简化类型定义
typedef typename super_type::reference  reference;
public:
NgxChainIterator(ngx_chain_t* c) : m_p(c)      //获取节点指针
{}

NgxChainIterator() = default;
~NgxChainIterator() = default;
private:
ngx_chain_t* m_p = nullptr;         //节点指针
mutable NgxChainNode m_proxy{m_p};       //代理对象
public:
...                //成员函数见后
};

迭代器核心操作

NgxChainIterator需要实现单向迭代器必需的dereference()、increment()和equal()三个操作：

private:
friend class boost::iterator_core_access;    //必需的友元声明

reference dereference() const     //解引用操作，返回代理对象
{
m_proxy = m_p;          //拷贝赋值节点
return m_proxy;          //返回节点对象的代理引用
}

void increment()         //前进迭代器
{
if(!m_p)           //检查空指针
{   return; }

m_p = m_p->next;         //指向下一个节点
}

bool equal(NgxChainIterator const& o) const  //比较迭代器
{
return m_p == o.m_p;
}

注意：NgxChainIterator的解引用操作返回的是一个代理对象NgxChainNode，而不是ngx_chain_t*，所以更容易使用。

NgxChainIterator还实现了其他一些操作，请参考GitHub资源。

4.5.5　C++封装数据块链

NgxChain封装了ngx_chain_t链表：

class NgxChain final : public NgxWrapper<ngx_chain_t>
{
public:
typedef NgxWrapper<ngx_chain_t>  super_type;  //简化类型定义
typedef NgxChain      this_type;
public:
NgxChain(ngx_chain_t* c):super_type(c)    //代理链表节点
{}

~NgxChain() = default;
public:
typedef NgxChainIterator   iterator;    //迭代器类型定义
typedef const iterator     const_iterator;

iterator begin() const        //产生迭代器起点
{
return iterator(get());
}

iterator end() const        //产生迭代器终点
{
return iterator();
}
public:
std::size_t size() const       //遍历链表，计算总长度
{
std::size_t len = 0;

for(auto& c : *this)
{
len += c.data().size();      //获取节点的 buf长度
}

return len;
}

链表较常用的操作是向尾部添加节点，所以NgxChain实现了两个函数：

public:
ngx_chain_t* tail() const       //查找末尾节点
{
auto p = get();         //头节点

for(;p->next;p = p->next);       //查找空指针

return p;          //返回next 为空的尾节点
}

void append(ngx_chain_t* ch) const     //向末尾追加节点
{
tail()->next = ch;         //调整next 指针
}

下面的代码综合示范了NgxBuf和NgxChain的用法：

NgxPool pool(r);          //内存池对象
NgxBuf buf(pool.buffer(10));       //创建一个10字节缓冲区对象

assert(buf.memory() && buf.memoryonly());    //在内存里，不在文件里
assert(buf.range().len == 0);        //无有效数据
assert(buf.boundary().len == 10);       //缓冲区大小是 10字节

ngx_memset(buf.range().data, 'a', 5);      //写入5个字节数据
buf->last += 5;           //移动缓冲区数据指针
assert(buf.range().len == 5);        //有 5 个字节的有效长度
cout << "str="<< buf.range() << endl;      //输出缓冲区数据

NgxChain ch = pool.chain();       //创建一个缓冲区链

auto node = *ch.begin();        //获取第一个节点
assert(node.last());         //next 指针已经置为空
node.set(buf);            //设置缓冲区数据
cout << node.data().range() << endl;      //输出缓冲区数据

NgxChain ch2 = pool.chain(ngx_bufs_t{2, 10});   //创建另一个链表，两个节点
node.link(ch2);           //链接到第一个链表

for(auto&x : ch)          //遍历链表，可以用 for
{
auto b = x.data();         //获得缓冲区代理
ngx_memset(b->pos, 'b', 5);       //设置缓冲区数据
b->last = b->pos + 5;         //移动缓冲区数据指针
cout << b.range() << ";";      //输出缓冲区数据
}

4.6　键值对

键值对（key-value pair）是一种映射关系，可以把一个值映射到另一个值。C++使用std::pair来表示，并且使用std::map和std::unordered_map来存储这样的数据，而Nginx则提供了两个结构ngx_keyval_t和ngx_table_elt_t，再结合ngx_array_t或ngx_list_t应用在不同的场景。

这两个结构比较简单，所以本书不使用C++封装。

4.6.1　简单键值对

ngx_keyval_t是一个简单的键值对结构，主要用在Nginx的配置解析环节，保存配置文件里成对的配置，定义如下：

// 定义在 core/ngx_string.h
typedef struct {
ngx_str_t   key;        //键
ngx_str_t   value;      //值
} ngx_keyval_t;

在Nginx里，通常使用ngx_array_t来存储ngx_keyval_t，相当于：

typedef NgxArray<ngx_keyval_t>   NgxKvArray;

4.6.2　散列表键值对

ngx_table_elt_t与ngx_keyval_t很类似，但多了两个成员：

// 定义在 core/ngx_hash.h
typedef struct {
ngx_uint_t        hash;            //散列（哈希）标记
ngx_str_t         key;             //键
ngx_str_t         value;           //值
u_char           *lowcase_key;     //key 的小写字符串指针
} ngx_table_elt_t;

ngx_table_elt_t主要用来表示HTTP头部信息，例如“Server:nginx/1.8.0”这样的字符串对应到ngx_table_elt_t，就是key="Server"，value="nginx/1.8.0"。

成员hash是一个散列标记，Nginx使用它在散列表结构里快速查找数据。可以简单地把它置为非零值（通常是1），也可以使用下面的两个函数计算散列值：

// 定义在 core/ngx_hash.h
ngx_uint_t ngx_hash_key(u_char *data, size_t len);   //计算散列值
ngx_uint_t ngx_hash_key_lc(u_char *data, size_t len);   //小写后再计算

成员lowcase_key指向了一个全小写的字符串，在大小写无关比较时可以避免重复计算。

函数ngx_hash_strlow()可以在小写化的同时计算出散列值：

ngx_uint_t ngx_hash_strlow(u_char *dst, u_char *src, size_t n);

Nginx在处理HTTP请求时使用ngx_list_t存储了HTTP头部信息，相当于：

typedef NgxList<ngx_table_elt_t>   NgxHeadersList;

4.7　总结

本章详细介绍了ngx_array_t、ngx_list_t等Nginx里的高级数据结构，这些数据结构类似C++里的泛型标准容器，可以存储各种类型的元素。它们管理组织Web服务器里繁多的数据，是Nginx宫殿里的立柱、横梁等重要建筑构件。

ngx_array_t是一个泛型动态数组，它结构简单，存取效率高，与C++里的vector一样，是最容易使用的一个数据容器。

ngx_list_t是一个有优化的单向链表。传统的链表一个节点只存储一个元素，因而存储成本高，而ngx_list_t的节点里是一个数组，可以存放多个元素，很好地摊平了链表指针的额外成本，节约了内存。

ngx_queue_t定义了双向链表的前后指针，它必须以数据结构的成员方式使用，是一种“侵入式”容器。但侵入式容器用法没有非侵入式容器那么简单，Nginx使用offsetof宏以一种巧妙的方法可以从成员变量的地址访问到整个元素。

ngx_buf_t和ngx_chain_t描述了Nginx里的缓冲区，可以表示连续或分散的缓冲数据。这些数据既可以在内存中，也可以在磁盘文件里，在处理HTTP请求时需要经常使用这两个数据结构接收和发送数据。

最后我们简单讨论了Nginx里的键值对结构，它们通常结合前面的几个容器来表示配置信息或者HTTP头部信息。

Nginx提供的数据结构虽然定义比较简单，但接口操作多，而且没有很好的分类，很容易误用或滥用。本章使用C++的封装特性分解了这些数据结构的操作，提取出迭代器、节点和容器等概念，把数据结构和操作绑定在一起，给出更安全可靠的接口，用法更简单，也不容易出错。

Nginx里还有很多高级数据结构，如红黑树、散列表、基数树等，本章并没有介绍，主要是因为它们通常在Nginx内部使用，编写实际的HTTP应用模块时很少遇到，而且C++中可以在标准库或Boost库里找到等价且更好的容器，例如std::map、boost::unordered_set等，感兴趣的读者可以阅读Nginx源码自行研究。

[1] ngx_array_destroy()函数也可以同样封装，但因为内存池会最终释放内存，所以通常没有必要调用它。

[2] 如果要追求效率，可以在push()里使用std::move()来消除拷贝的代价，当然我们也可以直接使用prepare()来操作数据。

[3] boost::iterator_facade的用法可参考推荐书目[4]。

[4] BOOST_EXPLICIT_OPERATOR_BOOL的用法可参考推荐书目[3]。

[5] offsetof是C标准里定义的一个宏，用来计算结构里成员的偏移量，相当于((size_t)&(((struct*)0)->member))。

[6] 本书暂不介绍Nginx里的文件相关操作，读者可参看core/ngx_file.*。

[7] 注意，使用ngx_alloc_chain_link()内部调用的是ngx_palloc()，获得的ngx_chain_t对象的buf和next指针可能是任意值，不能假设next指针是空指针。

第5章　Nginx开发综述

在前面的章节中我们学习了Nginx里的各种数据结构，对Nginx源码有了基本的认识，下面就来编写真正的Nginx模块。

由于Nginx模块开发涉及的要素很多，很难一下子讲解清楚，所以本章先从一个简单的例子开始，通过实际的代码来让读者熟悉模块开发的基本步骤。有了这些初步认识后再详细介绍配置文件解析、模块的定义和操作函数等较深入的细节，最后以C++实现对它们的封装，开发一个完整的C++Nginx模块。

5.1　最简单的Nginx模块

本节我们将利用前两章的C++封装类如NgxPool、NgxLog等实现一个最简单的http模块，它基本上什么也不做，只是在控制台输出一个字符串并记录运行日志。[1]

在编写模块前，我们先对这个模块提出三个问题:

- 是否需要在配置文件里配置？配置指令是什么？有什么样的参数？

- 怎样使用Nginx框架？怎样访问配置参数？怎样处理HTTP请求？

- 如何编译集成进Nginx？

接下来的小节将逐步解决这三个问题，完成模块的开发。

5.1.1　模块设计

不管Nginx模块是简单还是复杂，我们都需要对它进行设计，回答刚才提出的三个问题。设计可以只存在于我们的头脑中，或者是形成正式的书面文档。

对于这个简单功能的模块，我们的设计如下：

- 模块的名字是ndg_test_module。

- 配置指令是ndg_test on|off，开关模块的功能只能在location里配置。

- 使用ngx_command_t和相关函数解析配置指令。

- 使用ngx_http_module_t定义功能函数，创建配置数据并初始化。

- 使用ngx_module_t定义模块。

- 不直接处理HTTP请求，只在URL重写阶段里执行。

- 根据配置指令的on|off决定输出字符串的内容。

- 编写config脚本，用“--add-module”选项集成进Nginx。

- 为了简单起见，所有代码都实现在一个cpp里，名字是ModNdgTest.cpp。

这只是个很粗略的设计，但目前来说基本够用，下面就开始具体的实现。

5.1.2　配置解析

ndg_test_module在Nginx配置文件里的形式是：

location /test {         #一个location
ndg_test on;          #启用模块的功能
...             #其他配置指令
}

解析这个配置指令要定义配置数据结构、指令数组和管理函数。

配置的数据结构

我们必须定义一个含有对应信息的结构体，用来存储配置数据。这里结构体命名为NdgTestConf：[2]

struct NdgTestConf final        //禁止被继承
{
ngx_flag_t enabled = NgxUnsetValue::get();   //标志变量，构造时初始化
};

NdgTestConf里定义了enabled变量，它的类型是ngx_flag_t，可以把配置文件里的on|off信息转换为1|0保存。

NdgTestConf另外一项重要的工作是初始化，使用了C++11的新特性，明确设置为UNSET，这样在后续的流程中Nginx才能正确处理。

配置的解析

有了配置的数据结构仅仅是个开始，它还没有与配置指令关联起来。

Nginx提供结构ngx_command_t来实现配置指令解析，每条指令对应一个ngx_command_t对象。所有的ngx_command_t对象放在一个数组里，最后需要用一个宏ngx_null_command表示数组定义结束。

因为ndg_test_module只有一条指令，所以指令数组比较简单，代码如下：

static ngx_command_t ndg_test_cmds[] =    //配置指令数组
{
{
ngx_string("ndg_test"),      //指令的名字
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,   //指令的作用域和类型
ngx_conf_set_flag_slot,      //解析函数指针
NGX_HTTP_LOC_CONF_OFFSET,      //数据的存储位置
offsetof(NdgTestConf, enabled),    //数据的具体存储变量
nullptr          //暂无须关心
},

ngx_null_command        //空对象，结束数组
};

ngx_command_t各个成员的具体含义将在5.3节讲解，这里只做一个简要的说明：

- 指令的名字ndg_test，它就是出现在配置文件里的指令；

- 指令只能在location里出现，可接受的参数是on|off;

- 使用Nginx提供的标准函数解析指令；

- 整个数据结构存储在http/location作用域；

- 使用宏offsetof获取变量的地址，供解析函数使用；

- 数组的最后必须是ngx_null_command，起到哨兵的作用，相当于'\0'。Nginx使用它来标识命令数组的结束。

创建配置数据

Nginx要求模块自己分配配置数据结构的内存，所以我们还需要编写一个create()函数，在配置解析时使用内存池创建对象：

static void* create(ngx_conf_t* cf)    //创建配置数据结构
{
return NgxPool(cf).alloc<NdgTestConf>(); //由内存池分配内存，构造对象
}

create()的函数参数ngx_conf_t代表了Nginx在解析配置指令时的环境（Context），它有一个内存池成员pool，所以我们可以构造NgxPool对象然后分配内存。

如果对象里的成员没有在构造时初始化，那么必须在这里完成初始化，设置为UNSET。

create()函数将在5.1.4节里通过ngx_http_module_t插入到Nginx框架，被Nginx调用。

5.1.3　处理函数

基本的HTTP请求处理流程我们都很熟悉：接收请求头和请求体，检查权限，然后产生响应内容返回给客户端，最后记录日志。

Nginx在框架的级别上仔细梳理了HTTP请求处理流程，提炼出了重写、权限检查、内容产生等11个明晰的阶段（phases，详细的讲解可参见第6章），简化了开发工作。我们可以在这些阶段里插入自己的功能代码，实现所需的业务逻辑。

ndg_test_module的功能很简单，它只是用来测试，不响应用户请求，所以可以选择任何阶段。依据5.1.1的设计，它将在rewrite阶段执行。

我们需要实现两个函数：实际的处理功能和把它插入到Nginx的处理流程框架。[3]

处理函数

函数handler()先读取配置参数，然后根据参数向控制台输出字符串：

static ngx_int_t handler(ngx_http_request_t *r)
{
auto cf = reinterpret_cast<NdgTestConf*>(    //获取配置数据
ngx_http_get_module_loc_conf(r, ndg_test_module));

NgxLogError(r).print("hello c++");      //记录运行日志

if (cf->enabled)           //检查配置参数
{
std::cout << "hello nginx" << std::endl;    //输出字符串
}
else
{
std::cout << "hello disabled" << std::endl;  //输出字符串
}

return NGX_DECLINED;         //执行成功但未处理
}

因为我们在配置解析时指定了参数NGX_HTTP_LOC_CONF_OFFSET，所以要使用Nginx提供的函数宏ngx_http_get_module_loc_conf来获取模块存储在location域里的配置信息。它返回的是void*指针，需要转型为模块自己的配置数据类型。

之后的处理逻辑很简单，首先使用封装类NgxLogError记录一条日志，然后根据enabled的0/1值输出不同的字符串。

函数的结尾我们必须返回NGX_DECLINED而不能是NGX_OK，这是因为我们并没有真正地处理数据，Nginx在收到NGX_DECLINED返回值后会继续调用后续的模块处理。

注册处理函数

函数init()将在Nginx配置解析阶段调用，向Nginx框架注册我们自己的处理函数：

static ngx_int_t init(ngx_conf_t* cf)
{
auto cmcf = reinterpret_cast<ngx_http_core_main_conf_t*>(
ngx_http_conf_get_module_main_conf(    //注意获取函数
cf, ngx_http_core_module));      //注意使用的模块

NgxArray<ngx_http_handler_pt> arr(      //Nginx数组
cmcf->phases[NGX_HTTP_REWRITE_PHASE].handlers); //该阶段的 handler

arr.push(handler);           //加入自己的 handler

return NGX_OK;            //执行成功
}

在Nginx的模块架构里有一个特别的模块ngx_http_core_module，它负责管理所有的http功能模块。因为init()发生在配置解析阶段，所以需要用另外一个函数宏ngx_http_conf_get_module_main_conf获取它的配置结构，然后把处理函数handler加入到HTTP请求处理的rewrite阶段的handler数组里。Nginx在处理HTTP请求时会依次调用每个阶段里的所有handler，从而执行我们自己的功能。

5.1.4　模块集成

Nginx提供两个数据结构ngx_http_module_t和ngx_module_t，用来集成配置指令解析和处理函数。

集成配置函数

ngx_http_module_t是http模块专用的数据结构，包含8个在配置解析阶段可被Nginx调用的函数指针。对于模块ndg_test_module，我们只实现了其中的两个：

static ngx_http_module_t ndg_test_ctx =
{
nullptr,      //解析配置文件前被调用
init,         //解析配置文件后被调用
nullptr,      //创建http main 域的配置结构
nullptr,      //初始化http main 域的配置结构
nullptr,      //创建server域的配置结构
nullptr,      //合并server域的配置结构
create,       //创建location 域的配置结构
nullptr,      //合并location 域的配置结构
};

从代码的注释我们可以知道：函数create()将在解析配置文件location时被调用，创建出NdgTestConf对象，然后存储在Nginx的location内存域里。函数init()将在所有的配置指令解析完毕后被调用，修改ngx_http_core_module的handler数组，注册处理函数。

因为我们的配置指令只出现在location域里，并不需要合并参数，所以有关main和server的函数指针都置为nullptr。

集成配置指令

ngx_module_t是Nginx真正定义模块的数据结构，它集成ngx_http_module_t和ngx_command_t数组：

ngx_module_t ndg_test_module =             //模块定义，注意不是 static
{
NGX_MODULE_V1,                         //标准的填充宏
&ndg_test_ctx,                         //配置功能函数
ndg_test_cmds,                         //配置指令数组
NGX_HTTP_MODULE,                       //http 模块必需的 tag
nullptr,                               //init master
nullptr,                               //init module
nullptr,                               //init process
nullptr,                               //init thread
nullptr,                               //exit thread
nullptr,                               //exit process
nullptr,                               //exit master
NGX_MODULE_V1_PADDING                  //标准的填充宏
};

ngx_module_t结构比较复杂，定义了很多Nginx框架在加载模块时所必需的信息。其中最重要的就是配置功能函数和配置指令数组，其他的都可以使用预定义宏或者空指针填充。对于http模块，需要在配置指令数组后使用宏NGX_HTTP_MODULE作为模块的标识。

需要注意的是，ngx_module_t变量不能是static的，这是因为它将被Nginx框架所使用，如果声明为static那么Nginx将无法访问模块对象。

到这里，ndg_test_module所有的C++代码就编写完成了。

5.1.5　编译脚本和命令

Nginx只能在编译时静态地加载第三方模块，根据configure的要求，我们要编写一个Shell脚本，再用“--add-module”参数配置编译选项。

编译脚本

Nginx要求模块的Shell脚本名字必须是config，里面使用特定的Shell变量告诉configure模块相关的信息：

#脚本名必须是 config

#模块的名字，只是在 configure 时显示信息用，不必与代码里的相同
ngx_addon_name=ndg_test_module

#模块在 C/C++代码里的名字，可以有多个不同的模块
HTTP_MODULES="$HTTP_MODULES ndg_test_module"

#模块的源码，也可以有多个
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ModNdgTest.cpp"

#模块的包含路径，是我们存放 C++头文件的位置
HTTP_INCS="$HTTP_INCS $ngx_addon_dir/../../ngxpp"

在config脚本里必须定义的是ngx_addon_name、HTTP_MODULES和NGX_ADDON_SRCS，它们给出了模块最基本的信息，configure将利用这三个变量产生用于编译的Makefile和源码。

因为ndg_test_module的代码使用了C++包装类，所以需要用HTTP_INCS指定编译时的包含路径。此外还可以使用很多configure里提供的其他变量进行更复杂的Shell编程处理，这里从略。

编译命令

编写完config脚本后，模块的所有开发工作就完成了。只要在configure时使用“--add-module”参数就可以把模块集成进Nginx框架。

假设模块的代码和脚本存放在“~/ngx_cpp_dev/modules/test”下，那么配置的命令就是：

./configure --add-module=$HOME/ngx_cpp_dev/modules/test

配置完成后再执行make&&make install，我们的开发就大功告成。

5.1.6　测试验证

为了验证ndg_test_module的功能需要修改配置文件，新建一个location，并使用ndg_test指令打开或者关闭模块。

因为模块使用了cout输出信息字符串，所以我们需要使用“daemon off;”让Nginx在前台运行。相关的配置文件片段是：

master_process off;        #禁用进程池，使用单进程
daemon off;                 #禁止daemon，前台运行

...                         #其他配置

location /test {         #测试的 location 配置
ndg_test on;         #打开模块

alias /usr/local/nginx/html;    #设置静态文件的路径
index  index.html index.htm;    #设置index 文件
}

使用curl向test location发送测试请求：

curl -vo /dev/null 'http://localhost/test/index.html'

可以看到Nginx在控制台输出字符串“hello nginx”，查看error.log也可以看到打印的运行日志：

2015/xx/xx 09:20:12 [error] 31213#0: *1 hello c++, client: 127.0.0.1, ...

5.2　Nginx开发基本流程

在5.1节，我们通过开发一个简单的模块初步熟悉了Nginx的模块开发流程。以小见大，任何Nginx模块的开发过程都与之类似，都要经过设计、开发、编译、测试验证和调优五个步骤，本节将对此做简略阐述，然后在之后的小节详细讲解Nginx的开发知识。

5.2.1　设计

“凡事预则立，不预则废”，这句古语不仅对于Nginx模块开发，对于任何软件产品的开发都非常有警示作用。

在模块的设计阶段，我们必须做出模块各方面的总体决策，包括但不限于：

- 模块的数量和名字；

- 模块的配置指令和参数；

- 模块的基本业务逻辑；

- 模块的结构和组成；

- 模块的测试用例。

设计的成果最好以文档的形式保存下来，除基本的备忘作用之外，更重要的是把思维固化下来利于以后的交流、维护和经验总结。

5.2.2　开发

开发是整个Nginx模块开发流程中最重要的步骤，它直接实现设计，可以细分成配置指令开发、业务逻辑开发、框架注册和模块集成四个相对较为独立的部分。

配置指令开发

配置文件是Nginx的核心，所以这常常是模块开发时最先开始的工作。

我们要依据设计文档定义存储配置信息的数据结构，还有它的创建、解析、合并等操作函数——注意在创建配置数据结构时必须要初始化成员为UNSET，最后用一个ngx_command_t数组描述这些指令。

如果指令比较简单，那么可以使用Nginx框架提供的一些预定义函数简化开发工作。

业务逻辑开发

这是实现模块自身功能的地方，具体的逻辑因模块而异。通常的步骤是获取配置参数，获取HTTP请求头和请求体数据，做一些处理或者转发到外部，得到内容后设置HTTP响应头和响应体，返回给Nginx框架合适的错误码。

在处理过程中应该多使用Nginx的日志机制，记录运行日志便于排错。

框架注册

Nginx框架需要知道我们的功能如何介入HTTP请求处理流程，注册动作发生在启动时的解析配置文件阶段。

Nginx提供了两种注册方式，一种是5.1节使用的方式，在完成配置文件解析后修改ngx_http_core_module的phase[xxx].handlers数组，插入函数指针；另一种则是在解析到特定指令时直接设置该location的处理函数，忽略phase handlers函数数组。

模块集成

有了配置函数、业务逻辑函数和框架注册函数，我们要设置ngx_http_module_t和ngx_module_t里的成员变量和成员函数指针，这样Nginx框架就可以在配置解析阶段和HTTP请求处理阶段正确调用我们的函数，实现模块的功能。

5.2.3　编译

相对于开发来说Nginx的模块编译比较简单（当然也可以有复杂的逻辑），只要编写一个Shell脚本，并设置ngx_addon_name、HTTP_MODULES、NGX_ADDON_SRCS等变量的值。

在执行“configure--add-module”时，Nginx的脚本会调用config脚本，获取里面的变量，然后生成最后用于编译的Makefile和C源码文件。

5.2.4　测试验证

完成开发和编译步骤后必须要进行测试验证，可以是简单的自我测试，也可以是更正式的功能和性能测试，必须认真对待。测试的基本内容是验证Nginx在加入模块后能否正确启动运行、模块能否正确解析配置指令、能否正确完成设想的业务逻辑。

简单的测试工作可以用curl向Nginx发送构造好的HTTP请求，检查返回的响应和运行日志是否符合预期。如果发生了错误，那么就要返回开发和编译步骤，查找错误原因，排除故障后再重新进行测试，直至所有测试用例通过。

5.2.5　调优

如果模块的功能比较简单，代码量较少（如5.1节的ndg_test_module），通常开发流程进入到第4步测试验证就可以结束了，不需要调优。但如果模块的业务比较复杂，网络吞吐量很大，调优就非常有必要，可以让模块运行得更加快速稳定，资源利用率更高。

我们可以使用一些工具进行压力测试，或者观察线上真实环境，使用top、sar等UNIX工具检查Nginx的内存、CPU、磁盘以及网络使用状况，结合日志分析，找出程序运行的瓶颈，调整Nginx相关的配置参数，或者优化模块的代码，然后再观察Nginx参数是否有改善，反复迭代，直至达到预期的目标。

调优是一个没有固定模式的工作，需要开发和运维共同努力，还有实际经验的积累。

5.3　Nginx的模块

模块化架构是Nginx框架的核心，整个Nginx就是由许多的模块搭建起来的，本节我们将详细研究Nginx的模块。

5.3.1　模块的数据结构

ngx_module_t结构体描述了Nginx的模块，它的成员很多，但有的只是暂时无用的保留字段，我们并不需要关心。

ngx_module_t定义在core/ngx_conf_file.h里，代码如下：

// 定义在 core/ngx_core.h
typedef struct ngx_module_s      ngx_module_t;

// 定义在 core/ngx_conf_file.h
struct ngx_module_s {
ngx_uint_t             ctx_index;  //在 type 类模块数组里的序号
ngx_uint_t             index;      //在所有模块里的序号

ngx_uint_t             spare0;     //保留字段，暂无意义
ngx_uint_t             spare1;
ngx_uint_t             spare2;
ngx_uint_t             spare3;

ngx_uint_t             version;    //模块的版本，当前只能是 1

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

成员version指示Nginx模块数据结构的版本，目前只能取值为1。

commands成员是一个ngx_command_t数组指针，保存了本模块使用的所有配置指令信息，Nginx会使用它查找指令是否可被当前模块处理。

init_master等函数指针相当于C++里的成员函数，Nginx框架将在适当的时机回调这些函数，完成模块特定的功能[4]。但大多数情况下我们并不需要在模块的初始化和退出时执行动作，可以直接把它们置为空指针。

ngx_module_t里名为spareN和spare_hookN的成员都是Nginx的保留字段，目前的代码并没有使用。所以为了简化ngx_module_t的设置，Nginx定义了两个填充宏，用于初始化ngx_module_t前7个和后8个成员：

#define NGX_MODULE_V1           0, 0, 0, 0, 0, 0, 1
#define NGX_MODULE_V1_PADDING   0, 0, 0, 0, 0, 0, 0, 0

剩下的ctx_index、index、type、ctx等成员关系到ngx_module_t的重要功能，请读者继续阅读。

5.3.2　模块的种类

Nginx框架定义了五种类型的模块，分别是core模块、conf模块、event模块、http模块和mail模块，所有的Nginx模块都必须属于这五类模块。[5]

ngx_module_t使用成员type和ctx共同确定模块的类型。type表示模块的类型，相当于一个类型的标记（RTTI），而ctx则类似C++里的虚表指针，不同的模块可以有不同的定义，实现了模块的“子类化”——这是因为C语言没有C++的继承机制。

ngx_module_t::type取值必须是以下五个宏：

#define NGX_CORE_MODULE      0x45524F43    // "CORE"
#define NGX_CONF_MODULE      0x464E4F43    // "CONF"
#define NGX_EVENT_MODULE     0x544E5645    // "EVNT"
#define NGX_HTTP_MODULE      0x50545448    // "HTTP"
#define NGX_MAIL_MODULE      0x4C49414D    // "MAIL"

本书中开发的模块均为HTTP模块，所以只使用宏NGX_HTTP_MODULE。

ctx的类型是void*，意味着它的内容是不确定的，必须结合type才能确定ctx的具体含义。Nginx的五类模块都定义了各自的ctx结构[6]，名字是ngx_xxx_module_t。例如core模块的ctx是：

typedef struct {
ngx_str_t             name;
void               *(*create_conf)(ngx_cycle_t *cycle);
char               *(*init_conf)(ngx_cycle_t *cycle, void *conf);
} ngx_core_module_t;

目前Nginx一共只有为数不多的几个core模块，包括ngx_core_module、ngx_errlog_module、ngx_events_module、ngx_http_module、ngx_mail_module等，它们是Nginx框架里最底层最核心的模块，Nginx框架直接与这些核心模块交互，而ngx_events_module和ngx_http_module这样的核心模块则再自行构建出自己特定的模块体系和运行机制。

对于ngx_http_module来说，它只负责把http模块管理组织起来，接入Nginx框架，真正的HTTP业务逻辑则由HTTP核心模块ngx_http_core_module来实现。

5.3.3　http模块

http模块是我们最常用的模块，它的ctx类型是ngx_http_module_t，定义如下：

//定义在http/ngx_http_config.h
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个函数指针，它们都是配置解析相关的函数，在Nginx框架解析配置文件时被调用，具体的回调时机是：

- preconfiguration：开始解析http块前调用，常用于添加变量；

- postconfiguration：解析完http块后调用，常用于初始化模块handler；

- create_main_conf：创建http main级别的配置数据结构；

- init_main_conf：初始化http main级别的配置数据结构；

- create_srv_conf：创建server级别的配置数据结构；

- merge_srv_conf：合并main级别和server级别的配置数据结构；

- create_loc_conf：创建location级别的配置数据结构；

- merge_loc_conf：合并server级别和location级别的配置数据结构。

这些函数的调用与配置文件的解析密切相关，需要使用ngx_conf_t结构在Nginx的启动阶段设置模块的各种参数，比较常用的是preconfiguration、postconfiguration、create_loc_conf和merge_loc_conf这几个函数，我们会在之后的小节进一步介绍。

5.3.4　模块的类图

如果从C++的视角来观察Nginx的模块架构，那么ngx_module_t就是一个抽象基类，ngx_core_module_t、ngx_http_module_t以ctx的方式继承了ngx_module_t，最后的各个具体模块实现了类里的函数指针，是它们的实现类。

使用UML可以画出Nginx模块的示意类图，对于我们理解Nginx模块架构很有参考意义，如图5-1所示。

图5-1　Nginx模块的类图

5.3.5　模块的组织形式

Nginx包含了数十个功能各异的模块，之间的层次和调用关系非常复杂，如果不阅读Nginx源码，可能有的人会以为众多的模块是以某种复杂的数据结构组织起来的，但实际情况完全相反，Nginx并没有使用自定义的那些高级数据结构，而只是使用了最简单的C内建数组。

Nginx的configure脚本会生成源码文件objs/ngx_modules.c，在这里所有的Nginx模块都被放进了一个普通的数组：

// 定义在 objs/ngx_modules.c，是由 configure 脚本动态生成的
ngx_module_t *ngx_modules[] = {
&ngx_core_module,                      //core 模块
&ngx_errlog_module,                    //运行日志模块
&ngx_conf_module,                      //conf 模块
&ngx_events_module,                    //event模块
&ngx_event_core_module,                //event核心模块
&ngx_epoll_module,                     //epoll模块
&ngx_regex_module,                     //正则表达式模块
&ngx_http_module,                      //http 模块
&ngx_http_core_module,                 //http 核心模块
...                                     //其他模块
&ngx_http_not_modified_filter_module,   //not_modified 过滤模块
NULL                                    //空指针表示数组结束
};

ngx_modules数组简单地把所有Nginx模块线性地存储在一起。注意，数组只是保存了模块的指针，模块的定义还是在各自的实现文件里，所以模块的声明一定不能是static，否则ngx_modules会无法找到模块，导致编译错误。

ngx_modules数组仅仅起到集中所有模块的作用，为Nginx框架提供一个访问所有模块的入口，模块的层次和调用关系则由模块数据结构里的ctx_index、index和各种指针来实现。

ngx_module_t的index成员标记了模块在ngx_modules数组里的位置（序号）。在Nginx的启动阶段，main()函数会顺序遍历ngx_modules数组，设置每个模块的index成员，代码摘要如下：

// 位于core/nginx.c
ngx_uint_t          ngx_max_module;      //全局计数器变量

int ngx_cdecl
main(int argc, char *const *argv)       //Nginx的 main 函数
{
...
ngx_max_module = 0;         //初始化计数器
for (i = 0; ngx_modules[i]; i++) {     //遍历ngx_modules 数组
ngx_modules[i]->index = ngx_max_module++;  //设置模块的 index 成员
}
...
}

ctx_index的含义与index类似，但它标记的是模块在本类型——如event、http——所有模块里的序号。这样做的好处是节约存储，只需用一个ngx_modules数组就可以存放各个种类的模块，间接实现了模块的分类，而不必为每类模块单独建立数组。

如果以搜索引擎术语来理解，那么index近似于一级索引，用于查找所有的模块，而ctx_index则近似于二级索引，用于查找特定类型的模块。

在ngx_http_module （它是core模块而不是http模块）里，当调用ngx_http_block()函数解析配置文件http块时，Nginx同样遍历ngx_modules数组，但检查模块的type成员，只设置http模块的ctx_index成员：

// 位于http/ngx_http.c
static char *
ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
...
ngx_http_max_module = 0;         //初始化计数器
for (m = 0; ngx_modules[m]; m++) {
if (ngx_modules[m]->type != NGX_HTTP_MODULE) {   //检查模块类型
continue;
}

ngx_modules[m]->ctx_index = ngx_http_max_module++; //设置ctx_index
}
...
}

对于http模块来说ctx_index非常重要，模块相关的数据都是以数组的方式依次存放的，必须使用它来存取。从ngx_modules数组的排列可以看到，第0号的http模块是ngx_http_core_module，它定义了Nginx的HTTP处理框架。

ngx_modules数组在内存里的组织形式如图5-2所示。

图5-2　ngx_modules数组的内存组织形式

5.4　Nginx的配置

Nginx的模块化架构是与灵活的配置文件格式紧密联系在一起的，模块决定了配置文件的结构和指令，配置文件再用这些指令来调整模块的行为，两者相辅相成，要想完全掌握Nginx的模块架构和运行机制，就必须理解Nginx的配置解析功能。

本节将从ngx_cycle_t、ngx_http_conf_ctx_t、ngx_conf_t和ngx_command_t这几个关键数据结构入手，详细剖析Nginx的配置解析机制。

5.4.1　结构定义

本节简要介绍数据结构ngx_cycle_t、ngx_http_conf_ctx_t、ngx_conf_t和ngx_command_t，它们代表了Nginx配置数据的静态存储。

ngx_cycle_t

ngx_cycle_t是Nginx框架的核心数据结构，Nginx运行的整个“生命周期”（cycle）都要使用到的数据，但对于配置解析来说我们只需关心其中的一个成员：

// 定义在 core/ngx_core.h
typedef struct ngx_cycle_s       ngx_cycle_t;

// 定义在 core/ngx_cycle.h
struct ngx_cycle_s {
void****              conf_ctx;    //配置数据的起始存储位置
...           //其他数据成员
};

conf_ctx是Nginx存储配置数据的起点，但它的声明“非常可怕”，使用了连续4个星号，任何人都很难立即理解其含义。

我们可以使用typedef来简化代码，转化为较为容易理解的形式：

typedef void**        void_array;        //即 void*[]，存储 void*的数组
typedef void_array**  void_multi_array;  //即 void_array*[]
void_multi_array      conf_ctx;          //定义conf_ctx

这样就可以比较清晰地看到，conf_ctx实际上是一个二维数组，数组里的元素是void*指针，每一个指针又指向了另一个存储void*的数组。

不过由于void*指针的易变性，conf_ctx还可以根据模块的具体情况转换为void**类型，这时它表示的是一个普通数组，里面存储的是void*指针。

在ngx_cycle.c的ngx_init_cycle()函数里创建conf_ctx的代码是：

cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));

这实际上就是一个存储void*的普通数组，长度为ngx_max_module，可以用来存放所有模块的配置数据结构（但实际上Nginx并没有这么做）。

Nginx通过这种方式灵活地支持了不同模块的数据存储需求：对于http/mail模块，指针指向的可以是另外一个数组，而core模块指针则直接指向配置数据，形成了一个树形结构。

ngx_http_conf_ctx_t

ngx_http_module是core模块，它的配置数据结构是ngx_http_conf_ctx_t，用来存储http模块的配置数据：

// 定义在 http/ngx_http_config.h
typedef struct {
void**        main_conf;      //http main 域的存储数组
void**        srv_conf;       //server 域的存储数组
void**        loc_conf;       //location 域的存储数组
} ngx_http_conf_ctx_t;

这里的main_conf、srv_conf和loc_conf与conf_ctx的功能相似，也是void*数组。但它们使用的序号是ctx_index，只存放所有http模块create_xxx_conf创建的数据，其他event、mail等模块则不在其中。

使用三个数组的原因是Nginx把http配置设计为http/server/location三个层次，每个模块可以为不同的层次使用不同的配置，所以必须分别存储。

ngx_conf_t

ngx_conf_t结构定义在ngx_conf_file.h，是Nginx在解析配置文件时的重要数据结构，表示解析当前配置指令时的运行环境数据（Context）：[7]

// 定义在 core/ngx_core.h
typedef struct ngx_conf_s        ngx_conf_t;

// 定义在 core/ngx_conf_file.h
struct ngx_conf_s {
ngx_array_t*   args;    //配置文件里的指令字符串数组

ngx_pool_t*    pool;    //内存池对象
ngx_log_t*     log;     //日志对象

void*          ctx;            //当前环境，例如 ngx_http_conf_ctx_t
ngx_uint_t     cmd_type;       //当前的命令类型

...                            //其他数据成员
};

因为Nginx的配置文件是有层次的，分成了main/http/server/location等作用域，不同的域里指令的含义和处理方式都有可能不同（例如http块里的server指令和upstream块里的server指令就是完全不同的），所以Nginx在解析配置文件时必须使用ngx_conf_t来保存当前的基本信息，进入退出一个配置块都会变更ngx_conf_t，指令的解析必须要参考ngx_conf_t环境数据才能正确处理。

args参数是我们最常用到的成员，它是一个ngx_array_t，以ngx_str_t的形式存储了分割好的配置文件指令字符串。如果有NgxStrArray arr(cf->args)，那么arr[0]就是指令名，arr[1]是第一个参数，以此类推。

pool成员是内存池，可以使用它分配内存，通常是创建配置数据结构，我们已经在5.1.2节看到了它的用法。

log成员是日志对象，但在配置解析未完成时可能还没有文件可供输出，那么它就会打印到控制台上。

ctx是ngx_conf_t里最灵活的成员，在配置解析过程中随着配置块的切换而发生变化，指示了当前配置解析所需参考的环境数据，对于http模块来说ctx必定是ngx_http_conf_ctx_t。

解析http指令的ngx_http_block()里的相关代码如下：

//位于ngx_http.c
static char *
ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_conf_t              pcf;            //临时保存 ngx_conf_t
ngx_http_conf_ctx_t*   ctx;             //http 块的环境数据

ctx = ngx_pcalloc(                      //创建环境数据 ngx_http_conf_ctx_t
cf->pool, sizeof(ngx_http_conf_ctx_t));

pcf = *cf;                              //暂存之前的 ctx
cf->ctx = ctx;                       //设置本配置块的 ctx

cf->module_type = NGX_HTTP_MODULE;   //设置模块的类型标志量
cf->cmd_type = NGX_HTTP_MAIN_CONF;   //设置命令的类型标志量

rv = ngx_conf_parse(cf, NULL);          //使用新的 ctx 解析块内指令

*cf = pcf;                              //解析完毕，恢复之前的 ctx
return rv;
}

其他server、location指令的解析也与此类似，设置本配置块的ctx，这样块内的所有指令都会以此ctx确定正确的含义。

ngx_command_t

ngx_command_t结构定义在ngx_conf_file.h，它包含了Nginx框架解析指令所需要的全部信息：

// 定义在 core/ngx_core.h
typedef struct ngx_command_s     ngx_command_t;

// 定义在 core/ngx_conf_file.h
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;            //可以指向任意数据供解析时使用
};

ngx_command_t共有六个成员，它们的含义在注释里已经有了初步解释，下面的小节将做详细讲解。

5.4.2　配置解析的基本流程

完整地阐述Nginx的配置解析流程需要结合源码，为节约篇幅本节只做一个比较简略的说明。

Nginx框架流程

当启动/usr/local/nginx/sbin/nginx时，Nginx从入口函数main()开始执行，检查命令行参数和环境变量，初始化ngx_modules数组，然后进入初始化函数ngx_init_cycle()：

1）创建cycle->conf_ctx数组；

2）调用所有core模块的create_conf函数指针，创建出配置结构，填入数组；

3）设置ngx_conf_t的各个字段，作为配置解析的起始ctx；

4）执行ngx_conf_parse()，在模块数组里查找配置指令对应的解析函数并处理；

5）反复执行步骤4，直至整个配置文件处理完毕；

6）调用core模块init_conf函数指针，初始化core模块的配置。

可见，Nginx框架在ngx_init_cycle()里只处理核心模块，它看不到也并不关心event、http等模块，各个核心模块需要实现自己的配置解析功能，这给予了模块极大的自由，核心模块可以灵活定义自己的架构。

核心模块的init_conf函数指针需要在流程的最后执行，这是因为只有当解析完整个配置文件后才能确定核心模块的相关配置信息，如果有的参数没有配置，那么就可以在这个时候进行初始化，设置默认值。

HTTP框架流程

ngx_http_module模块定义了Nginx的HTTP框架，核心函数是解析http块的ngx_http_block()函数，它与ngx_init_cycle()的功能和流程类似：

1）创建ngx_http_conf_ctx_t结构，里面有三个配置数组；

2）调用所有http模块的create_xxx_conf()填充数组；

3）设置ngx_conf_t的各个字段，作为解析http模块的基本环境；

4）准备工作，执行每个模块的preconfiguration函数指针；

5）调用ngx_conf_parse()，解析http块内的所有指令，直至解析完毕；

6）调用init_main_conf函数指针，初始化main配置数据；

7）调用所有http模块的merge_srv_conf函数指针，合并main和srv域的配置数据；

8）最后执行postconfiguration，完成整个http块的配置解析。

server和location块的解析与http块类似，区别在于ngx_http_conf_ctx_t结构里数组元素的填充，可以直接使用上层的数据，无须重复创建，也不需要调用preconfiguration、postconfiguration等函数指针。

5.4.3　配置数据的存储模型

Nginx的配置文件以块的形式定义了层次关系，分成了main/http/server/location四个层次[8]，对应到框架内部的存储也是如此，不同的层次使用不同的内存区域（数组）保存数据，区域之间以指针连接表示层次关系，设计得非常精巧。

本节将结合源码和图表来解析Nginx配置数据的存储模型。

main层次

main层次的存储使用的是ngx_cycle_t::conf_ctx，它只存储core模块的配置数据。

在ngx_init_cycle()函数里，conf_ctx使用ngx_modules数组进行初始化，它只操作核心模块：

// 位于core/ngx_cycle.c
for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->type != NGX_CORE_MODULE) {  //检查core 模块
continue;
}

module = ngx_modules[i]->ctx;                      //获取模块的 ctx函数表

if (module->create_conf) {                         //模块是否有 create 函数
rv = module->create_conf(cycle);                 //创建模块的配置数据

cycle->conf_ctx[ngx_modules[i]->index] = rv;   //加入数组
}
}

由于core模块数量很少，所以ngx_cycle_t::conf_ctx数组几乎是空的，主要存储的是ngx_core_module模块的配置数据，用于解析master、daemon等指令。[9]

main层次配置数据的存储可以用图5-3来表示。

图5-3　main层次配置数据的存储

http层次

当Nginx框架遇到http指令时，就会调用ngx_http_block()函数，在这里会创建ngx_http_conf_ctx_t结构，为三个数组分配内存：

//位于http/ngx_http.c，ngx_http_block()
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));

ctx->main_conf = ngx_pcalloc(cf->pool,    //分配main_conf 的内存
sizeof(void *) * ngx_http_max_module);
...             //为另外两个数组分配内存

for (m = 0; ngx_modules[m]; m++) {
if (ngx_modules[m]->type != NGX_HTTP_MODULE) { //检查http 模块
continue;
}

module = ngx_modules[m]->ctx;       //获取模块的 ctx函数表
mi = ngx_modules[m]->ctx_index;      //获取模块的类型序号

if (module->create_main_conf) {     //创建main 配置
ctx->main_conf[mi] = module->create_main_conf(cf);
}

if (module->create_srv_conf) {      //创建server配置
ctx->srv_conf[mi] = module->create_srv_conf(cf);
}

if (module->create_loc_conf) {      //创建location 配置
ctx->loc_conf[mi] = module->create_loc_conf(cf);
}
}

从这段代码可以看到，http main层次的ngx_http_conf_ctx_t结构不仅保存了所有http模块的main配置数据，同时也保存了所有http模块的server和location配置数据。这种设计实现了Nginx配置文件的指令作用域功能：低层次的配置指令可以在高层次里出现，多个低层次的指令可以共享高层次指令的值，只需把两个层次的数据比较再合并即可。

http层次配置数据的存储可以用图5-4来表示。

图5-4　http层次配置数据的存储

server层次

ngx_http_core_module模块是第一个http模块，它的配置数据结构ngx_http_core_main_conf_t里有个动态数组成员servers，保存了所有server相关的信息（使用结构体ngx_http_core_srv_conf_t）。

每一个server块也同样使用ngx_http_conf_ctx_t里的三个数组来保存模块配置数据，代码与http块的处理类似。但因为http块的配置指令不会在server块里出现，所以不会创建main配置数据，而是直接用指针引用：

//位于http/ttp_core_module.c，ngx_http_core_server()
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));

ctx->main_conf = http_ctx->main_conf;      //指向上层的配置数组

for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->type != NGX_HTTP_MODULE) { //检查http 模块
continue;
}

module = ngx_modules[i]->ctx;       //获取模块的 ctx函数表

if (module->create_srv_conf) {      //创建server配置
mconf = module->create_srv_conf(cf);

ctx->srv_conf[ngx_modules[i]->ctx_index] = mconf;
}

if (module->create_loc_conf) {      //创建location 配置
mconf = module->create_loc_conf(cf);

ctx->loc_conf[ngx_modules[i]->ctx_index] = mconf;
}
}

这样，每个server块直接引用了上层http块的main配置，然后又拥有了只属于本块的srv和loc配置，既节约了存储成本，又可以简单快速地访问main配置，无须跳转。

server层次配置数据的存储可以用图5-5来表示。

图5-5　server层次配置数据的存储

location层次

ngx_http_core_module模块使用结构体ngx_http_core_loc_conf_t以队列的形式保存server里的所有location信息。

location块与http和server块一样，使用ngx_http_conf_ctx_t的三个数组保存模块配置数据，但用指针引用上层main和server的配置数据，只调用模块的create_loc_conf，只用一个数组持有所有http模块在本location里的配置数据。

它的实现在ngx_http_core_location()函数里，代码基本相同，为节约篇幅不再列出。

location层次配置数据的存储可以用图5-6来表示。

图5-6　locaion层次配置数据的存储

5.4.4　访问配置数据

通过5.4.3节的分析，我们看到Nginx使用多个ngx_http_conf_ctx_t结构表示不同的配置层次，每个结构代表一个块的配置，所有http模块的配置数据都存储在结构里的main_conf/srv_conf/loc_conf数组里，并且每个数组都以直接或者间接的方式存储了模块相关配置信息，所以可以用“ctx.xxx_conf[module.ctx_index]”的形式访问任意http模块当前的正确配置数据，完全不需要关心当前所在的配置块位置。

为了简化操作，Nginx定义了三个宏，用来在配置阶段直接获得http模块在当前ctx下的配置数据：

#define ngx_http_conf_get_module_main_conf(cf, module)  \
((ngx_http_conf_ctx_t *) cf->ctx)->main_conf[module.ctx_index]

#define ngx_http_conf_get_module_srv_conf(cf, module)   \
((ngx_http_conf_ctx_t *) cf->ctx)->srv_conf[module.ctx_index]

#define ngx_http_conf_get_module_loc_conf(cf, module)   \
((ngx_http_conf_ctx_t *) cf->ctx)->loc_conf[module.ctx_index]

另外，在处理HTTP请求时，Nginx框架也会把模块配置数组存放在ngx_http_request_t对象里：

struct ngx_http_request_s {
void**   main_conf;    //http 层次的配置数组
void**   srv_conf;     //server 层次的配置数组
void**   loc_conf;     //location 层次的配置数组
...
};

所以可以用同样的方法获取模块的配置：

#define ngx_http_get_module_main_conf(r, module)   \
(r)->main_conf[module.ctx_index]

#define ngx_http_get_module_srv_conf(r, module)   \
(r)->srv_conf[module.ctx_index]

#define ngx_http_get_module_loc_conf(r, module) \
(r)->loc_conf[module.ctx_index]

5.4.5　确定配置数据的位置

在5.4.3节，Nginx调用create_xxx_conf为模块的配置数据分配好了存储空间（三个数组），接下来的工作就是解析配置指令，为模块找到正确的位置存储，这需要由ngx_command_t的成员conf和offset来共同决定。

存储位置宏

在http/server/location三个层次里都使用的是ngx_http_conf_ctx_t结构，里面有三个数组用于存放模块的配置数据。这三个数组都是完全相同的void*[]类型，所以需要一种方式来决定数据存放在哪一个数组里，或者说是配置数据所在的作用域。

Nginx给出的答案是使用结构偏移量，也就是ngx_command_t结构里的conf成员[10]，让用户指定存储的位置（作用域）：

#define NGX_HTTP_MAIN_CONF_OFFSET  offsetof(ngx_http_conf_ctx_t, main_conf)
#define NGX_HTTP_SRV_CONF_OFFSET   offsetof(ngx_http_conf_ctx_t, srv_conf)
#define NGX_HTTP_LOC_CONF_OFFSET   offsetof(ngx_http_conf_ctx_t, loc_conf)

这三个宏利用offsetof获取了数组成员在ngx_http_conf_ctx_t结构内部的偏移量，Nginx在解析指令时使用偏移量得到要存储的数组：

//位于core/ngx_conf_file.c 的 ngx_conf_handler()
confp = *(void **) ((char *) cf->ctx + cmd->conf); //偏移量获得数组

if (confp) {           //检查数组是否有效
conf = confp[ngx_modules[i]->ctx_index];    //得到之前创建的配置数据
}

代码里的cf->ctx是当前解析指令所在层次的ngx_http_conf_ctx_t结构，指令的配置数据存储在当前层次的三个数组里，所以用ctx_index就可以得到。

存储偏移量

找到了配置数据结构的存储位置后，Nginx还必须用偏移量字段offset才能访问到结构里的成员，存放真正的数据。

偏移量字段是一个巧妙的设计，可以让配置解析函数即使不知道配置数据的内部细节也能够直接定位变量的具体地址，以这种方式来操作用户定义结构的“黑盒”，实现去耦合。

偏移量字段不是解析所必需的，如果我们自己编写解析函数，结构定义已知，那么这个字段就可以设置为0，在函数里直接操作数据成员。

5.4.6　配置解析函数

在ngx_command_t里配置解析函数set()的原型是：

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

它有三个参数：

- cf是当前指令解析的环境数据，它的args成员存储了指令的所有参数；

- cmd是当前指令的描述信息，也就是ngx_command_t数组里的元素；

- conf是在当前层次的ngx_http_conf_ctx_t数组里存放的配置数据结构。

通常使用cf和conf这两个参数，再加上一些字符串处理代码，就可以完成对一条指令的解析工作。

注意它的返回值不是ngx_int_t，而是char*，如果出错可以直接返回错误信息的字符串，否则返回NGX_CONF_OK，即NULL。

预定义的解析函数

Nginx预定义了14个配置解析函数，下面列出几个常用的函数。这些函数的名称均是“ngx_conf_xxx_slot”的形式，为了节约篇幅仅列出函数名中间的部分：

- set_flag：处理值on|off，转化为数字1|0；

- set_str：处理一个参数，转化为ngx_str_t；

- set_str_array：处理多个参数，转化为ngx_str_t数组；

- set_keyval：处理多个参数，转化为ngx_keyval_t数组；

- set_num：处理一个参数，转化为整数；

- set_size：处理一个参数，转化为整数，可以使用单位K/M；

- set_msec：处理一个参数，转化为毫秒数，可以使用单位m/h/d/w/M/y等；

- set_sec：类似set_msec，但转化为秒数。

在使用这些预定义的配置解析函数时变量必须初始化为UNSET值。

解析函数的实现

参考预定义的配置解析函数实现代码有助于我们开发自己的解析函数，这里以最简单的ngx_conf_set_flag_slot()函数为例，代码如下：[11]

char *
ngx_conf_set_flag_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
char  *p = conf;                           //指向配置数据结构体

ngx_str_t        *value;                   //字符串指针
ngx_flag_t       *fp;                      //存储变量的位置指针

fp = (ngx_flag_t *) (p + cmd->offset);  //使用偏移量得到存储位置

if (*fp != NGX_CONF_UNSET) {                      //检查UNSET值
return "is duplicate";                        //返回错误信息
}

value = cf->args->elts;              //获取参数字符串数组

//大小写无关比较 on|off字符串，设置值为 1|0
if (ngx_strcasecmp(value[1].data, (u_char *) "on") == 0) {
*fp = 1;
} else if (ngx_strcasecmp(value[1].data, (u_char *) "off") == 0) {
*fp = 0;
} else {
ngx_conf_log_error(...);
return NGX_CONF_ERROR;
}

return NGX_CONF_OK;
}

5.4.7　配置数据的合并

解析完所有的配置指令后，Nginx的配置解析工作并没有结束，http/server/location等多个层次里的配置数据数组还需要执行合并操作，Nginx会逐层次地调用模块的merge_srv_conf和merge_loc_conf函数指针，最终实现配置的跨层次生效。

在ngx_http_module_t里这两个函数指针的定义是：

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

函数的第一个参数cf通常不会使用；参数prev表示上个层次的配置数据；conf表示当前层次的配置数据，由于是void*所以需要先转型为配置数据结构才能做合并操作。

合并操作的通常做法是逐个检查当前结构里的值，如果未设置（UNSET），那么就默认使用高层次的对应值初始化。

配置数据的合并操作必须由我们自己实现，因为Nginx不知道我们的数据结构，也不知道我们的合并逻辑。不过Nginx预定义了10个配置合并宏，实现了通用的合并逻辑，实际上就是在3.2.3节介绍的条件赋值宏。

5.4.8　配置指令的类型

最后我们介绍ngx_command_t的成员type，它是一个标志量，确定了指令所能出现的作用域、参数的数量和类型。

Nginx使用一系列的宏来定义指令类型，这些宏实际上是二进制位，可以用逻辑或操作符“|”灵活组合来表达复杂的配置。

决定作用域的宏定义如下：[12]

//定义在http/ngx_http_config.h
#define NGX_HTTP_MAIN_CONF   0x02000000  //可以出现在 http块里
#define NGX_HTTP_SRV_CONF    0x04000000  //可以出现在 server 块里
#define NGX_HTTP_LOC_CONF    0x08000000  //可以出现在 location 块里
#define NGX_HTTP_UPS_CONF    0x10000000  //可以出现在 upstream 块里

在Nginx的指令解析函数ngx_conf_handler()里会使用当前配置解析的环境数据ngx_conf_t::cmd_type来检查ngx_command_t::type，如果不匹配则表明位置错误：

if (!(cmd->type & cf->cmd_type)) {    //检查指令的位置
continue;         //不匹配则忽略
}

决定参数数量的宏定义如下：

//定义在core/ngx_conf_file.h
#define NGX_CONF_NOARGS      0x00000001   //指令没有参数
#define NGX_CONF_TAKE1       0x00000002   //指令有1 个参数
#define NGX_CONF_TAKE2       0x00000004   //指令有2 个参数
#define NGX_CONF_TAKE3       0x00000008   //指令有3 个参数
#define NGX_CONF_TAKE4       0x00000010   //指令有4 个参数
#define NGX_CONF_TAKE5       0x00000020   //指令有5 个参数
#define NGX_CONF_TAKE6       0x00000040   //指令有6 个参数
#define NGX_CONF_TAKE7       0x00000080   //指令有7 个参数

#define NGX_CONF_MAX_ARGS    8            //指令最多有 8个参数

//指令可以有 1~N个参数，是上面宏的组合
#define NGX_CONF_TAKE12      (NGX_CONF_TAKE1|NGX_CONF_TAKE2)
#define NGX_CONF_TAKE13      (NGX_CONF_TAKE1|NGX_CONF_TAKE3)

决定参数类型的宏定义如下：

//定义在core/ngx_conf_file.h
#define NGX_CONF_FLAG        0x00000200   //参数取值为 on|off，只能有一个
#define NGX_CONF_ANY         0x00000400   //不限制参数的数量
#define NGX_CONF_1MORE       0x00000800   //参数数量必须超过 1个
#define NGX_CONF_2MORE       0x00001000   //参数数量必须超过 2个

在5.1.2节里ndg_test_module模块定义的类型是：

NGX_HTTP_LOC_CONF|NGX_CONF_FLAG

那么它的含义就是允许指令出现在location域，并且参数取值为on|off。

如果想让指令在其他域里也可以出现，那么就使用“|”来连接更多的宏，例如：

//允许指令出现在 http 块的任意位置，但 upstream 块除外
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG

5.5　Nginx模块源码分析

本节简要研究ngx_core_module和ngx_errlog_module这两个模块的源码，但并不解析其工作原理，仅仅是展现模块的定义和配置的解析部分。结合5.4节和5.5节可以更好地理解Nginx的模块架构和配置解析，同时也可以了解到核心模块并不神秘，甚至比大多数http模块还要简单。

5.5.1　ngx_core_module

ngx_core_module是Nginx最基本的模块，在所有的模块里它的编号是0，是ngx_modules数组里的第一个模块，定义了daemon、master_process和worker_processes等核心指令，至关重要。

ngx_core_module定义在文件nginx.c里，而配置数据结构ngx_core_conf_t则定义在ngx_cycle.h里。

配置结构定义

ngx_core_conf_t结构保存了Nginx运行所需的基本参数，与配置指令一一对应，摘要如下：

typedef struct {
ngx_flag_t     daemon;               //守护进程标志量
ngx_flag_t     master;               //master 进程标志量
ngx_int_t      worker_processes;     //worker 进程的数量
...                                  //其他成员变量
} ngx_core_conf_t;

指令数组

ngx_core_commands定义了ngx_core_module使用的配置指令，同时指定了指令的解析函数：

static ngx_command_t  ngx_core_commands[] = {

{ ngx_string("daemon"),            //守护进程指令
NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,          //简单地解析 on|off 的函数
0,
offsetof(ngx_core_conf_t, daemon),
NULL },

{ ngx_string("master_process"),    //master 进程指令
NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,          //简单地解析 on|off 的函数
0,
offsetof(ngx_core_conf_t, master),
NULL },

...                              //其他指令

ngx_null_command                //指令数组结束
};

这些指令的type字段都是NGX_MAIN_CONF|NGX_DIRECT_CONF，只能出现在配置文件最外层的main域（不是http main域）。

创建、初始化配置结构

依据core模块的ctx要求，ngx_core_module需要两个函数，用来创建和初始化配置数据结构ngx_core_conf_t。

ngx_core_module_create_conf()函数的代码与我们的ndg_test_module没有什么区别，同样是分配内存，并置值为UNSET：

static void *
ngx_core_module_create_conf(ngx_cycle_t *cycle)
{
ngx_core_conf_t  *ccf;                //配置结构数据指针

ccf = ngx_pcalloc(                    //创建配置结构数据
cycle->pool, sizeof(ngx_core_conf_t));

ccf->daemon = NGX_CONF_UNSET;      //置为UNSET
...                                   //其他UNSET
return ccf;
}

因为ngx_core_module的指令在配置文件的最外层，没有合并的需求，所以在配置文件解析结束后要用ngx_core_module_init_conf()函数来给出配置的默认值，相当于ngx_http_module_t的init_main_conf函数指针的作用：

static char *
ngx_core_module_init_conf(ngx_cycle_t *cycle, void *conf)
{
ngx_core_conf_t  *ccf = conf;              //配置结构数据指针

ngx_conf_init_value(ccf->daemon, 1);    //默认启用守护模式
ngx_conf_init_value(ccf->master, 1);    //默认启用 master 进程
...                                        //其他操作
return NGX_CONF_OK;
}

模块定义

ngx_core_module使用ngx_core_module_t结构集成创建和初始化函数：

static ngx_core_module_t  ngx_core_module_ctx = {
ngx_string("core"),               //模块名字
ngx_core_module_create_conf,      //创建配置结构
ngx_core_module_init_conf         //初始化配置结构
};

ngx_module_t  ngx_core_module = {    //模块定义
NGX_MODULE_V1,
&ngx_core_module_ctx,               //module context
ngx_core_commands,                  //module directives
NGX_CORE_MODULE,                    //module type
...                                //省略其他变量
};

业务逻辑

ngx_core_module自身没有业务逻辑操作，它只是提供了Nginx运行所需的基本参数，Nginx框架在运行时直接获取并使用，例如：

ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
if (!ngx_inherited && ccf->daemon) { ... }

这里的ngx_get_conf是一个很简单的宏，用来从conf_ctx数组里获取配置结构：

#define ngx_get_conf(conf_ctx, module)  conf_ctx[module.index]

5.5.2　ngx_errlog_module

ngx_errlog_module是ngx_modules数组里的第二个模块，它紧挨着ngx_core_module，定义在ngx_log.c。

与ngx_core_module不同的是它不需要额外的配置信息，所以没有创建和初始化函数。

指令数组

ngx_errlog_module只定义了一个指令error_log，所以指令数组非常简单：

static ngx_command_t  ngx_errlog_commands[] = {
{ngx_string("error_log"),     //运行日志指令
NGX_MAIN_CONF|NGX_CONF_1MORE,
ngx_error_log,               //指令解析函数
0,
0,
NULL},

ngx_null_command
};

模块定义

ngx_errlog_module的模块定义也很简单：

static ngx_core_module_t  ngx_errlog_module_ctx = {
ngx_string("errlog"),      //模块名字
NULL,                      //不需要配置结构
NULL
};
ngx_module_t  ngx_errlog_module = {    //模块定义
NGX_MODULE_V1,
&ngx_errlog_module_ctx,                //module context
ngx_errlog_commands,                     //module directives
NGX_CORE_MODULE,                         //module type
...                                      //省略其他变量
};

指令解析函数

ngx_error_log()是ngx_errlog_module的主要功能函数，它解析指令，设置ngx_cycle_t里的log变量：

char *
ngx_log_set_log(ngx_conf_t *cf, ngx_log_t **head)
{
value = cf->args->elts;       //获取配置字符串
...                               //省略内部操作
return NGX_CONF_OK;
}

5.6　Nginx的编译脚本

Nginx并没有使用autoconf、cmake等流行的编译配置工具，而是使用纯Shell脚本打造了自己的配置工具，优点是灵活且兼容性高，不会受到工具和操作系统的限制。

我们只需具备基本的Shell编程知识就可以很容易地理解编译机制，实现编译脚本。

5.6.1　运行机制

当使用参数“--add_module=PATH”时，configure脚本会把所有的第三方模块加入到脚本变量，代码是：

--add-module=*)                  NGX_ADDONS="$NGX_ADDONS $value" ;;

之后，configure调用auto/modules脚本，调用每个模块所在目录里的config脚本：

if test -n "$NGX_ADDONS"; then      #检查是否有第三方模块
for ngx_addon_dir in $NGX_ADDONS   #循环加入第三方模块
do
echo "adding module in $ngx_addon_dir"  #输出信息

if test -f $ngx_addon_dir/config; then  #是否有 config 脚本
. $ngx_addon_dir/config     #执行config脚本
echo " + $ngx_addon_name was configured"
fi
done
fi

config脚本里主要做的工作就是设置Shell变量，configure最后将使用这些变量产生Makefile和ngx_modules.c里的ngx_modules模块数组。

当然，config脚本也可以不仅限于设置变量，还可以仿造configure那样加入复杂的判断逻辑，检查编译器版本、外部依赖库等条件，但最后仍要设置变量，这是Nginx的编译框架所要求的。

5.6.2　使用的变量

在config里可以操作很多预定义的变量，基本的是两个：

- ngx_addon_name：模块的名字，仅供显示用，与编译无关；

- NGX_ADDON_SRCS：模块的源码文件，可以有多个。

依据模块的种类不同，需要使用不同的变量来设置模块在代码里的名字，这些名字将出现在ngx_modules.c里，集成进ngx_modules模块数组：[13]

- HTTP_MODULES：标准的http模块；

- HTTP_FILTER_MODULES：http过滤模块。

还有另外一些变量，用来设置编译的其他参数：

- CFLAGS：设置特殊的编译选项；

- HTTP_INCS：编译http模块需要包含的路径；

- CORE_INCS：编译所有模块都需要包含的路径；

- HTTP_DEPS：编译http模块依赖的文件；

- CORE_DEPS：编译所有模块都依赖的文件；

- NGX_ADDON_DEPS：仅模块自己依赖的文件；

- CORE_LIBS：编译时需要链接的外部库。

在脚本里写文件路径时我们可以使用变量$ngx_addon_dir，它就是“--add_module=PATH”的PATH，也就是模块的存放路径。

注意除ngx_addon_name外，其他变量的赋值都要使用如下方式：

VAR="$VAR xxx"       #注意变量的赋值方式

例如：

HTTP_MODULES="$HTTP_MODULES ndg_test_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ModNdgTest.cpp"

这种方式相当于把新的字符串附加到了变量的末尾，相当于C/C++里的+=操作符。

5.7　C++封装模块信息

在熟悉了Nginx的模块架构和配置解析机制之后，本节将使用C++对这些C接口进行封装，简化Nginx的模块开发工作。

5.7.1　NgxModuleConfig

在5.3.1节可以看到，ngx_module_t结构里最重要的成员就是ctx_index，在编写http模块代码的很多时候都需要用它来获取配置信息，所以我们实现一个NgxModuleConfig类，它相当于增强的ngx_http_conf_get_module_xxx_conf系列宏。

类定义

由于模块的配置有http/server/location三个层次，而每个层次又可能使用不同类型的配置结构体，为了避免客户代码里void*类型转换的麻烦，NgxModuleConfig直接把配置结构体作为模板参数，在编译期固化下来。

NgxModuleConfig的类定义如下：

template<typename T3 = void, typename T2 = void, typename T1 = void>
class NgxModuleConfig final
{
public:
NgxModuleConfig(ngx_uint_t idx): m_idx(idx)  //获取模块的序号
{}
~NgxModuleConfig() = default;
private:
ngx_uint_t m_idx = 0;         //保存模块的序号
...              //其他成员函数，见后
};

NgxModuleConfig并没有从NgxWrapper继承，这是因为它的目标很明确，只用来获取模块的配置，所以用一个m_idx成员来保存模块序号就足够了，不需要其他额外信息。

NgxModuleConfig使用了三个模板参数，分别表示loc_conf/srv_conf/main_conf里存储的配置信息。读者需要注意模板里的参数顺序，第一个模板T3表示的是loc_conf，而最后一个模板T1表示的是main_conf。因为大多数情况下我们都使用location层次的配置，只传递一个模板参数就足够了，可以简化代码的编写。

基本操作

NgxModuleConfig的基本操作是获取Nginx配置数组里的数据结构：

public:
ngx_uint_t index() const       //获取模块序号
{
return m_idx;
}

template<typename T, typename U>
T* get(U conf) const        //获取配置数据结构
{
return reinterpret_cast<T*>(conf[index()]);
}

get()函数抽象了ngx_http_conf_get_module_xxx_conf系列宏的操作，以模板函数的形式给出了通用获取配置信息的方法，模板参数U可以是一个任意数组。

获取ngx_conf_t里的配置信息

ngx_conf_t里的成员ctx类型是void*，为了获取配置信息需要先转换为ngx_http_conf_ctx_t类型：

private:
ngx_http_conf_ctx_t* ctx(ngx_conf_t* cf) const
{
return reinterpret_cast<ngx_http_conf_ctx_t*>(cf->ctx);
}

然后我们就可以调用get()函数，使用NgxModuleConfig的模板参数类型直接获取三个数组里存储的模块配置信息：

public:
T1* main(ngx_conf_t* cf) const     //获取http 层次的配置信息
{
return get<T1>(ctx(cf)->main_conf);   //使用模板参数 T1
}

T2* srv(ngx_conf_t* cf) const    //获取sever层次的配置信息
{
return get<T2>(ctx(cf)->srv_conf);    //使用模板参数 T2
}

T3* loc(ngx_conf_t* cf) const    //获取location 层次的配置信息
{
return get<T3>(ctx(cf)->loc_conf);    //使用模板参数 T3
}

获取ngx_http_request_t里的配置信息

同样的，我们可以获取ngx_http_request_t里的配置信息，因为ngx_http_request_t直接给出了三个数组指针，所以代码更简单：

public:
T1* main(ngx_http_request_t* r) const  //获取http 层次的配置信息
{
return get<T1>(r->main_conf);    //使用模板参数 T1
}

T2* srv(ngx_http_request_t* r) const //获取sever层次的配置信息
{
return get<T2>(r->srv_conf);   //使用模板参数 T2
}

T3* loc(ngx_http_request_t* r) const //获取location 层次的配置信息
{
return get<T3>(r->loc_conf);   //使用模板参数 T3
}

模板元编程

使用C++的高级技术模板元编程，NgxModuleConfig能够做得更好：如果T是void，那么函数返回void*指针；如果T是一个结构体，那么函数返回T&，这样可以直接用点号（“.”）操作配置数据结构里的成员，编写代码会更加方便。

首先我们需要修改get()函数，当T是void时返回void*，否则返回T&：

template<typename T, typename U>
typename std::enable_if<std::is_void<T>::value,void*>::type
get(U conf) const
{
return conf[index()];        //直接返回 void*
}

template<typename T, typename U>
typename std::enable_if<std::is_object<T>::value,T&>::type
get(U conf) const
{
return *reinterpret_cast<T*>(conf[index()]);  //注意*的使用，返回 T&
}

get()函数使用了一个元函数enable_if<>，可以在编译期根据T的类型来判断是否允许模板函数实例化。当T是一个实体对象类型时，get()会使用第二种形式，对指针使用*操作符返回引用，否则调用第一种形式返回指针。[14]

因为C++11不支持函数类型的自动推导（即函数的返回值使用auto），所以我们还要实现一个元函数eval_type<>，它计算函数应该返回的类型：

template<typename T>       //计算T应该返回的类型
struct eval_type :         //使用了元函数转发
std::conditional<      //标准条件元函数，相当于 if
std::is_void<T>::value,     //检查类型是否是 void
void*,        //返回void*
typename std::add_lvalue_reference<T>::type> //返回T&
{};

//定义宏简化元函数的调用
#define META_TYPE(t) typename eval_type<t>::type

由于eval_type<>的实现涉及比较深的模板元编程知识，本书暂不做过多的介绍，只需要知道eval_type<>可以在编译期计算类型，根据模板参数T返回不同的类型就可以了。

最后我们需要把main()/srv()/loc()等函数的返回值类型改用META_TYPE宏，例如：

META_TYPE(T1) main(ngx_http_request_t* r) const //自动计算正确的返回值类型
{
return get<T1>(r->main_conf);       //返回值类型由 T1确定
}

META_TYPE(T2) srv(ngx_http_request_t* r) const  //自动计算正确的返回值类型
{
return get<T2>(r->srv_conf);      //返回值类型由 T2确定
}

用法

使用NgxModuleConfig的main()/srv()/loc()函数可以很轻松地获取模块的配置信息，无须再做转型操作，而且因为这三个函数分别对ngx_conf_t*和ngx_http_request_t*做了重载，所以也不必再像使用Nginx自带宏那样去费心地选择，绝对不会出现笔误的低级错误。

在调用main()/srv()/loc()函数时需要使用auto&的形式来获取配置结构的引用，这是因为函数返回的是T&，而auto的推导原则是值类型，直接使用auto会导致不必要的拷贝成本。[15]

假设我们已经有了一个NgxModuleConfig对象，那么获取配置信息的代码就是：

NgxModuleConfig<...> conf(...);    //一个NgxModuleConfig对象
auto& loc = conf.loc(r);        //获取location 配置，无须转型
auto& srv = conf.srv(r);        //获取server配置，无须转型
auto x = loc.xxx;         //获取location 配置里的成员

当然，NgxModuleConfig的模板参数的书写和对象的构造不是太容易，它依赖于具体的Nginx模块，这个工作可以由接下来的NgxModule类完成。

5.7.2　NgxModule

NgxModule类封装了ngx_module_t结构，它主要的功能是生产模块对应的NgxModuleConfig对象。

类定义

NgxModule的实现比较简单，相当于一个工厂类，根据模块的ctx_index产生合适的NgxModuleConfig对象：

template<typename T3 = void, typename T2 = void, typename T1 = void>
class NgxModule           //注意不是 final
{
public:
NgxModule(ngx_module_t& m ):        //引用模块对象
m_conf(m.ctx_index)        //使用序号 ctx_index
{}

~NgxModule() = default;
private:
typedef NgxModuleConfig<T3, T2, T1> config_type;  //简化类型定义
config_type m_conf;          //NgxModuleConfig 对象
public:
const config_type& conf() const      //产生NgxModuleConfig
{
return m_conf;
}
};

NgxModule类没有使用准关键字final，这是为了后续扩展的考虑，某些特定的模块可以复用NgxModule的代码，增加模块自己的独有功能。第6章的NgxHttpCoreModule就使用了这种方法，封装了模块ngx_http_core_module。

对于5.1节的模块ndg_test_module，NgxModule可以这样使用：

typedef NgxModule<NdgTestConf> mod_type;     //定义具体的模块类型
mod_type  mod(ndg_test_module);       //代理Nginx模块
auto& cf = mod.conf().loc(r);        //获取location 配置

在第6章，我们还会扩充NgxModule的功能，通过它获取到模块相关的其他信息。

模块定义宏

NgxModule虽然简化了模块的配置获取，但用起来还有一点儿不方便，必须每次用ngx_module_t变量构造后才能使用。最好的方法是提供一个全局的可任意访问的单件，把NgxModule<T>和具体的ngx_module_t模块绑定在一起。

为了达成这个目的，可以定义一个工具宏NGX_MOD_INSTANCE，它为每个Nginx模块产生一个单件类：

#define NGX_MOD_INSTANCE(T, mod, ...)       \   //单件生成宏
struct T {                                 \   //单件类名
typedef NgxModule<__VA_ARGS__> mod_type;\   //NgxModule 类型定义
static mod_type& instance()           \   //单件访问函数
{                                      \
extern ngx_module_t mod;           \   //外部的模块变量
static mod_type m(mod);            \   //单件对象
return m;                         \   //返回单件引用
}                                      \
};

宏NGX_MOD_INSTANCE定义了一个单件类，代码也很简单，先typedef了NgxModule类型，然后声明外部的模块变量，构造对象并返回。

它使用了C99里的可变参数宏的语言特性，可以接受三个或者更多的参数，第一个是单件类的名字，第二个是模块的变量名，其他则是模块的配置结构类型，用法是：

NGX_MOD_INSTANCE(XModule, xxx_module, XConf)    //定义单件
auto& cf = XModule::instance().conf().loc(r);   //获取location 配置

5.8　C++封装配置解析

Nginx的配置使用的是ngx_command_t、ngx_http_module_t等结构，必须手工设置很多的细节，没有很明显的内聚性，本节的封装类只能一定程度上减轻这些工作量。

5.8.1　NgxCommand

类NgxCommand使用C++的默认参数特性，适当减少了在编写ngx_command_t数组时的代码量，类定义如下：

class NgxCommand final        //封装了ngx_command_t
{
private:
ngx_command_t m_cmd = ngx_null_command;  //内部成员
public:
template<typename T>       //构造函数
NgxCommand(const ngx_str_t& n,      //指令名称
ngx_uint_t t, T set,      //指令类型和解析函数
ngx_uint_t c = NGX_HTTP_LOC_CONF_OFFSET,  //使用默认值
ngx_uint_t off = 0, void* p = nullptr):
m_cmd{n, t, set, c, off, p}        //注意使用{}初始化
{}

NgxCommand() = default;       //默认构造为一个空对象
~NgxCommand() = default;
public:
operator const ngx_command_t& () const   //转型操作符
{
return m_cmd;
}
};

NgxCommand持有一个ngx_command_t成员变量，在构造时使用参数对其初始化，并且隐式转换为ngx_command_t类型，所以能够直接用在ngx_command_t数组里。

NgxCommand的构造函数最后三个参数提供了默认值，实际开发时可以少写一些代码。

5.8.2　NgxTake

类NgxTake封装了Nginx里参数数量的设置，用两个整数来表示参数的数量，减少宏的出现。

类定义

NgxTake的形式与NgxCommand类似，同样是在构造函数里完成工作，使用转型操作符：

class NgxTake final
{
public:
NgxTake(ngx_uint_t conf, int m, int n = -1):   //构造函数
m_type(conf | take(m, n))       //基本参数+数量
{}
~NgxTake() = default;
public:
operator ngx_uint_t () const      //转型操作符
{
return m_type;          //返回类型标志变量
}
private:
ngx_uint_t m_type = NGX_HTTP_LOC_CONF;    //类型标志变量
...              //其他成员函数
};

计算参数数量

NgxTake的take()函数把参数上下限m/n转换为Nginx的宏：

private:
static ngx_uint_t take(int m, int n)
{
static
ngx_uint_t takes[] = {       //Nginx的 TAKE 宏
NGX_CONF_NOARGS,
NGX_CONF_TAKE1,
...
NGX_CONF_TAKE7,
};

if(n < 0 || n < m)         //参数有效性检查
{
return takes[m];
}

if(n >= NGX_CONF_MAX_ARGS)      //参数数量太大
{
return m == 1 ?
NGX_CONF_1MORE :      //多于1个
NGX_CONF_2MORE;       //多于2个
}

ngx_uint_t tmp = 0;
for(int i = m; i <= n; ++i)       //逻辑或多个宏
{
tmp |= takes[i];
}

return tmp;
}
};

用法

NgxTake仅仅封装了参数数量的设置，并不包含其他的NGX_CONF_FLAG、NGX_HTTP_LOC_CONF等宏，这些宏需要在构造函数的conf参数里设置，例如：

NgxTake(NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, 1)   //使用一个参数，flag 类型

5.8.3　NGX_MODULE_NULL

ngx_http_module_t和ngx_module_t结构里有很多字段不是必须的，虽然Nginx提供了NGX_MODULE_V1等宏，但在初始化时还有一些函数指针需要使用nullptr填充，显得比较麻烦。

仿造Nginx的方式，我们也可以定义宏专门用于简化空函数指针的设置。简单的做法是定义NULL2、NULL3这样的宏，每个宏里有N个空指针，但使用Boost提供的预处理元编程库可以得到更优雅的解法：

#include <boost/preprocessor/repetition/enum.hpp>  //预处理元编程头文件

#define NGX_NULL_HELPER(z, n, t) t      //辅助宏
#define NGX_MODULE_NULL(n)      \     //实际使用的宏
BOOST_PP_ENUM(n, NGX_NULL_HELPER, nullptr)   //预处理元编程

这里我们使用了boost.preprocessor库的BOOST_PP_ENUM元函数，它可以迭代n次，调用辅助宏NGX_NULL_HELPER产生多个逗号分隔的字符串。例如，NGX_MODULE_NULL(3)在经过预处理后会变成：

nullptr, nullptr, nullptr       //产生三个 nullptr

5.9　C++开发Nginx模块

本节将使用前两节里的C++类重新实现5.1节的ndg_test_module，编写一个真正现代C++风格的Nginx模块，读者可以对比看看两者有哪些差异。

5.9.1　C++模块的基本组成

使用C语言编写Nginx模块，源码的组成通常比较简单，因为模块都是独立的，所以只需在一个*.c里实现指令解析、业务逻辑和模块集成等所有功能代码，再加上几个头文件，源文件的组织形式如图5-7所示。[16]

图5-7　C语言源文件的组织形式

由于C++提供了面向对象特性，能够以类封装不同的功能模块，所以最好不要采用C语言的简单组织形式，而是把不同的功能代码分别放在不同的源文件里。

本书采用下面的方式：

- XConf.hpp实现配置数据及相关操作；

- XHandler.hpp实现业务逻辑；

- XInit.hpp实现指令配置和模块集成；

- ModX.cpp定义ngx_module_t全局模块变量。

C++源文件的组织形式如图5-8所示。

图5-8　C++源文件的组织形式

5.9.2　配置信息类

我们使用面向对象的思想重新实现类NdgTestConf，除基本的配置信息外还要增加创建和合并等操作。

类定义

NdgTestConf仍然需要有一个成员来保存配置信息，这与5.1节相同：

// NdgTestConf.hpp
class NdgTestConf final         //禁止被继承
{
public:
typedef NdgTestConf this_type;       //自身类型定义
public:
NdgTestConf() = default;
~NdgTestConf() = default;
public:
ngx_flag_t enabled = NgxUnsetValue::get();   //标志变量，构造时初始化
...              //其他成员函数，见后
};

创建函数

静态成员函数create()实现了ngx_http_module_t结构所需的创建配置信息功能：

public:
static void* create(ngx_conf_t* cf)      //创建配置数据结构
{
return NgxPool(cf).alloc<this_type>();   //直接内存池分配内存
}

合并函数

5.1节的模块出于简单的目的并没有提供配置项合并功能，指令只能在location里出现。现在我们为它增加这个功能。当“ndg_test”指令出现在http块或者server块中时Nginx会调用merge()函数，合并两个配置结构数组里的值：

static char* merge(ngx_conf_t *cf, void *parent, void *child)
{
boost::ignore_unused(cf);       //不使用cf变量，消除警告

auto prev = reinterpret_cast<this_type*>(parent);   //上层的配置结构
auto conf = reinterpret_cast<this_type*>(child);   //本层的配置结构

NgxValue::merge(conf->enabled, prev->enabled, 0);  //合并变量，默认值 0

return NGX_CONF_OK;
}

模块单件定义

由于已经有了配置信息类NdgTestConf，所以可以使用5.7.2节的宏NGX_MOD_INSTANCE来定义模块的单件类，之后只要包含头文件NdgTestConf.hpp就可以随时获取模块的配置信息：

NGX_MOD_INSTANCE(NdgTestModule, ndg_test_module, NdgTestConf)

注意：在定义模块单件时第二个参数模块变量的名字必须要与最终在cpp里的模块变量名一致（即5.9.5节），否则编译时会找不到符号无法链接。

5.9.3　业务逻辑类

NdgTestHandler实现了模块的处理函数和框架注册：

// NdgTestHandler.hpp
class NdgTestHandler final
{
public:
typedef NdgTestHandler this_type;     //自身类型定义
public:
...              //其他成员函数，见后
};

处理函数

NdgTestHandler的处理函数是静态成员函数handler()，代码与5.1节基本相同，但增加了捕获可能发生异常的try-catch操作和NgxClock计时：

public:
static ngx_int_t handler(ngx_http_request_t *r)   //业务逻辑函数
try
{
NgxClock clock;            //计时器计算时间

auto& cf = NdgTestModule::instance().conf().loc(r);//获取配置

NgxLogError(r).print("hello c++");      //记录运行日志

if (!cf.enabled)           //检查模块是否禁用
{
cout << "hello disabled" << endl;      //输出字符串
return NGX_DECLINED;         //执行成功但未处理
}

cout << "hello nginx" << endl;        //输出字符串

std::cout << clock.elapsed() << "s"<< std::endl;   //输出时间

return NGX_DECLINED;         //执行成功但未处理
}
catch(const NgxException& e)         //捕获可能发生的异常
{
return e.code();         //返回错误码
}

注册函数

首先我们要为ngx_http_core_module定义代理类，因为它的三个配置数据结构都不同，所以宏有五个参数：[17]

NGX_MOD_INSTANCE(NgxHttpCoreModule, ngx_http_core_module,
ngx_http_core_loc_conf_t,
ngx_http_core_srv_conf_t, ngx_http_core_main_conf_t)

静态成员函数init()向Nginx框架注册handler()，使用NgxHttpCoreModule类可以轻松地获取配置数据：

public:
static ngx_int_t init(ngx_conf_t* cf)
{
auto& cmcf = NgxHttpCoreModule::instance().conf().main(cf);

typedef NgxArray<ngx_http_handler_pt> handler_array_t;

handler_array_t arr(cmcf.phases[NGX_HTTP_REWRITE_PHASE].handlers);
arr.push(&this_type::handler);

return NGX_OK;          //执行成功
}

注意：在注册handler时静态成员函数指针的使用方式必须要加取地址操作符“&”，这是与自由函数明显不同的地方。

5.9.4　模块集成类

类NdgTestInit整合NdgTestConf和NdgTestHandler，实现Nginx模块集成。

类定义

NdgTestInit的定义如下：

// NdgTestInit.hpp
class NdgTestInit final
{
public:
typedef NdgTestConf      conf_type;    //简化类型定义
typedef NdgTestHandler   handler_type;
typedef NdgTestInit      this_type;
public:
...                                    //其他成员函数，见后
};

配置指令解析

静态成员函数cmds()创建ngx_command_t数组，它使用了5.8节的NgxCommand和NgxTake，减少了编写的代码量：

public:
static ngx_command_t* cmds()
{
static ngx_command_t n[] =      //配置指令数组，静态变量
{
NgxCommand(
ngx_string("ndg_test"),      //指令的名字
NgxTake(          //指令的作用域和类型
NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, 1),
ngx_conf_set_flag_slot,      //解析函数指针
NGX_HTTP_LOC_CONF_OFFSET,      //数据的存储位置
offsetof(conf_type, enabled)    //数据的具体存储变量
),
NgxCommand()        //空对象，结束数组
};

return n;          //返回数组地址
}

与5.1节的代码相比，函数cmds()的变化并不是很大，这是由Nginx模块架构所决定的。

函数指针表

静态成员函数ctx()使用NdgTestConf和NdgTestHandler的成员函数填充ngx_http_module_t结构，指示Nginx框架在配置解析时需要调用的函数：

public:
static ngx_http_module_t* ctx()
{
static ngx_http_module_t c =
{
NGX_MODULE_NULL(1),       //解析配置文件前被调用，不使用
&handler_type::init,      //解析配置文件后被调用
NGX_MODULE_NULL(4),       //不使用的 4 个函数指针
&conf_type::create,       //创建location 域的配置结构
&conf_type::merge,       //合并location 域的配置结构
};

return &c;          //注意返回的是指针
}

这里我们使用了宏NGX_MODULE_NULL，不需要再逐个写出nullptr。

模块定义

最后，我们使用ctx()和cmds()定义ngx_module_t对象：

public:
static const ngx_module_t& module()
{
static ngx_module_t m =        //模块定义，静态变量
{
NGX_MODULE_V1,             //标准的填充宏
ctx(),                     //配置功能函数
cmds(),                    //配置指令数组
NGX_HTTP_MODULE,           //http 模块必须的 tag
NGX_MODULE_NULL(7),        //不使用的 7 个函数指针
NGX_MODULE_V1_PADDING      //标准的填充宏
};

return m;                    //返回对象的常量引用
}

5.9.5　实现源文件

ndg_test_module的所有功能都已经实现，但它们仅存在于头文件里，要想编译进Nginx还必须要有一个cpp文件。

ModNdgTest.cpp的代码非常简单，只有一行，定义Nginx编译所需的ngx_module_t变量，这也恰当地反映了Nginx模块的本质——ngx_module_t就是模块：

#include "NdgTestInit.hpp"       //包含头文件
auto ndg_test_module = NdgTestInit::module();  //定义ngx_module_t变量

由于模块名称和源文件名没有变动，所以config脚本无须修改，可以直接使用“--add-module”参数把模块集成进Nginx，再使用curl测试，步骤与5.1.6节相同。

5.9.6　增加更多功能

现在我们已经有了第一个可以运行的Nginx模块ndg_test_module，虽然它目前还不能做什么有实际意义的工作，基本是一个空的模块，但它毕竟能够嵌入Nginx框架里运行，所以完全可以进一步扩展它的功能，把它作为一个单元测试的容器，编写测试代码，学习验证Nginx的各种数据结构和接口函数。

例如，可以在NdgTestHandler里编写一个timer()函数测试NgxDatetime：

static void timer()
{
cout << NgxDatetime::today();    //输出当天日期
cout << NgxDatetime::http();     //输出当天日期的 http 格式

auto t = NgxDatetime::since();      //获取当前时间戳
auto s = NgxDatetime::http(t);      //转换http 字符串格式
assert(t == NgxDatetime::http(s));  //验证字符串转时间戳
...                                 //更多的测试语句
}

然后在handler()函数里执行：

static ngx_int_t handler(ngx_http_request_t *r)//业务逻辑函数
{
...             //其他功能
timer();           //执行测试函数
}

5.10　总结

本章讲解如何开发Nginx模块，并探究了Nginx最核心、最重要的模块架构和配置解析机制，内容较多，需要仔细耐心地阅读才能理解掌握，读者也可以结合之后的章节再回头加深体会。

基本开发流程

我们首先编写了一个Nginx模块ndg_test_module，它的功能虽然简单，但却很利于学习，包含了Nginx模块开发的所有基本要素，也走过了所有开发流程，完全可以藉此为起点，实现出更复杂的模块。

以ndg_test_module为例，我们简要介绍了Nginx模块开发的基本流程，包括设计、开发、编译、测试验证和调优五个步骤。其中前四个步骤是开发任何模块所必需的，而最后一个调优步骤因模块而异，小且简单的模块通常可以省略，但大型模块则必不可少，而且调优所花费的时间可能会比开发和测试更多。

模块架构和配置解析

本章的重点内容是讲解Nginx的模块架构和配置解析机制，这两者的联系非常密切，它们构成了整个Nginx宫殿，每一个模块就是宫殿里的一个房间。

可以把Nginx的配置文件想象成是一种领域特定语言（domain special language），而Nginx模块架构则是这种语言的解析器，编写Nginx模块就是为这种语言增加词汇和语义动作，不断地丰富词汇就能够让Nginx掌握更多的本领，变得更加强大。

ngx_module_t结构体描述了Nginx的模块，为了能够实现灵活健壮的模块架构，它保留了一些字段供将来使用，目前用到的主要是ctx_index、type、commands和ctx四个字段。ctx_index记录了模块的访问序号，type标记了模块的类型，commands定义了模块支持的指令集合，而ctx则是模块架构可扩展性的根本，实现了类似C++的继承特性，可以派生出无数种类的模块。对于http模块来说，ctx是一个函数指针的集合，这些函数指针会在Nginx解析配置的某个时间点被调用。

读取并解析配置文件在很多系统里都是一件无足称道的工作，很多程序仅把它视为简单的纯文本或者略微复杂的xml，只有在需要的时候才去获取配置数据。而Nginx的高明之处就是把配置文件与模块架构紧密地结合在一起，启动阶段模块负责解析配置文件，逐层次地在内存里建立完全对应的内存存储模型，这样运行的时候就无须频繁地访问文件，可以直接在内存里高效便捷地获取模块的配置。

Nginx为配置文件设计了精巧的存储结构，最顶层是ngx_cycle_t::conf_ctx，它只存储core模块的配置。为了支持HTTP服务的虚拟主机和虚拟目录，Nginx使用了ngx_http_conf_ctx_t结构，它内部有三个数组，用来存储http/server/location三个层次的配置。

每一个http/server/location块都会持有一个ngx_http_conf_ctx_t结构，这些结构之间使用指针互相连接，表述块的包含关系，形成了一个具有复杂结构的树，树的根则是http块的ngx_http_conf_ctx_t结构——因为配置文件里只能有一个http块定义。

解析Nginx配置文件主要的工作就是构建http配置树。在创建树前执行preconfiguration做准备，然后调用create_xxx_conf填充节点数组，遇到server和location指令再创建新的节点。叶子节点则是具体的指令，由具体的模块负责解释。当解析完毕时，配置树也创建完成，这时就要遍历树执行init和merge操作，逐个设置配置数据的值，避免出现UNSET，最后执行postconfiguration完成收尾工作。

ngx_command_t结构定义了解析配置指令的方式，成员conf确定了模块配置数据结构存储的数组，offset确定了结构里变量的具体位置。这样函数指针set就可以使用conf找到数组里的结构体对象，再用offset找到结构体里的成员，然后从cf->args里获取字符串，执行配置动作。ngx_command_t::type是用来确定指令的作用域、类型和参数的标志量，可以使用逻辑或操作组合很多特性，Nginx在解析流程里会参考type字段来检查指令是否正确。

我们还简略研究了ngx_core_module和ngx_errlog_module这两个核心模块，通过阅读官方代码，可以更好地理解Nginx的模块架构和配置解析机制。

编译脚本

Nginx使用Shell编程，用脚本的方式产生用于编译的Makefile和objs/ngx_modules.c，以静态方式加载模块。

auto/modules脚本会调用模块的config脚本，此时config脚本可以操作ngx_addon_name、NGX_ADDON_SRCS、HTTP_MODULES等Shell变量，Nginx用这些变量收集第三方模块的源文件、模块变量名、包含路径等信息，最后由auto/make脚本生成Makefile等文件。

C++封装

本章的第四部分是模块的C++实现。

NgxModuleConfig和NgxModule封装了模块最常用的获取配置信息操作，它们利用C++的模板特性，在编译期确定了main/srv/loc三个层次的配置类型，从而简化了客户代码，不需要再执行转型操作，直接使用关键字auto或decltype就可以得到正确的对象。

因为每个Nginx模块都是唯一的，符合单件的概念，所以我们实现了宏NGX_MOD_INSTANCE，它可以非常简单地为每个Nginx模块产生一个单件类，提供唯一的全局访问点。

在配置解析方面，本章提供了NgxCommand、NgxTake和NGX_MODULE_NULL三个工具，它们给Nginx的原始接口施加了一层薄薄的封装，虽然并没有做太多的工作，但确实可以减少编写实际模块的代码量。

本章的最后我们重新实现了开头的ndg_test_module模块，把模块分解为NdgTestConf、NdgTestHandler和NdgTestInit三个类，分别对应配置解析、业务逻辑和模块集成三个功能，再在ModNdgTest.cpp里定义ngx_module_t变量，完成了纯C++风格的Nginx模块。

[1] 实际上它并不是“最”简单的模块，真正最简单的模块应该是什么也不做，没有任何配置也不介入处理流程，但这样的模块对我们来说毫无意义。

[2] Nginx对配置结构体的命名有自己的规范，通常的形式是ngx_http_module_xxx_conf_t，但因为我们使用的是C++，所以不必受它的约束。

[3] 因为我们使用了C++异常，正确的做法是总使用try-catch块来捕获异常，防止错误发生——特别是在Nginx的请求处理阶段。但那样会导致代码过于冗余，所以本书之后的代码基本都不写try-catch块，请读者注意。

[4] 目前init_master、init_thread和exit_thread这三个函数并没有被框架调用，所以模块只能使用init_module等四个函数指针。

[5] Nginx 1.9.x版新增了一个stream类型的模块，目前还在开发中。

[6] ngx_conf_module比较特殊，它没有ctx扩展，所以ctx==nullptr。

[7] Context是程序开发时经常使用的术语，它的出现意味着代码的运行不是完全独立的，代码的行为可能会依据Context而发生改变，作用相当于一个全局变量的集合。Context直译是“上下文”，但这个名词在作者看来其实是一种“偷懒”的行为，直接照搬了文学领域的翻译，没有精确表达出在计算机编程领域的含义，所以本书中的Context中文均写作“运行环境数据”或“环境数据”，表示代码是运行在Context这个动态的环境之内。

[8] 本章暂不讨论event、mail等模块。

[9] http、mail等模块并没有实现create_conf函数，只有解析到http块或mail块时才动态创建配置数据。

[10] 实际上，conf成员是为http模块专门设计的，其他类型的模块因为没有复杂的层次关系，所以不会用到这个成员。

[11] 这里省略了post字段的处理代码。

[12] 这里省略了mail、event等模块使用的宏。

[13] 在auto/modules脚本里可以看到还有CORE_MODULE、HTTP_HEADERS_FILTER_MODULE等变量，读者可以自行研究。

[14] 我们也可以使用Boost程序库的enable_if<>、disable_if<>等元函数，代码会略简洁一些。但为了风格一致这里都使用了标准库里的元函数，更多的模板元编程知识请参考推荐书目[4]。

[15] 在C++14里可以使用含义更明确的decltype(auto)。

[16] 这里说的是逻辑较为简单的Nginx模块，如果模块需要实现的业务很复杂，当然也会分成多个.c和.h文件。

[17] 在之后的第6章我们会进一步封装ngx_http_core_module，让框架注册工作更加简单。

第6章　Nginx HTTP框架综述

在第5章里我们仔细研究了Nginx的模块架构和配置解析机制，学习了Nginx模块开发流程，具备了编写Nginx模块的基本能力。本章将在此基础上深入探讨Nginx的HTTP处理框架，也就是http模块的工作原理，剖析Nginx的HTTP处理流程和相关数据结构。

6.1　HTTP框架简介

Nginx的HTTP框架是由core模块ngx_http_module和http模块ngx_http_core_module共同定义的。

ngx_http_module定义了指令http，保存和管理各个层次里所有http模块的配置数据（我们已经在第5章了解了它的工作原理）；而ngx_http_core_module则是http模块里的“核心”模块，它定义了server、location、listen等HTTP核心指令，搭建了Nginx的HTTP处理框架。

6.1.1　模块分类

http模块处理现今应用得最广泛的HTTP协议，也是Nginx里数量最多的模块，按照功能分类可以分为四种：

- handler模块：直接处理客户端的请求，产生响应数据，是最常用的模块；

- filter模块：对handler模块产生的数据做各种加工过滤处理；

- upstream模块：实现反向代理功能，转发请求到上游服务器，从后端获取响应数据再发回给客户端；

- load-balance模块：不直接处理数据，而是实现负载均衡算法，从upstream块的配置里选择一个合适的上游服务器。

这四种模块的工作机制各不相同，本章重点讨论最复杂也是最常用的handler模块和filter模块。

6.1.2　处理流程

本节简要介绍Nginx的HTTP工作流程，这是理解Nginx HTTP框架的基础。

通用的处理流程

Web服务器的HTTP请求处理流程可以粗略地叙述为如下的顺序步骤：

1）监听端口，接受客户端的连接；

2）读取请求头，包括请求行（request line）和请求头（headers）；

3）读取或者丢弃请求体（body）；

4）生成并发送响应头；

5）生成并发送响应体。

上面只是最简单的HTTP处理流程，Nginx使用了操作系统提供的事件驱动模型（epoll、kqueue等）来实现并发，并且还增加了很多其他功能，如权限检查、重定向、缓存、记录访问日志等，使HTTP处理流程变得十分复杂。

Nginx的处理流程

如果我们忽略Nginx的异步机制，把注意力集中在处理逻辑上，那么可以看到Nginx的主处理流程与基本的HTTP处理流程区别并不大，它的工作流程如下：[1]

1）监听端口，设置回调为ngx_http_init_connection()。

2）接受客户端的连接，调用ngx_http_wait_request_handler()。

3）调用ngx_http_create_request()创建请求对象。

4）接收数据，调用ngx_http_process_request_line()解析请求行。

5）请求行接收完毕，继续接收数据，调用ngx_http_process_request_headers()解析请求头。

6）请求头接收完毕，调用ngx_http_process_request()设置异步读写事件。注意：为了提高运行效率，Nginx不会主动处理请求体。

7）调用ngx_http_handler()开始真正处理请求。

8）调用ngx_http_core_run_phases()按阶段处理请求。这是HTTP框架的核心部分，大部分http模块都在这里运行，最终产生响应数据。

9）调用ngx_http_send_header()发送响应头，从函数指针ngx_http_top_header_filter开始，通过header filter模块链过滤处理，最终发送处理过的响应头。

10）调用ngx_http_output_filter()发送响应体，从函数指针ngx_http_top_body_filter开始，通过body filter模块链过滤处理，最终发送处理过的响应体。

11）处理完毕，记录访问日志。

Nginx处理流程图如图6-1所示。

图6-1　Nginx处理流程图

在这些步骤中比较重要的是第3步和第8步：

ngx_http_create_request()创建请求对象ngx_http_request_t，初始化里面的内存池、起始时间、配置结构数组等成员，应用于整个HTTP处理生命周期。

ngx_http_core_run_phases()分阶段地组织所有的http模块（准确地说是handler模块），调用模块的处理函数，执行虚拟主机查找、重定向、权限检查、内容产生等各种操作，是HTTP框架运行机制中最根本的部分，掌握了它也就掌握了Nginx的HTTP框架。

6.1.3　请求结构体

在第5章我们初步接触了结构体ngx_http_request_t，它表示一个HTTP请求，是Nginx处理HTTP请求的核心数据结构，包含了处理过程中需要的各种数据和信息，比较复杂。

ngx_http_request_t的定义摘要如下：

// 定义在 http/ngx_http.h
typedef struct ngx_http_request_s     ngx_http_request_t;

// 定义在 http/ngx_http_request.h
struct ngx_http_request_s {
uint32_t              signature;      //"HTTP"的 ASCII 码

void**                ctx;             //处理请求的环境数据

void**                main_conf;       //main 层次的配置信息数组
void**                srv_conf;        //server 层次的配置信息数组
void**                loc_conf;        //location 层次的配置信息数组

ngx_pool_t*           pool;            //请求使用的内存池

time_t                start_sec;       //处理请求的开始时间
ngx_msec_t            start_msec;

ngx_int_t             phase_handler;   //当前处理阶段的索引号
ngx_http_handler_pt content_handler;   //内容处理函数指针
ngx_uint_t            access_code;     //访问权限码

...                                    //其他成员
};

ngx_http_request_t贯穿了Nginx处理HTTP请求的整个流程，可以说在Nginx中处理HTTP请求就是操作ngx_http_request_t数据结构。

由于ngx_http_request_t里的成员很多，因此本章只介绍与Nginx HTTP框架有关的部分，其他成员如请求行、请求头、请求体等将在第7章介绍。

ngx_http_request_t里有几个成员我们已经在第5章见过，例如main_conf/srv_conf/loc_conf，它们分别保存了http模块在配置文件中对应的配置信息，这样在处理HTTP请求时就不必访问配置文件，可以直接获取。

ngx_http_create_request()创建ngx_http_request_t对象，初始化它的各个成员，代码摘要如下：

//定义在http/ngx_http_request.c
ngx_http_request_t *
ngx_http_create_request(ngx_connection_t *c)    //创建请求对象
{
ngx_http_request_t         *r;       //结构体指针

...             //其他操作代码

r = ngx_pcalloc(pool, sizeof(ngx_http_request_t)); //创建结构体对象

r->pool = pool;          //设置请求的内存池
r->signature = NGX_HTTP_MODULE;       //设置请求的“签名”

r->main_conf = hc->conf_ctx->main_conf;     //设置main 配置指针
r->srv_conf = hc->conf_ctx->srv_conf;    //设置srv配置指针
r->loc_conf = hc->conf_ctx->loc_conf;    //设置loc配置指针

r->start_sec = tp->sec;         //设置起始时间
r->start_msec = tp->msec;

...             //其他操作代码

return r;             //返回创建好的对象
}

6.1.4　请求的处理阶段

在接收完请求头数据后，Nginx就会调用ngx_http_core_run_phases()驱动各个http模块处理请求。

为了能够更细粒度地控制处理流程，Nginx在这里划分出了11个精确的阶段（或者说是hook点），模块可以挂载到不同的阶段，相互协作以流水线的方式处理请求，从而达到高度的灵活性。

可以把阶段理解为一种高级形式的调用接口，它里面可以存储数个回调函数，Nginx会逐个调用里面的函数，完成请求的处理。

Nginx使用枚举定义了11个请求处理阶段，它们是：[2]

//定义在http/ngx_http_core_module.h
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0,   //读取HTTP 头后，开始读取内容

NGX_HTTP_SERVER_REWRITE_PHASE,  //server 重写URL

NGX_HTTP_FIND_CONFIG_PHASE,     //查找匹配的 location，不可介入
NGX_HTTP_REWRITE_PHASE,         //location 重写URL
NGX_HTTP_POST_REWRITE_PHASE,    //重写后，不可介入

NGX_HTTP_PREACCESS_PHASE,       //检查访问权限前

NGX_HTTP_ACCESS_PHASE,          //检查访问权限
NGX_HTTP_POST_ACCESS_PHASE,     //检查访问权限后，不可介入

NGX_HTTP_TRY_FILES_PHASE,       //访问静态文件，不可介入

NGX_HTTP_CONTENT_PHASE,         //处理请求，产生内容

NGX_HTTP_LOG_PHASE              //记录日志
} ngx_http_phases;                  //枚举类型定义

ngx_http_phases定义的这11个阶段含义都很明确，完整地描述了HTTP请求的处理流程。所有的handler模块都必须工作在这11个阶段中的一个或几个，所以handler模块又称为phase handler模块。

但有四个阶段只能由HTTP框架使用，用户不能注册开发模块在这些阶段处理请求（即使注册了也会被框架忽略），它们是：

- NGX_HTTP_FIND_CONFIG_PHASE

- NGX_HTTP_POST_REWRITE_PHASE

- NGX_HTTP_POST_ACCESS_PHASE

- NGX_HTTP_TRY_FILES_PHASE

所以我们开发的模块只能介入剩下的7个阶段。

NGX_HTTP_REWRITE_PHASE、NGX_HTTP_ACCESS_PHASE和NGX_HTTP_CONTENT_PHASE是开发Nginx模块比较常用的阶段，可以改写URI和权限控制，或者产生我们自己的响应内容。

6.1.5　请求的环境数据

ngx_http_request_t里的ctx是HTTP请求处理流程中一个非常重要的成员，它的类型是void**，即void*的数组，为每一个http模块都提供了专属的存储空间，模块可以在里面存储任意数据，整个请求的生命周期里都可以随时访问，就像是个全局变量。

在函数ngx_http_create_request()里创建ctx的代码如下：

ngx_http_create_request(ngx_connection_t *c)
{
...
r->ctx = ngx_pcalloc(r->pool,       //创建环境数据数组
sizeof(void *) * ngx_http_max_module); //长度是所有 http 模块的数量
...
}

ctx与loc_conf等都是void**类型，但两者存储的数据有根本性的区别：loc_conf等存储的是静态的配置数据，与配置的层次相关，与请求无关；ctx存储的是动态的运行期数据，与配置的层次无关，与请求紧密相关。

由于Nginx的异步和多阶段处理机制的原因，很多时候http模块不可能在一次调用中就完成请求的处理，所以可以用ctx来暂时存储处理的中间结果，当Nginx再次调用模块时再从ctx中取出上次的数据继续运行——这实际上是备忘录模式（memento）的具体应用。[3]

模块不能简单地使用全局变量来暂存数据。这是因为数据是请求相关的，每个请求的状态都不一样，而Nginx会并发处理大量的请求，单纯的全局变量不能应对这样复杂的情况。Nginx把ctx和请求绑定在一起，解决了这个问题。

我们可以使用模块的ctx_index直接访问ctx数组获取模块的环境数据，不过Nginx也提供了两个宏来操作ctx环境数据，用法与获取配置结构信息类似：

//定义在http/ngx_http.h
#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;

如果模块需要在处理过程中保存一些中间结果，通常的做法是定义一个struct，里面包含所有要存储的信息，然后使用内存池创建对象，调用ngx_http_set_ctx()加入ctx数组，之后就可以随时使用ngx_http_get_module_ctx()来获取并操作数据。

创建环境数据对象时一定要使用请求对象ngx_http_request_t的内存池成员r->pool，这样当请求结束时分配的内存才会被及时回收。

6.2　HTTP处理引擎

Nginx的模块ngx_http_core_module管理着所有的handler模块，它定义了请求的处理阶段，把所有的handler模块组织成一条流水线，使用一个“引擎”来驱动模块处理HTTP请求，产生响应内容，本节将详细阐述这一机制。

6.2.1　处理函数原型

Nginx定义了一个函数原型ngx_http_handler_pt，任何handler模块想要处理HTTP请求都必须实现这个函数，它的定义是：

//定义在http/ngx_http_request.h
typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);

ngx_http_handler_pt不仅可以返回NGX_OK、NGX_DECLINED、NGX_AGAIN等Nginx错误码，也可以直接返回200、302、404等标准HTTP状态码，Nginx会在函数ngx_http_finalize_request()中做出合适的处理。

6.2.2　处理函数的存储方式

Nginx使用ngx_http_phase_t结构存储每个阶段可用的处理函数（handler），它实际上是动态数组ngx_array_t，元素类型是ngx_http_handler_pt：

//定义在http/ngx_http_core_module.h
typedef struct {
ngx_array_t  handlers;        //处理函数数组
} ngx_http_phase_t;

ngx_http_core_module的main配置结构又定义了数组phases，用来存储处理阶段中所有可用的handler：

//定义在http/ngx_http_core_module.h
typedef struct {
...
ngx_http_phase_t phases[NGX_HTTP_LOG_PHASE + 1];  //处理阶段数组
} ngx_http_core_main_conf_t;

通过这种方式，Nginx把所有http模块的handler组织成了一个表，纵向是处理阶段，横向是各个模块的处理函数，形成了一个二维职责链，如图6-2所示。

图6-2　handler二维职责链

在配置文件解析完毕之后的postconfiguration函数里，模块可以向ngx_http_core_main_conf_t::phases数组添加元素，实现模块的handler注册，就像第5章我们所做的。

处理HTTP请求时Nginx框架将会分阶段按顺序调用这些函数，让每个模块都有执行的机会——不过实际的代码远不是双重循环那么简单。

6.2.3　内容处理函数

phases数组是模块注册handler最通用的方式，可以介入任意阶段，但产生内容的NGX_HTTP_CONTENT_PHASE阶段则有其特殊性。

由于很多模块都会工作在NGX_HTTP_CONTENT_PHASE阶段，如果都使用添加phases数组的方式很可能会导致数组元素过多，运行效率低，也很容易导致模块冲突。所以Nginx单独为内容处理函数提供另外一处存储位置。

ngx_http_core_module的location配置数据结构ngx_http_core_loc_conf_t里有一个成员handler，它直接确定了该location的处理函数：

//定义在http/ngx_http_core_module.h
struct ngx_http_core_loc_conf_s {      //location 配置结构
ngx_http_handler_pt  handler;       //该 location 的处理函数
...                                  //其他成员
};

Nginx查找location时会检查loc_conf的handler成员，如果不为空那么就会设置ngx_http_request_t的content_handler。在NGX_HTTP_CONTENT_PHASE阶段如果有content_handler，那么Nginx就不再检查phases数组，而直接调用这个函数处理。

这种方式可以让每一个location里都可以有自己的内容处理函数，彼此之间没有影响，大大增加了Nginx处理请求的灵活性。使用这种方式注册处理函数的模块又称作content handler模块。

因为在Nginx里有多个location配置，解析配置文件时location环境数据不断变动，所以这种方式不能在postconfiguration函数里使用，只能在解析到模块所在的location里时设置该模块的handler。

确定模块在某个location里起作用的唯一方式是定义模块自己的配置指令，在解析指令时通过Nginx传递的ngx_conf_t对象获得当前location的信息，然后才能设置配置里的handler，这也就意味着我们至少要编写一个自定义的配置解析函数。

6.2.4　引擎的数据结构

使用二维数组phases可以调用所有handler模块，但它的组织形式不够灵活，效率也不高。实际上，Nginx不会直接调用handler，而是为每个阶段实现一个特定的checker函数，在checker里调用handler，并根据返回值实现阶段的灵活跳转。

checker的相关定义如下：

//阶段处理数据结构
typedef struct ngx_http_phase_handler_s  ngx_http_phase_handler_t;

// checker 的函数原型
typedef ngx_int_t (*ngx_http_phase_handler_pt)(
ngx_http_request_t *r, ngx_http_phase_handler_t *ph);

struct ngx_http_phase_handler_s {
ngx_http_phase_handler_pt  checker;  //检查器函数
ngx_http_handler_pt        handler;    //模块的处理函数
ngx_uint_t                 next;     //下一处理阶段的序号
};

ngx_http_core_main_conf_t里使用ngx_http_phase_engine_t存储了所有handler模块的checker，定义了HTTP框架的处理引擎：

typedef struct {
ngx_http_phase_handler_t*  handlers;     //包含所有模块的 handler
ngx_uint_t                 server_rewrite_index;   //server 重写快速跳转索引
ngx_uint_t                 location_rewrite_index;//location 重写快速跳转索引
} ngx_http_phase_engine_t;

typedef struct {
...
ngx_http_phase_engine_t    phase_engine;   //阶段处理引擎数组
} ngx_http_core_main_conf_t;

6.2.5　引擎的初始化

在解析完http块后，ngx_http_block()函数会调用所有模块的postconfiguration函数指针执行模块的初始化工作，在这里模块可以向phases数组添加元素，把自己的handler注册到合适的阶段。

随后Nginx执行函数ngx_http_init_phase_handlers()，从phases数组生成处理引擎phase_engine。

ngx_http_init_phase_handlers()会遍历phases数组，计算handler模块的数量，统计所有已经注册的handler数量并分配内存[4]，再按阶段分类，把每个handler与对应阶段的checker组合起来，填入引擎数组，部分关键代码如下：

ph = ngx_pcalloc(...);         //引擎数组分配内存
n = 0;              //计数器初始化

for (i = 0; i < NGX_HTTP_LOG_PHASE; i++) {   //按阶段遍历，不含 log 阶段
h = cmcf->phases[i].handlers.elts;     //本阶段的 handler 数组

switch (i) {           //根据阶段选择 checker
...              //省略其他阶段的代码
case NGX_HTTP_SERVER_REWRITE_PHASE:    //rewrite 阶段
checker = ngx_http_core_rewrite_phase;   //该阶段使用的 checker
break;

case NGX_HTTP_CONTENT_PHASE:      //content 阶段
checker = ngx_http_core_content_phase;   //该阶段使用的 checker
break;

default:
checker = ngx_http_core_generic_phase;   //默认的checker
}

//反向遍历 phases 数组，向引擎数组添加元素
for (j = cmcf->phases[i].handlers.nelts - 1; j >=0; j--) {
ph->checker = checker;        //设置checker
ph->handler = h[j];           //设置handler
ph->next = n;                 //设置阶段跳转序号
ph++;
}
}                                        //phases 数组遍历结束

在ngx_http_init_phase_handlers()执行之后，phase_engine的handlers数组把各个模块的handler组成了一个线性的数组，原本的二维数组phases转变成了一维数组，可以高效地访问。而每个元素（ngx_http_phase_handler_t结构）里的next形成了静态链表，指示了阶段跳转的数组序号，也就是下一个阶段的起始位置，通过它可以跳过本阶段的其他handler，直接进入下个处理阶段。

初始化后的引擎数组可以用图6-4来表示。

图6-4　初始化后的引擎数组

还需要注意的一点是，phase_engine数组并不含有NGX_HTTP_LOG_PHASE阶段的模块，这是因为记录日志阶段比较特殊，它并不真正地处理请求，是请求处理的收尾工作，不需要跳转到其他阶段。

6.2.6　引擎的运行机制

结构体ngx_http_request_t里的成员phase_handler标记了在当前处理过程中所在阶段的handler序号，它被初始化为0，通常是NGX_HTTP_SERVER_REWRITE_PHASE阶段里的第一个模块ngx_http_rewrite_module。[5]

接收完请求头后，Nginx调用函数ngx_http_core_run_phases()执行引擎，代码摘要如下：

ph = cmcf->phase_engine.handlers;     //获取引擎数组首地址

while (ph[r->phase_handler].checker) {    //遍历引擎数组

rc = ph[r->phase_handler].checker(     //执行checker，间接调用 handler
r, &ph[r->phase_handler]);
if (rc == NGX_OK) {       //检查返回值
return;          //如果执行成功则退出引擎处理
}
}

ngx_http_core_run_phases()的代码比较简单，它遍历引擎数组，检查数组里的checker，如果有则执行之。在checker函数里会调用handler，并决定是否要在引擎数组里依次执行或者跳转。如果checker返回任何非NGX_OK的错误码，那么就表示请求还未处理完毕，需要继续在引擎数组里查找模块处理；否则Nginx会暂时退出处理引擎，等待下次调用。

不同阶段的checker流程大同小异，我们以NGX_HTTP_CONTENT_PHASE阶段使用的ngx_http_core_content_phase()为例，看一下它的主要处理逻辑（省略了部分代码）：

ngx_int_t
ngx_http_core_content_phase(ngx_http_request_t *r,
ngx_http_phase_handler_t *ph)
{
if (r->content_handler) {    //检查是否有 location 内容处理函数
ngx_http_finalize_request(     //直接调用内容处理函数
r, r->content_handler(r));
return NGX_OK;         //执行完毕，退出引擎
}
rc = ph->handler(r);  //没有location 特定的处理函数，执行此模块的 handler

if (rc != NGX_DECLINED) {     //其他错误码
ngx_http_finalize_request(r, rc);    //以合适的方式结束请求
return NGX_OK;         //执行完毕，退出引擎
}

ph++;    //模块返回 NGX_DECLINED，需要继续执行本阶段的下一个 handler
if (ph->checker) {      //下一个模块有 checker
r->phase_handler++;     //调整请求里的阶段索引号
return NGX_AGAIN;     //非 NGX_OK，要求引擎继续执行
}

return NGX_OK;        //没有可用的 handler，退出引擎
}

从这段代码里我们可以看到，Nginx会优先调用在配置文件location里设置的处理函数，如果没有location handler，则执行注册到本阶段的模块handler。

ngx_http_finalize_request()是HTTP框架里另一个非常重要的函数。它根据错误码或HTTP状态码决定应该如何结束当前阶段的处理，可能是继续执行处理引擎，也可能是暂时退出引擎，还有可能是终止当前请求，限于篇幅这里暂不做分析。

通常Nginx会默认编译ngx_http_static_module、ngx_http_index_module等模块，它们会注册到NGX_HTTP_CONTENT_PHASE阶段，所以一个location如果没有设置特殊的内容处理函数就会由这几个模块处理，读取磁盘上的文件。

从这里我们还可以明确NGX_OK、NGX_DECLINED、NGX_AGAIN等错误码在内容生成阶段的真实含义：[6]

- NGX_OK：模块正确运行，请求处理成功，不需要再有其他动作，引擎结束；

- NGX_DECLINED：模块正确运行，请求未处理，需要下一个模块继续处理；

- NGX_AGAIN：模块正确运行，请求部分处理，需要引擎继续运行。

6.2.7　日志阶段的处理

日志阶段的模块不在ngx_http_core_run_phases()里调用，而是在请求处理完毕时才会执行，函数是ngx_http_log_request()：

static void
ngx_http_log_request(ngx_http_request_t *r)
{
log_handler = cmcf->phases[NGX_HTTP_LOG_PHASE].handlers.elts;
n = cmcf->phases[NGX_HTTP_LOG_PHASE].handlers.nelts;

for (i = 0; i < n; i++) {    //遍历log handler 数组
log_handler[i](r);         //执行回调函数
}
}

因为在日志阶段请求已经处理完毕，不需要再做其他数据处理工作，所以日志模块的调用很简单，不使用引擎数组phase_engine，直接在一个for循环里执行phases数组里所有的log handler，也不做错误检查。

6.3　HTTP过滤引擎

filter模块是Nginx里另一大类http模块，与handler模块不同的是它并不直接产生响应数据，而是对handler模块产生的数据进行各种加工处理，例如增加响应头、对响应体编码、gzip压缩等，可以更灵活地控制输出数据，是职责链模式的实际应用。

早期版本的Nginx只提供响应头和响应体的过滤机制，1.8.0版后的Nginx增加了对请求体的过滤处理，本节主要阐述Nginx的响应过滤机制。

6.3.1　过滤函数原型

Nginx把过滤处理细分为响应头处理和响应体处理两种，定义了两个函数类型：

//定义在http/ngx_http_core_module.h
typedef ngx_int_t (*ngx_http_output_header_filter_pt)  //过滤响应头
(ngx_http_request_t *r);
typedef ngx_int_t (*ngx_http_output_body_filter_pt)   //过滤响应体
(ngx_http_request_t *r, ngx_chain_t *chain);

从这两个函数的名字可以看出，它们是输出过滤器（output），只能处理请求的响应内容。

函数ngx_http_output_header_filter_pt处理响应头，头信息已经包含在了ngx_http_request_t结构里，所以不需要其他参数。

函数ngx_http_output_body_filter_pt处理响应体，需要外部使用chain参数传递给它待输出的数据，这里的数据就是4.4节和4.5节介绍的ngx_buf_t和ngx_chain_t对象。

filter模块必须实现这两个函数以达到过滤处理的目的，但如果只处理响应头，那么就无须实现ngx_http_output_body_filter_pt。

6.3.2　过滤函数链表

Nginx定义了过滤函数链表的头节点：

//定义在http/ngx_http.c
ngx_http_output_header_filter_pt    ngx_http_top_header_filter;
ngx_http_output_body_filter_pt      ngx_http_top_body_filter;

这两个头节点是全局可见的，所以Nginx以一种隐晦却巧妙的方式把过滤函数组成了一个顺序链表：

1）模块实现自己的过滤器函数this_filter；

2）模块定义一个变量next_filter，作为指向下一个节点的指针；

3）模块在postconfiguration阶段用next_filter存储top_filter；

4）模块设置top_filter为this_filter。

写成代码就是：

next_filter   = top_filter;       //存储top_filter
top_filter   = this_filer;       //设置头节点

可以看出，这是一种典型的单链表添加头节点操作。

通过这种方式，每个filter模块在配置解析时都会把自己的过滤函数插入到链表头，同时内部又保存了原来的头节点。在过滤函数执行的最后只需要调用原头节点函数指针就可以让数据继续流到后面过滤模块，完成过滤链表的执行。

Nginx进一步约定，模块里的变量next_filter必须是静态的，而且名字是ngx_http_next_header|body_filter，从而简化了代码逻辑。每个模块里的变量名虽然相同，但因为声明为static，所以不会互相干扰。[7]

Nginx的过滤链表应用了职责链模式（chain of responsibility），HTTP响应数据顺序“流过”链表里的每一个模块，每个模块都可以对数据施加某种操作，前一个模块的输出作为后一个模块的输入，当数据走完整个职责链也就完成了处理。

职责链模式里链表的尾节点是关键，Nginx定义了两个特殊的filter模块：ngx_http_header_filter_module和ngx_http_write_filter_module，它们分别是响应头链表和响应体链表的尾节点，负责最终的拼接响应头和数据发送工作。

这两个模块也同时负责初始化链表，保证最初的头节点指针不为空：

//位于文件 http/ngx_http_header_filter_module.c
static ngx_int_t
ngx_http_header_filter_init(ngx_conf_t *cf)  //postconfiguration 阶段调用
{
ngx_http_top_header_filter = ngx_http_header_filter; //初始化链表头节点
return NGX_OK;          //注意，没有 next指针
}

//位于文件 http/ngx_http_write_filter_module.c
static ngx_int_t
ngx_http_write_filter_init(ngx_conf_t *cf)   //postconfiguration 阶段调用
{
ngx_http_top_body_filter = ngx_http_write_filter;   //初始化链表头节点
return NGX_OK;          //注意，没有 next指针
}

6.3.3　过滤函数的顺序

过滤链表是单向链表，没有可逆性和可交换性，所以链表里模块的执行顺序非常关键，错误的顺序可能会导致数据过滤后产生完全不同的结果。例如，如果链表里有gzip和gunzip两个模块，那么gzip=>...=>gunzip会得到一个非压缩数据，而gunzip=>...=>gzip则会得到一个压缩数据。

Nginx的过滤链表虽然是一个链表，但它的构造发生在配置解析的postconfiguration阶段，一旦构造完毕就不会改变，顺序在Nginx启动时就已经完全固定下来，运行时不能动态地增减。

在ngx_http_block()函数解析完配置文件后，会调用所有模块的postconfiguration函数指针：

for (m = 0; ngx_modules[m]; m++) {    //顺序遍历模块 ngx_modules 数组
module = ngx_modules[m]->ctx;    //获取http 模块的函数表

if (module->postconfiguration) {    //检查postconfiguration 函数指针
if (module->postconfiguration(cf) != NGX_OK) { //执行模块的初始化操作
return NGX_CONF_ERROR;
}
}
}

从代码里可以看到，Nginx是依据数组ngx_modules里的顺序调用postconfiguration函数指针的。所以按照单链表的添加方式，数组里位置在前的filter模块会在链表的尾部，而最后一个filter模块则是过滤链表的头节点，顺序与ngx_modules数组里的正好相反。

Nginx通过configure脚本，严格安排了每个filter模块的位置，保证filter模块可以正常工作，在生成的objs/ngx_modules.c里ngx_modules数组是：

ngx_module_t *ngx_modules[] = {
...            //core、event 等模块
&ngx_http_write_filter_module,          //最后一个 body filter 模块
&ngx_http_header_filter_module,         //最后一个 header filter 模块
&ngx_http_chunked_filter_module,
&ngx_http_range_header_filter_module,
&ngx_http_gzip_filter_module,
&ngx_http_postpone_filter_module,
&ngx_http_ssi_filter_module,
&ngx_http_charset_filter_module,
&ngx_http_userid_filter_module,

...                                         //通常第三方 filter 模块在这里

&ngx_http_headers_filter_module,
&ngx_http_copy_filter_module,           //重要的数据链复制过滤模块
&ngx_http_range_body_filter_module,     //第一个body filter 模块
&ngx_http_not_modified_filter_module,   //第一个header filter 模块
NULL
};

在开发filter模块时，config脚本里要使用Shell变量HTTP_FILTER_MODULES，而不是HTTP_MODULES，否则模块的位置错误会导致Nginx运行异常。

这些模块组成的过滤链表如图6-5所示。

图6-5　模块组成的过滤链表

6.3.4　过滤链表的运行机制

当handler模块生成了响应内容，调用函数ngx_http_send_header()发送响应头时，就会执行响应头过滤链表：

ngx_int_t
ngx_http_send_header(ngx_http_request_t *r)           //发送响应头
{
if (r->header_sent) {                          //响应头发送标志位
return NGX_ERROR;                             //已经发送则不再发送
}

if (r->err_status) {                           //检查请求的错误状态
r->headers_out.status = r->err_status;  //设置HTTP 状态码
r->headers_out.status_line.len = 0;
}

return ngx_http_top_header_filter(r);             //启动过滤链表
}

这样，HTTP处理引擎就和过滤引擎成功地连接在了一起，把响应内容传递给了过滤链表，让各个filter模块执行数据的过滤处理，最终发送给客户端。

响应体过滤链表的执行也基本类似，它使用的函数是ngx_http_output_filter():

ngx_int_t
ngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
rc = ngx_http_top_body_filter(r, in);    //启动过滤链表

return rc;           //返回错误码
}

6.3.5　请求体过滤

从1.8.0版开始，Nginx增加了对请求体的过滤处理，允许我们在读取请求体时用过滤链表的方式来处理请求数据。除工作的阶段不一样外，处理机制与响应体过滤基本类似。[8]

请求体过滤函数的原型与ngx_http_output_body_filter_pt完全相同：

//定义在http/ngx_http_core_module.h
typedef ngx_int_t (*ngx_http_request_body_filter_pt)
(ngx_http_request_t *r, ngx_chain_t *chain);

同样的，请求体过滤也有链表头节点：

//定义在http/ngx_http.c
ngx_http_request_body_filter_pt   ngx_http_top_request_body_filter;

当Nginx读取请求体时，会调用函数ngx_http_request_body_filter()，启动过滤链表。

6.4　Nginx模块源码分析

本节简要研究ngx_http_static_module和ngx_http_not_modified_filter_module这两个模块的源码，结合6.2节和6.3节来加深理解Nginx的HTTP处理框架，但仅研究与框架有关的注册和调用代码，并不解析内部的具体业务逻辑。

6.4.1　ngx_http_static_module

ngx_http_static_module是一个handler模块，它工作在NGX_HTTP_CONTENT_PHASE阶段，读取磁盘上的静态文件作为响应内容，提供Nginx作为Web服务器最基本的功能。

模块定义

ngx_http_static_module没有配置指令，模块定义非常简单：

ngx_http_module_t  ngx_http_static_module_ctx = {
NULL,                                   //preconfiguration
ngx_http_static_init,                   //postconfiguration
NULL,                                   //create main configuration
NULL,                                   //init main configuration
NULL,                                   //create server configuration
NULL,                                   //merge server configuration
NULL,                                   //create location configuration
NULL                                    //merge location configuration
};

ngx_http_static_module只使用了一个postconfiguration函数指针，用来注册模块的阶段处理函数。

注册处理函数

ngx_http_static_init()访问ngx_http_core_module的phases数组，向NGX_HTTP_CONTENT_PHASE阶段添加handler：

static ngx_int_t
ngx_http_static_init(ngx_conf_t *cf)
{
cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);

*h = ngx_http_static_handler;    //添加数组元素

return NGX_OK;
}

函数ngx_http_static_handler()是ngx_http_static_module的处理函数，它检查URI的有效性，映射URI到磁盘路径，再访问文件，最后调用ngx_http_output_filter()，把文件内容交给过滤链表处理。

6.4.2　ngx_http_not_modified_filter_module

ngx_http_not_modified_filter_module是一个header filter模块，是ngx_module数组里的最后一个元素，同时也是过滤链表的第一个节点。它处理响应头，检查If-Modified-Since头，决定是否返回304状态码。

模块定义

ngx_http_static_module没有配置指令，模块定义也同样简单：

static ngx_http_module_t  ngx_http_not_modified_filter_module_ctx = {
NULL,                                    //preconfiguration
ngx_http_not_modified_filter_init,       //postconfiguration
NULL,                                    //create main configuration
NULL,                                    //init main configuration
NULL,                                    //create server configuration
NULL,                                    //merge server configuration
NULL,                                    //create location configuration
NULL                                     //merge location configuration
};

注册处理函数

ngx_http_not_modified_filter_init()操作Nginx过滤链表头节点，注册自己的处理函数：

//静态变量，存储 next 指针
static ngx_http_output_header_filter_pt  ngx_http_next_header_filter;

static ngx_int_t
ngx_http_not_modified_filter_init(ngx_conf_t *cf)
{
//向链表添加节点
ngx_http_next_header_filter = ngx_http_top_header_filter;
ngx_http_top_header_filter = ngx_http_not_modified_header_filter;

return NGX_OK;
}

ngx_http_not_modified_header_filter()是模块的处理函数，它检查请求头。如果文件没有被修改，就设置HTTP状态码为NGX_HTTP_NOT_MODIFIED（304），然后调用ngx_http_next_header_filter执行后续的过滤模块。

6.5　C++封装

本章的前几节已经比较详尽地分析了Nginx的HTTP框架和运行机制，现在我们要使用C++来实现对它们的封装。

6.5.1　NgxModuleCtx

模块的环境数据存储在ngx_http_request_t的ctx数组里，形式上与模块的配置数据存储的main_conf/srv_conf/loc_conf相同，所以可以用与5.7.1节类似的方法来实现C++类，提供便捷的访问方式。

类定义

NgxModuleCtx与NgxModuleConfig类似，保存模块的ctx_index作为内部成员：

class NgxModuleCtx final
{
public:
NgxModuleCtx(ngx_uint_t idx): m_idx(idx)    //获取模块的序号
{}
~NgxModuleCtx() = default;
private:
ngx_uint_t m_idx = 0;                       //保存模块的序号
public:
ngx_uint_t index() const                    //获取模块的序号
{
return m_idx;                           //模块的序号
}
...                                         //其他成员函数，见后
};

访问环境数据

模仿函数宏ngx_http_get_module_ctx，使用index()可以得到模块存储的环境数据：

private:
typedef void* raw_pointer;                       //简化类型定义
raw_pointer& ctx(ngx_http_request_t* r) const  //返回void*&类型
{
return r->ctx[index()];                  //访问ctx数组
}

注意：ctx()成员函数返回的不是简单的void*指针，而是void*的引用形式，所以我们可以把它当作左值直接操作。

操作函数

ctx()成员函数实际上就是ngx_http_request_t::ctx数组里存储的指针，所以使用它可以操作任意模块的环境数据：

public:
bool empty(ngx_http_request_t* r) const   //是否有环境数据
{
return !ctx(r);          //指针为空则不存在
}

void clear(ngx_http_request_t* r) const   //清空环境数据
{
ctx(r) = nullptr;        //左值，直接置为空指针
}

模板成员函数data()检查模块的环境数据是否存在，如果没有就直接构造并返回：[9]

template<typename T>        //模板参数指明数据类型
T& data(ngx_http_request_t* r) const   //获取模块的环境数据
{
if(empty(r))          //检查是否存在
{
ctx(r) = NgxPool(r).alloc<T>();    //从内存池创建对象
}

return *reinterpret_cast<T*>(ctx(r));    //返回转型后的环境数据
}

注意data()函数返回的是对象的引用，而不是指针。

NgxModule工厂类

5.7.2节NgxModule类不仅可以生产NgxModuleConfig对象，还可以生产NgxModuleCtx：

template<...>
class NgxModule           //模块的代理类
{
public:
NgxModule(ngx_module_t& m ):      //引用模块对象
m_conf(m.ctx_index), m_ctx(m.ctx_index)  //使用序号 ctx_index
{}
private:
typedef NgxModuleCtx  ctx_type;       //简化类型定义
ctx_type     m_ctx;       //NgxModuleCtx 对象
public:
const ctx_type& ctx() const       //产生NgxModuleCtx 对象
{
return m_ctx;         //返回成员的引用
}

template<typename T>
T& data(ngx_http_request_t* r) const   //直接获取环境数据
{
return ctx().template data<T>(r);     //注意模板函数的调用方式
}
};

6.5.2　NgxHttpCoreModule

模块ngx_http_core_module是Nginx HTTP框架的核心，它的main/server/location三个层次的配置结构都非常重要，在编写http模块时需要频繁访问，所以有必要从NgxModule派生出一个专门的NgxHttpCoreModule类来代理它。

NgxHttpCoreModule继承了NgxModule获取配置结构的能力，然后增加本模块独有的注册HTTP处理函数的功能。

类定义

NgxHttpCoreModule是NgxModule的子类，但因为它有三个不同的配置结构，所以模板参数列表略长：

class NgxHttpCoreModule final :      //final禁止继承
public NgxModule<ngx_http_core_loc_conf_t,  //三个配置结构类型
ngx_http_core_srv_conf_t,
ngx_http_core_main_conf_t >
{
public:
typedef NgxModule<...>    super_type;   //父类定义
typedef NgxHttpCoreModule   this_type;    //自身类型定义
public:
NgxHttpCoreModule() : super_type(ngx_http_core_module) //初始化父类
{}
~NgxHttpCoreModule() = default;
...              //其他成员函数，见后
};

单件访问点

NgxHttpCoreModule需要自己实现单件访问函数instance()：

public:
static NgxHttpCoreModule& instance()   //单件访问函数
{
static NgxHttpCoreModule m;      //不需要额外的参数构造
return m;
}

注册处理函数

Nginx提供了两种注册HTTP处理函数的方法：

- 在配置文件解析完毕之后，也就是postconfiguration函数指针里，操作ngx_http_core_main_conf_t结构，向phases数组添加元素，就可以向框架注册应用于任何HTTP请求的阶段处理函数。

- 在配置解析到某个location内部指令时，设置ngx_http_core_loc_conf_t结构的handler成员，可以注册仅用于该location的处理函数。

使用C++的函数重载特性，我们可以把这两个注册动作整合在一个同名函数里：

public:
template<typename F>
void handler(ngx_conf_t* cf, F f,     //设置阶段处理 handler
ngx_http_phases p) const    //使用阶段参数
{
auto& c = conf().main(cf);       //获取main 配置

typedef NgxArray<ngx_http_handler_pt> handler_array_t;

handler_array_t arr(c.phases[p].handlers);  //获取phases数组
arr.push(f);          //设置handler
}

template<typename F>
void handler(ngx_conf_t* cf, F f) const    //设置内容处理 handler
{
conf().loc(cf).handler = f;    //获取location 配置并设置 handler
}

用法

NgxHttpCoreModule类把访问配置结构数组和处理函数的操作都封装了起来，所以注册handler的工作变得非常简单和易于理解，例如：

NgxHttpCoreModule::instance().handler(    //使用NgxHttpCoreModule 单件
cf, func, NGX_HTTP_REWRITE_PHASE);   //添加rewrite 阶段的 handler

NgxHttpCoreModule::instance().handler(cf,func);//添加内容 handler

6.5.3　NgxFilter

Nginx过滤机制要求每个模块使用内部的私有静态变量来保存next指针，使用类的静态变量可以达到同样的效果。由于C++11还不支持模板变量，所以本书采用模板函数来模拟实现。[10]

类定义

NgxFilter是一个模板类，定义如下：

template<typename Tag>         //Tag 模板参数供实例化用
class NgxFilter final
{
public:
typedef ngx_http_output_header_filter_pt header_filter_ptr;
typedef ngx_http_output_body_filter_pt   body_filter_ptr;
public:
NgxFilter() = default;
~NgxFilter() = default;
private:
template<typename T>       //模板参数确定变量的类型
static T& next()        //静态成员函数
{
static T next_filter;      //定义next 指针
return next_filter;
}
...             //其他成员函数，见后
};

静态成员函数next()的行为很像变量，它内部声明了一个变量，但类型需要由模板参数指定，所以只要变动模板参数T就可以得到对应类型的静态变量，行为很像C++14里的模板变量。

NgxFilter巧妙地利用了模板参数Tag，它的作用仅仅是一个编译期的标记，实例化后可以生成唯一的模板实例类，这样每个NgxFilter<Tag>类都可以拥有只属于自己的类静态变量，达到了与Nginx文件作用域静态变量同样的效果。

注册过滤链表

使用成员函数next()就可以操作Nginx的链表指针：

private:
template<typename T>       //T是函数指针类型
static void set(T& top, T p)     //向过滤链表添加头节点
{
next<T>() = top;        //保存头节点指针
top = p;          //设置头节点指针
}

init()函数的参数确定模块的过滤函数指针。如果指针不是空指针，那么就可以调用set()向过滤器链表添加头节点：

public:
static void init(header_filter_ptr header_filter,
body_filter_ptr body_filter)
{
if(header_filter)       //header_filter 指针
{
set(ngx_http_top_header_filter, header_filter);
}

if(body_filter)         //body_filter 指针
{
set(ngx_http_top_body_filter, body_filter);
}
}

在这里我们利用了C++函数自动推导模板参数的能力，实现了set()函数的模板实例化，最终访问next()里正确类型的静态变量。

调用后续过滤链表

成员函数next()存储了后续过滤链表指针，所以能够直接调用：

public:
static ngx_int_t next(ngx_http_request_t* r)   //调用header过滤链表
{
return next<header_filter_ptr>()(r);   //显式模板实例化
}

static ngx_int_t next(        //重载调用 body 过滤链表
ngx_http_request_t* r, ngx_chain_t *chain)
{
return next<body_filter_ptr>()(r, chain);   //显式模板实例化
}

在开发filter模块时，我们需要调用这两个next()函数，执行后续的过滤链表。

用法

使用NgxFilter必须要提供Tag模板参数，实例化出一个专用的过滤器类，例如：

typedef NgxFilter<tag>     this_filter;

然后在postconfiguration阶段就可以调用init()函数，注册过滤处理函数。

在第7章我们会使用NgxFilter开发一个filter模块，这里不再列出更多的示范代码。

6.6　总结

本章详细研究了Nginx的HTTP处理框架，解析了两类主要的http模块——handler模块和filter模块的工作原理，它们是Nginx宫殿里最光彩夺目的那些厅堂。

所有Web服务器对HTTP请求的基本处理流程都是类似的，但Nginx对请求处理的阶段划分则再次体现了作者Igor Sysoev的精妙设计思想。

接收完请求头数据后，Nginx就进入了阶段式处理引擎。Nginx把HTTP请求处理划分为NGX_HTTP_REWRITE_PHASE、NGX_HTTP_CONTENT_PHASE、NGX_HTTP_LOG_PHASE等11个阶段（但有4个阶段仅供框架使用），相当于在处理流程里提供了11个hook点，模块可以自由选择自己感兴趣的阶段，注册自己的处理函数，实现地址改写、访问控制、产生内容、记录日志等操作。

处理引擎产生响应内容后，Nginx会进入过滤处理阶段，可以把这个阶段理解为在NGX_HTTP_CONTENT_PHASE和NGX_HTTP_LOG_PHASE之间的一个无名阶段。在这里Nginx把filter模块组织成了一个链表，响应头和响应体流过这个链表进行各种加工处理，最后才真正发送给客户端。

handler模块和filter模块本质上都是职责链模式，请求被链里的每一个模块所处理，只是handler模块的流程更加复杂，因为存在URI改写，所以在引擎链表里可以反复跳转。

Nginx使用configure脚本生成objs/ngx_modules.c里的ngx_modules数组，它在编译期确定了http的模块正确执行顺序，运行时不能动态调整。在Nginx启动时框架顺序调用模块的postconfiguration函数指针，完成初始化，之后模块就会各就各位，各司其职，分工协作，在引擎函数的驱动下共同完成HTTP请求的处理。

开发handler模块需要实现ngx_http_handler_pt函数，Nginx提供了两种注册方式。一种是phase handler，操作ngx_http_core_main_conf_t结构的phases数组；另一种是content handler，操作ngx_http_core_loc_conf_t结构的handler成员。第一种方式是最通用的方式，可以挂载到任意处理阶段，处理所有的HTTP请求；而第二种方式则只对某个具体的location生效，而且只能用来产生响应内容。

开发filter模块需要实现ngx_http_output_header_filter_pt或ngx_http_output_body_filter_pt函数，分别过滤处理响应头和响应体。函数的注册要比handler模块简单一些，只要定义一个next指针就可以加入过滤链表。

handler模块和filter模块都可以实现处理HTTP请求的功能，在实践开发时我们要对这两者的应用范围和工作机制有正确的认识，选择合适的模块类型。handler模块的应用范围最广，不仅可以生成响应内容，还可以执行权限检查、请求重写等其他功能。而filter模块则不负责生产数据，它只能加工数据。在编写生成响应内容的handler模块时应该尽量让模块的工作简单，只产生原始数据，把对数据的复杂处理逻辑实现在另外的filter模块里，多个filter模块协作完成处理。

本章的最后我们使用C++封装了Nginx HTTP框架里三个非常重要且基本的功能：访问环境数据，注册处理函数和操作过滤链表，使用这些C++工具类可以很好地辅助开发http模块，简化实现代码。

[1] 当前版本的Nginx中HTTP处理流程与早期版本略微不同，少量函数做了改变，例如函数ngx_http_init_request()已经不复存在。

[2] 实际上还有请求体和响应体数据的过滤阶段，但过滤是在ngx_http_core_run_phases()外通过职责链的方式实现的，参见6.3节。

[3] 也可以理解成“断点恢复”。

[4] 这里虽然也计算了FIND_CONFIG等特殊阶段的数量，但在之后并不会真正加入引擎数组。

[5] 理论上引擎数组里的第一个模块应该是NGX_HTTP_POST_READ_PHASE阶段，但实际上很少有模块注册在这个阶段。

[6] 不同的阶段的checker函数逻辑不完全相同，所以NGX_OK、NGX_DECLINED、NGX_AGAIN等错误代码的含义也不一定相同，可以参考其他checker，如ngx_http_core_generic_phase()，进一步了解。

[7] 当然，我们也可以不必遵守Nginx的约定，使用任意的变量名，也不一定是静态的。

[8] Nginx还没有提供请求头的过滤机制，但我们完全可以在NGX_HTTP_POST_READ_PHASE阶段对请求头做处理。

[9] GitHub上的代码有所增强，使用boost::enable_if允许ctx以内存池作为参数构造。

[10] NgxFilter暂不支持请求体过滤链表，但可以很容易地添加支持，本书从略。

第7章　Nginx HTTP请求处理

本章将详细解析Nginx处理HTTP请求的核心结构体ngx_http_request_t，阐述其中的请求头、请求体、响应头、响应体等HTTP处理的关键数据成员和操作方法，并提供易用的C++高级接口，读者可以学习到开发可用的handler模块和filter模块的知识。

7.1　HTTP状态码

Nginx用宏定义了常用的HTTP状态码，包括常见的200、301、403等，以及一些Nginx自己特有的状态码。

下面仅列出了部分宏，完全的定义可阅读头文件http/ngx_http_request.h：

// 定义在 http/ngx_http_request.h
#define NGX_HTTP_OK                          200
#define NGX_HTTP_CREATED                     201
#define NGX_HTTP_ACCEPTED                    202
...              //其他2XX状态码

#define NGX_HTTP_SPECIAL_RESPONSE            300
#define NGX_HTTP_MOVED_PERMANENTLY           301
#define NGX_HTTP_MOVED_TEMPORARILY           302
...              //其他3XX状态码

#define NGX_HTTP_BAD_REQUEST                 400
#define NGX_HTTP_UNAUTHORIZED                401
#define NGX_HTTP_FORBIDDEN                   403
#define NGX_HTTP_NOT_FOUND                   404
...              //其他4XX状态码

#define NGX_HTTP_INTERNAL_SERVER_ERROR       500
#define NGX_HTTP_NOT_IMPLEMENTED             501
#define NGX_HTTP_BAD_GATEWAY                 502
...              //其他5XX状态码

在编写HTTP模块时应当尽量使用这些含义明确的Nginx预定义宏，当然，直接使用数字也是可以的。

7.2　请求结构体

前面我们研究Nginx的HTTP框架时都用到了结构体ngx_http_request_t，它包含了Nginx处理请求过程中需要的所有信息，处理HTTP请求实际上就是操作ngx_http_request_t里的各个成员。

本章将介绍ngx_http_request_t中与HTTP协议（RFC2616）[1]有关的部分，它的定义摘要如下：

// 定义在 http/ngx_http_request.h
struct ngx_http_request_s {
void**                    ctx;            //处理请求的环境数据
void**                    main_conf;      //main 层次的配置信息数组
void**                    srv_conf;       //server 层次的配置信息数组
void**                    loc_conf;       //location 层次的配置信息数组

ngx_pool_t*               pool;           //请求使用的内存池

ngx_buf_t*                header_in;      //HTTP 头部数据的缓冲区

ngx_http_headers_in_t     headers_in;     //请求头数据结构
ngx_http_headers_out_t    headers_out;    //响应头数据结构

ngx_http_request_body_t*  request_body;   //请求体数据结构

ngx_uint_t                method;         //请求方法
ngx_uint_t                http_version;   //HTTP 版本号

ngx_str_t                 request_line;   //存储完整的请求行字符串
ngx_str_t                 uri;            //请求的uri
ngx_str_t                 args;           //请求的参数
ngx_str_t                 exten;          //请求的扩展名
ngx_str_t                 unparsed_uri;   //未解析的原始 uri
ngx_str_t                 method_name;    //请求方法名
ngx_str_t                 http_protocol;  //HTTP 协议版本字符串

unsigned                  uri_changed:1;  //uri 是否已经被改写
unsigned                  header_only:1;  //是否只有头
unsigned                  discard_body:1; //是否丢弃请求体
unsigned                  reading_body:1; //是否正在读取请求体
unsigned                  header_sent:1;  //是否已经发送了响应头
unsigned                  allow_ranges:1; //是否支持 range协议

...                                       //其他成员
};

本章之后的小节将结合HTTP协议把它分解为请求行、请求头、请求体等部分逐步研究。

7.3　请求行

HTTP请求行包括请求方法，请求的URI、HTTP协议版本等信息，在RFC2616里的定义如下：

Request-Line   = Method SP Request-URI SP HTTP-Version CRLF

一个请求行的例子是：

GET /index.html HTTP/1.1

其中的“GET”是请求方法，“/index.html”是URI，也就是通常所说的URL或请求地址，“HTTP/1.1”是协议的版本。

Nginx在接收到客户端发来的HTTP请求后，会调用函数ngx_http_process_request_line()解析请求行数据，存储到ngx_http_request_t::request_line成员，然后再进一步分解出更详细的信息。

7.3.1　请求方法

ngx_http_request_t使用两个成员存储HTTP请求方法（method）信息：

ngx_uint_t method;        //方法的整数表示
ngx_str_t  method_name;        //方法的字符串表示

method是Nginx解析请求行后得到的方法标识，是一个整数，method_name则是方法的字符串表示，例如GET/HEAD/PUT/POST/COPY等，相当于变量$request_method。

Nginx使用宏定义了HTTP协议里已有的方法：

#define NGX_HTTP_UNKNOWN   0x0001    //错误方法
#define NGX_HTTP_GET       0x0002    //GET
#define NGX_HTTP_HEAD      0x0004    //HEAD
#define NGX_HTTP_POST      0x0008    //POST
#define NGX_HTTP_PUT       0x0010    //PUT
#define NGX_HTTP_DELETE    0x0020    //DELETE
...                                  //其他方法值

在编写http模块时应当总使用method成员与这些宏进行比较来确定请求方法，尽量避免使用字符串形式的method_name，因为整数的比较操作更快，method_name通常只用来记录日志或者调试。

检查method的一个小技巧是使用逻辑或操作符（&），可以一次检查多个值，例如：

if (r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))  //要求是get 或 head 方法

7.3.2　协议版本号

成员变量http_version标记了HTTP请求的版本：

目前HTTP协议有0.9、1.0和1.1三个版本，Nginx定义三个宏与它们对应：[2]

#define NGX_HTTP_VERSION_9                 9
#define NGX_HTTP_VERSION_10                1000
#define NGX_HTTP_VERSION_11                1001

判断协议的版本号只需直接比较http_version成员和这三个宏即可。

成员http_protocol存储的是请求行里完整的版本字符串，但因为我们处理的协议必定是HTTP，所以它的用处不大。

7.3.3　资源标识符

ngx_http_request_t里有四个成员描述请求里的资源标识符（URI）：

ngx_str_t   uri;              //不带参数的 uri，即$uri
ngx_str_t   args;             //uri 里的参数，即$args
ngx_str_t   exten;            //uri 里的文件扩展名
ngx_str_t   unparsed_uri;     //未解码的原始 uri 字符串，带参数

成员uri和unparsed_uri都表示请求行里的URI字符串，两者的区别是uri做了解码，且不含有“？”后面的参数，即$uri；而unparsed_uri是原始的未经任何处理的URI字符串，包含参数，它相当于$request_uri。

成员args表示URI里的参数，即“？”后面的字符串$args，不包含“？”，它也没有经过URL解码。

成员exten表示uri里的文件扩展名，如果文件没有扩展名，那么它就是个空字符串。

例如，如果有一个请求：

curl -v http://localhost/test/%40index.html?a=42

那么就有：

uri          = /test/@index.html
args         = a=42
exten        = html
unparsed_uri = /test/%40index.html?a=42

在HTTP请求处理的任意阶段可以使用下面两个函数实现URI的跳转：

ngx_int_t ngx_http_named_location(ngx_http_request_t *r, ngx_str_t *name);
ngx_int_t ngx_http_internal_redirect(ngx_http_request_t *r,
ngx_str_t *uri, ngx_str_t *args);

两个函数的区别是前者只能跳转到以“@”开头的location，不能传递参数；而后者更加灵活，可以跳转到本server块内部的任意location。

Nginx还提供一个函数ngx_http_parse_unsafe_uri()，它可以检查uri是否是一个不安全的uri（例如含有“../”），声明是：

ngx_int_t ngx_http_parse_unsafe_uri(
ngx_http_request_t *r, ngx_str_t *uri,ngx_str_t *args, ngx_uint_t *flags);

7.4　请求头

ngx_http_request_t用两个成员存储收到的HTTP头部信息：

ngx_buf_t*              header_in;     //接收头部数据的缓冲区
ngx_http_headers_in_t   headers_in;    //解析后的头部信息

header_in是一个缓冲区块，存放了Nginx收到的HTTP原始请求数据，我们通常不需要关心，而是直接使用已经解析好的headers_in成员。

headers_in的类型是ngx_http_headers_in_t，定义摘要如下：

typedef struct {
ngx_list_t                 headers;           //头信息链表

ngx_table_elt_t*           host;              //host 头
ngx_table_elt_t*           connection;        //connection 头
ngx_table_elt_t*           if_modified_since; //if_modified_since 头
ngx_table_elt_t*           user_agent;        //user_agent 头
ngx_table_elt_t*           content_length;    //content_length 头
ngx_table_elt_t*           content_type;      //content_type 头
ngx_table_elt_t*           range;             //range头
ngx_table_elt_t*           if_range;          //if_range 头
ngx_table_elt_t*           keep_alive;        //keep_alive 头

off_t                      content_length_n;  //content_length 头的整数形式
time_t                     keep_alive_n;      //keep_alive 头的整数形式

...                                           //其他成员

} ngx_http_headers_in_t;

ngx_http_headers_in_t结构体初看上去很复杂，但实际上很容易掌握。它的核心成员是headers，以链表的方式存储了所有的请求头，相当于NgxList<ngx_table_elt_t>，我们可以用4.2节里介绍的各种方法来操作它。

ngx_http_headers_in_t里的其他成员大多数是ngx_table_elt_t*指针，指向headers链表里的元素，为常用的头信息访问提供了快捷方式，无须遍历链表就可以直接获取头信息。如果指针是nullptr，就表示没有该头部信息。

对于content_length和keep_alive这两个头，ngx_http_headers_in_t还提供了整数形式的成员content_length_n和keep_alive_n，免去了我们自己解析数字的麻烦。

7.5　请求体

ngx_http_request_t用成员request_body存储收到的HTTP请求体数据。[3]

7.5.1　结构定义

ngx_http_request_body_t的定义如下：

typedef struct {
ngx_temp_file_t*                temp_file;    //存放请求体的临时文件
ngx_chain_t*                    bufs;         //存放请求体的内存区
ngx_buf_t*                      buf;          //接收缓冲区
off_t                           rest;         //待接收的剩余字节数
ngx_chain_t*                    free;
ngx_chain_t*                    busy;
ngx_http_chunked_t*             chunked;
ngx_http_client_body_handler_pt post_handler;  //接收完数据后的回调函数
} ngx_http_request_body_t;

ngx_http_request_body_t里的成员较多，大多数是由Nginx内部使用（相当于private成员），用于异步读取数据。我们通常只需关心成员bufs，它存储了收到的请求体数据。

7.5.2　操作函数

很多时候请求体是可有可无的（例如最常用的GET），所以可以直接“丢弃”请求体：

//定义在http/ngx_http.h
ngx_int_t ngx_http_discard_request_body(ngx_http_request_t *r);

调用函数ngx_http_discard_request_body()会让Nginx继续在连接上接收数据，但并不会存入request_body。

如果确实需要获取请求体数据，那么就需要使用Nginx提供的异步读取请求体函数ngx_http_read_client_request_body()，它的声明是：

//回调函数，定义在 http/ngx_http_request.h
typedef void (*ngx_http_client_body_handler_pt)(ngx_http_request_t *r);

//定义在http/ngx_http.h
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，用户可以在这个函数里处理收到的请求体数据。

7.6　响应头

Nginx使用结构体ngx_http_headers_out_t表示响应头，它包含了HTTP响应里的状态行和头部信息，在设计上与ngx_http_headers_in_t结构并不对称（这一点比较奇怪）。

7.6.1　结构定义

ngx_http_headers_out_t的定义摘要如下：：

typedef struct {
ngx_list_t                    headers;            //头信息链表

ngx_uint_t                    status;             //状态码
ngx_str_t                     status_line;        //状态行

ngx_table_elt_t*              server;
ngx_table_elt_t*              date;
ngx_table_elt_t*              content_length;
ngx_table_elt_t*              expires;
ngx_table_elt_t*              etag;

ngx_str_t                     content_type;

off_t                         content_length_n;   //响应数据长度
time_t                        date_time;
time_t                        last_modified_time;

...                                               //其他成员
} ngx_http_headers_out_t;

ngx_http_headers_out_t结构体与ngx_http_headers_in_t很类似，核心成员仍然是链表headers，用于存储响应头信息。我们也可以直接用server、date等指针直接指定标准头，不一定非要在链表里。

成员status是响应状态行里的状态码，它的取值为7.1节里定义的宏，如果正确处理了HTTP请求，值就应该是NGX_HTTP_OK。

还有一个比较重要的成员是content_length_n，用于设置响应体数据的长度，Nginx会自动把整数形式的content_length_n转换为响应头里的字符串形式。

7.6.2　操作函数

响应头均是以ngx_table_elt_t的形式存储，如果想要删除某个头信息，只需把头对应的ngx_table_elt_t的hash值置为0就可以了，例如：

r->headers_out.content_length->hash = 0;     //散列值清空
r->headers_out.content_length = nullptr;     //指针也要置空，更加安全

Nginx还定义了几个函数宏，可以便捷地清空常用的响应头信息：

//定义在头文件 http/ngx_http_core_module.h
#define ngx_http_clear_content_length(r)
#define ngx_http_clear_accept_ranges(r)
#define ngx_http_clear_last_modified(r)
#define ngx_http_clear_location(r)
#define ngx_http_clear_etag(r)

函数ngx_http_send_header()可以把响应头发送给客户端，内部会调用ngx_http_top_header_filter，让头信息经过过滤链表最终发送出去（我们已经在6.3.4节看到了它的实现源码）：

ngx_int_t ngx_http_send_header(ngx_http_request_t *r);

另一个函数ngx_http_clean_header()可以清除所有的响应头信息：

void ngx_http_clean_header(ngx_http_request_t *r);

7.7　响应体

Nginx里并没有为响应体定义专门的数据结构，实际上，响应体就是一些数据，可以用ngx_buf_t和ngx_chain_t来表示。

调用函数ngx_http_output_filter()可以把响应体数据经由过滤链表发送给客户端：

ngx_int_t ngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in);

必须要注意的是，Nginx采用异步的方式处理数据，函数调用时不一定会立即使用链表里的数据，数据必须在调用后的一段时间里依然可用，所以最好是在r->pool里分配待发送的内存存储数据，否则可能会发生严重的运行时错误。

函数ngx_http_send_special()可以发送用于控制的特殊ngx_buf_t对象：

#define NGX_HTTP_LAST   1     //设置为最后一个缓冲区，即 eof
#define NGX_HTTP_FLUSH  2     //刷新缓冲区
ngx_int_t ngx_http_send_special(ngx_http_request_t *r, ngx_uint_t flags);

7.8　C++封装

ngx_http_request_t结构体包含了太多太多的信息，是一个“万能类”，HTTP的所有操作都与它相关，如果把它按照具体的功能分解成数个功能明确的小类就可以很好地降低复杂度，让它更容易理解和使用。

在HTTP处理过程中，主要的操作是处理请求数据和响应数据，所以可以定义两个类：NgxRequest和NgxResponse，它们整合了请求头/请求体和响应头/响应体的处理。

7.8.1　NgxHeaders

ngx_http_headers_in_t和ngx_http_headers_out_t结构体可以理解为一个链表加若干指针信息，基本的功能仍然是操作链表。

因为请求头和响应头结构非常相似，所以我们实现一个通用的模板类NgxHeaders。

类定义

NgxHeaders的定义如下：

template<typename T,       //T = ngx_http_headers_in|out_t
T ngx_http_request_t::* ptr>
class NgxHeaders final : public NgxWrapper<T>
{
public:
typedef NgxWrapper<T>                 super_type;
typedef typename super_type::wrapped_type   ngx_headers_type;

typedef NgxList<ngx_table_elt_t>      ngx_headers_list_type;
typedef ngx_table_elt_t                     kv_type;

public:
NgxHeaders(ngx_headers_type& h):        //从头结构体构造
super_type(h),                          //代理头结构体
m_headers(h.headers)                    //代理头信息链表
{}

NgxHeaders(ngx_http_request_t* r):          //从请求结构体构造
NgxHeaders(r->*ptr)                  //使用成员变量指针获取头
{}

~NgxHeaders() = default;
public:
const ngx_headers_list_type& list() const  //访问头信息链表
{
return m_headers;
}
...                                         //其他成员函数，见后
private:
ngx_headers_list_type m_headers;
};

NgxHeaders代理了ngx_http_headers_in_t或ngx_http_headers_out_t对象，它还有一个NgxList<ngx_table_elt_t>成员变量m_headers，用来操作头信息链表。

NgxHeaders使用了与4.3节一样的成员变量指针技术，利用指针来获取ngx_http_request_t里的头结构体，所以请求头和响应头的类型定义就是：

typedef NgxHeaders<
ngx_http_headers_in_t, &ngx_http_request_t::headers_in>
NgxHeadersIn;         //请求头类型定义

typedef NgxHeaders<
ngx_http_headers_out_t, &ngx_http_request_t::headers_out>
NgxHeadersOut;          //响应头类型定义

查找头信息

非标准头不能直接用ngx_http_headers_in_t或ngx_http_headers_out_t里的快捷指针访问，我们必须遍历头信息链表，所以需要定义一个查找函数：

public:
ngx_headers_list_type::iterator find(string_ref_type key) const
{
auto v = [&](const kv_type& kv)     //查找谓词 lambda 表达式
{
return boost::iequals(     //boost大小写无关比较
NgxString(kv.key).str(), key);
};

return list().find(v);        //传入谓词进行查找
}

find()函数利用了NgxList的通用find()函数，使用boost::iequals算法进行大小写无关比较以找到字符串对应的头信息。[4]

访问头信息

find()函数返回的是链表的迭代器，使用迭代器就可以操作头信息：

public:
bool has(string_ref_type key) const     //是否存在头
{
return find(key) != list().end();    //与逾尾迭代器比较
}

ngx_str_t operator[](string_ref_type key) const  //快捷访问头信息
{
auto p = find(key);        //查找头

if(p == list().end())        //检查是否找到
{
return ngx_null_string;      //未找到返回空字符串
}

return p->value;          //找到返回实际头信息
}

重载的operator[]提供了快捷访问头信息的方法，可以像字典那样直接用名字找到对应的值。

修改头信息

对头信息链表进行元素操作也可以添加或者删除头：

public:
void add(const kv_type& kv) const     //信息需在外部创建好
{
auto& tmp = list().prepare();      //链表添加元素
tmp = kv;
}

void remove(string_ref_type key) const   //“删除”头
{
auto p = find(key);        //查找待删除的头

if(p == list().end())        //未找到则无操作
{   return;}

p->hash = 0;           //hash 值置为0
}

ngx_list_t实际上是没有删除元素操作的，所以我们需要把头信息的hash值置0，表示无效，对于Nginx来说效果就相当于删除。

7.8.2　NgxRequestBody

类NgxRequestBody封装了对Nginx请求体的操作。

类定义

NgxRequestBody的定义如下：

class NgxRequestBody final : public NgxWrapper<ngx_http_request_t>
{
public:
typedef NgxWrapper<ngx_http_request_t>  super_type;
typedef NgxRequestBody       this_type;
public:
NgxRequestBody(ngx_http_request_t* r):super_type(r)
{}
~NgxRequestBody() = default;
public:
...             //其他成员函数，见后
};

因为对请求体的操作需要使用ngx_http_request_t*指针，所以NgxRequestBody使用NgxWrapper代理了ngx_http_request_t而不是ngx_http_request_body_t。

丢弃或读取请求体

成员函数discard()和read()简单地封装了Nginx的C接口：

public:
void discard() const        //丢弃请求体数据
{
auto rc = ngx_http_discard_request_body(get());

NgxException::require(rc);
}

template<typename F>        //要求传入回调函数
ngx_int_t read(F f) const      //读取请求体数据
{
auto rc = ngx_http_read_client_request_body(get(), f);

NgxException::fail(rc >= NGX_HTTP_SPECIAL_RESPONSE, rc);

return NGX_DONE;         //处理尚未完成
}

注意在读取请求体数据时我们需要返回NGX_DONE，告诉Nginx处理尚未完成，这样才能在引擎里再次进入我们的模块，执行后续操作。

访问请求体数据

bufs()检查请求里的ngx_http_request_body_t指针，返回它的缓冲区指针：

public:
ngx_chain_t* bufs() const      //获取请求体数据
{
return get()->request_body?       //是否有请求体
get()->request_body->bufs : nullptr;  //返回数据或空指针
}

7.8.3　NgxRequest

类NgxRequest代理ngx_http_request_t里的请求相关功能，集成了NgxHeadersIn和NgxRequestBody。

类定义

NgxRequest把NgxHeadersIn和NgxRequestBody作为自己的成员，定义如下：

class NgxRequest final : public NgxWrapper<ngx_http_request_t>
{
public:
typedef NgxWrapper<ngx_http_request_t>   super_type;
typedef NgxRequest                             this_type;

typedef NgxHeadersIn    headers_type;             //请求头类型
typedef NgxRequestBody  body_type;                //请求体类型
public:
NgxRequest(ngx_http_request_t* r):super_type(r),  //构造，代理 request_t
m_headers(r), m_body(r)                       //初始化请求头和请求体
{}
~NgxRequest() = default;
public:
const headers_type& headers() const     //获取请求头
{
return m_headers;
}

const body_type& body() const      //获取请求体
{
return m_body;
}
...                                           //其他成员函数，见后
private:
headers_type     m_headers;    //请求头
body_type        m_body;       //请求体
};

操作函数

对请求数据的处理大都可以通过调用headers()和body()的成员函数来完成，但为了方便操作，NgxRequest还提供了检查请求方法的功能：

public:
//参数可以是 NGX_HTTP_GET|NGX_HTTP_HEAD|...
bool method(ngx_uint_t x) const     //检查请求方法
{
return get()->method & x;
}

读者也可以根据自己的需要为它添加其他常用的功能函数。

7.8.4　NgxResponse

NgxResponse代理ngx_http_request_t里的响应相关功能，因为没有响应体对应的数据结构，所以它的代码略多一些。

类定义

NgxResponse的定义如下：

class NgxResponse final : public NgxWrapper<ngx_http_request_t>
{
public:
typedef NgxWrapper<ngx_http_request_t>   super_type;
typedef NgxResponse                      this_type;

typedef NgxHeadersOut    headers_type;    //响应头类型
typedef boost::string_ref   string_ref_type;
public:
NgxResponse(ngx_http_request_t* r):super_type(r),   //构造，代理 request_t
m_headers(r), m_pool(r)         //初始化响应头和内存池
{}

~NgxResponse() = default;
public:
const headers_type& headers() const      //获取响应头
{
return m_headers;
}

...              //其他成员函数，见后
private:
headers_type     m_headers;        //响应头
NgxPool          m_pool;        //内存池对象
};

除响应头外，NgxResponse还有一个内存池成员，这是为了方便在发送数据时分配内存。

基本操作

NgxResponse提取出了发送响应数据时最常用的设置状态和长度操作：

public:
void status(ngx_uint_t x) const       //设置状态码
{
headers()->status = x;
}

void length(off_t len) const        //设置数据长度
{
headers()->content_length_n = len;
}

发送响应头

发送响应头时首先要检查header_sent标志位，如果已经发送就不需要再继续操作。调用ngx_http_send_header()可以发送响应头，如果返回的错误码不是NGX_OK，那么就意味着发生了错误，可以用异常传递出去，报告给Nginx框架处理：

public:
ngx_int_t send() const         //发送响应头
{
if(get()->header_sent)        //检查发送标志位
{   return NGX_OK; }

if(!headers()->status)        //状态码是否已经设置
{
headers()->status = NGX_HTTP_OK;      //默认值是 200
}

auto rc = ngx_http_send_header(get());   //发送响应头

NgxException::fail(
rc == NGX_ERROR || rc > NGX_OK, rc);     //检查错误码

return rc;
}

发送响应体

基本的发送响应体操作是调用ngx_http_output_filter()发送ngx_chain_t数据。为了简化操作，也是为了保证头已经正确发送，可以先调用发送响应头操作：

public:
ngx_int_t send(ngx_chain_t* out) const    //发送响应体数据
{
send();                //确保已经发送响应头

return out && !get()->header_only ?      //检查是否有数据
ngx_http_output_filter(get(), out) : NGX_OK;  //发送响应数据
}

我们还可以重载send()函数，支持直接发送ngx_buf_t、ngx_str_t和普通字符串：

ngx_int_t send(ngx_buf_t* buf) const      //发送ngx_buf_t 对象
{
NgxChainNode ch = m_pool.chain();     //创建ngx_chain_t
ch.set(buf);            //添加缓冲区到链里

return send(ch);           //发送数据
}

ngx_int_t send(ngx_str_t* str) const      //发送ngx_str_t 对象
{
NgxBuf buf = m_pool.buffer();       //创建缓冲区对象
buf.range(str);          //设置缓冲区数据

return send(buf);           //发送数据
}

ngx_int_t send(string_ref_type str) const    //发送C字符串
{
auto s = m_pool.dup(str);        //在内存池里复制一份
return send(&s);           //发送数据
}

在最后一个send()函数里我们调用了内存池的dup操作，为字符串制作了一份备份，保证数据在发送时始终可用。

特殊操作

NgxResponse的特殊操作有flush()、eof()和finalize()：

public:
ngx_int_t flush() const        //刷新缓冲区，强制发送
{
return ngx_http_send_special(get(), NGX_HTTP_FLUSH);
}

ngx_int_t eof() const          //结束数据的发送
{
return ngx_http_send_special(get(), NGX_HTTP_LAST);
}

void finalize(ngx_int_t rc = NGX_HTTP_OK) const   //结束当前请求的处理
{
ngx_http_finalize_request(get(), rc);
}

7.9　开发HTTP处理模块

现在我们已经完全掌握了Nginx的HTTP框架和处理请求相关的知识，本节将使用现代C++开发一个实用的handler模块。

7.9.1　模块设计

在模块开发之前，仍然要进行简单的设计：

- 模块名是ndg_echo_module，是一个内容处理模块；

- 模块使用content handler的方式注册处理函数；

- 模块的功能是向客户端输出一个指定的字符串信息，由配置指令ndg_echo确定；

- uri里的参数信息（$args）也一并输出；

- 配置指令是ndg_echo，接受一个字符串参数。

7.9.2　配置信息类

类NdgEchoConf定义了模块所需的配置数据，根据设计，只有一个字符串成员：

class NdgEchoConf final
{
public:
typedef NdgEchoConf this_type;      //简化类型定义
public:
NdgEchoConf()  = default;
~NdgEchoConf()   = default;
public:
ngx_str_t msg;                      //存储配置文件里的字符串
...                                 //其他成员函数，见后
};

成员函数create()供Nginx框架调用，生成配置结构体：

public:
static void* create(ngx_conf_t* cf)
{
return NgxPool(cf).alloc<this_type>();  //创建结构体对象
}

我们还需要使用宏NGX_MOD_INSTANCE定义模块的单件类，方便其他代码来获取配置数据和环境数据：

NGX_MOD_INSTANCE(NdgEchoModule, ndg_echo_module, NdgEchoConf)

7.9.3　业务逻辑类

类NdgEchoHandler实现模块的业务逻辑，向客户端发送字符串：

class NdgEchoHandler final
{
public:
static ngx_int_t handler(ngx_http_request_t *r)   //处理函数
try                                                //try-catch捕获异常
{
NgxRequest req(r);           //请求对象

if(!req.method(NGX_HTTP_GET))        //检查请求方法
{
return NGX_HTTP_NOT_ALLOWED;      //要求必须是 get
}

req.body().discard();        //抛弃请求体

auto& cf = NdgEchoModule::instance().conf().loc(r);//获取配置数据
NgxString msg = cf.msg;         //获取配置里的字符串

NgxString args = req->args;        //获取uri的参数

auto len = msg.size();         //计算响应体的长度
if(!args.empty())         //需要加上$args 的长度
{
len += args.size()+1;        //还有一个“,”的长度
}

NgxResponse resp(r);         //准备发送响应

resp.length(len);          //设置响应体长度
resp.status(NGX_HTTP_OK);         //设置状态码

if(!args.empty())         //如果有$args
{
resp.send(args);          //原样发送$args
resp.send(",");          //再发送一个逗号
}

resp.send(msg);            //发送配置字符串

return resp.eof();          //eof 表示发送结束
}
catch(const NgxException& e)         //try-catch捕获异常
{
return e.code();
}
};              //业务逻辑类结束

NdgEchoHandler类里只有一个handler()函数用来处理请求，它可以分为两个部分。前半段使用NgxRequest检查请求头参数，用NdgEchoModule获取配置数据，准备响应数据。后半段使用NgxResponse设置了最基本的状态码和响应体长度，然后调用send()发送数据。

我们并没有直接调用send()来发送响应头，这是因为发送响应头的操作已经隐含在了发送响应体的操作里。

最后我们必须调用eof()函数，它实际上是发送了一个last_buf标志位为1的ngx_buf_t对象，表示HTTP请求处理结束。

7.9.4　模块集成类

类NdgEchoInit整合配置信息类和业务逻辑类，实现模块集成。

类定义

NdgEchoInit的定义如下：

class NdgEchoInit final
{
public:
typedef NdgEchoConf      conf_type;    //简化类型定义
typedef NdgEchoHandler   handler_type;
typedef NdgEchoInit      this_type;
public:
...                                    //其他成员函数，见后
};

配置指令解析

因为模块使用了content handler的注册方式，所以我们必须实现自己的指令解析函数，在函数里向ngx_http_core_module注册处理函数：

private:
static char* set_echo(ngx_conf_t* cf, ngx_command_t* cmd, void* conf)
{
auto rc =          //调用Nginx的解析函数
ngx_conf_set_str_slot(cf, cmd, conf);

if(rc != NGX_CONF_OK)      //检查是否解析成功
{   return rc;}

NgxHttpCoreModule::instance().handler(  //设置location 处理函数
cf, &handler_type::handler);

return NGX_CONF_OK;
}

随后我们定义cmds()函数，设置指令数组：

public:
static ngx_command_t* cmds()
{
static ngx_command_t n[] =     //配置指令数组，静态变量
{
NgxCommand(
ngx_string("ndg_echo"),
NgxTake(NGX_HTTP_LOC_CONF, 1),
&this_type::set_echo,    //自定义指令解析函数
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(conf_type, msg)
),

NgxCommand()        //空对象，结束数组
};

return n;         //返回数组地址
}

函数指针表

ndg_echo_module是content handler模块，不需要postconfiguration执行初始化操作，也没有合并操作，它只工作在location层次，这与第5章的ndg_test_module不同：

public:
static ngx_http_module_t* ctx()
{
static ngx_http_module_t c =
{
NGX_MODULE_NULL(6),
&conf_type::create,       //创建location 域的配置结构
NGX_MODULE_NULL(1),
};

return &c;
}

模块定义

最后还要使用ctx()和cmds()来定义ngx_module_t对象：

public:
static const ngx_module_t& module()
{
static ngx_module_t m =          //模块定义，静态变量
{
NGX_MODULE_V1,               //标准的填充宏
ctx(),                       //配置功能函数
cmds(),                      //配置指令数组
NGX_HTTP_MODULE,             //http 模块必须的 tag
NGX_MODULE_NULL(7),          //不使用的 7 个函数指针
NGX_MODULE_V1_PADDING        //标准的填充宏
};

return m;                        //返回对象的常量引用
}

7.9.5　实现源文件

在cpp文件里我们定义Nginx编译所需的ngx_module_t变量：

#include "NdgEchoInit.hpp"       //包含头文件
auto ndg_echo_module = NdgEchoInit::module();  //定义ngx_module_t变量

7.9.6　编译脚本

ndg_echo_module使用的编译脚本也很简单，只需设置三个Shell变量：

ngx_addon_name=ndg_echo_module

HTTP_MODULES="$HTTP_MODULES ndg_echo_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ModNdgEcho.cpp"

执行./configure脚本把模块添加进Nginx框架再make就完成了模块的开发：

./configure --add-module=path/to/echo;make

7.9.7　测试验证

在Nginx的配置文件里定义如下的location，加入ndg_echo指令：

location /echo {
ndg_echo "hello nginx\n";   #启用ndg_echo_module 模块
}

使用curl就可以测试我们刚刚开发完成的新模块：

curl http://localhost/echo          #输出hello nginx
curl http://localhost/echo?chrono   #输出chrono,hello nginx

7.10　开发HTTP过滤模块

本节将使用第6章实现的NgxFilter类开发一个filter模块，它同时使用了header filter和body filter，并且展示了请求的环境数据结构（ctx）的用法。

7.10.1　模块设计

对这个filter模块的基本设计如下：

- 模块名是ndg_footer_module，过滤处理响应头和响应体；

- 配置指令ndg_header可以接受多个keyval参数，加入到响应头；

- 配置指令ndg_footer接受一个字符串参数，加入到响应体末尾；

- 两个指令不需要同时出现，也就是说可以只修改响应头或者只修改响应体；

- 需要使用ctx来记录状态，防止重复添加。

7.10.2　配置信息类

类NdgFooterConf定义了模块所需的配置数据，它有两个成员，分别存储响应头和响应体的修改信息：

class NdgFooterConf final
{
public:
typedef NdgFooterConf this_type;    //简化类型定义
public:
NdgFooterConf()   = default;
~NdgFooterConf()  = default;
public:
ngx_array_t*    headers;            //存储修改响应头的 kv数据
ngx_str_t       footer;             //存储修改响应体的字符串
...                                 //其他成员函数，见后
};

注意在Nginx里存储ngx_keyval_t数据时必须使用ngx_array_t*的形式，这样才能够被解析函数ngx_conf_set_keyval_slot()正确处理。

成员函数create()生成配置结构体：

public:
static void* create(ngx_conf_t* cf)
{
return NgxPool(cf).alloc<this_type>();
}

宏NGX_MOD_INSTANCE定义模块的单件类：

NGX_MOD_INSTANCE(NdgFooterModule, ndg_footer_module, NdgFooterConf)

7.10.3　环境数据类

因为响应头和响应体的过滤处理是彼此独立的两个过程，所以在开发filter模块时通常都需要使用环境数据对象ctx，在里面放置一些共享的信息，用来保证过滤处理的一致性。

一种比较常用的方式是直接用ngx_http_get_module_ctx()检查模块对应的ctx是否存在，如果不存在那么header filter就创建ctx，而body filter就不需要处理。

ndg_footer_module的功能比较简单，所以ctx里只有一个整数标志变量，记录当前处理的阶段，具体含义是：

- 0：还未开始处理，需要处理响应头；

- 1：响应头已经处理，需要处理响应体；

- 2：响应体已经处理完毕。

环境数据类NdgFooterCtx定义如下：

struct NdgFooterCtx final
{
int flag = 0;         //过滤处理的标志位
};

7.10.4　业务逻辑类

类NdgFooterHandler使用NgxFilter向Nginx过滤链表注册自己的处理函数，并实现对响应数据的过滤处理。

在编写过滤函数时我们不能简单地返回NGX_OK这样的错误码，必须要使用next指针调用返回，否则会导致过滤链表中断，无法正确发送数据。

比较好的做法是使用模板方法模式，用一个辅助函数执行处理逻辑，而在主函数里调用后续的链表操作，这样就保证了过滤链表能够正确执行。

类定义

NdgFooterHandler的定义如下，注意NgxFilter的用法：

class NdgFooterHandler final
{
public:
typedef NdgFooterHandler           this_type;  //简化类型定义
typedef NdgFooterModule            this_module;
typedef NgxFilter<this_type>  this_filter;  //过滤器工具类
...                                             //其他成员函数，见后
};

代码里最重要的是模板类NgxFilter的实例化，它使用NdgFooterHandler作为编译期tag，实例化出了专用的过滤器指针。

模块初始化

init()函数用来初始化filter模块，会作为postconfiguration函数指针被Nginx调用，把处理函数插入过滤链表。NgxFilter已经封装了过滤链表的头节点操作，所以我们的代码很简单：

public:
static ngx_int_t init(ngx_conf_t* cf)    //postconfiguration 调用
{
this_filter::init(      //插入过滤链表
&this_type::header_filter, &this_type::body_filter);
return NGX_OK;
}

处理响应头

通常情况下响应头过滤函数只会被调用一次，所以可以在这里执行模块的初始化操作，设置环境数据。

header_filter()函数调用辅助函数，然后再执行后续的过滤链表（省略了try-catch捕获异常的代码）：

static ngx_int_t header_filter(ngx_http_request_t *r)
{
do_header_filter(r);             //辅助函数，真正的业务逻辑
return this_filter::next(r);     //保证过滤链表可以继续处理
}

辅助函数do_header_filter()的基本工作流程是：[5]

1）检查ctx，如果不是0，则表示处理阶段不对，调用后续链表；

2）设置flag为1；

3）获取配置数据headers，遍历存储的kv值，加入到响应头；

4）获取配置数据footer，如果有则修改content_length；

do_header_filter()的实现代码如下：

static void do_header_filter(ngx_http_request_t *r)
{
auto& ctx =          //获取环境数据
this_module::instance().data<NdgFooterCtx>(r);
if(ctx.flag)           //要求必须是 0，表示还未处理
{  return; }          //退出，继续链表的后续处理

ctx.flag = 1;           //置标志为 1
NgxResponse resp(r);         //准备处理响应头

auto& cf =           //获取配置数据
this_module::instance().conf().loc(r);

NgxKvArray headers = cf.headers;     //获取要增加的头

for(auto i = 0u;i < headers.size();++i)    //遍历数组
{
auto& header = headers[i];     //取数组元素

ngx_table_elt_t kv;
kv.hash = 1;          //置 hash 为 1，表示有效
kv.key = header.key;        //头的key
kv.value = header.value;       //头的value

resp.headers().add(kv);       //加入响应头里
}

NgxString footer = cf.footer;      //获取要添加的字符串
if(footer.empty())        //空字符串则不添加
{  return; }

auto len = resp.headers()->content_length_n;  //获取原响应体长度
if(len > 0)          //已有长度
{
resp.length(len + footer.size());   //重新设置长度
}
}             //函数结束，继续链表的后续处理

处理响应体

body_filter()函数使用同样的模板方法模式处理响应体：

static ngx_int_t body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
do_body_filter(r, in);       //辅助函数，真正的业务逻辑
return this_filter::next(r, in);   //继续链表的后续处理
}

因为过滤链表的执行是在ngx_http_output_filter()里，已经是在数据的发送过程中，所以我们不能也没有必要再调用这个函数，处理响应数据主要就是操作ngx_buf_t和ngx_chain_t数据结构，添加数据，调整链表：

static void do_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
auto& cf =           //获取配置数据
this_module::instance().conf().loc(r);

NgxString footer = cf.footer;      //获取要添加的字符串
if(footer.empty())        //空字符串则不添加
{   return; }           //继续链表的后续处理

auto& ctx =          //获取环境数据
this_module::instance().data<NdgFooterCtx>(r);

if(ctx.flag != 1)     //要求必须是 1，即响应头已经处理，响应体还未处理
{    return; }          //继续链表的后续处理

NgxChain ch = in;         //检查响应数据链
auto p = ch.begin();        //迭代器初始化
for(; p != ch.end();++p)        //迭代器遍历链表
{
if(p->data().last())       //查找数据末尾，即 eof
{    break; }
}

if(p == ch.end())         //未找到eof
{   return; }           //继续链表的后续处理

ctx.flag = 2;           //置标志为 2，开始处理

NgxPool pool(r);          //内存池准备分配数据空间

NgxBuf buf = pool.buffer();     //创建一块缓冲区
buf.range(footer);        //添加字符串
buf.finish();          //设置为eof

if(!p->data().size())        //检查原链表的数据是否为空
{
p->set(buf);          //为空则直接设置
return;          //继续链表的后续处理
}

NgxChainNode n = pool.chain();     //创建一个链表节点
n.set(buf);          //缓冲区添加到链表
n.finish();          //链表结束

p->link(n);          //链接到原链表
p->data().finish(false);        //修改原链表的 eof 标志
}             //函数结束，继续链表的后续处理

7.10.5　模块集成类

filter模块的集成类NdgFooterInit与handler模块类似，故仅列出部分主要代码：

static ngx_command_t* cmds()     //指令数组函数
{
static ngx_command_t n[] =      //配置指令数组，静态变量
{
NgxCommand(
ngx_string("ndg_header"),   //添加头信息的指令
NgxTake(NGX_HTTP_LOC_CONF, 2),
ngx_conf_set_keyval_slot,    //Nginx的解析kv函数
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(conf_type, headers)
),

NgxCommand(
ngx_string("ndg_footer"),   //添加末尾字符串指令
NgxTake(NGX_HTTP_LOC_CONF, 1),
ngx_conf_set_str_slot,      //Nginx的解析函数
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(conf_type, footer)
),

NgxCommand()
};

return n;
}

static ngx_http_module_t* ctx()     //函数指针表
{
static ngx_http_module_t c =
{
NGX_MODULE_NULL(1),
&handler_type::init,       //初始化过滤链表
NGX_MODULE_NULL(4),
&conf_type::create,
NGX_MODULE_NULL(1),
};

return &c;
}

这段代码里最重要的是设置模块的postconfiguration函数指针，在解析配置文件后初始化过滤链表，把处理函数添加到链表的正确位置。

7.10.6　实现源文件

cpp文件中定义Nginx编译所需的ngx_module_t变量，只需要两行代码：

#include "NdgFooterInit.hpp"
auto ndg_footer_module = NdgFooterInit::module();

7.10.7　编译脚本

ndg_footer_module使用的编译脚本同样很简单，但需要注意使用的变量是HTTP_FILTER_MODULES，千万不要误用：

ngx_addon_name=ndg_footer_module

#注意使用的 Shell变量是HTTP_FILTER_MODULES
HTTP_FILTER_MODULES="$HTTP_FILTER_MODULES ndg_footer_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ModNdgFooter.cpp"

7.10.8　测试验证

我们可以利用7.9节定义的location/echo，过滤处理ndg_echo指令：

location /echo {
ndg_echo "hello nginx\n";   #启用ndg_echo_module 模块

ndg_header x-name chrono;     #启用添加头过滤处理
ndg_header x-value trigger;    #可以添加多个头信息

ndg_footer "ocarina of time\n";  #启用添加末尾字符串过滤处理
}

用curl测试ndg_footer_filter模块：

curl -v http://localhost/echo?zelda

返回的数据是：

< HTTP/1.1 200 OK
< Server: nginx/1.8.0
< Content-Length: 34
< x-name: chrono       #新添加的头信息
< x-value: trigger
zelda,hello nginx
ocarina of time        #新添加的末尾字符串

7.11　总结

本章主要研究了Nginx处理HTTP请求的核心结构体ngx_http_request_t。它是HTTP框架里最重要的数据结构之一，可以说是Nginx宫殿里的“镇宅之宝”，包含了处理过程中需要的所有信息，本章及之后的章节处理HTTP请求时都需要操作ngx_http_request_t中的各个成员。

依据RFC2616，HTTP协议由请求头、请求体、响应头和响应体等组成，ngx_http_request_t使用headers_in、request_body和headers_out等成员与之对应。

在接收请求数据的同时Nginx解析了请求行和请求头，把请求方法、资源标识符、协议版本号和头的键值对等信息都存储到了ngx_http_request_t里，可以很容易地访问，也可以操作它们来改写请求的参数。响应头和响应体的操作的处理比较简单，只要设置好数据再调用函数发送就可以了，Nginx框架会自动做好剩余的事情。

本章的后半部分内容以实例讲解了如何开发handler模块和filter模块。

开发handler模块最重要的工作是开发并注册ngx_http_handler_pt处理函数。在处理函数里，我们可以用NgxRequest来代理ngx_http_request_t对象，操作HTTP请求的各个字段，丢弃或者读取请求体。如果是content handler模块，就需要结合配置信息决定产生的响应数据，再用NgxResponse的send()函数发送响应头和响应体，从而完成请求的处理。

在创建响应数据时切记不能在栈上分配内存（函数内部的局部变量），因为Nginx在后续的异步处理时很可能栈内存已经无效，会发生内存越界错误，应当尽量使用请求里的内存池来分配内存。

向Nginx框架注册处理函数可以使用NgxHttpCoreModule类，它很好地封装了Nginx的两种handler注册机制，只需要一行代码就可以轻松完成注册工作。

filter模块的开发与handler模块略有不同，它主要的处理对象是响应头和ngx_chain_t里的数据块，业务逻辑较为简单。因为过滤操作是异步的，而且响应数据可能有多份，所以filter模块通常需要使用ctx来记录处理状态，并在header filter里初始化ctx。

实现自己的过滤处理函数通常需要使用next指针调用后续的过滤链表，否则会导致Nginx无法正确发送数据，使用模板方法模式可以很好地避免这个问题。

NgxFilter类封装了Nginx过滤链表的基本操作，包括初始化和调用后续链表，简化了filter模块的开发工作。

[1] 请读者参考http://www.ietf.org/rfc/rfc2616.txt。

[2] 2015年HTTP 2.0协议正式发布，它基于Google的SPDY协议，Nginx虽然也提供了支持，但应用得还不是很广泛，本书暂不讨论。

[3] 本书不讨论Nginx临时文件存储请求体的方式，所以需要用指令client_body_buffer_size保证缓冲区足够大，内存可以容纳数据。

[4] 这里我们也可以使用Nginx提供的大小写无关比较函数ngx_strncasecmp()，但代码会比较烦琐，读者可以自己尝试一下。

[5] 为了简化代码，filter的处理逻辑省略了r->header_only、r->method等请求属性的检查。

第8章　Nginx HTTP请求转发

handler模块和filter模块是Nginx里数量最多的模块，提供了非常丰富的功能，但它们也有“致命”的“弱点”——只能处理本地的资源，能力被限制在了单服务器范围内。如果仅有这两类模块，无疑会使Nginx的吸引力大打折扣。

为了访问外部资源，Nginx在HTTP处理框架里又实现了upstream框架，提供了全异步、高性能的请求转发机制，能够让Nginx超越单机的限制，访问任意的第三方应用服务器收发数据，获得“他山之石可以攻玉”的扩展能力。

upstream框架也是Nginx反向代理的基础，能够让Nginx成为网络里的核心节点，构建起大型的服务网络。

8.1　upstream框架简介

ngx_http_upstream_module是upstream框架的实现模块，它在Nginx的HTTP框架里定义了无阻塞访问后端服务器的机制，使用负载均衡算法选择后端，再用回调函数来处理后端返回的数据，不仅可以支持HTTP协议，还可以支持FastCGI、Memcached等任意协议，非常灵活。

Nginx选择了术语“upstream”而不是常见的“backend”，这是因为在复杂的网络中backend一词不够准确，一个所谓的backend可能并不是后端服务器而是前端服务器，含义容易混淆，而upstream能够更好更形象地描述Nginx在网络数据流里承上启下的位置和作用。

8.1.1　工作原理

ngx_http_upstream_module是一个比较特殊的http模块，它相当于content handler模块，工作在NGX_HTTP_CONTENT_PHASE，向上游服务器发起TCP连接，转发请求，获取响应数据，然后返回HTTP数据给下游客户端。

但它又是一个“不完整”的模块，不能独立处理请求，而是需要其他模块配合才能工作。ngx_http_upstream_module实现了大部分的底层网络收发逻辑和Nginx处理流程，并且在一些关键点定义了回调函数，其他模块需要实现这些函数，然后“插入”进流程来实现负载均衡算法或者处理特定上游服务器的数据。

为了叙述方便，本章之后称ngx_http_upstream_module为upstream框架，而实现回调函数的模块称为load-balance模块和upstream模块。

在整个请求转发的过程中Nginx扮演着“中间人”的角色，把下游的请求转发给上游的服务器（这里不一定是HTTP协议），对接收到的数据做适当的处理后再返回给下游。从下游的客户端来看，Nginx的请求转发处理是完全透明的。

Nginx请求转发机制的基本工作流程如下：

1）upstream框架初始化load-balance模块；

2）upstream模块在HTTP框架里注册处理函数，准备处理客户端请求；

3）upstream模块设置upstream的一些连接参数，如超时时间、缓冲区大小；

4）upstream模块调用ngx_http_upstream_init()启动upstream机制；

5）upstream框架回调create_request()，得到要发送的请求；

6）upstream框架调用load-balance模块，选择一个上游服务器地址；

7）upstream框架连接上游服务器，异步与上游服务器交互，收发数据；

8）upstream框架回调process_header()，处理收到的响应头；

9）upstream框架回调input_filter_init()和input_filter()处理响应体；

10）upstream框架回调finalize_request()，执行收尾工作；

11）HTTP框架发送响应头和响应体数据。

可以看到，upstream框架的工作流程与HTTP框架的工作流程有些相似，它一方面要接收来自客户端的数据，另一方面要接收来自上游服务器的数据。前者由HTTP框架处理，而后者则由upstream框架处理，同样也要解析响应头和响应体，但这些数据不一定是HTTP协议，Nginx无法控制，所以它把数据的解析处理工作交给了外部upstream模块的回调函数去实现，从而能够灵活支持HTTP、FastCGI、Memcached等协议。

upstream框架的工作流程可以用图8-1来表示。

图8-1　upstream框架的工作流程

8.1.2　请求结构体

Nginx的请求转发仍然离不开核心结构体ngx_http_request_t，它里面的成员upstream是请求转发机制的关键：

// 定义在 http/ngx_http_request.h
struct ngx_http_request_s {
ngx_http_upstream_t*  upstream;       //请求转发的关键结构体

ngx_http_request_t* main;        //主请求指针
unsigned                count:8;       //引用计数
unsigned                 subrequest_in_memory:1;   //是否在内存里做处理

...             //其他成员
};

ngx_http_request_t里还有两个成员与请求转发有关。

count是请求的引用计数，当我们使用upstream时必须把它加1，表示它发起了其他请求，这样Nginx就不会关闭连接销毁请求对象，当upstream框架处理完毕后会自动把它减1。

subrequest_in_memory是一个标志位，如果是1，那么收到的响应数据就会由upstream框架调用input_filter_init()和input_filter()进行过滤处理，否则不做任何处理直接发送给客户端（即透传）。

8.1.3　上游结构体

ngx_http_request_t::upstream的类型是ngx_http_upstream_t，它定义了upstream机制需要的所有信息，代码摘要如下：

//定义在http/ngx_http.h
typedef struct ngx_http_upstream_s    ngx_http_upstream_t;

//定义在http/ngx_http_upstream.h
struct ngx_http_upstream_s {
...              //其他成员

ngx_peer_connection_t              peer;      //连接结构体

ngx_chain_t*                      request_bufs;   //发送的请求数据

ngx_http_upstream_conf_t*          conf;      //上游的连接参数设置

ngx_http_upstream_headers_in_t  headers_in;   //上游的响应头

ngx_http_upstream_resolved_t*      resolved;     //上游服务器的地址

ngx_buf_t                         buffer;    //数据缓冲区
off_t                              length;    //缓冲数据的长度

ngx_chain_t*                      out_bufs;     //从上游接收到的数据

//处理上游服务器响应数据的回调函数
ngx_int_t    (*input_filter_init)(void *data);
ngx_int_t    (*input_filter)(void *data, ssize_t bytes);
void          *input_filter_ctx;

//发送接收请求的回调函数
ngx_int_t    (*create_request)(ngx_http_request_t *r);
ngx_int_t    (*reinit_request)(ngx_http_request_t *r);
ngx_int_t    (*process_header)(ngx_http_request_t *r);
void          (*abort_request)(ngx_http_request_t *r);
void          (*finalize_request)(ngx_http_request_t *r, ngx_int_t rc);
ngx_int_t    (*rewrite_redirect)(ngx_http_request_t *r,
ngx_table_elt_t *h, size_t prefix);
ngx_int_t    (*rewrite_cookie)(ngx_http_request_t *r,ngx_table_elt_t *h);

ngx_http_upstream_state_*t     state;      //处理的状态信息

unsigned                          buffering:1;     //是否使用缓冲
unsigned                          request_sent:1;    //是否已经发送请求
unsigned                          header_sent:1;   //是否已经发送响应头

...              //其他成员
};

从结构定义来看，ngx_http_upstream_t很像ngx_http_request_t，也有headers_in、header_sent等成员，这是因为它需要连接上游服务器收发数据，而这些成员用来记录数据收发的状态。

ngx_http_upstream_t特别的地方是定义了九个回调函数，这些回调函数需要由upstream模块来实现特定的功能，才能与上游服务器进行通信。

upstream框架也对部分回调函数提供了默认处理，所以九个回调函数不一定要全部实现，只有三个函数是必须由upstream模块提供的，它们是：

- create_request：生成发送到上游的请求数据；

- process_header：解析收到的响应头；

- finalize_request：请求结束时的收尾工作。

8.1.4　上游配置参数

在请求转发过程中Nginx会作为一个客户端发送TCP请求，所以需要设置超时时间、缓冲、限速等必要的参数，才能正确地连接上游服务器。

Nginx使用结构体ngx_http_upstream_conf_t来配置upstream框架连接上游服务器的参数，定义摘要如下：

//定义在http/ngx_http_upstream.h
typedef struct {
ngx_http_upstream_srv_conf_t* upstream;    //upstream 框架的 srv 配置信息

ngx_msec_t                       connect_timeout;    //连接超时时间
ngx_msec_t                       send_timeout;    //发送超时时间
ngx_msec_t                       read_timeout;    //接收超时数据

size_t                            buffer_size;     //缓冲区大小

ngx_bufs_t                       bufs;       //缓冲区数量设置
ngx_flag_t                       buffering;     //是否使用接收缓冲区

...              //其他成员
} ngx_http_upstream_conf_t;

ngx_http_upstream_conf_t里可以设置的参数有很多，但只有少数成员是我们需要关心的。

xxx_timeout用来设置upstream框架连接上游服务器的超时时间，它们的单位是毫秒，可以在配置文件里使用Nginx提供的ngx_conf_set_msec_slot()函数来设置。

buffer_size设置了upstream框架接收数据的缓冲区大小，可以用ngx_conf_set_size_slot()函数来设置。我们也可以使用Nginx的全局变量ngx_pagesize，它实际上就是系统默认的页面大小：

ngx_pagesize = getpagesize();      //定义在os/unix/ngx_posix_init.c

在ngx_http_upstream_conf_t结构体里也有一个upstream成员，但它与ngx_http_request_t里的upstream完全不同，它表示的是Nginx配置文件里的上游服务器地址配置信息，upstream框架用它来连接上游服务器。

8.2　upstream运行机制

upstream框架工作在Nginx的HTTP框架之内，由HTTP处理引擎调度，在内容产生阶段从上游获取数据再转发给下游，所以它的主要任务就是与上下游的交互并处理数据。

从upstream模块的角度来看，使用upstream机制要有如下几个步骤：

1）定义处理上游数据的回调函数；

2）调用函数ngx_http_upstream_create()初始化upstream机制；

3）设置upstream的连接参数，包括上游服务器的地址、超时时间等；

4）调用ngx_http_upstream_init()启动upstream机制；

5）使用create_request()提供请求数据；

6）使用process_header()和input_filter()处理收到的数据；

7）使用finalize_request()执行结束时的清理工作。

8.2.1　回调函数

ngx_http_upstream_t结构体定义了upstream模块需要实现的九个回调函数，它们是：

- create_request：构造待发送到上游的请求数据；

- reinit_request：如果连接上游失败，则在重连前执行初始化；

- process_header：解析收到的响应头数据；

- abort_request：暂无意义，目前upstream框架未调用；

- finalize_request：请求结束时的收尾工作；

- rewrite_redirect：响应头重定向操作；

- rewrite_cookie：改写cookie操作；

- input_filter_init：过滤响应体前的初始化；

- input_filter：过滤收到的响应体数据；

虽然回调函数比较多，但upstream模块可以根据自身的具体情况只实现部分函数，如果回调函数是空指针，upstream框架要么跳过处理步骤，要么使用默认的函数来处理。

下面简要介绍几个实际开发中较为常用的回调函数。

create_request

create_request在upstream机制启动时被调用，生成能够与上游服务器正确通信的请求数据，例如HTTP请求头、Memcached命令等，并把数据以ngx_chain_t的形式存放在ngx_http_upstream_t::request_bufs，upstream框架在连接上游成功后就会发送请求。

process_header

upstream框架把从上游收到的响应数据存储在ngx_http_upstream_t::buffer里，process_header的作用就是把数据分解出响应头和响应体两部分，并把响应头里的状态码、内容长度等信息存储到headers_in和state成员，转化为下游可以理解的HTTP格式，这个过程与HTTP框架的ngx_http_process_request_line()很相似。

由于upstream框架异步接收数据，process_header不可能一次就解析完毕，所以通常需要使用请求环境数据ctx来暂存解析结果，如果未解析完返回NGX_AGAIN继续解析，如果返回NGX_OK则表示响应头解析完毕，其后的数据都是响应体。

input_filter_init和input_filter

响应体数据也存放在buffer里，当ngx_http_request_t里的subrequest_in_memory标志位是1时，upstream框架就会调用input_filter_init和input_filter这两个函数对响应体执行过滤处理，加工出对下游有效的数据。

如果这两个回调函数为空，那么upstream框架会使用默认的ngx_http_upstream_non_buffered_filter_init()和ngx_http_upstream_non_buffered_filter()函数，它们只是简单地把buffer里收到的数据依次挂到out_bufs链表的末尾，这意味着upstream框架设置的buffer必须足够大，能够容纳上游的所有数据。

finalize_request

当响应数据发送完毕或者发生错误时，upstream框架会执行ngx_http_upstream_finalize_request()函数结束请求，这时会调用finalize_request回调函数，让upstream模块有机会做一些自己的收尾工作。

upstream框架要求finalize_request必须实现，但通常它都是空函数，不需要特别的操作。[1]

8.2.2　初始化upstream

Nginx HTTP框架接收到下游的连接就会调用ngx_http_create_request()创建ngx_http_request_t结构体，这时成员upstream默认是空指针，表示不使用upstream框架。

要启用Nginx的请求转发机制，就必须要调用函数ngx_http_upstream_create()，创建upstream对象。

函数ngx_http_upstream_create()的实现代码摘要如下：

//定义在http/ngx_http_upstream.c
ngx_int_t ngx_http_upstream_create(ngx_http_request_t *r)
{
ngx_http_upstream_t  *u;

u = r->upstream;           //upstream 对象

u = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_t));   //创建对象

r->upstream = u;           //指针赋值

u->headers_in.content_length_n = -1;     //初始化长度为无效值-1
u->headers_in.last_modified_time = -1;

return NGX_OK;
}

函数ngx_http_upstream_create()的工作很简单，只是创建了upstream对象，里面的很多字段都是无意义的0，必须由之后的代码进一步设置。

8.2.3　设置upstream

upstream机制的大部分连接参数由ngx_http_upstream_conf_t结构体决定，也就是ngx_http_upstream_t::conf成员。

设置基本参数

设置upstream参数的标准做法是在模块的配置数据结构里也定义一个ngx_http_upstream_conf_t成员，名字通常就是upstream，在配置解析时使用指令设置里面的各个字段，最后在请求转发前直接把它赋值给ngx_http_upstream_t::conf，即：

r->upstream->conf = &lcf->upstream;     //设置upstream 连接参数

在ngx_http_upstream_conf_t里必须要设置的是超时时间connect_timeout、send_timeout、read_timeout和缓冲区大小buffer_size这四个参数，因为从内存池里创建后这些参数的默认值都是0，会让upstream框架无法正常工作。

设置缓冲区

ngx_http_upstream_t结构里的buffering标志位指示upstream框架是否使用更多的缓冲区来接收上游的数据，但它与ngx_http_upstream_conf_t::buffering没有直接的关系，需要由我们自己来手工指定：

r->upstream->buffering = lcf->upstream.buffering;  //设置是否使用更多的缓冲区

如果buffering值是1，那么upstream框架会分配更多的缓冲区来接收数据，否则只使用固定的大小，即buffer_size。

设置上游服务器地址

通常我们会在配置文件里使用upstream指令配置上游服务器的列表，使用函数ngx_http_upstream_add()就可以设置ngx_http_upstream_conf_t::upstream成员，upstream框架会使用负载均衡算法在服务器列表里选择合适的服务器。

函数ngx_http_upstream_add()的声明是：

ngx_http_upstream_srv_conf_t *ngx_http_upstream_add(ngx_conf_t *cf,
ngx_url_t *u, ngx_uint_t flags);

我们也可以直接使用ngx_http_upstream_t里的成员resolved，它可以用原始Socket API的方式直接指定服务器的地址，例如：

sockaddr_in addr;

addr.sin_family = AF_INET;
addr.sin_port = htons(80);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");

u->resolved = NgxPool(r).alloc<ngx_http_upstream_resolved_t>();

u->resolved->sockaddr = (sockaddr*)&addr;
u->resolved->socklen = sizeof(sockaddr_in);
u->resolved->naddrs = 1;

实际开发中这两种方式都可以采用，第一种方式的好处是可以在配置文件里定义多个服务器，并且使用负载均衡算法；而第二种方式的好处是用法灵活，可以根据请求的具体情况任意设置上游服务器。

8.2.4　启动upstream

设置好upstream框架所需的回调函数和连接参数后，调用函数ngx_http_upstream_init()就可以启动upstream框架，开始与上游服务器异步交互。

ngx_http_upstream_init()的声明是：

void ngx_http_upstream_init(ngx_http_request_t *r);

如果下游有请求体需要转发，那么可以把ngx_http_upstream_init()作为ngx_http_read_client_request_body()的回调函数，即：

ngx_http_read_client_request_body(r, ngx_http_upstream_init);

这样当HTTP框架接收完请求体数据后会回调ngx_http_upstream_init()，启动upstream框架。

因为upstream框架发起了一个新的异步请求，所以通常请求的引用计数需要加1，告诉Nginx框架不要销毁请求对象。ngx_http_upstream_init()内部没有执行计数增加操作，这是特意的，因为ngx_http_read_client_request_body()函数里已经有了增加引用计数的代码，再增加会导致运行错误。

在启动upstream机制后，必须返回错误码NGX_DONE，让HTTP处理引擎继续运行。

8.2.5　处理upstream数据

upstream框架调用create_request()得到请求数据后就会向上游发送请求，然后在ngx_http_upstream_process_header()里处理接收到的数据，代码摘要如下：

static void ngx_http_upstream_process_header(...)
{
for ( ;; ) {
rc = u->process_header(r);      //接收数据解析响应头
if (rc == NGX_AGAIN) {       //还未解析完响应头
continue;          //继续循环处理
}
break;            //返回NGX_OK，响应头解析完毕
}

if (!r->subrequest_in_memory) {     //检查标志位
ngx_http_upstream_send_response(r, u);   //直接转发响应体数据
return;
}

if (u->input_filter == NULL) {     //是否有input_filter 回调函数
u->input_filter_init = ...;      //没有则使用默认函数处理
u->input_filter = ...;
u->input_filter_ctx = r;
}

if (u->input_filter_init(       //过滤处理初始化
u->input_filter_ctx) == NGX_ERROR) {
ngx_http_upstream_finalize_request(r, u, NGX_ERROR);
return;
}

if (u->input_filter(        //过滤处理响应体数据
u->input_filter_ctx, n) == NGX_ERROR) {
ngx_http_upstream_finalize_request(r, u, NGX_ERROR);
return;
}
}

它首先调用process_header()解析响应头，当process_header()返回NGX_OK时就表示响应头接收完毕，之后的数据都是响应体。如果subrequest_in_memory标志位是0，那么就调用ngx_http_upstream_send_response()直接把数据转发给下游，否则调用模块提供的input_filter处理响应体。

8.3　load-balance运行机制

当Nginx作为反向代理时，它连接的上游服务器可能不是简单的一台服务器，而是一个服务器集群，这就需要使用某种算法在集群里选择一台适当的服务器，尽量把请求均衡地转发给集群里的每台服务器，充分利用系统资源。

目前已经有很多相当成熟的负载均衡算法，例如轮询（round robin）、IP散列（ip hash）、一致性散列（consistent hash）等，Nginx在upstream框架里抽象出了load-balance模块，封装了这些算法。

load-balance模块是一种更为特殊的http模块，它不处理任何数据，只是实现选择服务器的算法，所以它的运行机制与handler、filter、upstream模块有很大不同。

load-balance模块的配置指令只能出现在upstream块里，在定义指令属性时要使用宏NGX_HTTP_UPS_CONF。

8.3.1　结构定义

load-balance模块涉及较多的数据结构，本节只介绍几个最关键的。

初始化结构体

结构体ngx_http_upstream_peer_t定义了load-balance模块的入口，有两个回调函数，用来初始化load-balance模块：

//定义在http/ngx_http_upstream.h
typedef ngx_int_t (*ngx_http_upstream_init_pt)(ngx_conf_t *cf,
ngx_http_upstream_srv_conf_t *us);
typedef ngx_int_t (*ngx_http_upstream_init_peer_pt)(ngx_http_request_t *r,
ngx_http_upstream_srv_conf_t *us);

typedef struct {
ngx_http_upstream_init_pt         init_upstream; //配置解析时初始化
ngx_http_upstream_init_peer_pt    init;          //请求时初始化
void*                             data;          //服务器列表指针
} ngx_http_upstream_peer_t;

它位于upstream框架的配置数据结构ngx_http_upstream_srv_conf_t里：

struct ngx_http_upstream_srv_conf_s {
ngx_http_upstream_peer_t         peer;     //初始化结构体
...                                             //其他成员
};

每一个upstream配置块对应一个ngx_http_upstream_srv_conf_t结构，也就是说，每一个upstream块只能使用一种负载均衡算法。

连接结构体

结构体ngx_peer_connection_t定义了load-balance模块实现负载均衡算法的回调函数和相关字段，定义摘要如下：

//定义在event/ngx_event_connect.h
typedef struct ngx_peer_connection_s  ngx_peer_connection_t;

typedef ngx_int_t (*ngx_event_get_peer_pt)(ngx_peer_connection_t *pc,
void *data);
typedef void (*ngx_event_free_peer_pt)(ngx_peer_connection_t *pc,
void *data, ngx_uint_t state);

struct ngx_peer_connection_s {
ngx_connection_t*        connection;   //TCP 连接对象

struct sockaddr*         sockaddr;     //socket 地址
socklen_t                socklen;
ngx_str_t*               name;

ngx_uint_t               tries;     //重试次数

ngx_event_get_peer_pt    get;      //执行算法，获取服务器地址
ngx_event_free_peer_pt   free;      //获取服务器地址后的更新操作
void*                    data;      //get 需要使用的数据

ngx_log_t*               log;      //日志对象
unsigned                 cached:1;     //是否使用磁盘缓存
};

它是upstream框架的核心数据结构ngx_http_upstream_t的成员：

struct ngx_http_upstream_s {
ngx_peer_connection_t   peer;      //使用算法连接服务器
...                                //其他成员
};

注意它的名字也是peer，小心不要和ngx_http_upstream_srv_conf_t::peer弄混了，后者在配置解析阶段起作用，只用于初始化算法。

服务器信息

结构体ngx_http_upstream_server_t存放了upstream配置块里每个服务器的信息，供模块计算时参考，它与upstream块的sever指令完全对应：

//定义在http/ngx_http_upstream.h
typedef struct {
ngx_str_t               name;           //服务器名字
ngx_addr_t*             addrs;          //服务器对应的地址数组
ngx_uint_t              naddrs;         //地址数组的长度
ngx_uint_t              weight;         //权重
ngx_uint_t              max_fails;      //允许的最大失败次数
time_t                  fail_timeout;   //失败的时间区间

unsigned                down:1;         //服务器是否下线
unsigned                backup:1;       //是否是备份服务器
} ngx_http_upstream_server_t;

在配置解析时，Nginx会使用函数ngx_parse_url()得到服务器对应的地址，填充ngx_http_upstream_server_t结构。因为一个域名可能会对应多个IP地址，所以成员addrs是一个数组。

ngx_http_upstream_srv_conf_t使用动态数组保存了一个upstream配置块里所有的服务器信息：

struct ngx_http_upstream_srv_conf_s {
ngx_array_t*             servers;    //ngx_http_upstream_server_t 数组
...                                     //其他成员
};

IP地址列表

upstream框架实现了round-robin算法作为默认的负载均衡算法，使用了三个名字非常相像的结构体来管理上游服务器IP，是其他算法的基础。

ngx_http_upstream_rr_peer_t与每个服务器的具体IP地址一一对应，摘要如下：

//定义在http/ngx_http_upstream_round_robin.h
typedef struct {
struct sockaddr*                 sockaddr;   //可连接的一个 IP地址
socklen_t                        socklen;    //sockaddr 结构体的长度
ngx_str_t                        name;       //地址的名字
ngx_str_t                        server;     //服务器的名字
...                                          //其他成员
} ngx_http_upstream_rr_peer_t;

结构体ngx_http_upstream_rr_peers_t（名字多了一个“s”）管理IP地址列表：

typedef struct ngx_http_upstream_rr_peers_s  ngx_http_upstream_rr_peers_t;

struct ngx_http_upstream_rr_peers_s {
ngx_uint_t                       number;     //服务器数量，即 peer 的长度
unsigned                         single:1;   //只有一台服务器时优化处理
ngx_str_t*                       name;       //upstream 块的名字

ngx_http_upstream_rr_peers_t*    next;       //backup 服务器 IP列表
ngx_http_upstream_rr_peer_t      peer[1];    //非 backup 服务器 IP列表
};

peer成员是一个数组，里面保存的是所有非backup的IP地址信息（虽然声明为长度1，但实际上Nginx用了个“hack”的方法把它变成了动态数组），而backup服务器保存在next.peer成员里。

结构体ngx_http_upstream_rr_peer_data_t是负载均衡算法使用的数据结构，从peers.peer就可以获得可用的IP地址列表：

typedef struct {
ngx_http_upstream_rr_peers_t*    peers;    //IP地址列表
ngx_uint_t                       current;  //round robin 算法参数
uintptr_t*                       tried;    //重试bit数组
uintptr_t                        data;
} ngx_http_upstream_rr_peer_data_t;

结构体关系图

load-balance模块使用的数据结构较多，命名也不够清晰，显得有些杂乱，图8-2中的UML图粗略地描述了它们之间的关系，可以帮助我们理解。

图8-2　load-balance模块相关类图

8.3.2　初始化模块入口

load-balance模块必须要定义一个配置指令以启用算法，在指令解析函数里设置入口函数指针peer.init_upstream，并决定服务器允许哪些附加配置参数，基本的代码是：

uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);
uscf->peer.init_upstream = ...;      //设置入口函数
uscf->flags = ...;           //设置服务器配置参数

uscf->flags决定了upstream配置块的server指令可以使用哪些参数，取值是如下宏的逻辑或：

#define NGX_HTTP_UPSTREAM_CREATE         0x0001   //不允许重复服务器
#define NGX_HTTP_UPSTREAM_WEIGHT         0x0002   //允许weight参数
#define NGX_HTTP_UPSTREAM_MAX_FAILS      0x0004   //允许max_falis 参数
#define NGX_HTTP_UPSTREAM_FAIL_TIMEOUT   0x0008   //允许fail_timeout 参数
#define NGX_HTTP_UPSTREAM_DOWN           0x0010   //允许down 参数
#define NGX_HTTP_UPSTREAM_BACKUP         0x0020   //允许backup参数

如果flags值是0，那么server指令就不能使用任何附加配置参数。

HTTP框架在配置文件解析完成后会调用ngx_http_upstream_module模块的ngx_http_upstream_init_main_conf()函数进行初始化，在这里upstream框架会调用peer.init_upstream初始化每个上游服务器列表所使用的load-balance模块，代码摘要如下：

static char *
ngx_http_upstream_init_main_conf(ngx_conf_t *cf, void *conf)
{
ngx_http_upstream_main_conf_t  *umcf = conf;   //main 配置
ngx_http_upstream_srv_conf_t  **uscfp;     //服务器配置数组

uscfp = umcf->upstreams.elts;       //得到所有 upstream 块

for (i = 0; i < umcf->upstreams.nelts; i++) {  //遍历数组

init = uscfp[i]->peer.init_upstream ?     //是否有模块初始化函数
uscfp[i]->peer.init_upstream:     //使用算法的初始化函数
ngx_http_upstream_init_round_robin;   //默认使用 round robin

if (init(cf, uscfp[i]) != NGX_OK) {    //执行初始化函数
return NGX_CONF_ERROR;
}
}
...
}

8.3.3　初始化IP地址列表

虽然我们可以实现任意的负载均衡算法，但round robin算法是Nginx负载均衡的基础，load-balance模块必须使用函数ngx_http_upstream_init_round_robin()初始化服务器IP地址列表，然后再基于这个列表实现特定的算法。

在peer.init_upstream函数里首先需要调用ngx_http_upstream_init_round_robin()函数，从配置文件里得到所有的服务器IP地址，构建出可用的IP地址列表。它的核心代码摘要如下：[2]

ngx_int_t
ngx_http_upstream_init_round_robin(ngx_conf_t *cf,
ngx_http_upstream_srv_conf_t *us)
{
server = us->servers->elts;        //服务器数组首地址

us->peer.init =          //默认使用 round robin
ngx_http_upstream_init_round_robin_peer;

n = 0;             //计算地址的总数
w = 0;             //计算总权重
for (i = 0; i < us->servers->nelts; i++) {  //遍历服务器数组
if (server[i].backup) {        //计算非backup 服务器
continue;
}

n += server[i].naddrs;         //计算地址的总数
w += server[i].naddrs * server[i].weight;    //计算总权重
}

peers = ngx_pcalloc(cf->pool,       //创建IP列表数据结构
sizeof(ngx_http_upstream_rr_peers_t)  //后面还有 n-1 个数组元素
+ sizeof(ngx_http_upstream_rr_peer_t) * (n - 1));

peers->single = (n == 1);        //是否只有一个服务器
peers->number = n;         //服务器数量
peers->weighted = (w != n);        //是否加权
peers->total_weight = w;         //总权重
peers->name = &us->host;         //upstream 块的名字

n = 0;
peer = peers->peer;          //IP地址列表首地址

for (i = 0; i < us->servers->nelts; i++) {  //遍历服务器数组
if (server[i].backup) {        //只处理非 backup 服务器
continue;
}

for (j = 0; j < server[i].naddrs; j++) {  //遍历服务器的每个 IP地址
peer[n].sockaddr = server[i].addrs[j].sockaddr;   //socket 地址
peer[n].socklen = server[i].addrs[j].socklen;  //socket 结构长度
peer[n].name = server[i].addrs[j].name;     //地址的名字
...              //权重等其他参数
n++;
}
}

us->peer.data = peers;        //添加到配置数据结构里

backup = ...;           //backup 服务器列表
peers->next = backup;         //挂到next 指针下

...             //处理过程与非 backup 类似

return NGX_OK;
}

函数首先设置了peer.init函数指针，默认使用round robin算法，然后创建IP列表数据结构ngx_http_upstream_rr_peers_t。这里Nginx的做法非常“取巧”，在分配内存时在结构体后面又多分配了n-1个长度的数组空间，这样就恰好与peer[1]连在了一起，构成了一个完整的peer[n]数组，可以存放所有的IP地址信息。

填充完IP地址列表信息之后是一个关键操作：

us->peer.data = peers;        //关键操作

这样，构造出的IP地址列表就又放回到了配置结构数据的us->peer.data里，在后续的请求处理时就可以直接访问。

调用ngx_http_upstream_init_round_robin()后，upstream框架使用的算法是round robin，所以load-balance模块需要改写peer.init函数指针为自己的初始化函数，即：

if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK) {
return NGX_ERROR;
}
us->peer.init = ...;         //设置模块的算法初始化函数

8.3.4　初始化算法

在启动函数ngx_http_upstream_init()里upstream框架会查找匹配的upstream块配置，并调用peer.init函数初始化负载均衡算法。

peer.init函数的主要工作是构造模块自己的ngx_http_upstream_rr_peer_data_t对象，从us->peer.data得到IP地址列表，用于后续的算法计算，这也可以利用ngx_http_upstream_init_round_robin_peer()函数来完成。

ngx_http_upstream_init_round_robin_peer()函数代码摘要如下：

ngx_int_t
ngx_http_upstream_init_round_robin_peer(ngx_http_request_t *r,
ngx_http_upstream_srv_conf_t *us)
{
ngx_http_upstream_rr_peer_data_t  *rrp;

rrp = r->upstream->peer.data;   //使用upstream 里的peer.data

if (rrp == NULL) {       //空指针则分配内存
rrp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_rr_peer_data_t));
r->upstream->peer.data = rrp;
}

rrp->peers = us->peer.data;     //得到配置里的 IP地址列表
rrp->current = 0;

r->upstream->peer.get = ngx_http_upstream_get_round_robin_peer;
r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer;

return NGX_OK;
}

这段代码里利用了结构体ngx_peer_connection_t里的data成员，使用它存放了IP地址列表数据。

之后load-balance模块要设置get/free回调函数，供upstream框架在连接上游服务器时调用，否则还是默认的round robin算法。

8.3.5　执行算法

upstream框架使用函数ngx_http_upstream_connect()连接上游服务器，这时会调用peer.get函数指针，使用负载均衡算法从IP地址列表里选择一个上游服务器：

//定义在http/ngx_http_upstream.c
static void ngx_http_upstream_connect(...)
{
...
rc = ngx_event_connect_peer(&u->peer);   //连接上游服务器
...
}

//定义在event/ngx_event_connect.c
ngx_int_t ngx_event_connect_peer(ngx_peer_connection_t *pc)
{
...
rc = pc->get(pc, pc->data);        //调用get函数指针
if (rc != NGX_OK) {
return rc;
}

s = ngx_socket(...);         //创建socket对象
...
}

如果请求结束或者上游服务器无法连接，upstream框架会执行peer.free函数指针，使用state参数传递错误原因，模块可以在这里调整IP地址列表和算法的内部状态，为下一次运行做好准备。

peer.free函数不是必须的，但不能是空指针，如果算法不需要free操作，upstream会使用round robin默认的free函数。

8.4　Nginx模块源码分析

本节简要研究ngx_http_memcached_module和ngx_http_upstream_ip_hash_module这两个模块的源码，它们分别实现了连接上游Memcached服务器和IP散列负载均衡算法。

8.4.1　ngx_http_memcached_module

ngx_http_memcached_module是一个upstream模块，它可以连接Memcached服务器，读取缓存数据（不提供写入操作）。

配置数据结构

ngx_http_memcached_module在配置数据结构里定义了upstream的连接参数：

typedef struct {
ngx_http_upstream_conf_t     upstream;    //upstream 连接参数
ngx_int_t                   index;
ngx_uint_t                  gzip_flag;
} ngx_http_memcached_loc_conf_t;

配置指令

ngx_http_memcached_module的核心指令是启动upstream框架的memcached_pass，其他指令都是设置连接参数：

static ngx_command_t  ngx_http_memcached_commands[] = {

{ ngx_string("memcached_pass"),     //启动upstream 框架
NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_CONF_TAKE1,
ngx_http_memcached_pass,        //指令解析函数
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },

{ ngx_string("memcached_connect_timeout"),   //连接超时时间
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_msec_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_memcached_loc_conf_t, upstream.connect_timeout),
NULL },

...              //其他指令
ngx_null_command
};

设置处理函数

函数ngx_http_memcached_pass()解析配置指令，设置模块的content handler：

static char *
ngx_http_memcached_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_memcached_loc_conf_t *mlcf = conf;
value = cf->args->elts;        //得到指令字符串

ngx_memzero(&u, sizeof(ngx_url_t));

u.url = value[1];         //解析url
u.no_resolve = 1;

mlcf->upstream.upstream = ngx_http_upstream_add(cf, &u, 0);
if (mlcf->upstream.upstream == NULL) {
return NGX_CONF_ERROR;
}

clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
clcf->handler = ngx_http_memcached_handler;  //注册处理函数
}

启动upstream机制

函数ngx_http_memcached_handler()设置upstream回调函数，启动upstream机制，代码摘要如下：

static ngx_int_t
ngx_http_memcached_handler(ngx_http_request_t *r)
{
if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {   //检查请求方法
return NGX_HTTP_NOT_ALLOWED;
}

rc = ngx_http_discard_request_body(r);      //丢弃请求体

if (ngx_http_upstream_create(r) != NGX_OK) {   //初始化upstream
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

mlcf = ngx_http_get_module_loc_conf(r, ngx_http_memcached_module);
u->conf = &mlcf->upstream;        //设置连接参数

//设置回调函数
u->create_request = ngx_http_memcached_create_request;
u->reinit_request = ngx_http_memcached_reinit_request;
u->process_header = ngx_http_memcached_process_header;
u->abort_request = ngx_http_memcached_abort_request;
u->finalize_request = ngx_http_memcached_finalize_request;

u->input_filter_init = ngx_http_memcached_filter_init;
u->input_filter = ngx_http_memcached_filter;
u->input_filter_ctx = ctx;

r->main->count++;           //增加引用计数
ngx_http_upstream_init(r);        //启动upstream

return NGX_DONE;            //必须返回 NGX_DONE
}

因为访问Memcached服务器不需要请求体，所以函数里丢弃了请求体，增加引用计数后直接启动了upstream机制。

回调函数

ngx_http_memcached_module解析数据的回调函数代码较为复杂，与Memcached协议关系密切，故这里不列出具体的代码，只介绍实现的基本功能。

函数ngx_http_memcached_create_request()从变量$memcached_key获取key，然后构造get命令。

函数ngx_http_memcached_process_header()解析Memcached数据，当找到换行符时表示响应头解析完毕，设置u->headers_in.content_length_n、u->headers_in.status_n和u->state->status等响应信息。

函数ngx_http_memcached_filter()处理响应数据，主要的工作是去掉Memcached数据末尾的“END”标识符，并把数据存放在u->out_bufs链表里。

函数ngx_http_memcached_finalize_request()是一个空函数，里面只记录了一条debug日志。

8.4.2　ngx_http_upstream_ip_hash_module

ngx_http_upstream_ip_hash_module是一个load-balance模块，它实现了IP散列负载均衡算法。

配置指令

ngx_http_upstream_ip_hash_module没有配置数据结构，只用一个ip_hash指令来启用算法，指令数组是：

static ngx_command_t  ngx_http_upstream_ip_hash_commands[] = {
{ ngx_string("ip_hash"),
NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS,    //注意使用的标志位
ngx_http_upstream_ip_hash,      //指令解析函数
0,
0,
NULL },

ngx_null_command
};

设置模块入口

函数ngx_http_upstream_ip_hash()解析配置指令，设置算法的入口函数peer.init_upstream：

static char *
ngx_http_upstream_ip_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);

if (uscf->peer.init_upstream) {     //已设置则配置出错
ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
"load balancing method redefined");
}

uscf->peer.init_upstream = ngx_http_upstream_init_ip_hash;
uscf->flags = ...;          //设置允许的配置参数

return NGX_CONF_OK;
}

初始化IP地址列表

函数ngx_http_upstream_init_ip_hash()调用round robin函数初始化IP地址列表，然后设置自己的算法初始化函数peer.init：

static ngx_int_t
ngx_http_upstream_init_ip_hash(ngx_conf_t *cf,
ngx_http_upstream_srv_conf_t *us)
{
if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK) {
return NGX_ERROR;
}

us->peer.init = ngx_http_upstream_init_ip_hash_peer;  //算法初始化函数

return NGX_OK;
}

初始化算法

ngx_http_upstream_ip_hash_module定义了专用的算法数据对象ngx_http_upstream_ip_hash_peer_data_t，必须要注意的是它利用了C语言的平坦内存模型，把ngx_http_upstream_rr_peer_data_t作为它的第一个成员，这样round robin算法可以忽略后面的成员，把它当作是ngx_http_upstream_rr_peer_data_t来操作：

typedef struct {
ngx_http_upstream_rr_peer_data_t   rrp;  //算法数据对象，必须是第一个
...             //其他成员
ngx_event_get_peer_pt get_rr_peer;    //round robin 算法
} ngx_http_upstream_ip_hash_peer_data_t;

函数ngx_http_upstream_init_ip_hash_peer()创建算法数据对象，并使用round robin算法得到配置里的IP地址列表，然后再设置get回调：

static ngx_int_t
ngx_http_upstream_init_ip_hash_peer(ngx_http_request_t *r,
ngx_http_upstream_srv_conf_t *us)
{
ngx_http_upstream_ip_hash_peer_data_t  *iphp;

iphp = ngx_palloc(...);      //创建算法数据结构
r->upstream->peer.data = &iphp->rrp;    //设置peer.data

//得到配置里的 IP地址列表
if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) {
return NGX_ERROR;
}

r->upstream->peer.get = ngx_http_upstream_get_ip_hash_peer;
iphp->get_rr_peer = ngx_http_upstream_get_round_robin_peer;

return NGX_OK;
}

IP散列算法没有内部状态，所以它不需要设置free回调函数。

执行算法

函数ngx_http_upstream_get_ip_hash_peer是IP散列算法的具体实现，根据IP地址计算散列值，然后从列表中选择一个地址：

static ngx_int_t
ngx_http_upstream_get_ip_hash_peer(ngx_peer_connection_t *pc, void *data)
{
ngx_http_upstream_ip_hash_peer_data_t  *iphp = data;

if (iphp->tries > 20 || iphp->rrp.peers->single) {  //重试次数过多
return iphp->get_rr_peer(pc, &iphp->rrp);   //退化为round robin
}

pc->cached = 0;           //不使用磁盘缓存
pc->connection = NULL;         //尚未连接

...              //算法计算散列值
peer = &iphp->rrp.peers->peer[p];      //得到一个地址

pc->sockaddr = peer->sockaddr;       //设置连接地址
pc->socklen = peer->socklen;
pc->name = &peer->name;

return NGX_OK;            //成功获得一个上游地址
}

8.5　C++封装

本节实现四个C++类，封装Nginx的upstream机制，方便开发upstream模块和load-balance模块。

8.5.1　NgxUpstream

ngx_http_upstream_t结构体是upstream框架的核心数据结构，类NgxUpstream是它的C++代理，定义如下：

class NgxUpstream final : public NgxWrapper<ngx_http_upstream_t>
{
public:
typedef NgxWrapper<ngx_http_upstream_t> super_type;
typedef NgxUpstream       this_type;
public:
NgxUpstream(ngx_http_upstream_t* u)    //直接代理结构体
:super_type(u)
{}

NgxUpstream(ngx_http_request_t* r):    //获取request_t 的成员
super_type(r->upstream)
{}

~NgxUpstream() = default;
...              //其他成员函数，见后
};

基本操作

类NgxUpstream使用成员函数headers()和state()操作常用的两个成员变量headers_in和state。注意在原数据结构里这两个成员的类型不同（分别是对象实体和指针），但函数统一返回了引用的形式，有利于客户编码：

public:
ngx_http_upstream_headers_in_t& headers() const  //获取上游返回的头部信息
{
return get()->headers_in;       //返回引用
}

ngx_http_upstream_state_t& state() const    //获取上游的状态信息
{
return *get()->state;        //返回引用
}

设置配置参数

成员函数conf()和buffering()用来设置upstream框架连接上游的基本参数：

public:
void conf(ngx_http_upstream_conf_t& cf) const  //设置上游的连接参数
{
get()->conf = &cf;         //设置配置结构体
get()->buffering = cf.buffering;      //缓冲标志位
}

void buffering(bool flag) const      //单独设置是否使用缓冲
{
get()->buffering = flag;        //缓冲标志位
}

设置请求数据

构造发送给上游服务器的请求是启动upstream机制时非常关键的功能，成员函数request()简化了这一操作，把数据挂在链表的末尾，允许多次调用，分步构造出最终的字符串：

public:
void request(ngx_chain_t* bufs) const    //构造请求数据
{
if(!get()->request_bufs)        //无请求数据则直接赋值
{
get()->request_bufs = bufs;
return;
}

NgxChain ch = get()->request_bufs;    //得到数据链表
ch.append(bufs);          //添加到链表末尾
}

8.5.2　NgxUpstreamHelper

NgxUpstream代理了ngx_http_upstream_t结构体，但它自身无法完成初始化、设置回调函数、启动等操作，需要用辅助类NgxUpstreamHelper来实现。

类定义

NgxUpstreamHelper使用模板参数，在编译期确定了upstream机制必须实现的三个回调函数，定义如下：

template<
ngx_int_t(*create_request)(ngx_http_request_t*) = nullptr,
ngx_int_t(*process_header)(ngx_http_request_t*) = nullptr,
void(*finalize_request)(ngx_http_request_t*,ngx_int_t) = nullptr
>
class NgxUpstreamHelper final : public NgxWrapper<ngx_http_request_t>
{
public:
typedef NgxWrapper<ngx_http_request_t>   super_type;  //简化类型定义
typedef NgxUpstreamHelper      this_type;
typedef NgxUpstream        upstream_type;
public:
NgxUpstreamHelper(ngx_http_request_t* r):
super_type(r), m_upstream(create())    //构造时创建 upstream 对象
{}

~NgxUpstreamHelper() = default;
public:
const upstream_type& upstream() const    //获取upstream 对象
{
return m_upstream;
}
private:
upstream_type m_upstream;        //upstream 对象
private:
static void default_finalize_request(ngx_http_request_t* r,ngx_int_t rc)
{
NgxLogDebug(r).print("default_finalize_request");
}
...             //其他成员函数，见后
};

成员m_upstream代理了ngx_http_request_t结构体里的upstream，它会在构造函数里调用create()函数初始化。

通常upstream模块的finalize_request()都是空操作，所以NgxUpstream-Helper提供了一个默认的实现——如果客户代码不提供finalize_request指针，就使用内置的静态成员函数default_finalize_request()。

初始化upstream

成员函数create()调用函数ngx_http_upstream_create()，初始化ngx_http_request_t里的upstream成员：

private:
ngx_http_upstream_t* create() const     //初始化upstream 对象
{
if(!get()->upstream)        //检查upstream 指针
{
auto rc = ngx_http_upstream_create(get()); //创建upstream 对象

NgxException::require(       //错误码检查
rc == NGX_OK, NGX_HTTP_INTERNAL_SERVER_ERROR);
}

return get()->upstream;        //返回upstream 对象
}

配置upstream

成员函数conf()可以直接设置upstream框架的连接参数，内部调用了NgxUpstream的conf()函数：

void conf(ngx_http_upstream_conf_t& cf) const
{
upstream().conf(cf);        //调用NgxUpstream 的 conf()
}

启动upstream

启动函数start()使用一个参数来决定是否读取客户端的请求体数据，它首先设置三个基本的回调函数，然后根据参数使用不同的方式调用ngx_http_upstream_init()启动upstream框架，最后返回NGX_DONE：

public:
ngx_int_t start(bool read_body = false) const  //启动upstream 框架
{
if(!upstream()->create_request)      //设置回调函数
{
upstream()->create_request = create_request;  //创建请求函数指针
upstream()->process_header = process_header;  //处理响应头函数指针
upstream()->finalize_request =     //检查函数指针是否为空
finalize_request?finalize_request:  //不为空则使用函数指针
&this_type::default_finalize_request;  //否则使用默认的空函数
}

if(read_body)          //是否要读取请求体
{
auto rc =           //读取完请求体后再启动
ngx_http_read_client_request_body(
get(), ngx_http_upstream_init);   //upstream_init 作为回调

NgxException::fail(rc >= NGX_HTTP_SPECIAL_RESPONSE);
}
else             //不读取请求体，直接启动
{
auto rc = ngx_http_discard_request_body(get());//丢弃请求体
NgxException::require(rc);

++get()->main->count;        //增加主请求的引用计数
ngx_http_upstream_init(get());     //启动upstream 框架
}

return NGX_DONE;          //返回NGX_DONE，继续引擎
}

这段代码中需要注意的是read_body==false时的处理，我们必须要增加主请求的引用计数，告诉Nginx框架不要销毁请求对象。

8.5.3　NgxHttpUpstreamModule

在设置load-balance模块入口函数时我们需要访问ngx_http_upstream_module的配置，所以可以为它实现一个专门的类。

类定义

NgxHttpUpstreamModule代理了ngx_http_upstream_module，只要把模块的配置结构类型作为模板参数传递给基类NgxModule就可以复用便捷访问配置数据的能力，定义如下：

class NgxHttpUpstreamModule final :
public NgxModule<void,         //没有loc配置
ngx_http_upstream_srv_conf_t,
ngx_http_upstream_main_conf_t>
{
public:
typedef NgxModule<...>    super_type;  //简化类型定义
typedef NgxHttpUpstreamModule   this_type;
public:
NgxHttpUpstreamModule() : super_type(ngx_http_upstream_module)
{}
~NgxHttpUpstreamModule() = default;
public:
static NgxHttpUpstreamModule& instance()    //单件访问点
{
static NgxHttpUpstreamModule m;
return m;
}
...             //其他成员函数，见后
};

初始化算法入口

对于load-balance模块来说，我们需要修改配置里的init_upstream函数指针，让upstream框架使用load-balance模块定义的算法：

public:
template<typename F>
void init(ngx_conf_t* cf, F f, ngx_uint_t flags = 0) const
{
auto& uscf = conf().srv(cf);      //获取配置数据

NgxException::fail(uscf.peer.init_upstream)  //已经设置则错误

uscf.peer.init_upstream = f;      //设置回调函数
uscf.flags = flags;         //设置算法参数
}

8.5.4　NgxLoadBalance

NgxLoadBalance类封装了load-balance模块的初始化操作。

类定义

NgxLoadBalance类定义如下：

template<typename T,         //算法数据结构
ngx_event_get_peer_pt   get_peer,     //执行算法函数指针
ngx_event_free_peer_pt  free_peer = nullptr,
ngx_http_upstream_rr_peer_data_t T::* ptr = &T::rrp>
class NgxLoadBalance final
{
typedef ngx_http_upstream_rr_peer_data_t  peer_data_t;
typedef ngx_http_upstream_srv_conf_t   srv_conf_t;
public:
...             //其他成员函数，见后
};

NgxLoadBalance类有四个模板参数。T是算法需要使用的data，它里面的rrp成员是round robin算法使用的数据结构，可以用成员变量指针来访问。

初始化IP地址列表

init()函数调用ngx_http_upstream_init_round_robin()初始化IP地址列表，并设置算法初始化函数：

public:
static void init(ngx_conf_t *cf, srv_conf_t* us,
ngx_http_upstream_init_peer_pt init_peer)
{
auto rc = ngx_http_upstream_init_round_robin(cf, us);
NgxException::require(rc);

us->peer.init = init_peer;        //设置算法初始化函数
}

初始化算法

在初始化算法时，我们需要创建算法数据结构，并设置get/free函数：

public:
static T& init(ngx_http_request_t* r, srv_conf_t* us)
{
auto& peer_data = *NgxPool(r).alloc<T>();    //创建算法数据结构

r->upstream->peer.data = &(peer_data.*ptr);  //设置算法数据指针

auto rc = ngx_http_upstream_init_round_robin_peer(r, us);
NgxException::require(rc);

r->upstream->peer.get  = get_peer;      //设置get函数
r->upstream->peer.free = free_peer?free_peer:   //设置free 函数
r->upstream->peer.free;

return peer_data;         //返回算法数据结构
}

8.6　开发upstream模块

完善的upstream模块要处理很多琐碎细节，本节的upstream模块仅作为示范，只有最基本的转发功能，读者可以以此为基础实现功能更完善的模块。

8.6.1　模块设计

作为示范，本节要实现的upstream模块功能比较简单，设计如下：

- 模块名是ndg_upstream_module；

- 使用content handler方式注册处理函数；

- 使用指令ndg_upstream_pass启动转发处理；

- upstream框架上游的连接参数均内部确定，不在配置文件里设置；

- 仅支持GET方法，仅转发请求行信息；

- 不解析响应头，收到的数据原样转发到下游。

8.6.2　配置信息类

与ngx_http_memcached_module等模块的实现类似，我们在模块的配置数据结构里添加ngx_http_upstream_conf_t成员，用来设置upstream框架的连接参数：

class NdgUpstreamConf final       //配置信息类
{
public:
typedef NdgUpstreamConf this_type;
public:
NdgUpstreamConf() = default;
~NdgUpstreamConf() = default;
public:
ngx_http_upstream_conf_t upstream;      //upstream 配置参数
public:
static void* create(ngx_conf_t* cf)      //创建配置结构体
{
auto& c = *NgxPool(cf).alloc<this_type>();   //内存池分配内存

c.upstream.connect_timeout   = 1000*30;    //设置超时时间，重要
c.upstream.send_timeout   = 1000*30;
c.upstream.read_timeout   = 1000*30;
c.upstream.buffer_size    = ngx_pagesize;   //设置缓冲区大小，重要

return &c;
}
};

注意在create()函数里我们必须设置超时时间和缓冲区大小这四个基本参数，保证在后面启动upstream时正确连接。

8.6.3　业务逻辑类

ndg_upstream_module模块的业务逻辑可以分解为两个类：NdgUpstreamCallback实现回调函数，NdgUpstreamHandler组装回调函数并启动upstream框架，这样做可以避免逻辑代码太过复杂导致类过于庞大。

生成upstream请求

create_request()函数的实现较为简单，转发到上游的请求行直接使用原请求行，然后再构造出一个标准的HTTP请求头就可以了：

class NdgUpstreamCallback final      //upstream 所需的回调函数
{
public:
static ngx_int_t create_request(ngx_http_request_t* r)
{
ngx_str_t msgs[] = {        //构造HTTP 请求头
r->request_line,         //原请求行
ngx_string("\r\n"),        //CRLF
ngx_string("Host: localhost\r\n"),    //host 头必须有
ngx_string("\r\n"),        //CRLF
ngx_null_string         //空字符串表示数组结束
};

NgxPool    pool(r);         //内存池
NgxUpstream   u(r);          //upstream 代理对象

NgxString s = msgs[0];         //准备遍历字符串数组
for(auto i = 0u;!s.empty();s = msgs[++i])    //空字符串作为哨兵
{
NgxBuf buf = pool.buffer();      //创建缓冲区
buf.range(s);         //设置缓冲区数据

NgxChainNode ch = pool.chain();     //创建数据链表
ch.set(buf);          //设置缓冲区

u.request(ch);          //添加upstream 请求数据
}

return NGX_OK;
}

这段代码中我们简化了请求数据的处理逻辑，使用一个数组来组织起HTTP请求头，然后利用NgxUpstream的成员函数request()逐个添加到upstream的request_bufs链表里，构造出了一个最简单的HTTP请求头。

处理响应头

真实的upstream模块处理响应头的代码非常复杂，通常需要使用状态机，并在ctx里保存解析的状态。但本节实现的ndg_upstream_module仅作为示范，所以省略了这部分代码，直接返回NGX_OK，表示没有响应头，所有数据都是响应体：

public:
static ngx_int_t process_header(ngx_http_request_t* r)
{
NgxUpstream u(r);         //代理upstream 对象

u.headers().status_n = NGX_HTTP_OK;    //设置响应状态
u.headers().content_length_n = 192;    //设置响应数据长度

u.state().status = NGX_HTTP_OK;      //设置upstream 状态

return NGX_OK;           //已经处理完响应头
}
};                //回调函数类结束

虽然我们不解析响应数据，但process_header()函数决不能是空函数，它还要把上游服务器的响应状态转换成下游能够接收的形式（也就是HTTP格式），必须解析出响应数据的状态码、数据长度等基本参数，这里的代码做了简化，直接硬编码。

读者可以参考ngx_http_memcached_module和ngx_http_proxy_module等模块的源码，了解响应头的解析处理逻辑。

启动upstream机制

类NdgUpstreamHandler使用NdgUpstreamCallback实例化了NgxUpstream-Helper，在处理函数中初始化upstream，设置连接参数，然后启动upstream机制。

由于upstream机制相关的代码均已经封装在了NgxUpstreamHelper里，所以处理函数的代码很简洁：

class NdgUpstreamHandler final       //业务逻辑类
{
public:
typedef NdgUpstreamHandler  this_type;    //简化类型定义
typedef NdgUpstreamCallback   callback_type;

typedef NdgUpstreamModule   this_module;    //本模块类型定义
typedef NgxUpstreamHelper<&callback_type::create_request,
&callback_type::process_header>
this_upstream;  //回调函数实例化
public:
static ngx_int_t handler(ngx_http_request_t *r)
{
NgxRequest req(r);          //处理原请求
if(!req.method(NGX_HTTP_GET))       //检查请求方法
{
return NGX_HTTP_NOT_ALLOWED;
}

this_upstream u(r);         //初始化upstream

auto& cf = this_module::instance().conf().loc(r); //获取模块配置

u.conf(cf.upstream);        //设置upstream 参数

return u.start();        //启动upstream 框架
}
};

在实例化NgxUpstreamHelper时我们只给出了两个模板参数，第三个模板参数finalize_request使用了默认的空函数，节约了代码。

8.6.4　模块集成类

NdgUpstreamInit整合模块的配置信息类和业务逻辑类，对于upstream模块来说重点是配置指令的解析处理，所以本节省略了函数指针表和模块定义。

类定义

NdgUpstreamInit的定义如下：

class NdgUpstreamInit final
{
public:
typedef NdgUpstreamConf      conf_type;   //简化类型定义
typedef NdgUpstreamHandler   handler_type;
typedef NdgUpstreamInit      this_type;
public:
...                                       //其他成员函数，见后
};

配置指令解析

upstream模块必须在指令解析函数里向ngx_http_core_module注册处理函数，并且调用ngx_http_upstream_add()，设置使用的upstream服务器：

private:
static char* set_upstream_pass(ngx_conf_t* cf,
ngx_command_t* cmd, void* conf)
{
ngx_url_t u;          //url 对象

u.url = NgxStrArray(cf->args)[1];     //得到配置字符串
u.no_resolve = true;

auto& lcf = *reinterpret_cast<conf_type*>(conf); //当前的配置结构体

lcf.upstream.upstream =       //设置upstream 服务器
ngx_http_upstream_add(cf, &u, 0);

if(!lcf.upstream.upstream)
{   return reinterpret_cast<char*>(NGX_CONF_ERROR); }

NgxHttpCoreModule::instance().handler(   //注册处理函数
cf, &handler_type::handler);

return NGX_CONF_OK;
}

因为配置指令并不真正地操作配置结构体，所以设置指令数组可以简化，无须写出NGX_HTTP_LOC_CONF_OFFSET和offset值，使用默认参数即可：

public:
static ngx_command_t* cmds()
{
static ngx_command_t n[] =
{
NgxCommand(
ngx_string("ndg_upstream_pass"),  //配置指令名
NgxTake(NGX_HTTP_LOC_CONF, 1),  //参数数量设置
&this_type::set_upstream_pass   //指令解析函数
),            //其他参数使用默认值

NgxCommand()       //空指令结束数组
};

return n;
}

8.6.5　实现源文件

ndg_upstream_module的实现源文件仍然是两行代码：

#include "NdgUpstreamInit.hpp "                        //包含头文件
auto ndg_upstream_module = NdgUpstreamInit::module();  //ngx_module_t变量

8.6.6　编译脚本

upstream模块属于http模块，在编译脚本里要使用HTTP_MODULES变量：

ngx_addon_name=ndg_upstream_module

HTTP_MODULES="$HTTP_MODULES ndg_upstream_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ModNdgUpstream.cpp"

编译命令是：

./configure --add-module=path/to/upstream;make

8.6.7　测试验证

因为upstream模块要访问外部的服务器，所以我们在Nginx的配置文件中先要在upstream块里定义上游服务器列表：

upstream backend{       #定义上游服务器列表
server 127.0.0.1:80;      #使用本机地址测试
}

然后定义一个新的server，端口是8080，在里面使用ndg_upstream_pass转发请求：

server {
listen         8080;        #监听端口号
server_name   localhost;

location / {         #定义location
ndg_upstream_pass backend;     #转发请求到 backend 块的服务器
}
}

使用curl向8080端口发送请求，ndg_upstream_module会把请求原样转发给本机的80端口：

curl  http://localhost:8080/echo?upstream -v #请求会转发到 80/echo 处理

得到的结果可能是：

* Connected to localhost (127.0.0.1) port 8080
> GET /echo?upstream HTTP/1.1      #请求行
> Host: localhost:8080        #请求8080 端口服务器
>
< HTTP/1.1 200 OK        #响应头信息
< Server: nginx/1.8.0
< Content-Length: 192       #返回数据的长度是 192
<
HTTP/1.1 200 OK          #以下都是从 80端口得到的数据
Server: nginx/1.8.0         #因为没有解析响应头
Content-Length: 37         #所有的数据都作为响应数据输出
Connection: keep-alive
x-name: chrono
x-value: trigger
#这里是原数据的\r\n\r\n分隔
upstream,hello nginx
ocarina of time

8.7　开发load-balance模块

load-balance模块的开发相对来说比较简单，回调函数只有四个，而且很多操作步骤都是模式化的，模块的重点是用算法操纵IP地址列表，选择出合适的上游服务器地址。

8.7.1　模块设计

本节的模块使用随机数作为负载均衡算法，随机选择IP地址，设计如下：

- 模块名是ndg_load_balance_module；

- 配置指令是ndg_load_balance；

- 不支持weight/down等附加配置参数；

- 使用boost.random库产生随机数。

8.7.2　配置信息类

ndg_load_balance_module没有任何配置数据需要存储，所以无须定义配置信息类，也就不需要模块单件类来访问配置数据或者请求环境数据。

当然load-balance模块也可以和其他http模块一样使用配置文件，但要用NGX_HTTP_SRV_CONF_OFFSET指定存储位置。这是因为upstream块没有location层次，使用NGX_HTTP_LOC_CONF_OFFSET没有意义。

8.7.3　业务逻辑类

与开发upstream模块类似，load-balance模块最好也把回调函数放在一个单独的类里实现，再用handler类组装起来。

算法数据结构

类NdgLoadBalanceCallback把算法数据结构和算法捆绑在了一起，不需要再定义额外的数据结构。

注意在声明数据成员时一定要把ngx_http_upstream_rr_peer_data_t作为第一个成员，这样在调用ngx_http_upstream_init_round_robin_peer()函数时才能正确设置peer.data。

NdgLoadBalanceCallback的定义如下：：

class NdgLoadBalanceCallback final
{
public:
typedef NgxLogDebug               log;         //记录运行日志
typedef NdgLoadBalanceCallback    this_type;   //简化类型定义
typedef boost::rand48             random_type; //随机数发生器
public:
ngx_http_upstream_rr_peer_data_t  rrp;         //必须是第一个成员！

u_char                            tries = 0;   //重试次数
ngx_event_get_peer_pt             get_rr_peer =  //默认的round robin 算法
ngx_http_upstream_get_round_robin_peer;
public:
...                                            //其他成员函数，见后
};

算法实现

get()函数从IP地址列表里随机选择一个，然后设置ngx_peer_connection_t对象的socket连接参数：

public:
static ngx_int_t get(ngx_peer_connection_t *pc, void *data)
{
auto& peer_data = *reinterpret_cast<this_type*>(data);
auto& peers = *peer_data.rrp.peers;    //IP地址列表

if(peer_data.tries++ > 5 || peers.single)    //检查重试次数
{
return peer_data.get_rr_peer(pc, &peer_data.rrp); //退化为 round robin
}

static random_type rand(time(0));      //随机数发生器

auto& peer = peers.peer[rand() % peers.number];//随机选择一个地址

pc->sockaddr   = peer.sockaddr;      //设置连接参数
pc->socklen    = peer.socklen;
pc->name    = &peer.name;
pc->cached    = false;
pc->connection   = nullptr;

log(pc->log).print("peer=%V", &peer.name);   //记录运行日志

return NGX_OK;           //成功得到了服务器地址
}

初始化算法

类NdgLoadBalanceHandler使用回调函数类实例化NgxLoadBalance：

class NdgLoadBalanceHandler final
{
public:
typedef NdgLoadBalanceHandler   this_type;   //简化类型定义
typedef NdgLoadBalanceCallback  callback_type;
typedef NdgLoadBalanceCallback  peer_data_type;

typedef NgxLoadBalance<peer_data_type,     //实例化NgxLoadBalance
&callback_type::get>   //只有get回调
this_load_balance;
public:
...             //其他成员函数，见后
};

实例化后的NgxLoadBalance已经封装好了基本的初始化逻辑，如果不设置算法数据，直接调用就可以完成模块的初始化工作：

public:
static ngx_int_t init(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
{
this_load_balance::init(cf, us, &this_type::init_peer);
return NGX_OK;
}
public:
static ngx_int_t init_peer(ngx_http_request_t *r,
ngx_http_upstream_srv_conf_t *us)
{
this_load_balance::init(r, us);      //忽略返回的数据对象
return NGX_OK;
}
};

8.7.4　模块集成类

因为没有配置信息类，所以NdgLoadBalanceInit只需使用业务逻辑类来初始化load-balance模块入口。

类定义

NdgLoadBalanceInit的定义如下：

class NdgLoadBalanceInit final
{
public:
typedef NdgLoadBalanceHandler   handler_type;  //简化类型定义
typedef NdgLoadBalanceInit      this_type;
public:
...                                              //其他成员函数，见后
};

配置指令解析

load-balance模块需要在指令解析函数里初始化入口函数，可以直接调用NgxHttpUpstreamModule::init()实现：

private:
static char* set_load_balance(ngx_conf_t* cf,
ngx_command_t* cmd, void* conf)
{
NgxHttpUpstreamModule::instance().init(cf, &handler_type::init);
return NGX_CONF_OK;
}

指令数组里需要注意的是使用参数NGX_HTTP_UPS_CONF，而且参数数量是0：

public:
static ngx_command_t* cmds()
{
static ngx_command_t n[] =
{
NgxCommand(
ngx_string("ndg_load_balance"),     //配置指令名
NgxTake(NGX_HTTP_UPS_CONF, 0),      //注意这里
&this_type::set_load_balance    //指令解析函数
),

NgxCommand()                            //空指令结束数组
};

return n;
}

函数指针表

ndg_load_balance_module不需要配置任何参数，所以在配置解析阶段不需要任何回调函数，8个函数指针都是空：

public:
static ngx_http_module_t* ctx()
{
static ngx_http_module_t c =
{
NGX_MODULE_NULL(8)        //宏生成8 个空指针
};

return &c;
}

8.7.5　实现源文件

ndg_upstream_module的实现源文件如下：

#include "NdgLoadBalanceInit.hpp"      //包含头文件
auto ndg_load_balance_module = NdgLoadBalanceInit::module();

8.7.6　编译脚本

load-balance模块虽然不处理请求，但它仍然是http模块，所以在编译脚本里也要使用HTTP_MODULES变量：

ngx_addon_name=ndg_load_balance_module

HTTP_MODULES="$HTTP_MODULES ndg_load_balance_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ModNdgLoadBalance.cpp"

编译命令是：

./configure --add-module=path/to/loadbalance;make

8.7.7　测试验证

可以为8.6节的ndg_upstream_module模块增加一些上游服务器的配置：

server {           #定义一个新的虚拟主机
listen       88;        #端口是 88
server_name  localhost;

location / {         #location 配置
proxy_set_header Host $host;   #转发host 头
proxy_pass http://localhost:80;   #使用proxy_pass 转发HTTP 请求
}
}

upstream backend{       #定义上游服务器列表
ndg_load_balance;       #启用负载均衡算法
server 127.0.0.1:80;      #本机80端口服务器
server localhost:88;      #本机88端口服务器
}

仍然使用curl向8080端口发送请求，ndg_load_balance_module会随机选择一个虚拟主机处理请求，可以通过运行日志看到运行结果，例如：

2015/xx/xx [error]: peer=127.0.0.1:88 while connecting to upstream ...

8.8　总结

本章讲解了Nginx里两种特殊的http模块：upstream模块和load-balance模块，它们都工作在upstream框架里，是Nginx宫殿里的阁楼和瞭望塔。

模块ngx_http_upstream_module实现了upstream框架，它可以高效地与上游服务器通信，转发下游的请求，再回传上游服务器返回的响应数据，支持HTTP、FastCGI、Memcached等多种协议，让Nginx有能力承担起核心节点的重任，分类或整合Web应用。

upstream框架是Nginx HTTP框架的一部分，本质上是content handler模块。它在内容产生流程的关键节点上定义了若干回调函数，执行选择上游地址、组织请求头、解析响应数据等操作，其他模块需要实现这些回调函数才能利用upstream框架完成整个请求转发工作。

由于upstream框架实现了主要的请求转发逻辑，所以开发upstream模块和load-balance模块都较为简单，只需要实现少量回调函数就可以。但它们的开发步骤略微烦琐，需要在配置解析阶段做的事情比较多，必须按照框架的要求，设置好各种参数和回调函数。

upstream模块必须实现的是create_request、process_header和finalize_request三个回调函数，然后在处理函数里设置好连接上游的参数，调用ngx_http_upstream_init()就可以启动upstream机制收发数据。

load-balance模块的核心是实现算法的get/free函数，但要想把模块接入upstream框架需要在配置解析阶段做很多准备，例如设置模块入口、初始化IP地址列表等。

本章实现了四个C++类，在模板参数列表里确定了回调函数，较好地封装了upstream模块和load-balance模块的初始化和设置操作，有助于我们的模块开发工作。

[1] 目前Nginx官方所有upstream模块的finalize_request都是空函数，所以upstream框架实在有必要为它提供一个默认的空实现，或者检查空指针。很可惜，直到目前的1.8.0版仍然没有。

[2] 这里省略了不使用upstream块直接给出上游服务器地址的处理代码。

第9章　Nginx HTTP子请求

Nginx框架定义了四种模块：handler、filter、upstream和load-balance，这四种模块互相协作可以处理绝大多数HTTP请求，但它们还存在一定的局限性：只能处理单一的请求，只能完成一件任务，对于要执行多个请求才能完成的复杂任务无能为力。例如，向数个不同的上游服务器获取数据，最后整理成一个汇总表格输出，显然任何一种模块都无法胜任。

如果是在客户端，这不是个问题，客户可以自己分解任务，逐个向Nginx发送请求，得到数据后再自行整理。这种方法虽然可行却增加了客户端的负担，而且运行效率得不到保证。

Nginx为此设计了子请求机制，把业务处理逻辑由客户端转移到了服务器端，目的是分解复杂的处理逻辑，把操作流程拆分成若干个相对简单的“子请求”。每一个子请求都可以由现有的handler或upstream模块完成，并且这些子请求仍然可以享用Nginx框架的异步高性能的好处，不会有效率上的损失，最后由主请求整合数据返回给客户端。

本章将详细介绍Nginx的子请求机制，使用它可以实现复杂的业务逻辑，让Nginx变身为一台高效的应用服务器。

9.1　子请求简介

由客户端发起的HTTP请求我们称之为主请求，它直接与客户端进行TCP通信，解析完请求头和请求体后在处理引擎中经过完整的11个阶段，最终产生响应数据输出，是典型的RPC（remote procedure call）。

子请求是由Nginx内部发起的特殊HTTP请求，因为已经在服务器内部，所以它不需要建立TCP连接，也没有请求数据解析的成本，直接找到对应的location进入处理引擎处理，相对于主请求资源消耗很少，本质上与发起一个函数调用没有区别，是LPC（local procedure call）。

子请求与主请求的功能和处理流程基本相同，都是由Nginx的HTTP处理引擎处理，使用相同的handler模块和filter模块链，区别主要在于两者的发起方式。子请求是Nginx内部的LPC，所以没有TCP连接和请求解析的成本，但因为不直接与客户端通信，所以它不能直接发送数据，通常也不会记录访问日志。[1]

子请求也存在功能限制，它不能跨server调用（因为不发起TCP连接），只能调用本server块内的location。不过我们可以利用upstream机制，在location里使用proxy_pass等反向代理指令访问其他服务器，再用子请求调用location，从而间接达到目的。

这里我们使用了“调用”这个词，它非常精确地描述了子请求的作用：可以把Nginx配置文件里的location当作是功能函数，通过子请求向它传递uri、headers、body等参数，“调用”后得到结果数据。

子请求的出现，让location不再是孤立的个体，而是成为了处理业务的有机组件，使用location完成多个简单的请求任务，再用子请求调用它们，最终即可实现复杂的应用逻辑。

9.1.1　工作原理

主请求可以发起多个子请求，而且子请求还可以再发起子请求，形成树状的调用层次。灵活使用子请求可以在Nginx服务器里访问任意的资源，在任意的层次上对数据进行整合，最后由主请求产生响应数据返回。

由于存在多级的请求树，所以发起子请求的请求又称为父请求，它的概念与主请求不同。主请求是绝对概念，只能有一个，相当于树的根，而父请求是相对的概念，是发起当前请求的上一级请求。主请求一定是父请求，而父请求则不一定是主请求。

发起子请求处理的基本流程是：

1）客户端发起请求，即主请求，由Nginx处理；

2）主请求调用ngx_http_subrequest()创建出子请求对象，加入待处理请求链表；

3）主请求返回NGX_DONE，暂停主请求的执行，等待子请求完成；

4） HTTP处理引擎从待处理请求链表里获取排队的子请求，依次处理；

5）子请求的处理过程与主请求完全相同，还可以再发起子请求（即2~4步）；

6）子请求处理完毕，使用回调函数通知主请求继续运行；

7）主请求获取子请求生成的响应数据，加工后返回给客户端。

子请求的创建和处理流程可以用图9-1来表示。

图9-1　子请求的创建和处理流程

9.1.2　请求结构体

子请求与主请求只有从属关系，本质上没有区别，所以结构体ngx_http_request_t也用来表示子请求对象。

ngx_http_request_t里子请求相关的成员如下：

// 定义在 http/ngx_http_request.h
struct ngx_http_request_s {

ngx_http_event_handler_pt     read_event_handler;  //读事件回调函数
ngx_http_event_handler_pt     write_event_handler; //写事件回调函数

ngx_http_request_t*           main;                //请求所属的主请求
ngx_http_request_t*           parent;              //请求所属的父请求

ngx_http_post_subrequest_t*   post_subrequest;     //子请求回调函数对象

ngx_http_posted_request_t*    posted_requests;     //等待执行的请求链表

ngx_http_postponed_request_t* postponed;           //请求数据链表

unsigned                      subrequests:8;       //子请求的数量
unsigned                      count:8;             //引用计数
unsigned                      internal:1;          //内部请求标志位

...                                                //其他成员
};

结构体ngx_http_request_t既可以表示主请求，也可以表示子请求。成员main指向当前请求所属的主请求，如果自身就是主请求，那么“r==r->main”表达式为真。

post_subrequest、posted_requests和postponed是三个名字相近的成员[2]，我们在阅读代码时务必要小心。它们保存了子请求结束时的回调函数、等待执行的请求链表和下级子请求产生的数据，是Nginx子请求运行机制的关键数据结构，详细信息可参见后面的小节。

为了保证运行效率，Nginx不允许创建无限数量的子请求，subrequests成员用来记录主请求的子请求数量，如果超过上限就会出错，无法创建新的子请求。

子请求数量的上限由宏NGX_HTTP_MAX_SUBREQUESTS确定，默认值是200，也就是说，一个主请求以及它的子请求，总共能够发起的子请求数量不能多于200个，这通常足够用了。

9.1.3　回调函数

子请求的处理过程是异步的，创建子请求后，当前的请求（即父请求）必须返回NGX_DONE暂时中断处理，HTTP处理引擎会去执行新发起的子请求。

父请求需要设置子请求的回调函数，而子请求处理结束时必须要用回调函数通知父请求，恢复父请求的运行。

子请求回调函数

当子请求执行完毕后（成功处理或者发生错误），Nginx会调用函数ngx_http_post_subrequest_pt，它的声明是：

typedef ngx_int_t (*ngx_http_post_subrequest_pt)(
ngx_http_request_t *r, void *data, ngx_int_t rc);

ngx_http_post_subrequest_pt有三个参数，r是当前的子请求对象（注意不是父请求）；data的类型是void*，允许传入任意数据；rc是请求执行完毕后的错误码，如果执行成功通常是NGX_OK或者NGX_HTTP_OK。

因为子请求的执行是异步的，所以data参数很重要，相当于函数的运行环境数据（context）。Nginx特意定义了一个数据结构ngx_http_post_subrequest_t，结构体里的data成员就是handler调用时传递的参数：

typedef struct {
ngx_http_post_subrequest_pt  handler;     //回调函数
void*                           data;      //函数的相关数据
} ngx_http_post_subrequest_t;        //子请求回调函数对象

它把回调函数和所需的数据绑定在了一起，用法相当于C++里的lambda表达式（闭包）。

在ngx_http_post_subrequest_pt里我们可以做很多事情，例如检查处理状态，检查子请求输出的头和响应数据等，但最重要的是设置父请求的回调函数，让父请求可以继续运行。

父请求回调函数

父请求的回调函数是ngx_http_request_t里的函数指针write_event_handler，它是父请求被重新“激活”处理后执行的函数，也可以说是断点恢复后的后续处理流程，声明是：

typedef void (*ngx_http_event_handler_pt)(ngx_http_request_t *r);

它与6.2.1节的HTTP处理函数ngx_http_handler_pt非常像，只是返回值类型不同。

在不同的处理阶段，父请求的回调函数也不相同。

对于rewrite、access等非内容产生阶段，由于整个处理流程还未走完，所以回调函数通常是ngx_http_core_run_phases()，让父请求继续在引擎里处理。

而在内容产生阶段（NGX_HTTP_CONTENT_PHASE），由于处理引擎已经执行完毕，所以回调函数应该是父请求自己的处理逻辑，整合从子请求收到的数据，发送数据，最后必须调用ngx_http_finalize_request()来结束请求。

9.1.4　待处理请求链表

结构体ngx_http_request_t里的posted_requests成员以链表的方式管理主请求发起的所有子请求，由HTTP处理引擎调度执行。注意：它只在主请求里有意义。

主请求可以发起多个子请求，这些子请求是平等的关系，但存在先后顺序，所以Nginx使用一个链表结构串起当前请求的所有子请求，链表的定义是：

typedef struct ngx_http_posted_request_s  ngx_http_posted_request_t;

struct ngx_http_posted_request_s {
ngx_http_request_t*               request;    //子请求对象
ngx_http_posted_request_t*        next;       //下一个链表节点
};

可以看到，ngx_http_posted_request_t就是一个非常简单的链表，使用指针来保存子请求对象。

在函数ngx_http_run_posted_requests()里Nginx会获取主请求的待处理请求链表，逐个执行请求的write_event_handler回调函数，驱动请求的处理流程。

9.1.5　子请求存储结构

虽然子请求的发起有先后顺序，但它们的完成顺序是不固定的，由于任务的不同，很有可能后发起的子请求会先完成。为了能够正确组织子请求返回的数据，Nginx使用ngx_http_request_t::postponed组织本级请求发起的所有下级子请求。

postponed的类型是ngx_http_postponed_request_t，定义如下：

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_postpone_filter_module，整合子请求对象和子请求产生的数据。

9.2　子请求运行机制

子请求的运行机制比handler/filter/upstrem等模块的运行机制要复杂一些，不仅有多个请求，而且这些请求之间还会彼此通信，协同工作。处理逻辑也分散在Nginx框架的各个角落，想要理解它的运行机制必须要多花些力气。

Nginx处理子请求有三个基本步骤：创建子请求、调度子请求/父请求和组织整理所有请求产生的数据。

9.2.1　创建子请求

在任意的HTTP处理阶段都可以调用函数ngx_http_subrequest()创建一个子请求对象。

函数声明

ngx_http_subrequest()的接口参数较多，是Nginx里比较少见的，声明如下：

ngx_int_t ngx_http_subrequest(ngx_http_request_t *r,
ngx_str_t *uri, ngx_str_t *args, ngx_http_request_t **sr,
ngx_http_post_subrequest_t *psr, ngx_uint_t flags);

入口参数的含义是：

- r：当前的请求对象，即父请求；

- uri：子请求的uri，也就是本server块内的某个location的名字；

- args：子请求的uri参数，可以为空（nullptr）；

- sr：输出参数，传出创建好的子请求对象；

- psr：子请求结束时的回调函数，见9.1.3节；

- flags：标志位，定制子请求的某些行为，通常是0。

创建一个子请求必须要确定的是uri参数，它决定从哪个location里执行子请求获得数据args参数是可选的，如果location支持参数，就可以传递uri之外的附加信息。

flags可以取值为NGX_HTTP_SUBREQUEST_IN_MEMORY，会置子请求结构体里的subrequest_in_memory标志位为true，这样upstream模块获取的上游数据都会保存在内存里，方便后续处理，对于非upstream模块则没有意义。[3]

函数实现

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)
{
r->main->subrequests--;      //主请求里的子请求计数器

if (r->main->subrequests == 0) {     //子请求达到上限
r->main->subrequests = 1;
return NGX_ERROR;         //子请求创建失败，返回错误
}

函数首先检查子请求计数器，如果超过了NGX_HTTP_MAX_SUBREQUESTS的限制，那么就不能再创建新的子请求，返回NGX_ERROR错误码。

接下来的代码是创建子请求对象，与创建请求对象的ngx_http_create_request()类似，只是大部分字段都是直接拷贝自父请求，无须重新分配内存：

sr = ngx_pcalloc(...);       //创建子请求对象
sr->signature = NGX_HTTP_MODULE;     //结构体的签名字符串

sr->ctx = ngx_pcalloc(...);     //创建模块的 ctx存储空间

cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
sr->main_conf = cscf->ctx->main_conf;   //模块的各层次配置数据
sr->srv_conf = cscf->ctx->srv_conf;
sr->loc_conf = cscf->ctx->loc_conf;

sr->pool = r->pool;         //复用父请求的内存池
sr->headers_in = r->headers_in;      //复用父请求的请求头
sr->request_body = r->request_body;     //复用父请求的请求体

sr->method = NGX_HTTP_GET;      //请求方法默认为 GET
sr->http_version = r->http_version;     //拷贝http 版本信息

sr->request_line = r->request_line;     //复用原请求行
sr->uri = *uri;         //改写子请求的 uri

if (args) {          //改写子请求的 args
sr->args = *args;
}

//设置子请求的标志位
sr->subrequest_in_memory = (flags & NGX_HTTP_SUBREQUEST_IN_MEMORY) != 0;
sr->waited = (flags & NGX_HTTP_SUBREQUEST_WAITED) != 0;

sr->unparsed_uri = r->unparsed_uri;     //复用原始 uri
sr->method_name = ngx_http_core_get_method;   //请求方法字符串默认为 GET
sr->http_protocol = r->http_protocol;   //复用原请求协议

ngx_http_set_exten(sr);        //改写子请求的扩展名

上面的一大段代码都是设置子请求HTTP相关的各种参数，例如method、uri、args等。值得注意的是子请求的内存池与父请求的内存池是同一个，这意味着在子请求里分配的内存是一直可用的，不会因为子请求的销毁而消失，而请求的环境数据ctx是重新分配的空间，所以子请求和父请求的模块ctx数据是彼此独立的。

子请求的默认方法是GET，而且其他参数也基本复制了父请求，但这并不是说子请求只能用GET方法，或者只能原样转发父请求的所有参数。在ngx_http_subrequest()执行后，我们可以使用输出参数sr获得创建好的子请求对象，它只是一个请求的“样板”，完全可以对它做任意的修改，例如把method改成POST、再次修改args、增加新的请求体数据，对此Nginx没有限制，用户可以非常灵活地定制子请求。

函数的后半部分是子请求调度相关数据的设置，重点是设置子请求的处理函数为ngx_http_handler()：

sr->main = r->main;         //设置主请求
sr->parent = r;         //设置父请求
sr->post_subrequest = ps;       //子请求结束时的回调函数对象

sr->write_event_handler = ngx_http_handler;   //子请求的执行函数

pr = ngx_palloc(...);        //子请求数据对象

if (r->postponed) {
for (p = r->postponed; p->next; p = p->next) //挂到父请求数据链表的末尾
p->next = pr;

} else {
r->postponed = pr;         //空链表则直接添加
}

sr->internal = 1;         //标记是内部请求，即子请求

tp = ngx_timeofday();        //计时器开始计时
sr->start_sec = tp->sec;
sr->start_msec = tp->msec;

r->main->count++;         //引用计数增加

*psr = sr;           //创建完毕，返回子请求对象

return ngx_http_post_request(sr, NULL);    //加入待执行请求链表
}

函数ngx_http_post_request()执行收尾工作，负责把子请求加入到主请求的待执行请求链表posted_requests里，是子请求能够运行的重要步骤：

ngx_int_t
ngx_http_post_request(ngx_http_request_t *r, ngx_http_posted_request_t *pr)
{
ngx_http_posted_request_t  **p;

if (pr == NULL) {         //创建一个链表节点
pr = ngx_palloc(r->pool, sizeof(ngx_http_posted_request_t));
}

pr->request = r;          //设置链表里的请求对象
pr->next = NULL;

for (p = &r->main->posted_requests; *p; p = &(*p)->next) //找到链表末尾
*p = pr;            //添加到链表末尾等待调度

return NGX_OK;
}

这样，在ngx_http_subrequest()执行完毕后，主请求里的执行请求链表posted_requests就会增加一个新节点，但子请求并没有立即开始运行，而是等待处理引擎调度。

9.2.2　处理引擎

子请求的处理涉及HTTP框架里的多个函数，本节只简要介绍两个比较重要的函数。

子请求调度

ngx_http_run_posted_requests()是驱动子请求运行的关键函数，代码摘要如下：

void
ngx_http_run_posted_requests(ngx_connection_t *c)
{
for ( ;; ) {
pr = r->main->posted_requests;      //获取待执行的请求链表

r->main->posted_requests = pr->next;    //移除头节点
r = pr->request;         //得到当前待执行的请求

r->write_event_handler(r);       //执行请求的 handler
}
}

函数ngx_http_run_posted_requests()的逻辑很简单，遍历主请求的posted_requests成员，也就是待执行的请求链表，然后执行请求的write_event_handler回调函数。

因为在创建子请求时已经设置了write_event_handler为ngx_http_handler()，所以它会把子请求的阶段设置到server_rewrite（跳过了NGX_HTTP_POST_READ_PHASE阶段），进而从头遍历引擎数组，让引擎里的http模块去处理子请求，流程与正常的请求相同。

ngx_http_handler()里子请求的相关代码是：

//ngx_http_handler()函数，定义在 http/ngx_http_core_module.c
r->phase_handler = cmcf->phase_engine.server_rewrite_index; //设置阶段起点
ngx_http_core_run_phases(r);      //执行处理引擎

在处理完一个请求后，ngx_http_run_posted_requests()会把请求从链表中移除，随着请求不断被处理，最终链表会全部清空。

结束子请求

请求或子请求结束时都会调用函数ngx_http_finalize_request()，它是Nginx HTTP处理的核心函数之一，内部逻辑非常复杂，处理了大量可能发生的情况，这里只介绍与子请求相关的部分。

当子请求处理完毕正常结束时，ngx_http_finalize_request()会调用post_subrequest成员，执行子请求结束时的回调函数，并把父请求加入待处理请求链表，等待引擎调度从而“激活”：

void ngx_http_finalize_request(ngx_http_request_t *r, ngx_int_t rc)
{
...
if (r != r->main && r->post_subrequest) {   //判断子请求
rc = r->post_subrequest->handler(    //执行子请求的回调函数
r, r->post_subrequest->data, rc); //传递请求的错误码
}
...

if (ngx_http_post_request(pr, NULL) != NGX_OK) { //父请求加入待处理链表
...
}
}

因此，子请求需要在回调函数里设置好父请求的write_event_handler，这样子请求结束后调度函数ngx_http_run_posted_requests()会在待处理请求链表里找到父请求，从而继续父请求的后续工作。

9.2.3　数据整理

现在父请求和子请求都可以正常运行了，但处理它们产生的数据又是一个新的问题，必须保证子请求产生的数据能够按照它们创建的先后顺序正确输出，而不能是异步执行完成后的乱序。

Nginx实现了一个filter模块ngx_http_postpone_filter_module，它专门处理子请求的数据，把数据插入到主请求postponed成员里的合适位置。

ngx_http_postpone_filter_module是Nginx必须编译的内置模块，不能使用选项删除，是子请求运行机制里必不可少的组件。它相当于模块数据发送过程中的一个“钩子”，中断了子请求的过滤链表处理，把子请求产生的数据“钩”回到父请求。

ngx_http_postpone_filter_module是body filter，只处理响应体数据，不处理响应头，核心功能是函数ngx_http_postpone_filter()，代码摘要如下：

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 = r->connection;        //获得当前的连接对象

if (r != c->data) {       //r不是当前正在处理的请求
ngx_http_postpone_filter_add(r, in);    //数据加入父请求链表末尾
return NGX_OK;          //直接结束过滤链表
}

if (r->postponed == NULL) {     //没有需要延后发送的数据
if (in || c->buffered) {      //使用主请求发送数据
return ngx_http_next_body_filter(r->main, in);
}

return NGX_OK;
}

if (in) {           //有需要延后发送的数据
ngx_http_postpone_filter_add(r, in);   //当前数据加入父请求链表末尾
}

do {           //循环处理现有的待发送数据
pr = r->postponed;         //获取头节点

if (pr->request) {        //节点里是子请求

r->postponed = pr->next;     //移除头节点
c->data = pr->request;       //置为连接处理的当前请求
return ngx_http_post_request(pr->request, NULL); //加入待处理请求链表
}

if (ngx_http_next_body_filter(     //节点是数据，由主请求发送
r->main, pr->out) == NGX_ERROR) {
return NGX_ERROR;
}

r->postponed = pr->next;      //处理下一个节点

} while (r->postponed);        //直至所有的数据节点处理完毕

return NGX_OK;
}

模块处理子请求的关键是c->data，它表示当前处理的请求对象，也就是可发送数据的请求，是保证子请求数据正确顺序的关键，Nginx通过调整c->data来控制子请求数据发送的顺序。

当响应数据流经模块时，如果请求的顺序不对，那么数据就会被加入主请求的postponed链表里，否则就会走主请求的过滤链表发送给客户端。

更细致地解析ngx_http_postpone_filter_module与postponed链表数据的关系需要较多的篇幅，而实际上它能发挥作用的时候并不是很多，故本书从略，读者只需理解它的基本工作原理就足够了，暂不必深究细节。

9.3　C++封装

在Nginx里提供的子请求的接口只有一个用于创建的ngx_http_subrequest()，设置回调函数和子请求参数都要用户自己编写代码。本节实现两个简单的C++类，分别封装回调函数和创建子请求。

9.3.1　NgxSubRequestHandler

通常使用子请求需要实现两个回调函数，所以我们使用类NgxSubRequestHandler把它们绑定在一起，增强内聚性。

模板参数

NgxSubRequestHandler是一个模板类，它的第一个模板参数是客户需要实现的回调函数类，含有sub_post和parent_post两个回调函数，第二个模板参数是子请求所在的处理阶段。

回调函数类的形式是：

class SubrequestCallback        //子请求处理所需的回调函数
{
public:
static ngx_int_t sub_post(ngx_http_request_t* r, void* data, ngx_int_t rc);
static ngx_int_t parent_post(ngx_http_request_t* r);
};

注意parent_post()函数的返回类型是ngx_int_t，而不是void，这样回调函数类可以专注于业务逻辑的实现，而NgxSubRequestHandler封装了子请求处理时必须做的设置父请求回调函数和调用ngx_http_finalize_request()。

基本形式

在非内容产生阶段，父请求的回调函数应该是ngx_http_core_run_phases，以便让HTTP处理引擎继续执行后续的模块。

NgxSubRequestHandler的定义如下：

template<typename T, ngx_http_phases ph>
class NgxSubRequestHandler final
{
public:
typedef NgxSubRequestHandler this_type;
public:
static ngx_int_t sub_post(ngx_http_request_t* r, void* data, ngx_int_t rc)
{
r->parent->write_event_handler =      //设置父请求的回调函数
ngx_http_core_run_phases;
return T::sub_post(r, data, rc);    //执行子请求的回调函数
}
};

内容产生阶段特化

在内容产生阶段，必须明确指定父请求的回调函数，这样才能继续处理父请求，同时还要在父请求处理结束时调用ngx_http_finalize_request()。NgxSubRequestHandler使用模板技术，完全封装了这些流程化的操作：

template<typename T>         //注意，模板特化
class NgxSubRequestHandler<T, NGX_HTTP_CONTENT_PHASE> final
{
public:
typedef NgxSubRequestHandler this_type;
public:
static ngx_int_t sub_post(ngx_http_request_t* r, void* data, ngx_int_t rc)
{
r->parent->write_event_handler =      //设置父请求的回调函数
&this_type::parent_post;    //必须是自定义的回调函数
return T::sub_post(r, data, rc);      //执行子请求的回调函数
}

static void parent_post(ngx_http_request_t* r)   //父请求的回调函数
{
ngx_http_finalize_request(      //结束请求
r, T::parent_post(r));   //执行父请求的回调函数
}
};

9.3.2　NgxSubRequest

类NgxSubRequest利用NgxSubRequestHandler包装子请求的回调函数，调用ngx_http_subrequest()创建出子请求对象，定义如下：

template<typename T, ngx_http_phases ph = NGX_HTTP_CONTENT_PHASE>
class NgxSubRequest final : public NgxWrapper<ngx_http_request_t>
{
public:
typedef NgxWrapper<ngx_http_request_t> super_type;  //简化类型定义
typedef NgxSubRequest       this_type;
typedef NgxSubRequestHandler<T, ph>    this_handler;  //回调函数封装
public:
NgxSubRequest(ngx_http_request_t* r):super_type(r)
{}
~NgxSubRequest() = default;
public:
ngx_http_request_t* create(ngx_str_t& uri,    //子请求uri
ngx_str_t& args,     //子请求参数
void* psr_data = nullptr,  //回调使用的数据
ngx_uint_t flags = 0) const  //子请求标志位
{
auto psr =             //子请求回调函数
NgxPool(get()).alloc<ngx_http_post_subrequest_t>();

psr->handler = &this_handler::sub_post;    //设置回调函数
psr->data = psr_data;        //设置函数使用的数据

ngx_http_request_t* sr;         //子请求指针

auto rc = ngx_http_subrequest(       //创建子请求
get(), &uri, &args, &sr, psr, flags);
NgxException::require(rc);        //检查错误码

return sr;           //返回子请求对象
}
};

这段代码里的关键是NgxSubRequestHandler的实例化，得到了子请求的回调函数，然后创建回调函数结构体，最后创建出子请求对象返回。

得到子请求对象后，用户可以修改它的method、uri、args等任意成员，定制出目标location所需的正确请求。

如果子请求创建失败，create()会抛出异常，客户代码需要使用try-catch块来捕获异常。

9.4　数据回传模块

在Nginx里创建子请求很容易，但获取并处理子请求返回的数据却不是一件容易的事情。虽然Nginx提供了模块ngx_http_postpone_filter_module，但它处理的方式过于“死板”，只能把数据拷贝到主请求的输出链里，没有设置回调函数接口，所以我们无法对数据做更多的加工处理，大大限制了子请求的用途。

处理子请求数据的另一个办法是创建子请求时使用标志位NGX_HTTP_SUBREQUEST_IN_MEMORY，让upstream块把收到的数据都保存在r->upstream->buffer里，这样在子请求结束的回调函数中就可以从buffer里获取数据，然后回传给父请求。但这种方法有很大的局限性，首先是它只能用于upstream模块，不能用于普通的handler模块；其次是数据必须保存在内存里，而且受buffer_size大小的限制。所以，本书不推荐使用NGX_HTTP_SUBREQUEST_IN_MEMORY标志处理子请求数据。

ngx_http_postpone_filter_module功能虽然有限，但为解决这个问题提供了很好思路。我们可以开发一个类似功能的filter模块，它在Nginx过滤链表里安装一个“钩子”，把子请求的响应头和响应体数据都“钩”回到父请求里，让父请求能够自由使用任何http模块产生的数据，真正释放出子请求的潜力。

9.4.1　模块设计

数据回传filter模块的基本设计如下：

- 模块名是ndg_data_hook_module，过滤处理响应头和响应体；

- 不使用配置指令，没有配置数据结构；

- 在请求环境数据（ctx）里保存子请求的数据；

- 父请求需要在ctx里设置一个标志位，子请求才会把数据传回父请求；

- 需要调整模块在ngx_modules数组中的位置，必须在ngx_http_postpone_filter_module之前。

9.4.2　环境数据类

在Nginx里父请求和子请求的环境数据ctx的存储是互相独立的，可以利用这一点在模块的ctx里存储我们想要的数据，通过r->parent得到父请求，从而操作父请求里的模块ctx。

类定义

ndg_data_hook_module的环境数据类定义如下：

struct NdgDataHookCtx final
{
public:
ngx_flag_t                hook = false;   //数据回传标志位，默认不处理

ngx_int_t                 status = 0;   //子请求的处理状态
ngx_http_headers_out_t*   headers = nullptr; //子请求的响应头数据
ngx_chain_t*             body = nullptr;   //子请求的响应体数据

ngx_flag_t                do_hook = false;   //子请求里处理 body 的标志位
public:
...            //其他成员函数，见后
};

NdgDataHookCtx是模块的核心数据结构，它使用status、headers和body保存子请求产生的数据，hook和do_hook是两个标志变量，用来启用或者禁用数据回传功能。

启动数据回传

默认情况下hook变量值是false，模块不会有任何动作，直接把数据转发到过滤链表的下一个节点，发起子请求后必须显式地调用成员函数hooking()启用数据回传功能：

public:
void hooking()         //启用数据回传功能
{
hook   = true;        //回传标志位置 true
status   = 0;          //错误码初始化
headers   = nullptr;        //响应头初始化为空指针
body   = nullptr;        //响应体初始化为空指针
}

在子请求里会检查父请求模块的ctx，如果hook==true，那么就需要把数据回传到父请求里。

设置处理状态

子请求的处理状态是一个重要的参数，父请求需要通过它来检查子请求执行是否成功，这只能在子请求结束时的回调函数里用参数rc来判断。NdgDataHookCtx简单地封装了判断逻辑：

public:
void state(ngx_int_t rc)       //检查子请求的处理状态
{
if(status)           //状态已经设置则不处理
{   return;}

if(rc == NGX_OK)         //错误码是 NGX_OK
{
status = NGX_HTTP_OK;       //设置为NGX_HTTP_OK
return;
}

if(rc < NGX_OK)          //发生Nginx内部错误
{
status = NGX_HTTP_INTERNAL_SERVER_ERROR;  //设置为500
return;
}

status = rc;          //设置为子请求的 HTTP 状态码
}

9.4.3　业务逻辑类

ndg_data_hook_module是filter模块，需要利用6.5.3节的NgxFilter辅助类插入Nginx过滤链表，实现对响应数据的过滤处理。

类定义

NdgDataHookHandler类定义如下：

class NdgDataHookHandler final      //业务逻辑类
{
public:
typedef NdgDataHookHandler  this_type;   //简化类型定义
typedef NdgDataHookCtx   ctx_type;    //ctx 类型定义

typedef NdgDataHookModule   this_module;   //模块定义
typedef NgxFilter<this_type>   this_filter;   //过滤链表工具类定义
public:
static ngx_int_t init(ngx_conf_t* cf)   //postconfiguration 调用
{
this_filter::init(       //插入过滤链表
&this_type::header_filter, &this_type::body_filter);
return NGX_OK;
}
public:
...            //其他成员函数，见后
};

处理响应头

当数据流经ndg_data_hook_module模块时，它需要检查父请求里是否设置了数据回传标志hook，然后设置do_hook标志，告诉body filter要数据回传：[4]

public:
static ngx_int_t header_filter(ngx_http_request_t *r) //过滤响应头
{
auto pr = r->parent;       //获取父请求
if(!pr)               //无父请求则不需要处理
{  return this_filter::next(r); }

auto& ctx = this_module::ctx();    //准备获取模块的环境数据

if(ctx.empty(pr))           //父请求未设置 ctx，不需要处理
{   return this_filter::next(r); }

auto& pr_ctx_data = this_module::data(pr);  //得到父请求的 ctx 数据

if(!pr_ctx_data.hook)         //父请求不要求回传数据
{   return this_filter::next(r); }

this_module::data(r).do_hook = true;    //父请求要求回传数据，设置标志

pr_ctx_data.headers = &r->headers_out;      //设置指针，回传响应头

r->filter_need_in_memory = true;    //子请求的参数设置
r->header_sent = true;

return NGX_OK;              //结束过滤链表
}

header_filter()的代码比较多，因为它必须要检查父请求是否设置了hook标志。如果父请求要求hook，那么它就设置do_hook，用于之后的body_filter()。

响应头回传的工作也可以在子请求结束回调时做，但在这里做更好，因为这只是一个指针的赋值，成本几乎为0，而且可以节约回调函数里的客户代码。

处理响应体

body_filter()的处理比较简单，只须要把数据挂到父模块的body指针上就可以了：

public:
static ngx_int_t body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
if(!in)            //空数据链无须处理
{   return NGX_OK;}

if( this_module::ctx().empty(r) ||    //检查环境数据
!this_module::data(r).do_hook)     //没有标志位则不处理
{   return this_filter::next(r, in); }

auto& pr_ctx_data = this_module::data(r->parent); //获取父请求的 ctx

auto rc = ngx_chain_add_copy(    //拷贝到父请求模块的 ctx 里
r->pool, &pr_ctx_data.body, in);
NgxException::require(rc);

return NGX_OK;              //结束过滤链表
}

函数里我们调用了Nginx的ngx_chain_add_copy()函数，它并不真正拷贝数据，只是在目标链表里新建链表节点，引用原链表里的数据块，没有效率损失。

在回传之后我们必须直接返回NGX_OK，终止过滤链的执行，否则数据会走到ngx_http_postpone_filter_module，从主请求里输出，破坏我们的回传计划。

9.4.4　模块集成类

ndg_data_hook_module模块没有配置数据，所以集成类非常简单，只需设置postconfiguration函数指针初始化过滤链表：

class NdgDataHookInit final
{
public:
typedef NdgDataHookHandler  handler_type;   //简化类型定义
typedef NdgDataHookInit     this_type;
public:
static ngx_http_module_t* ctx()      //函数指针表
{
static ngx_http_module_t c =
{
NGX_MODULE_NULL(1),
&handler_type::init,        //初始化过滤链表
NGX_MODULE_NULL(6),         //没有配置数据的创建函数
};

return &c;
}
public:
static const ngx_module_t& module()
{...}             //省略模块初始化代码
};

9.4.5　编译脚本

ndg_data_hook_module模块在编译进Nginx时需要小心，因为它会中途截断过滤链表的执行，所以链表的顺序很重要，在它之后的filter模块都会失效。

模块的编译脚本config的内容是：

ngx_addon_name=ndg_data_hook_module

HTTP_FILTER_MODULES="$HTTP_FILTER_MODULES ndg_data_hook_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ModNdgDataHook.cpp"

Shell变量HTTP_FILTER_MODULES会把filter模块安排在ngx_http_headers_filter_module和ngx_http_userid_filter_module之间（参见6.3.3节），位置正好在ngx_http_postpone_filter_module之前，所以它可以正常工作。

但如果有多个第三方filter模块，那么就需要协调ndg_data_hook_module与它们之间的关系。例如，如果想让数据被7.10节开发的ndg_footer_module处理后再回传，那么就要执行下面的命令：

./configure                         \     #配置Nginx 的编译选项
--add-module=path/to/datahook   \     #hook 模块在前
--add-module=path/to/footer       #footer模块在后

这样生成的ngx_modules数组就是：

ngx_module_t *ngx_modules[] = {
...
&ngx_http_postpone_filter_module,      //Nginx数据整理模块
&ngx_http_ssi_filter_module,
&ngx_http_charset_filter_module,
&ngx_http_userid_filter_module,

&ndg_data_hook_module,        //过滤链表的后位置
&ndg_footer_module,         //过滤链表的前位置

&ngx_http_headers_filter_module,
&ngx_http_copy_filter_module,
&ngx_http_range_body_filter_module,
&ngx_http_not_modified_filter_module,     //过滤链表头节点
NULL
};

9.5　在模块里使用子请求

本节将使用前两节实现的C++工具类和数据回传模块，示范Nginx子请求的用法。

9.5.1　模块设计

示范子请求用法的模块设计如下：

- 模块名是ndg_subrequest_module，是内容处理模块；

- 模块使用content handler的方式注册处理函数；

- 配置指令ndg_subrequest_loc，确定发起子请求使用的uri，即location；

- 配置指令ndg_subrequest_args发起子请求使用的args；

- 父请求把子请求的响应头和响应体均原样输出。

9.5.2　配置信息类

NdgSubrequestConf定义模块所需的配置信息，它有两个ngx_str_t成员，存储发起子请求需要的uri和args：

class NdgSubrequestConf final
{
public:
typedef NdgSubrequestConf this_type;     //简化类型定义
public:
NdgSubrequestConf() = default;
~NdgSubrequestConf() = default;
public:
ngx_str_t loc;           //子请求uri
ngx_str_t args;          //子请求args
public:
static void* create(ngx_conf_t* cf)      //创建结构体对象
{
return NgxPool(cf).alloc<this_type>();
}

...             //其他成员函数，见后
};

NdgSubrequestConf还实现了merge()函数，为字符串提供默认值：

public:
static char* merge(ngx_conf_t *cf, void *parent, void *child)
{
auto prev = reinterpret_cast<this_type*>(parent);
auto conf = reinterpret_cast<this_type*>(child);

NgxValue::merge(conf->loc, prev->loc, ngx_string("/echo"));
NgxValue::merge(conf->args, prev->args, ngx_null_string);

return NGX_CONF_OK;
}
};

9.5.3　业务逻辑类

ndg_subrequest_module需要发起子请求，获取子请求产生的数据，然后再生成自己的数据，主要功能都在回调函数类NdgSubrequestCallback里实现。

子请求回调函数

子请求回调函数要实现的功能较为简单，只是把子请求的错误码回传给父请求，放置在ndg_data_hook_module模块的ctx里，不使用data参数：

class NdgSubrequestCallback final
{
public:
typedef NdgDataHookModule hook_module;      //简化类型定义
public:
//子请求结束时的回调函数，注意参数 r表示的是子请求对象，data 参数不使用
static ngx_int_t sub_post(ngx_http_request_t* r, void* data, ngx_int_t rc)
{
auto& pr_ctx_data = hook_module::data(r->parent); //获取父请求的模块 ctx
pr_ctx_data.state(rc);        //回传错误码

return NGX_OK;           //回调函数执行成功
}
...              //父请求回调函数见后
};

父请求回调函数

在父请求回调函数里需要从ndg_data_hook_module模块获取回传的状态码、响应头和响应体，然后和其他content handler模块一样，构造响应数据，调用send()发给客户端：

public:
//父请求结束时的回调函数，注意参数 r表示的是父请求对象
static ngx_int_t parent_post(ngx_http_request_t* r)
{
auto& ctx_data = hook_module::data(r);     //获取模块的 ctx

if(ctx_data.status != NGX_HTTP_OK)      //检查子请求的处理状态
{ return NGX_HTTP_INTERNAL_SERVER_ERROR;}

ngx_str_t msg = ngx_string("subrequest");    //要输出的一个字符串

NgxChain chain = ctx_data.body;       //子请求的响应数据
auto len = msg.len + chain.size();      //计算输出总长度

NgxResponse resp(r);         //准备发送响应数据

resp.length(len);          //设置内容长度

NgxHeadersOut h(ctx_data.headers);      //子请求的头信息

h.list().visit(           //遍历子请求的头信息
[&](ngx_table_elt_t& x)         //lambda 表达式
{
resp.headers().add(x);        //添加到输出响应头
});

for(auto& ch : chain)          //遍历子请求数据
{
if(ch.data().special())       //不发送控制信息
{   continue; }

if(ch.data().last())         //去掉last 信息
{   ch.data().finish(false);}

resp.send(ch.data());         //发送子请求数据
}

resp.send(&msg);           //发送本请求的信息
resp.eof();            //响应数据结束

return NGX_OK;            //处理成功，返回NGX_OK
}

代码里要注意的是对子请求数据的处理，首先要检查子请求的处理状态。如果不是NGX_HTTP_OK就意味着子请求处理发生了错误，之后可以从ctx的headers和body成员分别得到子请求返回的头信息和数据。

由于ndg_data_hook_module模块把子请求的数据原封不动地“钩”到了父请求里，所以数据链里可能会有sync、flush、eof等用于控制的特殊缓冲区，必须要对它们做特殊处理，否则可能会影响父请求的正常输出。

回调函数最后需要返回NGX_OK或NGX_HTTP_INTERNAL_SERVER_ERROR等错误码，它会传递给ngx_http_finalize_request()，最终结束整个请求。

发起子请求

完成回调函数类后，我们用NgxSubRequest把它组装起来，在handler里发起子请求：

class NdgSubrequestHandler final
{
public:
typedef NdgSubrequestHandler   this_type;     //简化类型定义
typedef NdgSubrequestCallback  callback_type;
typedef NdgDataHookModule      hook_module;
typedef NdgSubrequestModule    this_module;

typedef NgxSubRequest<callback_type>  this_subrequest;
public:
static ngx_int_t handler(ngx_http_request_t *r)
{
NgxRequest req(r);

if(!req.method(NGX_HTTP_GET))        //检查请求方法
{   return NGX_HTTP_NOT_ALLOWED; }

auto& cf = this_module::instance().conf().loc(r);  //获取配置

this_subrequest(r).create(cf.loc, cf.args);   //使用配置创建子请求

hook_module::data(r).hooking();      //要求执行数据回传

return NGX_DONE;           //返回NGX_DONE
}
};

NdgSubrequestHandler首先要做的就是用回调函数类实例化NgxSubRequest，组装好调用子请求必须的两个回调函数，然后使用create()创建子请求（这时子请求还没有开始执行）。要想把子请求的数据回传到父请求，就需要调用ndg_data_hook_module模块的hooking()函数，设置回传标志位。

函数结束时必须返回NGX_DONE，暂停请求的处理，等待子请求结束时被“激活”，然后在回调函数parent_post()里执行后续的操作。

9.5.4　测试验证

ndg_subrequest_module的模块集成和编译与之前开发的模块基本相同，这里不再列出详细的实现代码，直接进入最后的测试验证环节。

在Nginx的配置文件里定义子请求访问的location：

location /hello {        #子请求访问的 location
ndg_echo "hello";               #输出字符串“hello”

ndg_header x-name chrono;       #加入自定义头信息
ndg_footer "\n";                #响应数据末尾添加换行
}

然后再定义发起子请求的location：

location /sub {          #发起子请求的 location
ndg_subrequest_loc "/hello";     #访问"/hello"
ndg_subrequest_args "chrono";    #添加子请求参数

ndg_footer "\n";        #响应数据末尾添加换行
}

使用curl访问/sub，模块会使用子请求获取/hello的数据，整合后输出：

curl  http://localhost/sub -v     #执行curl 命令

> GET /sub HTTP/1.1         #请求头，访问/sub
>
< HTTP/1.1 200 OK        #请求处理成功
< Server: nginx/1.8.0
< Content-Length: 24
< x-name: chrono         #子请求里的头信息
<
chrono,hello          #子请求的响应数据
subrequest          #父请求的响应数据

9.6　总结

本章介绍了Nginx里独特的子请求机制，能够把服务器里的location变成一个个的函数调用，使用子请求可以组合这些location，完成复杂的业务逻辑，让Nginx超越简单的Web服务器成为应用服务器。

沿用之前的Nginx宫殿比喻，子请求就像是宫殿里穿梭不息的仆人，连接起了各个房间（模块），他们在这些房间里搜集原料，最后生产出精美的工艺品（响应数据）。

子请求的设计思想是“分而治之”，很像是解决数学难题，把主问题分解为若干个易于解决的小问题，每个小问题都解决后整个问题也就解决了。任何复杂的业务都是如此，总是可以分解出一些粒度略小的任务，这些任务可以用一个子请求来表示。分解操作可以递归进行，直至每个任务都能够用一个简单的HTTP请求完成。最后我们整理子请求的处理结果，进行分类汇总等加工操作，就完成了整个业务。

子请求本质上与普通请求没有区别，只是它由Nginx在内部发起，省略了TCP连接和请求数据解析的步骤，而且可以并发多个，因而比客户端直接发起请求的成本要低，运行效率更高。Nginx创建子请求默认是GET方法，大部分参数也都拷贝自父请求，但用户可以任意修改子请求的method/uri/headers/body等参数，定制程度非常高。

与创建子请求和调度子请求的设计相比，Nginx对子请求数据的处理显得有些“草率”。ngx_http_postpone_filter_module模块虽然能够把子请求的数据传回父请求，但没有提供回调函数，我们无法对数据做额外的处理，所以实用意义不是很大。

但ngx_http_postpone_filter_module使用子请求数据的思路是值得借鉴的，所以本章开发了数据回传模块ndg_data_hook_module，能够把子请求的响应数据全部“钩”到父请求里，这样父请求就可以像函数调用一样随意地使用配置文件里的location，非常灵活易用。

作为示范，ndg_data_hook_module只实现了比较简单的数据回传功能，一次只能处理一个子请求的数据，通常情况下足够用。读者也可以进行改进，使用动态数组存储多个子请求的响应数据，充分利用Nginx强大的并发处理能力。

[1] 如果要让子请求记录访问日志需要使用配置指令log_subrequest on。

[2] 与第8章负载均衡模块里的round robin算法情形比较相似。

[3] 另一个取值是NGX_HTTP_SUBREQUEST_WAITED，但目前在Nginx里并没有太大用处。

[4] 由于在本模块里需要频繁使用ctx数据，所以本书定义了一个特别的宏NGX_MOD_CTX_INSTANCE，它简化了获取ctx的操作，无须使用模板参数指定类型，读者可参见GitHub资源。

第10章　Nginx变量

本书第1章曾简单地介绍了Nginx的变量，它与配置文件里固定的参数不同，值是可变的。用户可以在Nginx配置文件里以“$var”的形式获取Nginx预定义的各种信息，例如$uri、$args、$remote_addr，也可以使用set和map指令来定义新的变量，善用变量能够极大地增加Nginx配置的灵活性，让配置文件更像是一种小型语言。[1]

Nginx变量表面上与编程语言的变量很相似，但实际上它只是Nginx内部存储的一些字符串，是模块对外导出的一段数据。变量不仅可以用于配置文件，还可以用在C/C++代码里，用作模块间的简单通信，或者部分替代配置指令的功能来设置参数。

目前Nginx的变量只用于HTTP处理，只在http块内生效，而且很多变量与具体的请求相关——这是显而易见的，因为如URI、IP地址、响应时间等每个请求都不相同。

10.1　结构定义

Nginx定义了两个结构体来实现变量机制：变量值ngx_variable_value_t和变量访问对象ngx_http_variable_t。

10.1.1　变量值

ngx_variable_value_t与ngx_str_t类似，表示一个内存里的字符串空间，但它增加了一些属性成员：

//定义在core/ngx_string.h
typedef struct {
unsigned    len:28;         //变量字符串长度

unsigned    valid:1;        //变量是否有效
unsigned    no_cacheable:1;      //变量值是否允许缓存
unsigned    not_found:1;       //变量是否存在
unsigned    escape:1;       //变量是否被 escape 处理

u_char*     data;        //变量的内存地址
} ngx_variable_value_t;

ngx_variable_value_t只是一个简单的字符串值，并不与变量发生直接关系。它的结构比较简单，可以认为是ngx_str_t的一个变体，成员len只使用了28位来表示长度，余下的4位作为标志位，表示变量的几种状态。

HTTP框架里又对ngx_variable_value_t做了重定义：

//定义在http/ngx_http_variables.h
typedef ngx_variable_value_t  ngx_http_variable_value_t;

10.1.2　变量访问对象

Nginx在ngx_variable_value_t之上定义了ngx_http_variable_t结构体，为变量值的读写增加了一个间接层，它表示真正的Nginx变量，使用get/set函数而不是简单的字符串来访问变量值，增加了处理的灵活性。

ngx_http_variable_t定义如下：

//定义在http/ngx_http_variables.h
typedef struct ngx_http_variable_s  ngx_http_variable_t;

struct ngx_http_variable_s {
ngx_str_t                 name;       //变量名
ngx_http_set_variable_pt  set_handler;    //设置变量值函数
ngx_http_get_variable_pt  get_handler;    //获取变量值函数
uintptr_t                   data;      //set/get 函数使用的辅助参数
ngx_uint_t                  flags;     //变量属性标志位
ngx_uint_t                  index;     //变量所在的数组序号
};

ngx_http_variable_t结构体里的关键成员是get_handler和set_handler，这两个函数会根据ngx_http_request_t和data来计算变量的值。如果Nginx内部已经有值（例如$uri、$args），那么就填充ngx_http_variable_value_t的len/data直接输出，否则就需要在内存池里分配内存，拼接好字符串再输出。所以，通过Nginx变量来获取信息是有一定计算成本的。

get/set函数的声明是：

typedef void (*ngx_http_set_variable_pt) (ngx_http_request_t *r,
ngx_http_variable_value_t *v, uintptr_t data);
typedef ngx_int_t (*ngx_http_get_variable_pt) (ngx_http_request_t *r,
ngx_http_variable_value_t *v, uintptr_t data);

结构体里的flags成员可以取值为下面的宏，设置变量的属性：

#define NGX_HTTP_VAR_CHANGEABLE     1    //是否可以修改
#define NGX_HTTP_VAR_NOCACHEABLE    2    //是否允许缓存
#define NGX_HTTP_VAR_INDEXED        4    //是否索引
#define NGX_HTTP_VAR_NOHASH         8    //是否允许 hash

10.1.3　变量的存储

Nginx把变量对象统一存储在ngx_http_core_module配置数据结构的variables成员里：

typedef struct {
...
ngx_array_t                variables;         //存储所有的变量对象
ngx_hash_keys_arrays_t*    variables_keys;   //存储hash key
...
} ngx_http_core_main_conf_t;

variables是一个动态数组，它的元素是ngx_http_variable_t，在配置解析结束时Nginx会调用ngx_http_variables_init_vars()函数，把所有模块定义的变量集中存储在这里。所以，变量的定义是跨server和location生效的。

10.1.4　请求结构体

请求结构体ngx_http_request_t为变量值提供了独立的存放空间，因此每个请求获取的变量值都是独立的：

struct ngx_http_request_s {
...
ngx_http_variable_value_t*  variables;   //存放所有可能的变量值
...
};

在函数ngx_http_create_request()创建请求对象时，会依据cmcf->variables数组为变量值分配内存：

ngx_http_request_t *
ngx_http_create_request(ngx_connection_t *c)   //创建请求
{
...
r->variables = ngx_pcalloc(r->pool,     //数组长度与 cmcf里的相同
cmcf->variables.nelts*sizeof(ngx_http_variable_value_t));
...
};

可以看到，请求里的变量值数组variables与配置里的变量对象数组variables是完全对应的。

但为了节约内存，r->variables并不会主动存放变量值，只有当用户在实际访问变量时才会调用变量的get_handler函数，取出变量值并放入数组。

需要注意的是，对于子请求来说，变量值数组是与父请求共享的，而不是独立的，在创建子请求的函数ngx_http_subrequest()里代码是：

sr->variables = r->variables;       //直接引用父请求的变量数组指针

这既是优点也是缺点，优点是节约内存，不会因为创建大量子请求而增加内存消耗，缺点则是某个子请求可能修改了变量而父请求并不知情，导致预料之外的错误。

如果想要父请求和子请求的变量之间互不影响，就需要在子请求创建后修改它自己的variables成员，创建一个新数组，然后拷贝父请求的数组内容，制作变量的副本。

10.2　运行机制

Nginx的每个http模块都可以在配置解析时向框架注册变量，导出模块专有的信息，变量统一存储在ngx_http_core_module的cmcf->variables动态数组里。

在HTTP请求处理阶段，我们可以从中获取变量对象，使用它的get_handler函数计算得到变量值。

10.2.1　注册变量

函数ngx_http_add_variable()是Nginx变量机制的核心函数，它创建一个命名的变量访问对象ngx_http_variable_t。

ngx_http_add_variable()的声明是：

ngx_http_variable_t *ngx_http_add_variable(ngx_conf_t *cf,
ngx_str_t *name, ngx_uint_t flags);

http模块通常使用一个静态数组存放变量定义，在配置解析前的preconfiguration时调用ngx_http_add_variable()，把它们添加进Nginx内部。例如ngx_http_upstream_module里的代码是：

//代码在http/ngx_http_upstream.c 里
static ngx_http_variable_t  ngx_http_upstream_vars[] = {
{ ngx_string("upstream_addr"), NULL,     //变量$upstream_addr
ngx_http_upstream_addr_variable, 0,    //变量的get 函数
NGX_HTTP_VAR_NOCACHEABLE, 0 },      //访问标志位

...             //其他变量定义

{ ngx_null_string, NULL, NULL, 0, 0, 0 }    //空变量对象表示数组结束
};

for (v = ngx_http_upstream_vars; v->name.len; v++) {  //遍历数组
var = ngx_http_add_variable(cf, &v->name, v->flags); //添加变量

var->get_handler = v->get_handler;       //设置get函数
var->data = v->data;
}

http块解析时Nginx会调用所有模块的preconfiguration函数注册变量，然后执行ngx_http_variables_init_vars()函数，把变量存入cmcf->variables。

10.2.2　获取变量

函数ngx_http_get_variable()可以访问Nginx变量值，它使用变量名和hash key在ngx_http_core_module里查找已经添加的变量，再调用get_handler获取变量值。[2]

ngx_http_get_variable()的声明是：

ngx_http_variable_value_t* ngx_http_get_variable(ngx_http_request_t *r,
ngx_str_t *name, ngx_uint_t key);

如果变量是可以修改的（即NGX_HTTP_VAR_CHANGEABLE，例如set指令定义的变量），那么可以直接修改ngx_http_variable_value_t里的len和data成员，无须调用set_handler就能够修改变量值。

10.3　C++封装

Nginx变量的内部实现比较复杂，但接口却比较简单，因此C++封装类的代码也就很容易理解。

10.3.1　NgxVariableValue

类NgxVariableValue代理了ngx_http_variable_value_t，提供对变量值的基本操作，定义如下：

class NgxVariableValue final : public NgxWrapper<ngx_variable_value_t>
{
public:
typedef NgxWrapper<ngx_variable_value_t>  super_type;
typedef NgxVariableValue       this_type;
public:
NgxVariableValue(ngx_variable_value_t* v): super_type(v) {}
~NgxVariableValue() = default;
public:
...              //成员函数见后
};

读取变量值

调用ngx_http_get_variable()得到的变量值不一定是有效的，所以需要进行判断：

public:
bool valid() const          //变量值是否有效
{
return get() && get()->valid && !get()->not_found; //检查标志位
}

使用成员len和data就可以获得变量值，它们可以转换为通用的ngx_str_t：

public:
ngx_str_t str() const          //获取变量值字符串
{
return valid()?           //是否有效
ngx_str_t{get()->len, get()->data}:    //返回字符串
ngx_str_t ngx_null_string;       //无效返回空字符串
}

修改变量值

操作ngx_http_variable_value_t里的成员就可以修改变量值，但同时还要设置valid、not_found等标志位，否则即使修改字符串也是无效的：

public:
void set(ngx_str_t& str)          //修改变量值
{
get()->len = str.len;          //设置字符串长度
get()->data = str.data;         //设置字符串内容

get()->valid = true;         //变量有效
get()->not_found = false;         //可以找到
get()->no_cacheable = false;       //可以缓存
}

void clear()           //清空变量
{
get()->len = 0;            //字符串清空
get()->data = nullptr;

get()->valid = false;          //变量无效
get()->not_found = true;        //变量不能被找到
}

10.3.2　NgxVariable

类NgxVariable简单地封装了ngx_http_variable_t的设置读写函数操作：

class NgxVariable final : public NgxWrapper<ngx_http_variable_t>
{
public:
typedef NgxWrapper<ngx_http_variable_t>   super_type;
typedef NgxVariable        this_type;
public:
NgxVariable(ngx_http_variable_t* v): super_type(v){}
~NgxVariable() = default;
public:
template<typename T>
void handler(ngx_http_set_variable_pt p, T data = nullptr) const
{
get()->set_handler = p;
get()->data = reinterpret_cast<uintptr_t>(data);
}

template<typename T>
void handler(ngx_http_get_variable_pt p, T data = nullptr) const
{
get()->get_handler = p;
get()->data = reinterpret_cast<uintptr_t>(data);
}
};

10.3.3　NgxVariables

类NgxVariables负责模块变量的初始化，它模仿了Nginx的惯用手法，使用模板参数里提供的变量数组简化了模块的变量添加工作：

template<ngx_http_variable_t*(*get_vars)()>   //模板参数是函数指针
class NgxVariables final        //用于添加变量
{
public:
typedef NgxVariables     this_type;     //简化类型定义
typedef NgxVariable    var_type;
public:
static ngx_int_t init(ngx_conf_t *cf)
{
if(!get_vars)          //不能是空指针
{  return NGX_OK; }

for (auto v = get_vars(); v->name.len; ++v)  //遍历数组
{
var_type var = ngx_http_add_variable(cf, &v->name, v->flags);

NgxException::fail(!var);       //不能是空指针

var.handler(v->get_handler, v->data);   //添加get函数
}

return NGX_OK;
}
};

NgxVariables的模板参数get_vars是一个函数指针，我们可以把变量数组的定义和get函数都封装在一个类里，再用静态成员函数实例化NgxVariables，最后在preconfiguration时调用init()即可。

10.3.4　NgxVarManager

访问Nginx变量必须要结合具体的HTTP请求，类NgxVarManager保存了ngx_http_request_t指针，集中管理所有的变量，提供便捷的读写操作。

类定义

NgxVarManager内部保存了一个请求对象m_r，调用ngx_http_get_variable()直接获取变量值：

class NgxVarManager final
{
public:
typedef NgxVarManager       this_type;           //简化类型定义
typedef boost::string_ref   string_ref_type;
public:
NgxVarManager(ngx_http_request_t* r): m_r(r) {}   //构造获得请求对象
~NgxVarManager() = default;
private:
ngx_http_request_t* m_r = nullptr;                //请求结构体指针
private:
ngx_http_variable_value_t* var(string_ref_type name) const
{
auto s = NgxPool(m_r).dup(name);      //拷贝一份字符串
auto key = ngx_hash_strlow(s.data, s.data, s.len);  //计算hash key

return ngx_http_get_variable(m_r, &s, key);   //获取变量值
}
...              //其他成员函数见后
};

注意在调用ngx_http_get_variable()前我们使用了ngx_hash_strlow()函数，计算key的同时把字符串转化为小写的形式。

读写变量值

成员函数var()得到了变量值对象，可以对变量值进行读写操作：

public:
ngx_str_t get(string_ref_type name) const    //读变量值
{
auto v = var(name);         //获取变量值对象
return NgxVariableValue(v).str();
}

void set(string_ref_type name,       //修改变量值
string_ref_type value) const
{
auto s = NgxPool(m_r).dup(value);      //内存池复制字符串

auto v = var(name);         //获取变量值对象
NgxVariableValue(v).set(s);
}

void clear(string_ref_type name) const    //清空变量值
{
auto v = var(name);         //获取变量值对象
NgxVariableValue(v).clear();
}
};

10.3.5　NgxVariableValueProxy

NgxVarManager的成员函数get()/set()/clear()可以比较方便地操作Nginx变量，但如果能够用类似字典的vars["name"]方式来使用无疑会更加方便。

operator[]的语义是左值，所以我们要实现一个代理类NgxVariableValueProxy，接管对变量值的操作。

类定义

类NgxVariableValueProxy定义如下：[3]

class NgxVariableValueProxy final
{
public:
typedef NgxVariableValueProxy   this_type;
typedef boost::string_ref    string_ref_type;
public:
NgxVariableValueProxy(ngx_variable_value_t* v, ngx_http_request_t* r):
m_v(v), m_pool(r)          //成员初始化
{}
~NgxVariableValueProxy() = default;
private:
NgxVariableValue   m_v;         //被代理的变量值对象
NgxPool    m_pool;       //内存池对象
public:
operator ngx_str_t() const       //转化为ngx_str_t 类型
{
return m_v.str();
}

void operator=(string_ref_type value)    //重载赋值操作符
{
if(!m_v.valid())        //检查变量是否有效
{    return; }

if(value.empty())         //空字符串则清空变量
{
m_v.clear();        //清空变量
return;
}

auto s = m_pool.dup(value);       //内存池复制字符串
m_v.set(s);          //修改变量值
}
};

类NgxVariableValueProxy有两个成员变量，m_v是被代理的变量值对象，m_pool是内存池，用于在修改变量值时制作字符串的备份。它重载了两个操作符，转型操作符相当于get()，operator=相当于set()和clear()。

重载operator[]

NgxVarManager重载operator[]，返回代理对象NgxVariableValueProxy：

public:
NgxVariableValueProxy operator[](string_ref_type name) const
{
auto v = var(name);                     //获取变量值对象
return NgxVariableValueProxy(v, m_r);   //返回代理对象
}

10.4　在模块里使用变量

NgxVariables和NgxVarManager这两个类封装了Nginx变量相关的各种操作，利用它们就能够在模块里处理Nginx变量。

10.4.1　添加变量

作为示例，我们在Nginx模块里获取连接的RTT（Round-Trip Time）参数，导出为$rtt变量。

类NdgVariablesHandler封装了变量数组和get函数：

class NdgVariablesHandler final
{
public:
typedef NdgVariablesHandler this_type;
public:
static ngx_http_variable_t* get_vars()      //变量定义数组
{
static ngx_http_variable_t  vars[] = {    //数组里定义了一个变量

{ ngx_string("rtt"), nullptr,         //变量名字是 rtt
&this_type::get_rtt, 0,       //get 函数
NGX_HTTP_VAR_NOCACHEABLE, 0 },      //不允许修改

{ ngx_null_string, nullptr, nullptr, 0, 0, 0 }
};

return vars;           //返回变量数组
}
public:
static ngx_int_t get_rtt(ngx_http_request_t *r,  //get 函数
ngx_http_variable_value_t *v, uintptr_t data)
{
auto sockfd = r->connection->fd;     //获取socket描述符

tcp_info info;           //调用getsockopt 获取信息
socklen_t len = sizeof(info);       //rtt 的单位是微秒

auto rc = getsockopt(sockfd, SOL_TCP, TCP_INFO, &info, &len);
assert(!rc);

auto buf = NgxPool(r).nalloc<u_char>(10);    //分配字符串存储空间
ngx_str_t str{10, buf};

NgxString(str).printf("%d", info.tcpi_rtt / 1000); //格式化，转换为毫秒

NgxVariableValue(v).set(str);       //设置变量值

return NGX_OK;
}
};

在模块的ngx_http_module_t里设置函数指针preconfiguration为NgxVariables::init()，即可完成变量的添加工作：

//实例化变量初始化类
typedef NgxVariables<&handler_type::get_vars>  these_variables;

static ngx_http_module_t* ctx()
{
static ngx_http_module_t c =      //配置阶段的函数指针表
{
&these_variables::init,       //preconfiguration
NGX_MODULE_NULL(7)         //其余七个是空指针
};

return &c;
}

10.4.2　读写变量

NgxVarManager集中管理了Nginx的所有变量，只要用请求对象构造后就可以使用operator[]访问任意变量，很像std::map，示范代码如下：

NgxVarManager var(r);          //构造NgxVarManager 对象

cout << var["request_method"]<< endl;      //访问$request_method
cout << var["var1"]<< endl;        //访问变量$var1

var["var1"] = "1234567";        //修改变量$var1 的值
cout << var["var1"]<< endl;        //输出变量$var1 的值

NgxVarManager能够非常方便地读写变量，所以模块可以使用指令set来定义变量，配置所需的各种参数，部分地取代配置指令的作用，不需要再额外编写解析指令的代码。

不过变量的读写有少量的计算和内存使用成本，需要对此进行评估。如果能够直接从请求结构体里获取就最好直接访问（例如$request_method可以用r->method），尽量不要频繁使用变量导致影响性能。

10.5　总结

本章介绍了Nginx的变量机制，这是Nginx区别于其他Web服务器的一大特色。

如果把模块比作Nginx宫殿里的房间，那么配置指令就是房间的门窗，可以高效地设置模块的各种参数，但指令创建的成本比较高，而且沟通的方向是单向的。变量是另一种模块与外界通信的方式，它像是房间的水管和电线，可以在房间的任何地方布设任意数量，以很随意的方式与外部沟通交流，输入输出多种信息，模块之间也可以使用变量互相交换信息，用法非常灵活。

如果不考虑脚本引擎，Nginx变量机制还是比较简单的。核心是两个结构体：ngx_variable_value_t和ngx_http_variable_t。前者表示变量值，是个简单的字符串；后者表示变量访问对象，使用get/set函数获取变量值。

Nginx在ngx_http_core_module里存储所有的变量对象，而在每个请求里存放变量值，这种做法分离了变量和变量的表示，能够依据具体的请求来计算相应的变量值。模块要在配置解析阶段使用函数ngx_http_add_variable()注册变量，然后在处理HTTP请求时调用函数ngx_http_get_variable()来获取变量值。[4]

本章实现了几个变量的C++封装类，如NgxVariables和NgxVarManager，可以很容易地完成变量的注册和读写工作。

[1] 在变量机制之外Nginx还实现了脚本（script）引擎，可以让模块处理复杂的变量组合。由于它涉及的内容较多，本书暂不讨论，感兴趣的读者可以阅读Nginx源码深入研究。

[2] ngx_http_get_variable()对“http_”、“sent_http_”等变量前缀有特殊处理。

[3] NgxVariableValueProxy还重载了operator!和operator<<，这里未列出，读者可参考GitHub资源。

[4] Nginx里还提供ngx_http_get_variable_index()和ngx_http_get_indexed_variable()等函数访问变量值，但使用上不如ngx_http_get_variable()方便，故本书未介绍。

第11章　Nginx辅助设施

本章将介绍一些Web开发的辅助技术，包括MD5/SHA1摘要、URI编解码、正则表达式、共享内存、异步定时器等，它们是开发Web服务器不可或缺的实用工具。

有很多第三方库可以实现这些功能，但Nginx非常体贴地在核心框架之外集成了它们的实现或封装，让我们不必费心费力地再去网络上搜索。当然，如果对Nginx内置的实现不满意，我们还可以改用其他的实现，例如C++标准库或者Boost程序库。

11.1　摘要算法

摘要算法（digest，又称为散列算法、哈希算法），是一种单向不可逆函数，能够把任意长度的数据“压缩”为一个固定大小的“摘要”字符串，而这个“摘要”是唯一的，可以看作是原数据的“指纹”。[1]

摘要算法被广泛应用于身份鉴定、密码学、网络金融等领域，在Web服务器里则通常用于数据标识、完整性校验和防篡改。

Nginx提供了当前较为流行的三种摘要算法：MD5、SHA-1和MurmurHash2。

11.1.1　MD5

MD5（Message Digest）应该是在程序员群体里最广为人知的摘要算法，它能够生成128位（16字节）的摘要，运行速度快，性能稳定，适用于各种要求不高的场合。

Nginx在头文件<ngx_md5.h>里提供对MD5算法的支持。注意：它不包含在<ngx_core.h>里，我们必须单独包含它。

接口函数

MD5算法使用的结构体和函数如下：

typedef ...  ngx_md5_t;

void ngx_md5_init(ngx_md5_t *ctx);
void ngx_md5_update(ngx_md5_t *ctx, const void *data, size_t size);
void ngx_md5_final(u_char result[16], ngx_md5_t *ctx);

用法

计算MD5摘要值需要先定义一个ngx_md5_t变量，用函数ngx_md5_init()初始化，然后调用ngx_md5_update()输入待摘要的数据，最后从ngx_md5_final()里得到16字节的摘要结果。

示范MD5用法的代码如下：

ngx_md5_t    md5;    //计算MD5所需的结构体
u_char    buf[16];     //MD5 值输出，长度是 16字节

ngx_md5_init(&md5);       //初始化结构体
ngx_md5_update(&md5, "metroid", 7);  //输入数据
ngx_md5_final(buf, &md5);      //输出MD5值

11.1.2　SHA-1

SHA-1（Secure Hash Algorithm）是另一种摘要算法，它生成160位（20字节）的摘要，安全性要好于MD5算法。

Nginx在头文件<ngx_sha1.h>里提供对SHA-1算法的支持，同样的，我们必须单独包含它。

接口函数

Nginx的SHA-1算法实现只是简单复用了已有的结构体和函数，它们的实际形式与MD5算法基本相同：

typedef SHA_CTX     ngx_sha1_t;

#define ngx_sha1_init     SHA1_Init
#define ngx_sha1_update   SHA1_Update
#define ngx_sha1_final    SHA1_Final

用法

计算SHA-1摘要值的步骤也与MD5相同，但最后的摘要结果是20字节，示范代码如下：

ngx_sha1_t    sha;   //计算SHA1 所需的结构体
u_char     buf[20];  //SHA1 值输出，长度是 20字节

ngx_sha1_init(&sha);      //初始化结构体
ngx_sha1_update(&sha, "prime", 5);   //输入数据
ngx_sha1_final(buf, &sha);     //输出SHA1 值

11.1.3　MurmurHash

MurmurHash是摘要算法家族里的新成员（发明于2008年），对文本字符串有很好的效果，具有更好的随机性。

Nginx在头文件<ngx_murmurhash.h>里提供了MurmurHash2算法，接口非常简单：

uint32_t ngx_murmur_hash2(u_char *data, size_t len);

MurmurHash算法与MD5、SHA1不同，计算不需要额外的结构体，直接调用ngx_murmur_hash2()函数就可以得到一个32位（4字节）的数字摘要值。

11.1.4　C++封装

MD5、SHA-1和MurmurHash这三种摘要算法的接口都比较简单，获取摘要值的操作也很流程化，但C++封装仍然可以得到更易用的接口。

由于MD5和SHA-1的使用方法类似，所以我们可以利用模板元编程，把结构体和函数作为元数据传递给类，执行摘要流程。而对于MurmurHash，可以利用模板特化技术，特化出一个专门的类。

摘要算法元数据

元数据类集中定义了三种摘要算法的结构体和操作函数，它们都是编译期的常量：

struct NgxMd5Meta         //MD5 算法元数据
{
typedef ngx_md5_t ctx_type;     //计算MD5所需的结构体

static constexpr int len = 16;     //MD5 摘要的长度

static constexpr auto init_func   = &ngx_md5_init;
static constexpr auto update_func   = &ngx_md5_update;
static constexpr auto final_func  = &ngx_md5_final;
};

struct NgxSha1Meta        //SHA1 算法元数据
{
typedef ngx_sha1_t ctx_type;    //计算SHA1 所需的结构体

static constexpr int len = 20;     //SHA1 摘要的长度

static constexpr auto init_func   = &ngx_sha1_init;
static constexpr auto update_func   = &ngx_sha1_update;
static constexpr auto final_func  = &ngx_sha1_final;
};

struct NgxMurmurHash2Meta     //MurmurHash2 算法元数据
{
static constexpr auto update_func = &ngx_murmur_hash2;
};

摘要算法类

摘要算法类NgxDigest从模板参数DigestMeta里得到所需的结构体类型和函数指针，计算MD5和SHA-1摘要，流程是完全相同的：

template<typename DigestMeta>       //模板参数是摘要算法元数据
class NgxDigest final
{
public:
typedef DigestMeta meta;         //简化类型定义
public:
NgxDigest()
{
init();           //构造函数里初始化结构体
}
~NgxDigest() = default;
public:
void init()           //初始化
{
meta::init_func(&m_ctx);        //调用元函数里的函数指针
}

void update(const void* data, std::size_t len)   //计算摘要值
{
meta::update_func(&m_ctx, data, len);   //调用元函数里的函数指针
}

const u_char* final()         //结束摘要
{
meta::final_func(m_buf, &m_ctx);      //调用元函数里的函数指针

return m_buf;           //返回摘要值
}
private:
typename meta::ctx_type m_ctx;      //计算摘要所需的结构体

u_char m_buf[meta::len];         //摘要值
u_char m_hex[meta::len*2];       //摘要值的字符串表示
};

使用Nginx的ngx_hex_dump()函数，可以把二进制数组转换为十六进制的字符串数组（即Hex编码）：

ngx_str_t str()         //返回摘要的字符串表示
{
ngx_hex_dump(m_hex, m_buf, sizeof(m_buf));   //hex 编码

return ngx_str_t{sizeof(m_hex), m_hex};   //返回Nginx字符串
}

我们还可以重载操作符operator()，把NgxDigest变成一个函数对象，一次性完成摘要值的计算工作：

public:
ngx_str_t operator()(const void* data, std::size_t len)
{
init();           //初始化
update(data, len);         //计算摘要值
final();          //结束摘要

return str();          //返回摘要的字符串表示
}

特化摘要算法类

MurmurHash算法的执行流程与MD5和SHA-1不同，所以我们使用C++的模板特化技术，生成一个特别版的NgxDigest：

template<>           //模板特化
class NgxDigest<NgxMurmurHash2Meta> final
{
public:
typedef NgxMurmurHash2Meta meta;      //简化类型定义
public:
NgxDigest() = default;
~NgxDigest() = default;
public:
uint32_t operator()(const void* data, std::size_t len)
{
return meta::update_func((u_char*)data, len); //调用元函数里的函数指针
}
};

用法

使用typedf可以简化模板类NgxDigest，给出易用的名字：

typedef NgxDigest<NgxMd5Meta>     NgxMd5;
typedef NgxDigest<NgxSha1Meta>    NgxSha1;
typedef NgxDigest<NgxMurmurHash2Meta>   NgxMurmurHash2;

这些摘要类可以像函数一样直接调用：

NgxMd5     md5;
NgxSha1     sha1;
NgxMurmurHash2   murmur2;

cout << md5("abcd",4) <<endl;
cout << sha1("abcd",4) <<endl;
cout << murmur2("abcd",4) << endl;

11.2　编码和解码

HTTP协议是一种文本协议，为了支持特殊字符和二进制数据，在HTTP处理过程中会有各种编码和解码操作，Nginx对此提供了较为完善的支持。

11.2.1　CRC校验

CRC（Cyclic Redundancy Code）从一段数据产生出校验和，用于检验数据的完整性[2]，历史相当悠久。CRC有多个版本，Nginx实现了CRC16和CRC32，本书只介绍CRC32。

Nginx在头文件ngx_crc32.h里定义了两个计算CRC32的函数，声明是：

uint32_t ngx_crc32_short(u_char *p, size_t len);
uint32_t ngx_crc32_long(u_char *p, size_t len);

这两个函数的计算结果是相同的，区别仅在于内部实现：ngx_crc32_short()使用了较小的查找表，处理短字符串（30~60）速度较快，而ngx_crc32_long()在处理长字符串时更有优势。

Nginx也可以分段计算CRC32，相当于分解执行了ngx_crc32_long()函数：

#define   ngx_crc32_init(crc)
void      ngx_crc32_update(uint32_t *crc, u_char *p, size_t len);
#define   ngx_crc32_final(crc)

很多读者应该都已经熟知CRC算法，而且CRC的计算也比较简单，所以这里不再列出示例代码。

11.2.2　Base64编码解码

Base64与Hex编码类似，是一种把二进制数据转换为可见字符的方法。它使用64个字符[3]，编码后的数据长度大约是原长度的4/3。

标准的Base64编码使用了字符'/'和'+'，而这两个字符会被URI编码为%2F和%2B的形式，所以Nginx也提供支持URI的Base64编码，字符改用'-'和'_'。

Nginx里的Base64编码解码函数声明如下：

//计算编码或解码后的数据长度
#define    ngx_base64_encoded_length(len)  (((len + 2) / 3) * 4)
#define    ngx_base64_decoded_length(len)  (((len + 3) / 4) * 3)

//标准Base64编解码
void       ngx_encode_base64(ngx_str_t *dst, ngx_str_t *src);
ngx_int_t  ngx_decode_base64(ngx_str_t *dst, ngx_str_t *src);

//适用于URL 的 Base64 编解码
void       ngx_encode_base64url(ngx_str_t *dst, ngx_str_t *src);
ngx_int_t  ngx_decode_base64url(ngx_str_t *dst, ngx_str_t *src);

这两组函数除编码字符集的区别外还有一处不同，ngx_encode_base64()使用'='填补结果字符串，使长度为4的整数倍，而ngx_encode_base64url()则不会。

示范Base64编码的代码如下：

ngx_str_t src = ngx_string("mario");    //源字符串
ngx_str_t dst;            //目标字符串

dst.len = ngx_base64_encoded_length(src.len);   //计算目标长度
dst.data = NgxPool(r).nalloc<u_char>(dst.len);  //分配内存

ngx_encode_base64(&dst, &src);       //标准Base64编码
cout << dst << endl;         //输出bWFyaW8=
ngx_encode_base64url(&dst, &src);       //URL Base64 编码
cout << dst << endl;         //输出bWFyaW8

11.2.3　URI编码解码

URI只允许使用有限的字符集，如果含有特殊字符或者中文则必须要编码，Nginx为此提供了两个函数：

uintptr_t  ngx_escape_uri(u_char *dst, u_char *src, size_t size,
ngx_uint_t type);
void    ngx_unescape_uri(u_char **dst, u_char **src, size_t size,
ngx_uint_t type);

如果ngx_escape_uri()的dst参数是空指针，那么它会返回需要编码的字符数量n，所以目标字符串的长度就应该是原长度+2*n。

type参数可以指定编解码的类型:

#define NGX_ESCAPE_URI             0     //URI 编码字符集
#define NGX_ESCAPE_ARGS            1     //URI 参数编码字符集
#define NGX_ESCAPE_URI_COMPONENT   2
#define NGX_ESCAPE_HTML            3
#define NGX_ESCAPE_REFRESH         4
#define NGX_ESCAPE_MEMCACHED       5     //memcached 编码字符集
#define NGX_ESCAPE_MAIL_AUTH       6     //邮件编码字符集

#define NGX_UNESCAPE_URI           1     //不处理？后的参数
#define NGX_UNESCAPE_REDIRECT      2     //只处理不可见字符

常用的编码类型是NGX_ESCAPE_URI，使用URI的编码集编码。解码时类型0（Nginx没有对0做宏定义）表示解码整个字符串，否则不处理“？”后的参数。

URI编解码的示范代码如下：

ngx_str_t src = ngx_string("test&/echo?name=#link");  //一个URI
ngx_str_t dst;

dst.len = src.len+           //计算编码后长度
2*ngx_escape_uri(nullptr,src.data, src.len, NGX_ESCAPE_URI);
dst.data = NgxPool(r).nalloc<u_char>(dst.len);   //分配内存

ngx_escape_uri(dst.data, src.data, src.len, NGX_ESCAPE_URI);
cout << dst << endl;

程序的输出是：

test&/echo%3Fname=%23link        #注意“&”没有被编码

11.2.4　HTML和JSON编码

HTML和JSON也是Web服务器经常处理的数据格式，Nginx也提供了它们的编码操作：

uintptr_t  ngx_escape_html(u_char *dst, u_char *src, size_t size);
uintptr_t  ngx_escape_json(u_char *dst, u_char *src, size_t size);

如果dst参数是空指针，那么函数会返回结果字符串增加的长度。

HTML编解码的示范代码如下：

ngx_str_t html = ngx_string("<html>");      //html 文本
ngx_str_t out;

out.len = html.len+           //计算编码后长度
ngx_escape_html(nullptr, html.data, html.len);
out.data = NgxPool(r).nalloc<u_char>(out.len);   //分配内存

ngx_escape_html(out.data, html.data, html.len);
cout << out << endl;

输出结果是：

&lt;html&gt;             #“<>”已经被编码

11.3　正则表达式

正则表达式是文本处理领域里的一个强大工具，它定义了一套完善的语法规则，可以执行匹配、查找、提取、验证等许多操作，在Web服务器里处理HTTP相关文本时非常有用。

Nginx自身不具备正则表达式功能，而是利用了著名的PCRE库，相关的数据结构和函数均定义在头文件<ngx_regex.h>里。

PCRE库功能强大，速度也足够快，但本书认为更应该使用C++11的标准库里的std::regex或者Boost的xpressive库，它们同样提供了完善且快速的正则表达式功能，而且接口更加清晰易用。

本节简略介绍功能更丰富的Boost.xpressive库。

Boost.xpressive库使用类cregex/sregex定义正则表达式，使用类cmatch/smatch保存正则匹配的结果。功能函数有匹配的regex_match()、查找的regex_search()和替换的regex_replace()等。

下面的代码示范了Boost.xpressive库的部分用法：

using namespace boost::xpressive;       //打开名字空间

cmatch what;             //存储匹配的结果
auto reg = cregex::compile("(.*?):(.*)");     //一个正则表达式

auto rc = regex_match("host: 127.0.0.1", what, reg);   //匹配正则表达式
assert(rc);              //断言匹配成功
cout << what[1] << "=" << what[2] << endl;     //输出匹配的子表达式

auto reg2 = cregex::compile(R"((\w+)=(\w+))");   //另一个正则表达式

regex_search("a=1&b=2", what, reg2);       //正则查找
cout << what[1] << "=" << what[2] << endl;     //输出匹配的子表达式

Boost.xpressive库还支持静态正则表达式，可以在编译期实现正则表达式，运行效率更高，甚至可以自定义小型语法解析器，读者可以进一步阅读推荐书目[3]了解更多的用法。

11.4　共享内存

共享内存是操作系统里一种高效的进程间通信方式，也是Nginx里worker进程间共享数据的一种便捷手段，Nginx封装了mmap、shmget等系统调用，可以在多种操作系统上支持共享内存特性。

11.4.1　结构定义

Nginx定义ngx_shm_t结构表示一块共享内存：

//定义在os/unix/ngx_shmem.h
typedef struct {
u_char*       addr;         //共享内存的地址
size_t        size;         //共享内存的大小
ngx_str_t     name;         //共享内存的名字
ngx_log_t*    log;          //日志对象
ngx_uint_t    exists;       //是否存在的标志位
} ngx_shm_t;

还有一个ngx_shm_zone_t结构，专门用来管理共享内存块：

//定义在core/ngx_cycle.h
typedef struct ngx_shm_zone_s  ngx_shm_zone_t;
typedef ngx_int_t (*ngx_shm_zone_init_pt) (ngx_shm_zone_t *zone, void *data);

struct ngx_shm_zone_s {
void*                    data;      //初始化函数的回调参数
ngx_shm_t                shm;       //共享内存结构体
ngx_shm_zone_init_pt     init;      //初始化函数指针
void*                    tag;       //共享内存关联的标记
};

Nginx会在配置解析阶段把所有的ngx_shm_zone_t结构放入ngx_list_t链表，集中创建共享内存。

11.4.2　操作函数

我们可以越过Nginx框架，直接创建和释放共享内存：

ngx_int_t  ngx_shm_alloc(ngx_shm_t *shm);   //创建共享内存
void       ngx_shm_free(ngx_shm_t *shm);  //释放共享内存

但Nginx并不推荐这种方式，而是建议使用函数ngx_shared_memory_add()，在配置解析阶段向链表里添加ngx_shm_zone_t对象，由Nginx在ngx_init_cycle()里统一为模块创建共享内存。

ngx_shared_memory_add()的声明如下：

ngx_shm_zone_t *ngx_shared_memory_add(ngx_conf_t *cf, ngx_str_t *name,
size_t size, void *tag);

11.4.3　C++共享内存

虽然使用Nginx的ngx_shm_t结构和相关函数可以很容易地创建共享内存，但它的使用却是一大难题，因为我们必须自行管理内存区域（分配、回收、整理），还需要使用原子量或者互斥锁解决多进程并发操作的问题，Nginx对此并没有提供更方便的接口。

在C++里可以使用boost.interprocess库，它实现了可移植的进程间通信功能，接口简单易用，其中就包括了共享内存操作。

boost.interprocess库是一个头文件库，不需要编译即可使用，但它的一些底层系统调用需要使用librt，所以Nginx在configure时要使用--with-ld-opt="-lrt"参数，或者在模块的config脚本里修改CORE_LIBS变量。

在boost.interprocess库里可以使用类managed_shared_memory创建一块共享内存，然后就能够在里面创建任意的对象，如原子量、字符串甚至vector、map等标准容器，所有的Nginx worker进程都可以共享使用。

下面的代码简单示范了在共享内存里原子量的用法，统计Nginx发送的总字节数，更详细的代码可参考GitHub资源：

//简化类型定义
typedef NgxLogDebug          log;
typedef boost::interprocess::managed_shared_memory shmem_type;
typedef boost::atomic<long>                  atomic_type;

auto& cf = this_module::instance().conf().main(r);  //获取配置数据

shmem_type segment(           //打开或创建共享内存
boost::interprocess::open_or_create, "ndg_shmem",  //使用一个名字
cf.size);            //还要提供大小

auto& counter =            //使用原子变量
*segment.find_or_construct<atomic_type>("counter")(0);

counter += r->connection->sent;       //累加发送字节数

log(r).print("sent=%l", static_cast<long>(counter));   //记录日志

需要注意的是共享内存属于系统级资源，不会因为Nginx进程结束而自动消失，所以我们需要在Nginx退出时销毁共享内存，也就是使用模块的exit_master回调函数，例如：

static void exit_master(ngx_cycle_t *cycle)
{
//退出master 进程时销毁共享内存
boost::interprocess::shared_memory_object::remove("ndg_shmem");
}

static ngx_module_t m =          //模块定义
{
NGX_MODULE_V1,
ctx(),            //函数指针表
cmds(),             //配置指令数组
NGX_HTTP_MODULE,           //HTTP 模块标志
NGX_MODULE_NULL(6),          //其他回调
&exit_master,           //退出master 进程回调
NGX_MODULE_V1_PADDING
};

boost.interprocess库不仅提供了共享内存，还有互斥量、条件变量、信号量、文件锁、消息队列等很多现代操作系统中常见的进程间通信机制，都可用于Nginx模块开发，读者可以参考Boost库的文档以了解更多用法。

11.5　定时器

定时器是异步I/O里非常重要的概念，可以异步定期执行函数，常用于事件的超时处理，我们也可以使用定时器延后某些费时的操作，提高系统的整体响应效率。

Nginx在event模块里实现了精确到毫秒级别的定时器机制，使用红黑树结构管理所有定时器对象，效率很高。

11.5.1　结构定义

ngx_event_t是event模块的核心对象，表示异步事件：

//定义在core/ngx_core.h
typedef struct ngx_event_s          ngx_event_t;
typedef void (*ngx_event_handler_pt)(ngx_event_t *ev);

//定义在event/ngx_event.h
struct ngx_event_s {
void*               data;     //任意数据

unsigned            timedout:1;   //是否已经超时
unsigned            timer_set:1;    //是否已经加入红黑树管理

ngx_event_handler_pt handler;   //事件回调函数

ngx_log_t*           log;      //事件使用的日志对象
ngx_rbtree_node_t     timer;     //红黑树节点

...            //其他成员
};

ngx_event_t结构比较复杂，但对于定时器我们只需关心里面的三个成员：data、handler和log。其中最重要的是handler，它告诉了Nginx当定时器超时需要回调的函数，而data成员可以是任意数据，回调时handler可以从它获取相关的信息。

11.5.2　操作函数

event模块负责定时器的管理，其具体工作原理我们暂不必深究，调用函数宏ngx_add_timer()就可以在Nginx里添加一个定时器事件：

#define ngx_add_timer        ngx_event_add_timer
void ngx_event_add_timer(ngx_event_t *ev, ngx_msec_t timer);

ngx_add_timer()实际上是函数ngx_event_add_timer()的别名，它的时间单位是毫秒，作用是向Nginx定时器红黑树结构里添加一个新节点。Nginx会定期检查红黑树，取出超时的定时器对象，执行handler。

11.5.3　C++定时器事件

创建定时器必须要声明一个ngx_event_t对象，但建立大量的定时器事件会造成内存浪费，所以最好的方法是使用对象池模式复用ngx_event_t对象。

我们定义两个类，本节的NgxTimerEvent表示一个定时器事件，之后的NgxTimer是一个工厂类，负责NgxTimerEvent的生产和回收。

类定义

NgxTimerEvent类保存了一个ngx_event_t对象，使用boost::function存储任意的函数或函数对象供定时器回调：

template<typename T>         //T=NgxTimer
class NgxTimerEvent final
{
public:
NgxTimerEvent()   = default;      //构造和析构
~NgxTimerEvent()  = default;
public:
typedef boost::function<void()> func_type;   //回调函数类型
private:
ngx_event_t   m_ev;          //事件结构体
func_type     m_func;        //回调函数对象
public:
...              //成员函数见后
};

NgxTimerEvent类有一个模板参数T，这是给NgxTimer准备的，使用模板参数的方式可以避免麻烦的前向声明。

成员m_func使用boost::function存储回调函数，要求函数的类型是无参的void()，但我们可以使用bind或者lambda表达式来适配函数，从而传递任意数量的参数。

成员函数

成员函数init()初始化ngx_event_t对象，设置回调函数为NgxTimer::call，把data成员设置为this指针，最后调用ngx_add_timer()添加定时器事件：[4]

public:
ngx_int_t init(ngx_msec_t ms, func_type f)
{
m_func = f;           //存储回调函数

NgxValue::memzero(m_ev);      //清空结构体

m_ev.handler  = &T::call;      //设置回调函数
m_ev.data     = this;       //设置回调所需的数据
m_ev.log      = ngx_cycle->log;     //设置log指针

ngx_add_timer(&m_ev, ms);       //添加定时器

return NGX_OK;
}

NgxTimerEvent重载operator()执行存储的函数，完成真正的回调：

void operator()() const
{
m_func();          //执行回调函数
}

11.5.4　C++定时器工厂

NgxTimer作为工厂类，使用对象池模式管理NgxTimerEvent对象，可以节约内存。

类定义

NgxTimer模板实例化了实际使用的NgxTimerEvent对象：

class NgxTimer final
{
public:
typedef NgxTimerEvent<NgxTimer>   timer_event_type;
typedef timer_event_type::func_type func_type;
public:
NgxTimer() = default;
~NgxTimer() = default;
...              //成员函数见后
};

对象池管理

NgxTimer使用std::deque作为对象池容器，保存NgxTimerEvent对象：

private:
typedef std::deque<timer_event_type*> timer_pool_type;

static timer_pool_type& pool()       //对象池
{
static timer_pool_type p;
return p;
}

static timer_event_type& acquire()      //从对象池获取对象
{
if(pool().empty())        //若对象池空则创建对象
{
return *boost::factory<timer_event_type*>()();
}

auto p = pool().front();       //获取一个已有的对象
pool().pop_front();

return *p;
}

static void release(timer_event_type& e)     //归还对象池
{
pool().push_back(&e);
}

定时器操作

静态成员函数add()从对象池里获取一个NgxTimerEvent对象，初始化ngx_event_t，加入Nginx的定时器调度：

public:
static ngx_int_t add(ngx_msec_t ms, func_type f)
{
return acquire().init(ms, f);       //获取对象，加入定时器
}

当定时器事件发生时，Nginx会调用NgxTimer::call()函数。这时可以从ev->data得到之前存储的NgxTimerEvent对象，执行回调后再把它放回到对象池，以便后续复用：

public:
static void call(ngx_event_t *ev)     //被 Nginx 定时器调用的函数
{
auto& e = *reinterpret_cast<timer_event_type*>(ev->data);

e();            //执行回调函数

release(e);          //归还对象池
}

用法

经过C++封装后Nginx的定时器使用起来非常简单，我们只需调用NgxTimer::add()函数，向它传递一个毫秒时间和一个可执行物（函数指针、函数对象、lambda表达式等）就可以了。

下面的函数timer_call()每隔1秒调用一下自身，输出一个字符串：

static void timer_call()
{
cout<< "timer"<< endl;        //输出字符串
NgxTimer::add(1000, &timer_call);     //添加一个定时器
}

然后我们在2秒之后启动timer_call()：

NgxTimer::add(2000, &timer_call);      //2秒后执行 timer_call()

11.6　总结

本章简要介绍了五个在Web服务器开发中比较常用的技术，是Nginx宫殿外锦上添花的装饰。

摘要算法可以为数据生成独一无二的标识，用于完整性校验和防篡改。Nginx内置了MD5、SHA-1和MurmurHash2三种算法，在内部的文件缓存和安全连接模块使用了MD5算法。

HTTP传输中经常要对数据做编码解码操作，Nginx提供了CRC、Base16、Base64和URI/HTML/JSON编解码等函数。

正则表达式是处理文本的强有力工具，Nginx利用PCRE库实现了对正则表达式的支持。PCRE库由C语言实现，小巧快速，但学习和使用的成本较高。

共享内存是进程间通信的一种高效方式，Nginx封装了操作系统的API，可以便捷地创建共享内存。但Nginx没有提供操作共享内存的高级接口，需要程序员自行管理内存，还要使用锁机制处理并发访问。

最后我们讨论了定时器，它是Nginx事件驱动模型里的重要组成部分，Nginx使用红黑树结构管理定时器对象，能够高效地处理超时事件。

这些技术里除定时器外与Nginx框架的联系都不是很密切，所以完全可以按照自己的喜好选择Nginx之外的实现。例如可以改用std::regex或者boost::xpressive来书写正则表达式，或者使用boost.interprocess库以C++类的方式操作共享内存。

[1] 与现实中的指纹一样，实际上摘要算法也存在“碰撞”的可能，只是几率极小，目前MD5、SHA-1算法均已经找到有效的攻击方法，但在低安全性要求的领域仍可使用。

[2] 某种程度上来说，CRC也可以算作一种摘要算法，只不过它的算法非常简单，值不唯一，所以用途明确，只是错误校验。

[3] 相应的，Hex编码可以称为Base16。

[4] 更好的方法是使用std::chrono库的时间类型，如seconds/milliseconds，明确地表示时间分辨率。

第12章　Nginx与设计模式

通过前面章节的阅读，读者应该对Nginx有比较深入的认识，熟悉它的核心运行机制，具备开发Nginx各种功能模块的能力。本章将从设计模式的角度对Nginx做一个总结。

12.1　设计模式简介

Nginx是一个Web服务器，但它同时又是一个开发框架（Framework），允许开发者将各种功能嵌入这个框架里，因而它必然应用了大量的设计模式以保证系统拥有良好的架构，掌握这些设计模式的用法可以更好地理解Nginx的“Why”。

设计模式主要用于面向对象的软件系统，但其解决问题的思想是共通的。虽然Nginx的开发语言是C，但这并不妨碍Nginx使用设计模式，而且这些模式相互嵌套，紧密联系，增强了整个Nginx框架的扩展性和灵活性。

经典的设计模式有23个，分为创建、结构和行为三大类，此外还有很多其他的设计模式，本章按照应用的重要程度列出Nginx里使用的设计模式。

12.2　框架级别的模式

这一级别的模式从宏观上搭建了Nginx的核心框架，决定了Nginx的整体架构体系。

前摄器（Proactor）

前摄器模式是异步处理事件的设计模式，核心概念是前摄器、异步的操作处理器和异步的事件多路分离器，可以不使用多线程提供高性能的并发处理能力。[1]

Linux、FreeBSD系统里的epoll、kqueue等系统调用都是前摄器模式的具体应用，是高性能Web服务器的实现基础。Nginx也正是利用了epoll、kqueue才能够无阻塞地处理海量的并发连接，所以前摄器模式是Nginx快速高效的最根本秘密。

包装外观（Wrapper Facade）

包装外观模式整理底层的系统API，分类、重命名或者简单包装，最后给出一个统一易用的接口，它可以屏蔽不同操作系统之间的差异，增强软件的可移植性。

为了能够跨平台运行，Nginx大量应用了包装外观模式，使用宏、函数等手段重定义了许多系统函数，减少了UNIX平台实现细节差异可能产生的影响。

桥接（Bridge）

桥接模式分离了架构的设计与实现，架构是稳定的，而实现可以任意变化，增强了系统的灵活性。

Nginx的模块架构就应用了桥接模式，它使用ngx_module_t定义模块，结构体里有若干函数指针和扩展字段，然后桥接实现了丰富多彩的core、conf、event、http和mail功能模块，搭建起整个Nginx框架。

模板方法（Template Method）

模板方法模式确定了操作的主要步骤和流程，并在关键节点定义了回调函数，允许外界实现回调函数来扩展或增强原操作的功能，是框架设计中最常用最基本的模式。

作为开发框架，Nginx在配置解析阶段、请求处理阶段都定义了数量繁多的回调函数，模块可以根据自身的需求实现特定的回调函数，从而插入Nginx框架，成为框架的一个有机组成。

策略（Strategy）

策略模式封装各种算法，把算法变成一个个接口一致的组件，在运行时能够互相替换，改变行为的内部逻辑。

策略模式在Nginx里最直接的应用就是load-balance模块，它封装了不同的负载均衡算法，而各种handler/filter/upstream模块其实也可以算是策略模式，替换不同的模块就可以改变Nginx的行为，产生不同的响应数据。

12.3　业务级别的模式

业务级别应用的模式要比框架级别的模式低一个层次，它们工作在框架内部，决定了某个较具体的功能的设计结构。

对象池（Object Pool）

对象池模式是一个特殊的工厂模式，它预先从系统中生产出一些对象提供给外部，对象在使用完毕后又放回池中等待下一次使用，可以摊平对象构造和销毁的成本，提高运行效率。

Nginx里的内存池是对象池模式的典型应用，它从操作系统里申请足够的内存，之后所有的内存分配都在池里进行，最后一次性释放，有效地减少了系统调用次数，也避免了内存泄漏的风险。

ngx_pool_t/ngx_http_request_body_t等结构里也有对象池模式的应用，它们使用一个链表保存闲置的ngx_chain_t对象，当要使用缓冲区时就直接从链表里取出对象重复利用，无须创建新的对象。

职责链（Chain of Responsibility）

职责链模式把许多对象连成一个链表，链表里的每一个对象都有机会处理请求，对象彼此独立，既可以协作也可以竞争对请求的处理，非常灵活，是处理数据时常用的设计模式。

Nginx的HTTP框架里实现了处理引擎和过滤引擎，是职责链模式的具体应用，它们把http模块组织成链表，逐个地加工处理HTTP请求。不过处理引擎更加复杂，它是二维形式的职责链，分为多个阶段，每个阶段里的模块也组织成职责链，而且在处理过程中因为URI重写的原因还可以在链表里跳转，导致职责链从头执行。

命令（Command）

命令模式把请求封装为一个对象，让对象携带尽可能多的相关信息，可以简化后续的处理操作，通常配合职责链模式一起使用。

很明显，Nginx在处理HTTP请求时的ngx_http_request_t结构体就是命令对象，它存储了非常多的信息，如连接、配置、数据、状态、变量、子请求等，在各个模块之间反复传递，由模块职责链进行处理。

备忘录（Memento）

备忘录模式捕获一个对象的内部状态，并在对象之外保存这个状态作为“备忘”，这样当对象再次启动时就可以通过备忘录恢复原状态，无差错地继续运行。

Nginx在请求结构体里设计的ctx成员就是备忘录，它为每个模块提供了保存运行数据的空间，模块可以把任何数据作为“备忘”存储在ctx里，不会因为框架的异步机制而导致运行状态不一致。

中介者（Mediator）

中介者扮演“中间人”的角色，系统里的每个对象只与中介者通信，简化对象之间的多对多联系，协调保证它们共同工作。中介者模式的缺点是它必须维护所有的连接关系，很容易造成自身过度复杂。

ngx_http_upstream_module里定义的upstream框架就是中介者模式（同时也应用了模板方法模式），它协调load-balance模块和upstream模块共同工作，获取上游服务器的地址，然后转发下游的请求和上游的响应数据。它也具有中介者模式的缺点，内部逻辑比较复杂，不太容易理解。

12.4　代码级别的模式

代码级别的模式与具体的代码实现相关，更多地偏向微观的编写程序技巧。

组合（Composite）

组合模式定义了对象的层次关系，可以把对象组合成树形结构，许多对象不断组合就可以得到一个很大的复合对象，但这个大对象在外部表现和内部行为等方面与小对象完全相同。

Nginx里的子请求设计就是组合模式，请求结构体使用main、parent和posted_requests等指针组织成了一个请求树，根是主请求，下面的分支是各个子请求。但从外部来看，一个主请求和子请求是没有任何区别的，可以一致处理。

观察者（Observer）

观察者模式定义了对象间一对多的联系，当被观察的对象状态变化时观察者对象能够立即得到通知。

Nginx里的父请求与子请求的通信就应用了观察者模式。子请求设置父请求的处理函数，子请求处理结束时在ngx_http_finalize_request()中发送通知，这样父请求就能够及时被唤醒从而继续运行。

适配器（Adapter）

适配器模式也是对接口的包装，它把原接口适配为一个新的接口再插入系统，无须改动任何一方就能够复用原代码。

Nginx在event模块里使用了适配器模式，把epoll、kqueue、select等不同的异步IO接口统一适配为ngx_event_actions_t结构体。

原型（Prototype）

原型模式是工厂模式，可以从一个已有的对象复制出一个相同或者相似的新对象。

Nginx在创建子请求时使用了原型模式，它从父请求里拷贝了大部分的字段，创建了一个基本相同的新请求对象。

访问者（Visitor）

访问者模式解耦数据和访问数据的操作，可以在不改变数据的前提下任意增加访问它们的新操作，两者也可以独立变化。

Nginx变量机制就应用了访问者模式，外界不能直接操作模块的内部数据，只能通过变量提供的get/set函数来间接访问。所以可以很容易地增加变量来暴露更多的信息，也可以随时变动模块的内部实现，而对外导出的变量则不受影响。

工厂方法（Factory Method）

工厂方法模式把对象的创建工作封装在一个函数里，改变函数就能改变生产出的对象。

Nginx创建模块配置数据结构的函数指针create_main/srv/loc_conf是工厂方法模式，每个模块都可以实现自己的工厂函数，创建出模块专属的配置数据结构。

代理（Proxy）

代理模式包装一个对象，目的是控制对象的访问，只有通过代理对象才能与被包装的对象通信。

ngx_str_t和ngx_buf_t代理了一块内存空间，表示一个字符串或者数据块，前者是只读字符串，而后者允许对数据块做更多的操作。

空对象（Null Object）

空对象模式是一个可用的对象，但内容和操作都是空的、没有意义的，是空指针概念的强化，最常见的应用是作为“哨兵”表示数组或者链表的结尾。

Nginx里的指令定义数组、变量定义数组都使用了空对象模式，用一个成员全是0的对象作为数组的最后一个元素，这样在遍历数组时就无须知道数组的长度，检测到空对象就是数组的结束。

12.5　总结

本章简略论述了Nginx里应用的近二十个设计模式，并分类为框架级别、业务级别和代码级别三类。其实这个分类标准并不是非常精确，有的模式可能跨越了两个级别（例如职责链和中介者），很难说它就明确地属于业务或框架级别。还要补充的是，分类只是依据模式在Nginx里应用的层次，并不是说某个设计模式就只能应用在框架、业务或者代码级别，实际上设计模式的应用没有任何限制，作用域可大可小，层次可高可低。

作为一个设计精巧的软件系统，Nginx里模式应用得非常紧凑，很多模式是相互重叠的。例如Nginx在处理HTTP请求时使用了职责链模式，职责链里的每一个节点又是策略模式，并使用备忘录模式暂存数据，在职责链里传递的是命令模式，而这个命令又应用了组合模式。多个模式紧密结合，让Nginx架构拥有了良好的扩展性和灵活性，值得我们深入体会其使用方法。

有一些设计模式没有在Nginx里出现，如装饰、状态、迭代器等，这是因为它们具有较明显的面向对象特征，在C语言里实现较困难。

[1] 与前摄器模式相对应的是反应器模式（actor）。

第13章　结束语

本章是全书的结束语，简略讨论本书未能涉及的内容，并为读者进一步学习Nginx给出了建议。

13.1　本书的遗憾

Nginx系统非常复杂，关于它的开发和运维都是很大的话题。由于选题、篇幅、时间和作者水平等因素的限制，本书的内容只是沧海一粟，在广度和深度上做了些不得已的取舍。

对于“指南”的定位来说，本书是足够了，但其实还可以有更多内容奉献出来共同探讨。遗憾的是写作过程中很多文字写出来后个人又觉得不太满意，最终成书时不得不删除，也许以后能有机会再次整理与读者见面。

由于与主题“模块开发”偏离较远，所以本书没有过多讲解Nginx在模块开发之外的其他部分，例如configure脚本、Nginx的启动初始化流程、worker进程调度和负载均衡、epoll事件驱动模型等。这些都是Nginx系统的重要组成部分，支撑了上层的HTTP处理功能，它们的工作原理较难理解，不过一旦掌握就能够更深层次地理解Nginx的运行机制，从而更好地定制Nginx。

在模块开发方面，本书只讲解了HTTP处理流程中与实际开发相关的要点，略去了一些次要的部分，未能对整体机制进行完整全面的论述，也没有细致入微地分析Nginx源代码，对Nginx运行机制的解析仍处在中等层次。篇幅所限，在Nginx的HTTP框架里还有很多内部实现细节暂无法深入研究，例如管理TCP连接、异步收取请求体数据、server/location的查找定位、各阶段的详细处理逻辑、请求结束时的重要函数ngx_http_finalize_request()等，读者可以书后多加留意。

世上无完美，本书并不期望成为一本面面俱到的“Nginx开发大全”，懂得放弃，留下一些遗憾，也可以成为将来前进的动力。

13.2　下一步

所谓“行百里者半九十”，阅读本书仅仅是踏入Nginx领域的一个起点，要想真正地掌握Nginx，接下来还需要读者自己去努力。

下面列出了一些进一步研究Nginx的方向供读者参考：

- configure脚本是Nginx的重要组成部分，它生成objs/ngx_modules.c，决定了Nginx使用的模块和顺序。

- Nginx的one-master/multi-worker进程模型可以充分利用当前流行的多核心架构提高服务器的性能，master进程管理worker进程，worker进程之间使用accept_mutex来均衡连接竞争[1]。Nginx进程模型的核心数据结构是ngx_cycle，它代表了整个Nginx的生命周期。

- Nginx使用事件驱动机制处理请求，尽量避免操作阻塞。在Linux上使用的是epoll Edge Triggered模式，处理异步IO事件和定时器事件。

- HTTP框架里的很多内部实现细节，例如连接池、监听端口管理、URI重定向、阶段的checker函数、ngx_http_finalize_request()等。

- Nginx在变量机制之上实现了脚本引擎，解析含有变量的字符串，在模块里使用脚本引擎可以获得更灵活的配置能力。

- 使用磁盘文件，让Nginx不局限于使用内存，可以读写磁盘里的数据作为缓冲。

- Nginx官方源码包含了大量的功能模块，非常有学习价值，阅读它们的源码能够加深理解Nginx的工作原理，也可以学习到更多的模块开发技巧。

- 网络上还存在很多第三方Nginx模块，实现了更多更有用的功能，把它们集成进Nginx能够大大增强Nginx的服务能力。

- 使用gdb调试是学习Nginx的一个有效手段，通过gdb跟踪到Nginx的内部，可以细致地观察它的状态和工作流程。

- 学习C++11/14的新特性和Boost程序库，了解C++最新最前沿的技术。

- 自己动手编写模块，可以从最简单的小功能开始，开发出可用的模块，勤记笔记，通过实践来逐渐积累Nginx的开发经验。

13.3　临别赠言

Nginx是一个伟大的Web服务器，C++是一门伟大的编程语言，本书在融合这两个伟大作品的方向上做出了一点尝试，愿读者能够在书中汲取力量，在Web服务器的开发道路上有所成就。

[1] Nginx的1.9.x版增加了对SO_REUSEPORT的支持，需要Linux 3.9以上。

附录A　推荐书目

这里列出了一些与本书相关的技术书籍，希望能够帮助读者更好地学习Nginx开发。

［1］Erich Gamma等著．李英军等译．《设计模式可复用面向对象软件的基础》．北京：机械工业出版社

软件开发历史上里程碑式的著作，设计模式的开山作品，里面提出的23个设计模式已经成为软件界的经典，被无数其他论文或书刊引用，也被无数的软件系统所验证并使用。本书是每一个精益求精的程序员都必须拥有的宝典和圣经，可以说是字字珠玑，值得经常翻阅以获取设计灵感。

［2］Niolai M.Josuttis著．侯捷/孟岩译．《C++标准程序库自修教程与参考手册》．武汉：华中科技大学出版社

全面分析讲解C++98标准库，内容详细丰富，是学习STL的经典书籍，也可能是最好的书籍。现在作者已经根据C++11标准出版了第2版，1100余页的煌煌巨著，已有中译本。

［3］罗剑锋著．《Boost程序库完全开发指南——深入C++“准”标准库（第3版）》．北京：电子工业出版社

本书是国内第一本介绍Boost程序库的技术书籍，基于2014年末发布的1.57版，完整介绍了Boost程序库的方方面面，包括时间与日期、内存管理、容器、算法、操作系统相关、并发编程等，并且使用了最新的C++11/14标准，想要了解C++的最前沿技术不容错过。

［4］罗剑锋著．《Boost程序库探秘——深度解析C++准标准库（第2版）》．北京：清华大学出版社

书目[3]的姊妹篇，深入探讨了Boost程序库里的迭代器、函数对象、指针容器、侵入式容器、流处理和序列化等许多高级组件，并全面完整地阐述了C++模板元编程。

［5］Stanley B Lippman等著，王刚等译．《C++Primer第5版》．北京：电子工业出版社

C++入门的经典教材，全面论述了C++11标准的方方面面，不仅适合初学者，对于C++熟手也有很大的参考意义。

［6］W.Richard Stevens等著．尤晋元等译．《UNIX环境高级编程第2版》．北京：人民邮电出版社

UNIX编程名著，UNIX/Linux程序员必备，无须过多介绍。

［7］W.Richard Stevens等著．杨继张译．《UNIX网络编程第1卷：套接口API第3版》．北京：清华大学出版社

UNIX网络编程的权威著作，深入介绍UNIX下网络编程的方方面面。

［8］W.Richard Stevens著．杨继张译．《UNIX网络编程第2卷：进程间通信第2版》．北京：清华大学出版社

接续第1卷，详细介绍了UNIX各种进程间的通信机制，如管道、消息队列、锁、信号量、共享内存等。

［9］Douglas等著．於春景译．《C++网络编程卷1运用ACE和模式消除复杂性》．武汉：华中科技大学出版社

ACE开发者编写的网络中间件著作，站在领域分析的高度，详细分析了C++网络编程相关的若干基本问题，以及ACE是如何解决这些问题的。

［10］Douglas等著．马维达译．《C++网络编程　卷2　基于ACE和框架的系统化复用》．北京：电子工业出版社

本书在卷1基础上深入阐述了ACE框架的设计原理和诸多用于网络通信的设计模式。

附录B　GDB调试简介

gdb是UNIX/Linux系统里老牌的调试工具，是研究程序、解决问题的“屠龙刀”。本文简要介绍gdb调试Nginx的一些基本知识。

设置Nginx

要使用gdb调试Nginx，需要在configure时使用--with-cc-opt为gcc增加额外的选项[1]，启用编译选项-g，并且使用-O0禁止编译器优化，即：

./configure --with-cc-opt="-g -O0"    #增加gcc 的调试选项

这样make出的Nginx程序就带有了调试信息，具备了使用gdb调试的基础。

我们还可以在配置文件里使用指令“master_process off”和“daemon off”关闭Nginx的master/worker进程机制，只在前台运行一个worker进程，避免fork进程带来的麻烦，简化后续的调试工作。

运行gdb

可以使用gdb直接启动Nginx程序，这将使用默认的配置：[2]

gdb /usr/local/nginx/sbin/nginx    #开始调试 Nginx

有的时候需要指定配置文件来启动Nginx，那么就要使用gdb的--args选项，给出完整的Nginx启动参数：

gdb --args ./sbin/nginx  -c x.conf    #指定配置文件调试 Nginx

如果Nginx已经运行了多个进程，gdb的-p选项可以指定要调试的进程号，直接调试正在运行的worker进程：

gdb -p xxxx          #调试某个正在运行的进程

当Nginx发生意外导致core dump时，可以指定core文件进行调试：

gdb /usr/local/nginx/sbin/nginx core-file  #用 core调试Nginx

调试命令

gdb提供了丰富的调试命令，可能很多读者都已经很熟悉了，下面仅列出作者在实际工作中比较常用的一些命令，供读者参考备忘：

- 回车键 ：重复上一条命令；

- h ：查看帮助信息；

- r ：运行程序启动调试；

- p ：查看变量值；

- watch ：监视变量；

- what ：查看变量的类型；

- pt ：查看变量的真实类型，非typedef；

- l ：列出源码；

- b ：设置断点，可以用函数名或者文件加行号的形式；

- i b ：查看所有的断点信息；

- d ：删除断点；

- c ：继续运行程序；

- n ：步进执行程序，不会进入函数内部；

- s ：步进执行程序，会进入函数内部；

- fin ：退出当前函数；

- up ：进入上级函数调用栈；

- down ：进入下级函数调用栈；

- bt ：查看当前函数调用栈信息；

- wh ：启动可视化调试模式，可以非常直观地看到源码，离开窗口使用^x,^a；

- q ：退出gdb。

[1] 也可以使用“CFLAGS="-g-O0"./configure ...”的方式。

[2] 运行Nginx需要root权限，所以gdb调试Nginx同样也需要root权限，或者使用sudo命令。

附录C　Nginx的字符串格式化

ngx_sprintf()是Nginx专门为格式化字符串提供的函数，它模仿了标准C函数printf()的接口和用法，并增加了一些Nginx特有的格式化标志，本文列出了这些格式供参考：

- %u：修饰格式，表示无符号整数，如size_t、u_int等；

- %x：修饰格式，以十六进制表示整数；

- %X：修饰格式，功能同%x，但使用大写字母；

- %T：打印time_t类型的变量；

- %O：打印off_t类型的变量，可以使用%x修饰；

- %z：打印ssize_t或size_t类型的变量，可以使用%u%x修饰；

- %d：打印int/u_int类型的变量，可以使用%u%x修饰；

- %l：打印long类型的变量，可以使用%u%x修饰；

- %i：打印ngx_int_t/ngx_uint_t类型的变量，可以使用%u%x修饰；

- %D：打印int32_t/uint32_t类型的变量，可以使用%u%x修饰；

- %L：打印int64_t/uint64_t类型的变量，可以使用%u%x修饰；

- %A：打印ngx_atomic_int_t/ngx_atomic_uint_t，可以使用%u%x修饰；

- %f：打印double类型的变量，可以用%.N指定小数点后的位数；

- %P：打印ngx_pid_t类型的变量；

- %M：打印ngx_msec_t类型的变量；

- %r：打印rlim_t类型的变量；

- %p：打印指针地址；

- %V：打印ngx_str_t*类型的变量，注意变量必须是地址形式；

- %v：打印ngx_variable_value_t*类型的变量，注意变量必须是地址形式；

- %s：打印标准的NULL结尾C字符串；

- %*s：打印指定长度的字符串；

- %Z：打印'\0'；

- %N：打印'\n'；

- %c：打印一个字符；

- %%：打印一个%字符。

下面的代码简单示范了部分格式化标志的用法：

ngx_int_t      i   = -100;
unsigned long  ul  = 65535L;
double         f   = 0.618;
ngx_str_t      str = ngx_string("metroid");

NgxLogError(r).print(
"%i,%uL,%.5f,%V,%uxz,%p,%P",
i, ul, f, &str, ul, str.data,ngx_getpid()
);

在error.log里的输出是：

-100,65535,0.61800,metroid,ffff,0000000000482212,8686

附录D　Nginx里的void*变量

Nginx使用的数据结构里会经常出现类型是void*的数据成员，也有void**甚至void****，含义因语境而十分多变。

它利用了C语言中void*指针可以转换为任意指针类型的能力，可以指向任意类型的数据，在C语言里变通实现了C++等面向对象语言的多态、泛型的能力，甚至犹有过之，可以说是Nginx里最难理解的“阿米巴变形虫”。

本文按照字母顺序列出使用void*的数据结构，对这些“善变”的字段做一个简单总结。

- ngx_array_t::elts，在动态数组里存储泛型的数据。

- ngx_buf_t::tag，关联任意数据，相当于缓冲区的ctx。

- ngx_conf_t::ctx，指向ngx_http_conf_ctx_t结构，表示当前解析处理指令的相关数据。

- ngx_connection_t::data，存储当前正在处理的http_request_t对象指针。

- ngx_cycle_t::conf_ctx，配置信息的起始存储位置，存储模块的配置信息。

- ngx_event_t::data，异步事件发生时回调函数使用的参数。

- ngx_http_post_subrequest_t::data，子请求回调函数使用的数据。

- ngx_http_request_t::ctx，模块处理请求时的环境数据结构，用于暂存中间计算结果。

- ngx_http_request_t::loc_conf，模块的location层次配置信息数组。

- ngx_http_request_t::main_conf，模块的http层次配置信息数组。

- ngx_http_request_t::srv_conf，模块的server层次配置信息数组。

- ngx_http_upstream_peer_t::data，服务器IP地址列表。

- ngx_list_part_t::elts，在链表节点里存储泛型的数据。

- ngx_module_t::ctx，指向不同模块的具体功能结构，通常是函数指针表。

- ngx_peer_connection_t::data，在负载均衡模块里传递给get/set函数的数据。

- ngx_pool_cleanup_t::data，内存池清理所需的数据，通常指向需释放的资源。

附录E　Nginx C++模块简介

本文简要介绍书内实现的Nginx C++工具模块ngx_cpp_module，它能够辅助程序员以C++快速开发Nginx模块。

使用方法

ngx_cpp_module包含了本书内开发的所有C++类，均是以头文件的形式提供，不需要编译，除C++11和Boost程序库外没有其他的外部依赖。

ngx_cpp_module也依据Nginx开发规范实现了自己的config脚本，但它没有任何编译动作，只是添加了包含路径支持：

#ngx_cpp_module 的 config 脚本
ngx_addon_name=ngx_cpp_module           #模块的名字
HTTP_INCS="$HTTP_INCS $ngx_addon_dir"   #添加http 模块的包含路径，无编译源码

只需在configure时使用“--add-module”添加ngx_cpp_module，然后其他模块就可以使用它提供的工具类，非常方便，例如：

./configure --add-module=path/to/ngxpp  \  #加入C++工具模块包含路径
--add-module=xxxx              #其他C++模块

类列表

ngx_cpp_module提供总括性头文件NgxAll.hpp，里面包含了所有的C++类实现，其他模块可以直接include该文件。

这些C++类大都只含有一个指针成员，拷贝的代价极低，非常的轻量级，所以无须实现转移构造函数，默认的拷贝构造函数就足够了。

下面依照出现的章节顺序列出了本书实现的主要C++类，供读者开发时快速参考：

- NgxUnsetValue：泛型的无效值“-1”；

- NgxValue：整数的初始化、合并、UNSET操作；

- NgxException：错误码的异常形式；

- NgxPool：内存池；

- NgxAlloctor：使用内存池的分配器，可以用于标准容器；

- NgxString：引用形式的字符串；

- NgxClock：计时器；

- NgxDatetime：日历日期；

- NgxLog：记录运行日志；

- NgxArray：泛型的动态数组；

- NgxList：泛型的单向链表；

- NgxQueue：泛型的双端队列；

- NgxBuf：数据缓冲区；

- NgxChain：数据块链表；

- NgxModuleConfig：获取模块的配置信息；

- NgxModule：代理模块对象，获取配置和环境数据；

- NgxCommand：设置配置指令；

- NgxTake：简化指令的参数数量设置；

- NgxModuleCtx：操作模块请求相关的环境数据，暂存处理的中间结果；

- NgxHttpCoreModule：注册模块的处理函数；

- NgxFilter：操作HTTP过滤链表；

- NgxHeaders：处理HTTP请求头或响应头；

- NgxRequest：处理HTTP请求；

- NgxResponse：处理HTTP响应；

- NgxUpstreamHelper：转发HTTP请求；

- NgxLoadBalance：实现负载均衡算法；

- NgxSubRequest：发起HTTP子请求；

- NgxVariables：使用模板元数据添加Nginx变量；

- NgxVarManager：操作Nginx变量；

- NgxDigest：摘要算法，需配合元数据使用；

- NgxTimer：定时器。

附录F　Nginx Lua模块简介

模块化架构决定了Nginx的高度扩展性，我们可以使用C/C++语言编写任意的功能模块来增强它的功能，把Nginx变成一台全能的应用服务器。但使用C/C++实现Nginx模块对开发者的要求较高，我们必须遵循Nginx的开发规范，了解工作原理和内部处理流程，熟悉各种数据结构和函数，并在恰当的时机使用它们，程序员必须很好地掌握Nginx的运行机制才能开发出一个较为完善的模块。而且由于C/C++的静态编译语言特性，开发周期长、成本高，很难快速迭代。

由agentzh和chaoslawful开发的Lua模块（以下简称ngx_lua）成功地把Lua语言嵌入了Nginx，可以用Lua调用Nginx底层接口，以脚本的方式直接实现复杂的HTTP业务逻辑，简化开发流程，极大地降低了Nginx二次开发的门槛。

获取方式

ngx_lua属于第三方模块，Nginx的官方安装包并不包含它，需要我们自行编译集成。

在ngx_lua的官方网站上有详细的文档，包括安装、示例和接口等信息：

https://github.com/openresty/lua-nginx-module/

此外，agentzh还把Nginx、ngx_lua以及大量很有用的Nginx模块集合在了一起，制作了一个简单易用的服务器开发包，命名为OpenResty:

http://openresty.org/

Lua语言

Lua语言是一种动态语言，与Python、Perl、Ruby等不同，它设计的目标是要能够嵌入其他应用程序里，提供脚本化的扩展和定制功能，所以Lua是一个小巧紧凑而非大而全的语言。

Lua可以很容易地嵌入C/C++程序并调用宿主函数，标准解释器非常小，只有一百多K，代码的执行效率也非常高，速度只比C语言的实现低大约10%到20%。

Lua功能丰富，表（table）结构十分灵活，可以模拟出其他语言里的数组、集合、字典、类等特性，它还提供闭包（即C++里的lambda）支持函数式编程，提供协程（coroutine）支持并发编程。

ngx_lua基于Lua5.1或者LuaJIT 2.0/2.1，充分利用了Lua内建的协程特性，可以无阻塞地处理并发连接，而且功能代码不需要编译，可以就地修改脚本并运行，加快了开发和调试的速度，缩短了开发周期，在如今这个快节奏的时代里弥足珍贵。

本文不可能详细地介绍Lua的语言细节，但有一个很有用的中文网站可供读者参考：

http://book.luaer.cn/

配置指令

ngx_lua本质上仍是一个Nginx http模块，它也提供了一些指令来调整模块的行为，较常用的指令列举如下，更详细的说明请参见官网：

- lua_package_path：设置Lua脚本的查找路径；

- lua_package_cpath：设置C模块（*.so）的查找路径；

- lua_shared_dict：定义一块共享内存；

- lua_check_client_abort：检测客户端断连；

- access_by_lua：在access阶段执行Lua代码；

- set_by_lua：在rewrite阶段设置Nginx变量值；

- rewrite_by_lua：在rewrite阶段执行Lua代码；

- content_by_lua：在content阶段执行Lua代码，产生响应内容；

- header_filter_by_lua：在header_filter阶段执行Lua代码；

- body_filter_by_lua：在body_filter阶段执行Lua代码；

- log_by_lua：在log阶段执行Lua代码；

- xxx_by_lua_file：与同名的指令功能相同，但执行磁盘上的脚本文件。

通常情况下我们需要在Nginx的配置文件里使用指令xxx_by_lua或xxx_by_lua_file，在ngx_lua的帮助下介入Nginx的某个处理阶段，利用Lua语言来实现业务逻辑。

接口函数

ngx_lua在ngx.*里提供Nginx的各种调用，涵盖了大部分的Nginx功能，而且接口比C语言更高级、更容易使用。

下面仅列出常用的ngx_lua接口名称，详细的说明请参见官网：

- ngx.var：操作Nginx的变量；

- ngx.now：当前的时间戳，精确到毫秒；

- ngx.time：当前的时间戳，只有秒数；

- ngx.sleep：非阻塞睡眠；

- ngx.log：记录运行日志；

- ngx.ctx：操作请求的环境数据；

- ngx.status：操作响应的状态值；

- ngx.header：操作响应头；

- ngx.print：输出响应数据；

- ngx.say：同ngx.print，但会多出一个换行；

- ngx.flush：强制刷新响应数据；

- ngx.eof：强制结束响应输出；

- ngx.exit：结束当前请求的处理流程；

- ngx.re.xxx：使用Nginx的正则表达式功能，匹配、查找和替换；

- ngx.shared.xxx：操作共享内存数据；

- ngx.timer.at：定时器执行Lua函数；

- ngx.req.get_headers：获取请求头；

- ngx.req.read_body：读取请求体；

- ngx.req.discard_body：丢弃请求体；

- ngx.req.get_body_data：获取请求体数据；

- ngx.location.capture：调用子请求，非常强大易用。

这些接口中最重要的就是ngx.location.capture，它对应Nginx里的子请求，超越了ngx_lua自身的能力限制，可以访问Nginx里的任意location，把proxy、fastcgi、memcached、redis、mango等等模块完美地“粘合”起来，创造出无尽的可能。

示例

下面的代码示范了部分ngx_lua指令和接口的用法（省略了http/server等配置），Lua代码直接写在了Nginx的配置文件里，用单引号（'）避免了代码里的双引号（"）冲突：

set $name "chrono";                         #设置一个变量

location /luatest {                         #一个location
content_by_lua '                          --Lua 使用双横线注释
ngx.print(ngx.var.uri.." var = ")       --输出uri字符串
ngx.say(ngx.var.name)                   --输出变量

ngx.var.name = "zelda"                  --修改变量值
ngx.say(ngx.var.name)                   --输出变量

local res, err = ngx.location.capture(  --调用子请求
"/subreq",                          --location 名字
{args = {x = "abcd"}})              --使用表传递参数

ngx.print(res.status.."=>")             --输出调用的状态值
ngx.say(res.body or "-")                --输出调用的结果
';                                        #Lua代码结束
}

location /subreq {                          #被调用的 location
content_by_lua '
local arg = ngx.var.arg_x;              --获取HTTP 参数

if not arg then                         --检查参数
ngx.print("bad args")
return
end

local str = ngx.re.sub(                 --正则替换
arg, "a", "X", "ijo")

ngx.print(str)                          --输出替换结果
';                                        #Lua代码结束
}

使用curl请求Nginx，将会执行配置文件里的Lua代码：

curl  http://localhost/luatest

得到的结果是：

/luatest var = chrono
zelda
200=>Xbcd