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