代码改变世界

Nginx学习笔记(五):高级数据结构

2019-12-02 14:39  陈心朔  阅读(672)  评论(0编辑  收藏  举报

 

动态数组

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<T*>(arr.elts);
cout<<p[0]<<endl;

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