【Linux网络编程】Nginx -- Nginx 数据结构

【1】基本数据结构

【1.1】整型

文件路径,src/core/ngx_config.h

typedef intptr_t        ngx_int_t;      // 有符号整数
typedef uintptr_t       ngx_uint_t;     // 无符号整数
typedef intptr_t        ngx_flag_t;     // 相当于bool,标志量用

说明
// /* Types for `void *' pointers.  */
// #if __WORDSIZE == 64
// #ifndef __intptr_t_defined
// typedef long int		intptr_t;
// #define __intptr_t_defined
// #endif
// typedef unsigned long int	uintptr_t;
// #else
// #ifndef __intptr_t_defined
// typedef int			intptr_t;
// #define __intptr_t_defined
// #endif
// typedef unsigned int		uintptr_t;
// #endif
//
// 在64位的机器上,intptr_t和uintptr_t分别是long int、unsigned long int的别名;
// 在32位的机器上,intptr_t和uintptr_t分别是int、unsigned int的别名;

【1.2】字符串类型

文件路径
src/core/ngx_string.h
src/core/ngx_string.c

// 字符串,相当于boost::string_ref或者std::string_view
typedef struct {
    size_t      len;    // 字符串的长度
    u_char     *data;   // 字符串的起始地址,注意是u_char,不是char
} ngx_str_t;


// key-value结构体,用于解析配置文件里的数据
typedef struct {
    ngx_str_t   key;        // key
    ngx_str_t   value;      // value
} ngx_keyval_t;


// nginx变量值,用于http/stream模块
typedef struct {
    unsigned    len:28;             //字符串长度,只有28位,剩下4位留给标志位

    unsigned    valid:1;            //变量值是否有效
    unsigned    no_cacheable:1;     //变量值是否允许缓存,默认允许
    unsigned    not_found:1;        //变量未找到
    unsigned    escape:1;

    u_char     *data;               //字符串的地址,同ngx_str_t::data
} ngx_variable_value_t;


// 初始化字符串,只能用于初始化,str必须是个字面值
#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

// 把字符串置为空字符串,运行时设置,str是指针
#define ngx_str_null(str)   (str)->len = 0; (str)->data = NULL

【1.3】缓冲区(ngx_buf_t)数据类型

文件路径
src/core/ngx_buf.h
src/core/ngx_buf.c

// 用于ngx_buf_t,关联任意的数据
typedef void *            ngx_buf_tag_t;
// ngx_buf_t 处理大数据的关键数据结构,既可以用于内存数据也可以用于磁盘数据
typedef struct ngx_buf_s  ngx_buf_t;

// 表示一个单块的缓冲区,既可以是内存也可以是文件
// start和end两个成员变量标记了数据所在内存块的边界
// 如果内存块是可以修改的,在操作时必须参考这两个成员防止越界
//
// ngx_buf_t 本质上提供的仅仅是一些指针和标志位
struct ngx_buf_s {
    //实际数据可能被包含在多个缓冲区中,
    //缓冲区的start和end指向这块内存的开始地址和结束地址,
    //缓冲区的pos和last指向本缓冲区实际包含的数据的开始和结尾
    u_char          *pos;           //内存数据的起始位置
    u_char          *last;          //内存数据的结束位置

    off_t            file_pos;      //文件数据的起始偏移量
    off_t            file_last;     //文件数据的结束偏移量

    u_char          *start;         /* start of buffer */   //内存数据的上界
    u_char          *end;           /* end of buffer */     //内存数据的下界

    ngx_buf_tag_t    tag;           //void*指针,可以是任意数据
                                    //表示当前缓冲区的类型,指向对应模块的变量地址

    ngx_file_t      *file;          //存储数据的文件对象

    ngx_buf_t       *shadow;        //当前缓冲区的影子缓冲区


    /* the buf's content could be changed */
    unsigned         temporary:1;   //内存块临时数据,1表示数据在内存中且可以修改

    /*
     * the buf's content is in a memory cache or in a read only memory
     * and must not be changed
     */
    unsigned         memory:1;      //内存块数据,1表示数据在内存中且不允许修改

    /* the buf's content is mmap()ed and must not be changed */
    unsigned         mmap:1;        //内存映射数据,1表示该段内存是使用mmap映射过来的且不允许修改

    unsigned         recycled:1;        //标志位,1表示可以回收
    unsigned         in_file:1;         //标志位,1表示缓冲区在文件里
    unsigned         flush:1;           //标志位,1表示要求Nginx立即输出本缓冲区(flush 操作)
    unsigned         sync:1;            //标志位,1表示要求Nginx同步操作本缓冲区,可能会导致Nginx进程阻塞
    unsigned         last_buf:1;        //标志位,1表示最后一块缓冲区,ngx_buf_t由ngx_chain_t链表串联
    unsigned         last_in_chain:1;   //标志位,1表示链里的最后一块缓冲区

    unsigned         last_shadow:1;     //标志位,1表示最后一个影子缓冲区
    unsigned         temp_file:1;       //标志位,1表示缓冲区在临时文件里

    /* STUB */ int   num;
};

【1.4】chain (ngx_chain_t)数据类型

文件路径
文件路径
src/core/ngx_buf.h
src/core/ngx_buf.c

// 把缓冲区块简单地组织为一个单向链表
// 如果节点是链表的尾节点就必须要把next置为nullptr,表示链表结束
struct ngx_chain_s {
    ngx_buf_t    *buf;      //缓冲区指针,指向当前的 ngx_buf_t 缓冲区
    ngx_chain_t  *next;     //下一个链表节点
};

chain 链表图示

nginx 直接输出文字 nginx字符集_数组

【2】ngx_array_t 数据结构

【2.1】数据结构体

文件路径
src/core/ngx_array.h
src/core/ngx_array.c

// ngx_array_t 动态数组用连续的内存存放着大小相同的元素,使得它按照下标检索数据的效率非常高,可以用O(1)的时间来访问随机元素;
// 相比数组,优势在于,数组通常是固定大小的,而 ngx_array_t 可以在达到容量最大值时自动扩容;
// ngx_array_t 与 ngx_queue_t 的一个显著不同点在于,ngx_queue_t 并不负责为容器元素分配内存,
// 而 ngx_array_t 是负责容器元素的内存分配的;
//
// 1. 访问速度快;
// 2. 允许元素个数具备不确定性;
// 3. 负责元素占用内存的分配,这些内存将由内存池统一管理;
//
// nginx的动态数组,表示一块连续的内存,其中顺序存放着数组元素,概念上和原始数组很接近
// 如果数组内的元素不断增加,当nelts > nalloc时将会引发数组扩容
// 所以是“不稳定”的,扩容后指向元素的指针会失效
//
// 数组空 arr->nelts == 0
// 添加元素 T* p = ngx_array_push(arr)
// 访问元素 T* values = arr->elts
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 数据结构图示

nginx 直接输出文字 nginx字符集_nginx 直接输出文字_02

【2.2】相关方法

// 创建新的动态数组
// 参数 : p,内存池对象;n,初始分配元素的最大个数;size,单个元素占用的内存空间
// 使用内存池创建一个可容纳n个大小为size元素的数组,即分配了一块n*size大小的内存块
// size参数通常要使用sizeof(T)
ngx_array_t *ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size);

// 销毁数组对象,内存被内存池回收
// 参数 a 动态数组结构体指针;
// 销毁已经分配的数组元素空间和 ngx_array_t 动态数组对象;
// 
// “销毁”动态数组,不一定归还分配的内存
// 数组创建后如果又使用了内存池则不会回收内存
// 因为内存池不允许空洞
void ngx_array_destroy(ngx_array_t *a);

// 向动态数组添加一个或 n 个元素,返回新添加元素的首地址
// 向数组添加元素,用法比较特别,它们返回的是一个void*指针,用户必须把它转换为真正的元素类型再操作
// 不直接使用ngx_array_t.elts操作的原因是防止数组越界,函数内部会检查当前数组容量自动扩容
void *ngx_array_push(ngx_array_t *a);
void *ngx_array_push_n(ngx_array_t *a, ngx_uint_t n);


// 参数 array 动态数组结构体指针; pool 内存池对象; n 初始分配的元素最大个数; size 单个元素占用的内存空间
// 重新分配数组的内存空间,相当于resize
static ngx_inline ngx_int_t
ngx_array_init(ngx_array_t *array, ngx_pool_t *pool, ngx_uint_t n, size_t size);

创建数组后内存池使用图示

nginx 直接输出文字 nginx字符集_链表_03

nginx 直接输出文字 nginx字符集_nginx 直接输出文字_04

添加 10 个元素后内存池使用图示

nginx 直接输出文字 nginx字符集_数据_05

【3】ngx_list_t 数据结构

【3.1】数据结构体

相关文件路径
src/core/ngx_list.h
src/core/ngx_list.c

// 链表的节点
typedef struct ngx_list_part_s  ngx_list_part_t;

// 类似ngx_array_t是一个简单的数组,也可以“泛型”存储数据
// next指针指向链表里的下一个节点
// 用于存储http头信息

// 链表中的节点结构
struct ngx_list_part_s {
    void             *elts;     // 指向该节点数据区的首地址
    ngx_uint_t        nelts;    // 该节点实际存储的元素的个数
    ngx_list_part_t  *next;     // 链表中下一个节点的指针
};

// ngx_list_t 单向链表负责容器内元素内存分配,并且用单链表将多段内存块连接起来,
// 每段内存块存储了多个元素,类似于 “数组 + 单链表”
//
// 定义链表(实际上是头节点+元信息)
// 成员size、nalloc和pool与ngx_array_t含义是相同的,确定了节点里数组的元信息
// ngx_list_t 为存储数组的链表
// 每个链表元素ngx_list_part_t又是一个数组,拥有连续的内存, 
// 它既依赖于ngx_list_t里的size和nalloc来表示数组的容量,同时又依靠每个ngx_list_part_t成员中的nelts来表示数组当前已使用了多少容量;
// ngx_list_t是一个链表容器,而链表中的元素又是一个数组;
// ngx_list_part_t数组中的元素才是用户想要存储的东西,ngx_list_t链表能够容纳的元素数量为ngx_list_part_t数组元素的个数与每个数组所能容纳的元素的乘积;
//
// 优势 :
// 1. 链表中存储的元素是灵活的,可以是任何一种数据结构;
// 2. 链表元素需要占用的内存由ngx_list_t管理,并且已经通过数组分配好了;
// 3. 小块的内存使用链表访问效率是低下的,使用数组通过偏移量来直接访问内存则要高效得多;
typedef struct {
    ngx_list_part_t  *last;     // 链表的尾节点,链表最后一个数组元素
    ngx_list_part_t   part;     // 链表的头节点,链表的首个数组元素
    size_t            size;     // 链表存储元素的大小,用户待存储的一个数据所占用的字节数必须小于或等于 size
    ngx_uint_t        nalloc;   // 每个节点能够存储元素的数量,即最多能够存储的数据个数
    ngx_pool_t       *pool;     // 链表使用的内存池
} ngx_list_t;

ngx_list_t 数据结构图示

nginx 直接输出文字 nginx字符集_数据_06

【3.2】相关方法

// 使用内存池创建链表,每个节点可容纳n个大小为size的元素
ngx_list_t *ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size);

// 用于初始化一个已有的链表
static ngx_inline ngx_int_t
ngx_list_init(ngx_list_t *list, ngx_pool_t *pool, ngx_uint_t n, size_t size);

// 向链表里添加元素,返回一个void*指针,需要转型操作
// 返回新分配元素的地址,通常使用 ngx_list_push 获取新分配元素的地址再对新分配元素赋值
void *ngx_list_push(ngx_list_t *list);

创建链表后内存池的使用图示

nginx 直接输出文字 nginx字符集_nginx 直接输出文字_07

添加元素后内存池的使用图示

nginx 直接输出文字 nginx字符集_数据_08

nginx 直接输出文字 nginx字符集_数据_09

【4】ngx_queue_t 数据结构

【4.1】数据结构体

文件路径
src/core/ngx_queue.h
src/core/ngx_queue.c

// ngx_queue_t 双向链表是 Nginx 提供的轻量级链表容器,它与 Nginx 的内存池无关,
// 这个链表不会负责分配内存来存放链表元素,即任何链表元素都需要通过其他方
// 式来分配所需要的内存空间, ngx_queue_t 只是把这些
// 已经分配好内存的元素用双向链表连接起来;ngx_queue_t 的功能虽然很简单,但它非常轻量
// 级,对每个用户数据而言,只需要增加两个指针的空间即可,消耗的内存很少;
//
// 优势
// 1. 实现了排序功能;
// 2. 非常轻量级,是一个纯粹的双向链表,不负责链表元素所占内存的分配,与 Nginx 封装的 ngx_pool_t 内存池完全无关;
// 3· 支持两个链表间的合并;
typedef struct ngx_queue_s  ngx_queue_t;

// 队列结构,两个指针
// 需作为结构体的成员使用
// 取原结构使用ngx_queue_data(q, type, link)
struct ngx_queue_s {
    ngx_queue_t  *prev;
    ngx_queue_t  *next;
};

【4.2】相关方法

// 初始化双向链表
// 参数 : q 链表容器结构体 ngx_queue_t 的指针
// 初始化头节点,把两个指针都指向自身,自动置为空链表
ngx_queue_init(q)

// 判断链表是否为空
// 参数 : h 链表容器结构体 ngx_queue_t 的指针
// 检查头节点的前驱指针,判断是否是空队列
ngx_queue_empty(h)

// 参数 : h 链表容器结构体 ngx_queue_t 的指针
//        x 插入元素结构体中 ngx_queue_t 的指针
// 向队列的头插入数据节点
ngx_queue_insert_head(h, x)

// 参数 : h 链表容器结构体 ngx_queue_t 的指针
//        x 插入元素结构体中 ngx_queue_t 的指针
// 向队列的尾插入数据节点
// 在节点前插入数据
ngx_queue_insert_tail(h, x)

// 返回链表容器 h 中的第一个元素的 ngx_queue_t 结构体指针
// 参数 : h 链表容器结构体 ngx_queue_t 的指针
// 获取队列的头尾指针,可以用它们来实现队列的正向或反向遍历
// 直到遇到头节点(ngx_queue_sentinel)停止
ngx_queue_head(h)

// 返回链表容器 h 中的最后一个元素的 ngx_queue_t 结构体指针
// 参数 : h 链表容器结构体 ngx_queue_t 的指针
// 获取队列的头尾指针,可以用它们来实现队列的正向或反向遍历
// 直到遇到头节点(ngx_queue_sentinel)停止
ngx_queue_last(h)

// 返回链表结构体的指针
// 参数 : h 链表容器结构体 ngx_queue_t 的指针
// 返回节点自身,对于头节点来说就相当于“哨兵”的作用
ngx_queue_sentinel(h)

// 针对链表元素的方法
// 节点的后继指针
ngx_queue_next(q)

// 针对链表元素的方法
// 节点的前驱指针
ngx_queue_prev(q)

// “删除”当前节点,实际上它只是调整了节点的指针
// 把节点从队列里摘除,并没有真正从内存里删除数据
// 参数 : x 插入元素结构体中 ngx_queue_t 的指针
ngx_queue_remove(x)

// 拆分队列
// 参数 h 链表容器,q 链表 h 中的元素,n 另一个链表容器结构体指针
// 将链表 h 以元素 q 为分界点拆分为两段,h 指向前一段,n 指向后一段(q 为头节点)
ngx_queue_split(h, q, n)

// 合并两个队列
// 参数 h 链表容器,n 另一个链表容器结构体指针
// 将链表 n 添加到 h 的末尾
ngx_queue_add(h, n)

// 获取队列中节点数据地址
// 返回 q 元素所属结构体的地址
// 针对链表元素的方法
// 从作为数据成员的ngx_queue_t结构访问到完整的数据节点
// q    :指针,实际指向ngx_queue_t对象
// type :节点的类型,是一个名字
// link :节点里ngx_queue_t成员的名字
//
// +-------+---------+----+
// | node | queue | ...  |
// +-------+---------+----+
// 结构的起始指针 = queue的起始位置 - queue在结构内的偏移
ngx_queue_data(q, type, link)

// 返回链表中间元素(N/2+1)
// 队列的中间节点
ngx_queue_t *ngx_queue_middle(ngx_queue_t *queue);

// 调用特定的比较函数对队列进行排序
// 使用一个比较函数指针对队列元素排序,但效率不是很高
// cmp 是可以自定义的比较函数
void ngx_queue_sort(ngx_queue_t *queue,
    ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *));

头部插入节点图示

nginx 直接输出文字 nginx字符集_nginx 直接输出文字_10

尾部插入节点图示

nginx 直接输出文字 nginx字符集_链表_11

删除节点图示

nginx 直接输出文字 nginx字符集_nginx 直接输出文字_12

拆分链表图示

nginx 直接输出文字 nginx字符集_链表_13

合并链表图示

nginx 直接输出文字 nginx字符集_数组_14

【5】ngx_hash_t 数据结构

【5.1】数据结构体

文件路径
src/core/ngx_hash.h
src/core/ngx_hash.c

// 散列表中元素的数据结构
// 散列的桶,不定长结构体
// 末尾的1长度数组是C语言常用的技巧
typedef struct {
    // 指向实际的元素
    // 指向用户自定义元素数据的指针,
    // 如果当前ngx_hash_elt_t槽为空,则value的值为0
    void             *value;

    // name的实际长度
    // name即散列表元素的key
    u_short           len;

    // 存储key
    // 在分配内存时会分配适当的大小
    // 元素关键字的首地址
    u_char            name[1];
} ngx_hash_elt_t;

// 散列表结构体
// 表面上好像是使用开放寻址法
// 但实际上是开链法,只是链表表现为紧凑的数组
// 用链表存储key相同的元素
typedef struct {
    // 指向散列表的首地址
    // 散列桶存储位置
    // 二维数组,里面存储的是指针
    ngx_hash_elt_t  **buckets;

    // 数组的长度
    // 即桶的数量
    // 散列表中槽的总数
    ngx_uint_t        size;
} ngx_hash_t;

结构体图示

nginx 直接输出文字 nginx字符集_nginx 直接输出文字_15

 哈希初始化结构

// 计算散列值的函数原型
// nginx提供两个:
// 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);
// 传入的data是元素关键字的首地址,len是元素关键字的长度
typedef ngx_uint_t (*ngx_hash_key_pt) (u_char *data, size_t len);

哈希初始化结构 ngx_hash_init_t,Nginx 的 hash 初始化结构是 ngx_hash_init_t,用来将其相关数据封装起来作为参数传递给 ngx_hash_init() 函数
// 初始化散列表的结构体
typedef struct {
    // 待初始化的散列表结构体
    // 指向普通的完全匹配散列表
    ngx_hash_t       *hash;

    // 散列函数
    // 通常是ngx_hash_key_lc
    // 用于初始化预添加元素的散列方法
    ngx_hash_key_pt   key;

    // 散列表里的最大桶数量
    // 散列表中槽的最大数目
    ngx_uint_t        max_size;

    // 桶的大小,即ngx_hash_elt_t加自定义数据
    // 散列表中一个槽的空间大小,它限制了每个散列表元素关键字的最大长度
    ngx_uint_t        bucket_size;

    // 散列表的名字,记录日志用
    char             *name;

    // 使用的内存池
    // 内存池,
    // 分配散列表(最多3个,包括1个普通散列表、1个前置通配符散列表、1个后置通配符散列表)中的所有槽
    ngx_pool_t       *pool;

    // 临时用的内存池
    // 临时内存池,它仅存在于初始化散列表之前;
    // 主要用于分配一些临时的动态数组,带通配符的元素在初始化时需要用到这些数组
    ngx_pool_t       *temp_pool;
} ngx_hash_init_t;

待添加元素的 hash 元素结构

// 待添加元素的 hash 元素结构
// 初始化散列表的数组元素
// 存放的是key、对应的hash和值
typedef struct {
    // 元素关键字
    ngx_str_t         key;
    // 由散列方法算出来的关键码
    ngx_uint_t        key_hash;
    // 指向实际的用户数据
    void             *value;
} ngx_hash_key_t;

【5.2】相关方法

// Nginx Hash Key 的计算函数
// 计算散列值
// 使用 BKDR 算法将任意长度的字符串映射为整型
ngx_uint_t ngx_hash_key(u_char *data, size_t len);

// 小写后再计算hash
// 小写后再使用 BKDR 算法将任意长度的字符串映射为整型
ngx_uint_t ngx_hash_key_lc(u_char *data, size_t len);

// 小写化的同时计算出散列值
ngx_uint_t ngx_hash_strlow(u_char *dst, u_char *src, size_t n);

// 简单地对单个字符计算散列
#define ngx_hash(key, c)   ((ngx_uint_t) key * 31 + c)
哈希初始化函数

// 估算key的长度,字符串长度+2,再加一个指针
// 第一个void*是结构体里的value指针
// 2是ushort len的长度
#define NGX_HASH_ELT_SIZE(name)                                               \
    (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))

// 初始化散列表hinit
// 输入一个ngx_hash_key_t数组,长度是nelts
// 函数执行后把names数组里的元素放入散列表,可以hash查找
// Nginx散列表是只读的,初始化后不能修改,只能查找
ngx_int_t
ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts);
哈希查找函数

// 表面上好像是使用开放寻址法
// 但实际上是开链法,只是链表表现为紧凑的数组
// 用链表存储key相同的元素
// 参数hash是散列表结构体的指针,key则是根据散列方法算出来的散列关键字,
// name和len则表示实际关键字的地址与长度;
// ngx_hash_find返回散列表中关键字与name、len指定关键字完全相同的槽中,
// ngx_hash_elt_t结构体中value成员所指向的用户数据;
// 如果ngx_hash_find没有查询到这个元素,就会返回NULL;
void *ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len);
// 对于关键字为“*.test.com”这样带前置通配符的情况,建立了一个专用的前置通配符散列表,存储元素的关键字为com.test.;
// 若要检索smtp.test.com是否匹配.test.com,可用ngx_hash_find_wc_head方法检索,
// ngx_hash_find_wc_head方法会把要查询的smtp.test.com转化为com.test.字符串再开始查询
void *ngx_hash_find_wc_head(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);
// 对于关键字为“www.test.*”这样带通配符的情况,建立一个后置通配符散列表,存储元素的关键字为www.test;
// 从而若要检索www.test.cn是否匹配www.test.*,可用ngx_hash_find_wc_tail函数检索,
// ngx_hash_find_wc_tail方法会把要查询的www.test.cn转化为www.test字符串再开始查询;
void *ngx_hash_find_wc_tail(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);
// 通用的散列表函询函数,1. 精确查询,2. 确定后向通配符,3. 确定前向通配符
void *ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key,
    u_char *name, size_t len);

【6】ngx_rbtree_t 数据结构

【6.0】红黑树相关知识点

【6.0.1】什么是二叉查找树

  • 二叉查找树或者是一棵空树,或者是具有下列性质的二叉树;
  • 1. 每个节点都有一个作为查找依据的关键码(key),所有节点的关键码互不相同;
  • 2. 左子树(如果存在)上所有节点的关键码都小于根节点的关键码;
  • 3. 右子树(如果存在)上所有节点的关键码都大于根节点的关键码;
  • 4. 左子树和右子树也是二叉查找树;
  • 自平衡二叉查找树,在不断地向二叉查找树中添加、删除节点时,二叉查找树自身通过形态的变换,始终保持着一定程度上的平衡

【6.0.2】红黑树概念与性质

红黑树是指每个节点都带有颜色属性的二叉查找树,其中颜色为红色或黑色;

除了二叉查找树的一般要求以外,对于红黑树还有如下的额外的特性

  • 特性1: 节点是红色或黑色;
  • 特性2: 根节点是黑色;
  • 特性3: 所有叶子节点都是黑色(叶子是NIL节点,也叫“哨兵”);
  • 特性4: 每个红色节点的两个子节点都是黑色(每个叶子节点到根节点的所有路径上不能有两个连续的红色节点);
  • 特性5: 从任一节点到其每个叶子节点的所有简单路径都包含相同数目的黑色节点;

这些特性加强了红黑树的关键性质:从根节点到叶子节点的最长可能路径长度不大于最短可能路径的两倍,这样这个树大致上就是平衡的了;

最短的可能路径都是黑色节点,最长的可能路径有交替的红色节点和黑色节点;

根据特性5可知,所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能大于其他路径长度的两倍;

【6.1】数据结构体

文件路径
src/core/ngx_rbtree.h
src/core/ngx_rbtree.c

// 红黑树的key类型,无符号整数
// 通常我们使用这个key类型
typedef ngx_uint_t  ngx_rbtree_key_t;

// 红黑树的key类型,有符号整数
typedef ngx_int_t   ngx_rbtree_key_int_t;


// 红黑树节点
typedef struct ngx_rbtree_node_s  ngx_rbtree_node_t;

// 红黑树节点
// 通常需要以侵入式的方式使用,即作为结构体的一个成员
// 在C语言里利用平坦内存特点,后面放自己的数据
// 使用宏offsetof(node, color)计算得到地址
// 参考ngx_http_limit_conn_module.c
//
// 一般都将ngx_rbtree_node_t节点结构体放在自定义数据类型的第1位,以方便类型的强制转换
struct ngx_rbtree_node_s {
    // 节点的key,用于二分查找,红黑树的排序主要依据key成员
    ngx_rbtree_key_t       key;

    // 左子节点
    ngx_rbtree_node_t     *left;

    // 右子节点
    ngx_rbtree_node_t     *right;

    // 父节点
    ngx_rbtree_node_t     *parent;

    // 节点的颜色
    // 根节点是黑色
    // 新插入的节点必定是红色
    u_char                 color;

    // 节点数据,只有一个字节,通常无意义
    // 由用户定义自己的数据复用
    u_char                 data;
};

// ngx_rbtree_t( 红黑树)在检索特定关键字时不再需要遍历容器,
// ngx_rbtree_t 容器在检索、插入、删除元素方面非常高效,且其针对各种类型的数据的平均时间都很优异;
// ngx_rbtree_t 还支持范围查询,支持高效地遍历所有元素;
// 定义红黑树结构
typedef struct ngx_rbtree_s  ngx_rbtree_t;

// 插入红黑树的函数指针
// 为解决不同节点含有相同关键字的元素冲突问题,红黑树设置了
// ngx_rbtree_insert_pt 函数指针,从而可灵活地添加冲突元素
typedef void (*ngx_rbtree_insert_pt) (ngx_rbtree_node_t *root,
    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);

// 定义红黑树结构
struct ngx_rbtree_s {
    // 必须的根节点
    ngx_rbtree_node_t     *root;

    // 哨兵节点,通常就是root,用于标记查找结束
    ngx_rbtree_node_t     *sentinel;

    // 节点的插入方法
    // 常用的是ngx_rbtree_insert_value、ngx_rbtree_insert_timer_value
    ngx_rbtree_insert_pt   insert;
};

【6.2】相关方法

红黑树初始化操作

// 简单的函数宏,检查颜色
#define ngx_rbt_red(node)               ((node)->color = 1)
#define ngx_rbt_black(node)             ((node)->color = 0)
#define ngx_rbt_is_red(node)            ((node)->color)
#define ngx_rbt_is_black(node)          (!ngx_rbt_is_red(node))
#define ngx_rbt_copy_color(n1, n2)      (n1->color = n2->color)

/* a sentinel must be black */

// 哨兵节点颜色是黑的
#define ngx_rbtree_sentinel_init(node)  ngx_rbt_black(node)

// 初始化红黑树,最初根节点就是哨兵节点
ngx_rbtree_init(tree, s, i)
旋转操作

// 左旋
static ngx_inline void ngx_rbtree_left_rotate(ngx_rbtree_node_t **root,
    ngx_rbtree_node_t *sentinel, ngx_rbtree_node_t *node);
// 右旋
static ngx_inline void ngx_rbtree_right_rotate(ngx_rbtree_node_t **root,
    ngx_rbtree_node_t *sentinel, ngx_rbtree_node_t *node);
插入操作

// 获取红黑树键值最小的节点
// 在红黑树里查找最小值
// 二叉树,必定是最左边的节点
static ngx_inline ngx_rbtree_node_t *
ngx_rbtree_min(ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);

// 向红黑树插入一个节点
// 插入后旋转红黑树保持平衡
/* 插入节点的步骤:
 * 1、首先按照二叉查找树的插入操作插入新节点;
 * 2、然后把新节点着色为红色(避免破坏红黑树性质5);
 * 3、为维持红黑树的性质,调整红黑树的节点(着色并旋转),使其满足红黑树的性质;
 */
void ngx_rbtree_insert(ngx_rbtree_t *tree, ngx_rbtree_node_t *node);

// 普通红黑树插入函数
// 向红黑树添加数据节点,每个数据节点的关键字是唯一的
// 这里只是将节点插入到红黑树中,并没有判断是否满足红黑树的性质
void ngx_rbtree_insert_value(ngx_rbtree_node_t *root, ngx_rbtree_node_t *node,
    ngx_rbtree_node_t *sentinel);
	
// 定时器红黑树专用插入函数
// 向红黑树添加数据节点,每个数据节点的关键字表示时间或时间差
void ngx_rbtree_insert_timer_value(ngx_rbtree_node_t *root,
    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
删除操作

// 在红黑树里删除一个节点
// 删除节点后旋转红黑树保持平衡
void ngx_rbtree_delete(ngx_rbtree_t *tree, ngx_rbtree_node_t *node);