动态数组

ngx_array_t 表示一块连续的内存,其中存放着数组元素,概念上和原始数组很接近

  1. // 定义在 core/ngx_array.h
  2. typedef struct
  3. {
  4. void * elts; // 数组的内存位置,即数组首地址
  5. ngx_uint_t nelts; // 数组当前的元素数量
  6. size_t size; // 数组元素的大小
  7. ngx_uint_t nalloc; // 数组可容纳的最多元素容量
  8. ngx_pool_t * pool; // 数组可使用的内存池
  9. }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 就可以访问数组里的元素,不过需要转换为实际元素类型

  1. auto p = reinterpret_cast<T*>(arr.elts);
  2. cout<<p[0]<<endl;

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

  1. // 使用内存池 p 创建一个可容纳 n 个大小为 size 元素的数组,即分配了一块 n*size 大小的内存块
  2. ngx_array_t * ngx_array_create(ngx_pool_t * p, ngx_uint_t n, size_t size);
  3. // 销毁动态数组,归还分配的内存
  4. void ngx_array_destory(ngx_array_t * a);
  5. // 向数组添加元素,它们返回一个 void* 指针(可添加元素的位置),需要转换类型才能再操作
  6. // 不直接使用 elts 操作的原因是防止越界,函数内部会检查当前数组容量自动扩容
  7. void * ngx_array_push(ngx_array_t * a);
  8. void * ngx_array_push_n(ngx_array_t * a, ngx_uint_t n);

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


单向链表

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

  1. // 定义在 core/ngx_list.h
  2. struct ngx_list_part_s
  3. {
  4. void * elts; // 数组元素指针
  5. ngx_uint_t nelts; // 数组里的元素数量
  6. ngx_list_part_t * next; // 下个节点指针
  7. };

ngx_list_t 定义了链表,实际上是 头结点 + 元信息:

  1. // 定义在 core/ngx_list.h
  2. typedef struct
  3. {
  4. ngx_list_part_t * last; // 尾指针
  5. ngx_list_part_t * part; // 链表头结点
  6. size_t size; // 链表存储元素的大小
  7. ngx_uint_t nalloc; // 每个节点能够存储元素的数量
  8. ngx_pool_t * pool; // 链表使用的内存池
  9. } ngx_list_t;

链表里的每一个节点就是一个简化的 ngx_array_t 数组结构

  1. // 使用内存池创建链表,每个节点可容纳n个大小为size的元素
  2. ngx_list_t * ngx_list_create(ngx_pool_t * pool, ngx_uint_t n, size_t size);
  3. // 向链表里添加元素,返回的指针需要转型赋值
  4. void * ngx_list_push(ngx_list_t * list);

eg.

  1. part = &list.part; // 获取头结点
  2. data = part->elts; // 获取节点内数组地址
  3. for(i = 0; ; i++) // 遍历链表
  4. {
  5. if(i >= part->nelts) // 检查数组越界
  6. {
  7. if(part->next == NULL) // 检查是否到链表尾
  8. {
  9. break;
  10. }
  11. part = part->next; // 跳至下一个节点
  12. data = part->data; // 下一个节点的数组地址
  13. i = 0;
  14. }
  15. ... data[i] ... // 在本节点访问元素
  16. }

双端队列

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

  1. // 定义在 core/ngx_queue.h
  2. struct ngx_queue_s
  3. {
  4. ngx_queue_t * prev; // 前驱指针
  5. ngx_queue_t * next; // 后继指针
  6. };

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

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

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

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

  1. // 函数宏 ngx_queue_init() 初始化头结点,把两个指针指向自身
  2. #define ngx_queue_init(q) \
  3. (q)->prev = q; \
  4. (q)->next = q
  5. // 函数宏 ngx_queue_sentinel() 返回节点自身,对于头结点就相当于哨兵
  6. #define ngx_queue_sentinel(h) (h)
  7. // ngx_queue_empty() 检查头结点的前驱指针,判断是否为空队列
  8. #define ngx_queue_empty(h) \
  9. (h == (h)->prev)
  10. // 函数宏 ngx_queue_insert_head() 和 ngx_queue_insert_tail() 用来向头尾插入数据节点
  11. #define ngx_queue_insert_head(h, x)
  12. #define ngx_queue_insert_tail(h, x)
  13. // 函数宏 ngx_queue_head() 和 ngx_queue_last() 获取队列的头尾指针
  14. // 可以用来实现队列正向或反向遍历,直到遇到头结点 ngx_queue_sentinel()
  15. #define ngx_queue_head(h) (h)->next
  16. #define ngx_queue_last(h) (h)->prev
  17. // 函数 ngx_queue_sort() 使用一个比较函数指针对队列元素排序,效率不是很高
  18. void ngx_queue_sort(ngx_queue_t * queue,
  19. ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *));

数据节点操作

  1. // 在节点的后面插入数据,它其实就是 ngx_queue_insert_head
  2. #define ngx_queue_insert_after ngx_queue_insert_head
  3. // 删除节点,实际上只是调整了节点的指针,把节点从队列中摘除
  4. #define ngx_queue_remove(x) \
  5. (x)->next->prev = (x)->prev;\
  6. (x)->prev->next = (x)->next;
  7. // 获取节点数据
  8. #define ngx_queue_data(q, type, link) \
  9. (type *)((u_char *) q - offsetof(type, link)) // 返回结构体指针(offsetof是一个宏,计算结构里成员的偏移量)

可以把双端队列分解为 节点、迭代器和队列容器三个概念:

节点保存数据,迭代器遍历数据,而队列容器则是头节点。

这三个概念可以使用C++封装成不同的类,达到解耦的目的


红黑树

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

  1. // 定义在 core/ngx_rbtree.h
  2. typedef ngx_uint_t ngx_rbtree_key_t;
  3. typedef ngx_int_t ngx_rbtree_key_int_t;
  4. struct ngx_rbtree_node_s
  5. {
  6. ngx_rbtree_key_t key; // 红黑树的键,用于排序比较
  7. ngx_rbtree_node_t * left; // 左节点指针
  8. ngx_rbtree_node_t * right; // 右结点指针
  9. ngx_rbtree_node_t * parent; // 父节点指针
  10. u_char color; // 1 红色 / 0 黑色
  11. u_char data; // 节点数据,只有一字节,通常无意义
  12. };

与 ngx_queue_t 一样,ngx_rbtree_node_t 也要作为结构体的一个成员,以侵入方式来使用

例如保存字符串的红黑树节点:

  1. typedef struct
  2. {
  3. ngx_rbtree_node_t node; // 红黑树节点,不必是第一个成员
  4. ngx_str_t str; // 节点的其他信息
  5. } ngx_str_node_t;
  6. // 节点的插入方法,函数指针类型
  7. typedef void (*ngx_rbtree_insert_pt) (ngx_rbtree_node_t * root, ngx_rbtree_node_t, ngx_rbtree_sentinel);
  8. struct ngx_rbtree_s
  9. {
  10. ngx_rbtree_node_t * root; // 红黑树的根节点
  11. ngx_rbtree_node_t * sentinel; // 哨兵节点,相当于空指针、空对象
  12. ngx_rbtree_insert_pt insert; // 节点的插入方法
  13. };

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

  1. // 红黑树键值是标准的 uint/int
  2. void ngx_rbtree_insert_value(root, node, sentinel);
  3. // 定时器红黑树专用插入函数,键值是毫秒值
  4. void ngx_rbtree_insert_timer_value(...);
  5. // 字符串红黑树专用插入函数,键值是字符串的hash值
  6. void ngx_str_rbtree_insert_value(...);

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

  1. void ngx_rbtree_insert_value(...) // 插入标准的红黑树,键值是整数
  2. {
  3. ngx_rbtree_node_t ** p; // 树节点指针
  4. for(;;)
  5. { // 比较当前节点与插入节点,选择走左/右
  6. p = (node->key < temp->key) ? &temp->left : &temp->right;
  7. if(*p == sentinel) // 直到遇到哨兵节点结束
  8. break;
  9. temp = *p; // 移动当前指针
  10. }
  11. *p = node; // 找到位置,插入
  12. node->parent = temp;
  13. node->left = sentinel;
  14. node->right = sentinel;
  15. ngx_rbt_red(node);
  16. }
  1. // 初始化宏,初始化后红黑树中仅有一个哨兵节点 s ,同时也是根节点
  2. #define ngx_rbtree_init(tree, s, i) // tree 使用 s 作为哨兵节点,插入方法是 i
  3. // 红黑树的插入
  4. void ngx_rbtree_insert(ngx_rbtree * tree, ngx_rbtree_node_t * node);
  5. // 红黑树的删除
  6. void ngx_rbtree_delete(ngx_rbtree * tree, ngx_rbtree_node_t * node);
  • 操作后若树的平衡性被破坏会自动旋转以保持平衡
  1. // 查找最小节点,顺着指针找最左边的节点
  2. ngx_rbtree_node_t * ngx_rbtree_min()

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

  1. // Nginx还提供查找下一个节点的功能,利用它可以实现正序遍历红黑树
  2. ngx_rbtree_node_t * ngx_rbtree_next(ngx_rbtree_t * tree, ngx_rbtree_node_t * node);
  3. // 对于常用的字符串红黑树,Nginx提供了专用的查找函数,它可以在树里找到任意字符串,不存在则返回 nullptr
  4. ngx_str_node_t * ngx_str_rbtree_lookup(*rbtree, *name, hash);

缓冲区

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

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

  1. // ngx_buf_t 表示一个单块的缓冲区,既可以是内存也可以是文件
  2. // 它的结构分为两个部分:缓冲区信息和标志位信息
  3. typedef void * ngx_buf_tag_t;
  4. // 定义在 core/ngx_buf.h
  5. struct ngx_buf_s
  6. {
  7. u_char * pos; // 内存数据的起始位置
  8. u_char * last; // 内存数据的结束位置
  9. off_t file_pos; // 文件数据的起始偏移量
  10. off_t file_last; // 文件数据的结束偏移量
  11. u_char * start; // 内存数据的上界
  12. u_char * end; // 内存数据的下界
  13. ngx_buf_tag_t tag; // void* 指针,可以是任意关联对象
  14. ngx_file_t * file; // 存储数据的文件对象
  15. ... // 标志位信息
  16. };

因为Nginx的缓冲数据可能在内存或者磁盘文件中,所以 ngx_buf_t 使用 pos/last 和 file_pos/file_last 来指定数据在内存或文件中的具体位置,究竟数据在哪里则要靠后面的标志位信息来确定

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

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

  1. // ngx_buf_t 的标志位都是bool值,使用位域的方式节约内存
  2. struct ngx_buf_s
  3. { ... // 缓冲区信息
  4. unsigned temporary:1; // 内存块临时数据,可以修改
  5. unsigned memory:1; // 内存块数据,不允许修改
  6. unsigned mmap:1; // 内存映射数据,不允许修改
  7. unsigned in_file:1; // 缓冲区在文件里
  8. unsigned flush:1; // 要求Nginx立即输出本缓冲区
  9. unsigned sync:1; // 要求Nginx同步操作本缓冲区
  10. unsigned last_buf:1; // 最后一块缓冲区
  11. unsigned last_in_chain:1;// 链里最后一块缓冲区
  12. unsigned temp_file:1; // 缓冲区在临时文件里
  13. };

其中 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)

  1. // 从内存池里分配一块 size 大小的缓冲区
  2. ngx_buf_t * ngx_create_temp_buf(ngx_pool_t * pool, size_t size);

函数返回的 ngx_buf_t 结构内成员都已经初始化好了, pos 和 last 都指向内存块的首位置,表示空缓冲区,而temporary 标志位是1。

  1. // 从内存池创建一个 ngx_buf_t 结构,然后手工指定它的成员,关联到已经存在的内存
  2. #define ngx_alloc_buf(pool) ngx_palloc(pool, sizeof(ngx_buf_t))
  3. #define ngx_calloc_buf(pool) ngx_pcalloc(pool, sizeof(ngx_buf_t)
  4. // 检查缓冲区是否在内存里
  5. #define ngx_buf_in_memory(b) (b->temporary || b->memory || b->mmap)
  6. #define ngx_buf_in_memory_only(b) (ngx_buf_in_memory(b) && !b->in_file)
  7. // 判断起控制作用的特殊缓冲区
  8. #define ngx_buf_special(b)
  9. // 计算缓冲区大小,根据是否在内存里使用恰当的指针
  10. #define ngx_buf_size(b)
  11. // 拷贝内存,返回拷贝数据后的终点位置,在连续复制多段数据时很方便
  12. // 定义在 core/ngx_string.h
  13. #define ngx_cpymem(dst, src, n) (((u_char*)memcpy(dst, src, n)) + (n))
  14. // 设置内存
  15. #define ngx_memzero(buf, n) (void) memset(buf, 0, n)
  16. #define ngx_memset(buf, c, n) (void) memset(buf, c, n)

数据块链

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

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

  1. // 定义在 core/ngx_buf.h
  2. struct ngx_chain_s
  3. {
  4. ngx_buf_t * buf; // 缓冲区指针
  5. ngx_chain_t * next; // 下一个链表节点
  6. };
  7. // 从内存池里获取 ngx_chain_t 对象
  8. ngx_chain_t * ngx_alloc_chain_link(ngx_pool_t * pool); // 内部调用 ngx_palloc(),获得的对象buf/next可能是任意值
  9. // 释放 ngx_chain_t 对象
  10. #define ngx_free_chain(pool, cl)

由于 ngx_chain_t 在 Nginx 里应用的很频繁,所以 Nginx 对此进行了优化。

在内存池里保存了一个空闲 ngx_chain_t 链表,分配时从这个链表中摘取,释放时再挂上去

  1. typedef struct
  2. {
  3. ngx_int_t num; // 缓冲区的数量
  4. size_t size; // 缓冲区的大小
  5. } ngx_bufs_t; // 创建链表的参数结构
  6. // 创建多个缓冲区,返回一个链接好的数据块链表
  7. 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 的配置解析环节,保存配置文件里成对的配置。

  1. // 定义在 core/ngx_string.h
  2. typedef struct
  3. {
  4. ngx_str_t key;
  5. ngx_str_t value;
  6. } ngx_keyval_t;

在 Nginx 里,通常使用 ngx_array_t 来存储 ngx_keyval_t,相当于

  1. typedef NgxArray<ngx_keyval_t> NgxKvArray;

散列表键值对

  1. // 定义在 core/ngx_hash.h
  2. typedef struct
  3. {
  4. ngx_uint_t hash; // 散列(哈希)标记
  5. ngx_str_t key; // 键
  6. ngx_str_t value; // 值
  7. u_char * lowcase_key; // key 的小写字符串指针
  8. } ngx_table_elt_t;

ngx_table_elt_t 主要用来表示HTTP头部信息,例如

"Server:nginx" 这样的字符串对应到 ngx_table_elt_t 就是 key = "Server",value = "nginx"

成员 hash 是一个散列标记,Nginx使用它在散列表中快速查找数据,

可以简单地把它置为非零值(通常为1),也可以使用下面两个函数计算散列值:

  1. ngx_uint_t ngx_hash_key(u_char * data, size_t len); // 计算散列值
  2. ngx_uint_t ngx_hash_key_lc(u_char * data, size_t len); // 小写后再计算

成员 lowcase_key 指向一个全小写的字符串,在大小写无关比较时可避免重复计算。

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

Nginx 在处理HTTP请求时使用 ngx_list_t 存储了HTTP 头部信息,相当于:

  1. typedef NgxList<ngx_table_elt_t> NgxHeaderList;

 

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

Nginx学习笔记(五):高级数据结构的更多相关文章

  1. C#可扩展编程之MEF学习笔记(五):MEF高级进阶

    好久没有写博客了,今天抽空继续写MEF系列的文章.有园友提出这种系列的文章要做个目录,看起来方便,所以就抽空做了一个,放到每篇文章的最后. 前面四篇讲了MEF的基础知识,学完了前四篇,MEF中比较常用 ...

  2. (转)Qt Model/View 学习笔记 (五)——View 类

    Qt Model/View 学习笔记 (五) View 类 概念 在model/view架构中,view从model中获得数据项然后显示给用户.数据显示的方式不必与model提供的表示方式相同,可以与 ...

  3. matlab学习笔记9 高级绘图命令_2 图形的高级控制_视点控制和图形旋转_色图和颜色映像_光照和着色

    一起来学matlab-matlab学习笔记9 高级绘图命令_2 图形的高级控制_视点控制和图形旋转_色图和颜色映像_光照和着色 觉得有用的话,欢迎一起讨论相互学习~Follow Me 参考书籍 < ...

  4. Hadoop学习笔记(7) ——高级编程

    Hadoop学习笔记(7) ——高级编程 从前面的学习中,我们了解到了MapReduce整个过程需要经过以下几个步骤: 1.输入(input):将输入数据分成一个个split,并将split进一步拆成 ...

  5. java之jvm学习笔记五(实践写自己的类装载器)

    java之jvm学习笔记五(实践写自己的类装载器) 课程源码:http://download.csdn.net/detail/yfqnihao/4866501 前面第三和第四节我们一直在强调一句话,类 ...

  6. Nginx学习笔记4 源码分析

    Nginx学习笔记(四) 源码分析 源码分析 在茫茫的源码中,看到了几个好像挺熟悉的名字(socket/UDP/shmem).那就来看看这个文件吧!从简单的开始~~~ src/os/unix/Ngx_ ...

  7. Learning ROS for Robotics Programming Second Edition学习笔记(五) indigo computer vision

    中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 Learning ROS for Robotics Pr ...

  8. Nginx学习笔记~目录索引

    回到占占推荐博客索引 前几天整理了<Docker的学习笔记索引>,受到了很多朋友的关注,今天把Nginx的文章也整理一下,以后将永久更新,像大叔之前的<EF文章系列>,< ...

  9. Typescript 学习笔记五:类

    中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...

  10. ES6学习笔记<五> Module的操作——import、export、as

    import export 这两个家伙对应的就是es6自己的 module功能. 我们之前写的Javascript一直都没有模块化的体系,无法将一个庞大的js工程拆分成一个个功能相对独立但相互依赖的小 ...

随机推荐

  1. elasticsearch使用BulkProcessor批量入库数据

    在解决es入库问题上,之前使用过rest方式,经过一段时间的测试发现千万级别的数据会存在10至上百条数据的丢失问题, 在需要保证数据的准确性的场景下,rest方式并不能保证结果的准确性,因此采用了el ...

  2. win10 'make' 不是内部或外部命令

    win10 解决“ 'g++' 不是内部或外部命令,也不是可运行的程序或批处理文件”的问题 https://www.jianshu.com/p/9bffbaf12bed windows下提示make不 ...

  3. 沃顿商学院的MBA课程

    沃顿商学院的MBA课程,分为必修课和选修课两部分 (一)必修课: 1.领导力:团队合作和领导力的基础 2.营销学:营销管理 3.微观经济学:微观经济基础 4.经济学:管理经济学的高级话题 5.统计学: ...

  4. cache-control: max-age=1,s-maxage=1

    cache-control: max-age=1,s-maxage=1

  5. JS数组常见方法的深浅拷贝分类

    一.涉及浅拷贝类方法,会改变原数组 1,pop():   删除 arrayObject 的最后一个元素,把数组长度减 1,并且返回它删除的元素的值.如果数组已经为空,则 pop() 不 改变数组,并返 ...

  6. Java基础 import 要在所有的class前面

        JDK :OpenJDK-11      OS :CentOS 7.6.1810      IDE :Eclipse 2019‑03 typesetting :Markdown   code ...

  7. android studio 运行按钮为灰色的解决办法之一

    sync project with gradle files按钮(如下图)同步一下就好了 3.2的  3.3同步按钮变成了一只大象+箭头

  8. 异常检测-基于孤立森林算法Isolation-based Anomaly Detection-2-实现

    参考https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.IsolationForest.html#sklearn.en ...

  9. WPF-支持异步操作的ObservableCollection-AsyncObservableCollection

    在进行WPF开发过程中,需要从一个新的线程中操作ObservableCollection,结果程序抛出一个NotSupportedException的错误 public class AsyncObse ...

  10. Opencv拉普拉斯算子做图像增强

    Opencv拉普拉斯算子——图像增强 #include <iostream> #include <opencv2/opencv.hpp> using namespace std ...