Nginx学习笔记 —— 高级数据结构

动态数组

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;

elts 就是原始的数组,定义成 void*,使用时应强转成相应的类型
nelts 相当于 vector.size();
size 相当于 sizeof(T);
nalloc 相当于 vector.capacity();
pool 数组使用的内存池,相当于 vector 的 allocator

数组里的元素不断增加,当 nelts > nalloc 时将引起数组扩容,ngx_array_t 会向内存池 pool 申请一块两倍原大小的空间————这个策略和 std::vector 是一样的

但 ngx_array_t 扩容成本太高,它需要重新分配内存并且将数据拷贝,所以最好一次性分配足够的空间,避免动态扩容

操作函数
使用 ngx_array_t.elts 就可以访问数组里的元素,不过需要转换为实际元素类型

auto p = reinterpret_cast(arr.elts);
cout<0]<

ngx_array_t 没有越界检查,需要自行确保数组索引的有效性

// 使用内存池 p 创建一个可容纳 n 个大小为 size 元素的数组,即分配了一块 n*size 大小的内存块
ngx_array_t * ngx_array_create(ngx_pool_t * p, ngx_uint_t n, size_t size);

// 销毁动态数组,归还分配的内存
void ngx_array_destory(ngx_array_t * a);

// 向数组添加元素,它们返回一个 void* 指针(可添加元素的位置),需要转换类型才能再操作
// 不直接使用 elts 操作的原因是防止越界,函数内部会检查当前数组容量自动扩容 
void * ngx_array_push(ngx_array_t * a);

void * ngx_array_push_n(ngx_array_t * a, ngx_uint_t n);

清空数组可以直接置 nelts 为 0, 但之前分配的内存并不会释放,还可以用来存储数据


单向链表

Nginx 的单向链表 ngx_list_t 融合了 ngx_array_t 的特点,在一个节点里存储多个元素,降低了链表的存储成本

// 定义在 core/ngx_list.h
struct ngx_list_part_s
{
    void *              elts;       // 数组元素指针
    ngx_uint_t          nelts;      // 数组里的元素数量
    ngx_list_part_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 数组结构

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

// 向链表里添加元素,返回的指针需要转型赋值
void * ngx_list_push(ngx_list_t * list);

eg.

part = &list.part;              // 获取头结点
data = part->elts;              // 获取节点内数组地址

for(i = 0; ; i++)               // 遍历链表
{
    if(i >= part->nelts)        // 检查数组越界
    {
        if(part->next == NULL)  // 检查是否到链表尾
        {
            break;
        }

        part = part->next;      // 跳至下一个节点
        data = part->data;      // 下一个节点的数组地址
        i = 0;
    }

    ... data[i] ...             // 在本节点访问元素
}

双端队列

在Nginx 里它被实现为双向循环链表 ngx_queue_t,是侵入式容器

// 定义在 core/ngx_queue.h
struct ngx_queue_s
{
    ngx_queue_t *   prev;       // 前驱指针
    ngx_queue_t *   next;       // 后继指针
};

结构体需要添加它作为成员,为数据结构增加了双向链表的指针

struct X                        // 一个可放入队列的数据结构
{
    int x = 0;                  // 携带的数据
    ngx_queue_t queue;          // ngx_queue_t 成员,名字任意
};

结构体内可以有不止一个 ngx_queue_t 成员,这意味着它可以同时属于多个不同的双向链表

ngx_queue_t 使用一个头结点来表示队列,这个头节点可以是单纯的 ngx_queue_t 结构

// 函数宏 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_insert_head
#define ngx_queue_insert_after  ngx_queue_insert_head

// 删除节点,实际上只是调整了节点的指针,把节点从队列中摘除
#define ngx_queue_remove(x)     \
    (x)->next->prev = (x)->prev;\
    (x)->prev->next = (x)->next;

// 获取节点数据
#define ngx_queue_data(q, type, link)   \
    (type *)((u_char *) q - offsetof(type, link))   // 返回结构体指针(offsetof是一个宏,计算结构里成员的偏移量)

可以把双端队列分解为 节点、迭代器和队列容器三个概念:
节点保存数据,迭代器遍历数据,而队列容器则是头节点。
这三个概念可以使用C++封装成不同的类,达到解耦的目的


红黑树

在Nginx里红黑树主要用在事件机制里的定时器,检查连接超时,此外还在 reslover、cache里用于快速查找

// 定义在 core/ngx_rbtree.h
typedef ngx_uint_t      ngx_rbtree_key_t;
typedef ngx_int_t       ngx_rbtree_key_int_t;

struct ngx_rbtree_node_s
{
    ngx_rbtree_key_t        key;        // 红黑树的键,用于排序比较
    ngx_rbtree_node_t *     left;       // 左节点指针
    ngx_rbtree_node_t *     right;      // 右结点指针
    ngx_rbtree_node_t *     parent;     // 父节点指针
    u_char                  color;      // 1 红色 / 0 黑色
    u_char                  data;       // 节点数据,只有一字节,通常无意义
};

与 ngx_queue_t 一样,ngx_rbtree_node_t 也要作为结构体的一个成员,以侵入方式来使用
例如保存字符串的红黑树节点:

typedef struct
{
    ngx_rbtree_node_t   node;   // 红黑树节点,不必是第一个成员
    ngx_str_t           str;    // 节点的其他信息
} ngx_str_node_t;

// 节点的插入方法,函数指针类型
typedef void (*ngx_rbtree_insert_pt) (ngx_rbtree_node_t * root, ngx_rbtree_node_t, ngx_rbtree_sentinel);

struct ngx_rbtree_s
{
    ngx_rbtree_node_t *     root;           // 红黑树的根节点
    ngx_rbtree_node_t *     sentinel;       // 哨兵节点,相当于空指针、空对象
    ngx_rbtree_insert_pt    insert;         // 节点的插入方法
};

insert决定了红黑树的节点插入操作,用户可以针对不同的节点类型实现不同的插入方法,但必须符合 ngx_rbtree_insert_pt 的定义

// 红黑树键值是标准的 uint/int
void ngx_rbtree_insert_value(root, node, sentinel);

// 定时器红黑树专用插入函数,键值是毫秒值
void ngx_rbtree_insert_timer_value(...);

// 字符串红黑树专用插入函数,键值是字符串的hash值
void ngx_str_rbtree_insert_value(...);

参考这三个函数可以实现自己的插入函数:

void ngx_rbtree_insert_value(...)       // 插入标准的红黑树,键值是整数
{
    ngx_rbtree_node_t ** p;             // 树节点指针
    for(;;)
    {                                   // 比较当前节点与插入节点,选择走左/右
        p = (node->key < temp->key) ? &temp->left : &temp->right;

        if(*p == sentinel)              // 直到遇到哨兵节点结束
            break;

        temp = *p;                      // 移动当前指针
    }

    *p = node;                          // 找到位置,插入
    node->parent = temp;
    node->left   = sentinel;
    node->right  = sentinel;
    ngx_rbt_red(node);
}
// 初始化宏,初始化后红黑树中仅有一个哨兵节点 s ,同时也是根节点
#define ngx_rbtree_init(tree, s, i)     // tree 使用 s 作为哨兵节点,插入方法是 i

// 红黑树的插入
void ngx_rbtree_insert(ngx_rbtree * tree, ngx_rbtree_node_t * node);
// 红黑树的删除
void ngx_rbtree_delete(ngx_rbtree * tree, ngx_rbtree_node_t * node);
  • 操作后若树的平衡性被破坏会自动旋转以保持平衡
// 查找最小节点,顺着指针找最左边的节点
ngx_rbtree_node_t * ngx_rbtree_min()

ngx_rbtree_min() 只会返回 ngx_rbtree_node_t* 类型,若想得到完整的结构体指针,则需要利用宏 offsetof 计算偏移量再强制类型转换

// Nginx还提供查找下一个节点的功能,利用它可以实现正序遍历红黑树
ngx_rbtree_node_t * ngx_rbtree_next(ngx_rbtree_t * tree, ngx_rbtree_node_t * node);

// 对于常用的字符串红黑树,Nginx提供了专用的查找函数,它可以在树里找到任意字符串,不存在则返回 nullptr
ngx_str_node_t * ngx_str_rbtree_lookup(*rbtree, *name, hash);

缓冲区

作为web服务器,Nginx需要频繁收发处理大量的数据,这些数据有时是连续的内存块,有时是分散的内存块,甚至有时数据过大,内存无法存放,只能保存成磁盘文件

ngx_str_t 结构可以表示内存块,但不能应对复杂的情景,所以Nginx实现了 ngx_buf_t 和 ngx_chain_t

// ngx_buf_t 表示一个单块的缓冲区,既可以是内存也可以是文件
// 它的结构分为两个部分:缓冲区信息和标志位信息
typedef void *      ngx_buf_tag_t;

// 定义在 core/ngx_buf.h
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 来指定数据在内存或文件中的具体位置,究竟数据在哪里则要靠后面的标志位信息来确定

start 和 end 两个成员变量标记了数据所在内存块的边界,如果内存块是可修改的,那么在操作时必须防止越界

tag 通常指向的是使用该缓冲区的对象

// 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 表示整个处理过程的最后一块缓冲区,标志着 TCP/HTTP 请求处理的结束;

而 last_in_chain 表示当前数据块链(ngx_chain_t)里的最后一块,之后可能还有数据需要处理。

从 ngx_buf_t 的定义可以看到,一个有数据的缓冲区不是在内存里就是在文件里,所以内存标志位成员变量(temporary/memory/mmap)和文件标志成员变量(in_file/temp_file)不能全为0,否则Nginx 会认为这是个特殊(special)或无效的缓冲区。

如果缓冲区既不在内存也不在文件里,那么它就不含有有效数据,只起到控制作用,例如刷新(flush)或者同步(sync)

// 从内存池里分配一块 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 结构,然后手工指定它的成员,关联到已经存在的内存
#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)

// 判断起控制作用的特殊缓冲区
#define ngx_buf_special(b)

// 计算缓冲区大小,根据是否在内存里使用恰当的指针
#define ngx_buf_size(b)

// 拷贝内存,返回拷贝数据后的终点位置,在连续复制多段数据时很方便
// 定义在 core/ngx_string.h
#define ngx_cpymem(dst, src, n)     (((u_char*)memcpy(dst, src, n)) + (n))

// 设置内存
#define ngx_memzero(buf, n)         (void) memset(buf, 0, n)
#define ngx_memset(buf, c, n)       (void) memset(buf, c, n)

数据块链

在处理HTTP/TCP请求时会经常创建多个缓冲区来存放数据,Nginx 把缓冲区块简单地组织为一个单向链表

ngx_chain_t 把多个分散的 ngx_buf_t 连接为一个顺序的数据块链:

// 定义在 core/ngx_buf.h
struct ngx_chain_s
{
    ngx_buf_t *     buf;        // 缓冲区指针
    ngx_chain_t *   next;       // 下一个链表节点
};

// 从内存池里获取 ngx_chain_t 对象
ngx_chain_t * ngx_alloc_chain_link(ngx_pool_t * pool);  // 内部调用 ngx_palloc(),获得的对象buf/next可能是任意值

// 释放 ngx_chain_t 对象
#define ngx_free_chain(pool, cl)

由于 ngx_chain_t 在 Nginx 里应用的很频繁,所以 Nginx 对此进行了优化。
在内存池里保存了一个空闲 ngx_chain_t 链表,分配时从这个链表中摘取,释放时再挂上去

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_chain_t 分解为节点、迭代器和容器三个概念,不同C++类封装不同的操作


键值对

键值对是一种映射关系,C++使用std::pair 来表示,并且使用 std::map / std::unordered_map 存储这样的数据;

而 Nginx 提供两个结构:ngx_keyval_t 和 ngx_table_elt_t ,再结合 ngx_array_t 或 ngx_list_t 应用在不同的场合

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;

散列表键值对

// 定义在 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” 这样的字符串对应到 ngx_table_elt_t 就是 key = “Server”,value = “nginx”

成员 hash 是一个散列标记,Nginx使用它在散列表中快速查找数据,
可以简单地把它置为非零值(通常为1),也可以使用下面两个函数计算散列值:

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_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>    NgxHeaderList;

C++封装实现:https://github.com/chen892704/Nginx-Learning

你可能感兴趣的:(Nginx)