Categories: Nginx 教程

Nginx 基本数据结构

基本数据结构

Nginx 的作者为追求极致的高效,自己实现了很多颇具特色的 Nginx 风格的数据结构以及公共函数。比如,Nginx 提供了带长度的字符串,根据编译器选项优化过的字符串拷贝函数 ngx_copy 等。所以,在我们写 Nginx 模块时,应该尽量调用 Nginx 提供的 api,尽管有些 api 只是对 glibc 的宏定义。本节,我们介绍 string、list、buffer、chain 等一系列最基本的数据结构及相关api的使用技巧以及注意事项。

ngx_str_t

在 Nginx 源码目录的 src/core 下面的 ngx_string.h|c 里面,包含了字符串的封装以及字符串相关操作的 api。Nginx 提供了一个带长度的字符串结构 ngx_str_t,它的原型如下:

    typedef struct {
        size_t      len;
        u_char     *data;
    } ngx_str_t;

在结构体当中,data 指向字符串数据的第一个字符,字符串的结束用长度来表示,而不是由\0来表示结束。所以,在写 Nginx 代码时,处理字符串的方法跟我们平时使用有很大的不一样,但要时刻记住,字符串不以\0结束,尽量使用 Nginx 提供的字符串操作的 api 来操作字符串。

那么,Nginx 这样做有什么好处呢?首先,通过长度来表示字符串长度,减少计算字符串长度的次数。其次,Nginx 可以重复引用一段字符串内存,data 可以指向任意内存,长度表示结束,而不用去 copy 一份自己的字符串(因为如果要以\0结束,而不能更改原字符串,所以势必要 copy 一段字符串)。我们在 ngx_http_request_t 结构体的成员中,可以找到很多字符串引用一段内存的例子,比如 request_line、uri、args 等等,这些字符串的 data 部分,都是指向在接收数据时创建 buffer 所指向的内存中,uri,args 就没有必要 copy 一份出来。这样的话,减少了很多不必要的内存分配与拷贝。

正是基于此特性,在 Nginx 中,必须谨慎的去修改一个字符串。在修改字符串时需要认真的去考虑:是否可以修改该字符串;字符串修改后,是否会对其它的引用造成影响。在后面介绍 ngx_unescape_uri 函数的时候,就会看到这一点。但是,使用 Nginx 的字符串会产生一些问题,glibc 提供的很多系统 api 函数大多是通过\0来表示字符串的结束,所以我们在调用系统 api 时,就不能直接传入 str->data 了。此时,通常的做法是创建一段 str->len + 1 大小的内存,然后 copy 字符串,最后一个字节置为\0。比较 hack 的做法是,将字符串最后一个字符的后一个字符 backup 一个,然后设置为\0,在做完调用后,再由 backup 改回来,但前提条件是,你得确定这个字符是可以修改的,而且是有内存分配,不会越界,但一般不建议这么做。接下来,看看 Nginx 提供的操作字符串相关的 api。

    #define ngx_string(str)     { sizeof(str) - 1, (u_char *) str }

ngx_string(str) 是一个宏,它通过一个以\0结尾的普通字符串 str 构造一个 Nginx 的字符串,鉴于其中采用 sizeof 操作符计算字符串长度,因此参数必须是一个常量字符串。

    #define ngx_null_string     { 0, NULL }

定义变量时,使用 ngx_null_string 初始化字符串为空字符串,符串的长度为 0,data 为 NULL。

    #define ngx_str_set(str, text)                                               
        (str)->len = sizeof(text) - 1; (str)->data = (u_char *) text

ngx_str_set 用于设置字符串 str 为 text,由于使用 sizeof 计算长度,故 text 必须为常量字符串。

    #define ngx_str_null(str)   (str)->len = 0; (str)->data = NULL

ngx_str_null 用于设置字符串 str 为空串,长度为 0,data 为 NULL。

上面这四个函数,使用时一定要小心,ngx_string 与 ngx_null_string 是“{}”格式的,故只能用于赋值时初始化,如:

    ngx_str_t str = ngx_string("hello world");
    ngx_str_t str1 = ngx_null_string;

如果向下面这样使用,就会有问题,这里涉及到c语言中对结构体变量赋值操作的语法规则,在此不做介绍。

    ngx_str_t str, str1;
    str = ngx_string("hello world");    // 编译出错
    str1 = ngx_null_string;                // 编译出错

这种情况,可以调用 ngx_str_set 与 ngx_str_null 这两个函数来做:

    ngx_str_t str, str1;
    ngx_str_set(&str, "hello world");    
    ngx_str_null(&str1);

按照 C99 标准,您也可以这么做:

    ngx_str_t str, str1;
    str  = (ngx_str_t) ngx_string("hello world");
    str1 = (ngx_str_t) ngx_null_string;

另外要注意的是,ngx_string 与 ngx_str_set 在调用时,传进去的字符串一定是常量字符串,否则会得到意想不到的错误(因为 ngx_str_set 内部使用了 sizeof(),如果传入的是 u_char*,那么计算的是这个指针的长度,而不是字符串的长度)。如:

   ngx_str_t str;
   u_char *a = "hello world";
   ngx_str_set(&str, a);    // 问题产生

此外,值得注意的是,由于 ngx_str_set 与 ngx_str_null 实际上是两行语句,故在 if/for/while 等语句中单独使用需要用花括号括起来,例如:

   ngx_str_t str;
   if (cond)
      ngx_str_set(&str, "true");     // 问题产生
   else
      ngx_str_set(&str, "false");    // 问题产生
   void ngx_strlow(u_char *dst, u_char *src, size_t n);

将 src 的前 n 个字符转换成小写存放在 dst 字符串当中,调用者需要保证 dst 指向的空间大于等于n,且指向的空间必须可写。操作不会对原字符串产生变动。如要更改原字符串,可以:

    ngx_strlow(str->data, str->data, str->len);
    ngx_strncmp(s1, s2, n)

区分大小写的字符串比较,只比较前n个字符。

    ngx_strcmp(s1, s2)

区分大小写的不带长度的字符串比较。

    ngx_int_t ngx_strcasecmp(u_char *s1, u_char *s2);

不区分大小写的不带长度的字符串比较。

    ngx_int_t ngx_strncasecmp(u_char *s1, u_char *s2, size_t n);

不区分大小写的带长度的字符串比较,只比较前 n 个字符。

u_char * ngx_cdecl ngx_sprintf(u_char *buf, const char *fmt, ...);
u_char * ngx_cdecl ngx_snprintf(u_char *buf, size_t max, const char *fmt, ...);
u_char * ngx_cdecl ngx_slprintf(u_char *buf, u_char *last, const char *fmt, ...);

上面这三个函数用于字符串格式化,ngx_snprintf 的第二个参数 max 指明 buf 的空间大小,ngx_slprintf 则通过 last 来指明 buf 空间的大小。推荐使用第二个或第三个函数来格式化字符串,ngx_sprintf 函数还是比较危险的,容易产生缓冲区溢出漏洞。在这一系列函数中,Nginx 在兼容 glibc 中格式化字符串的形式之外,还添加了一些方便格式化 Nginx 类型的一些转义字符,比如%V用于格式化 ngx_str_t 结构。在 Nginx 源文件的 ngx_string.c 中有说明:

    /*
     * supported formats:
     *    %[0][width][x][X]O        off_t
     *    %[0][width]T              time_t
     *    %[0][width][u][x|X]z      ssize_t/size_t
     *    %[0][width][u][x|X]d      int/u_int
     *    %[0][width][u][x|X]l      long
     *    %[0][width|m][u][x|X]i    ngx_int_t/ngx_uint_t
     *    %[0][width][u][x|X]D      int32_t/uint32_t
     *    %[0][width][u][x|X]L      int64_t/uint64_t
     *    %[0][width|m][u][x|X]A    ngx_atomic_int_t/ngx_atomic_uint_t
     *    %[0][width][.width]f      double, max valid number fits to %18.15f
     *    %P                        ngx_pid_t
     *    %M                        ngx_msec_t
     *    %r                        rlim_t
     *    %p                        void *
     *    %V                        ngx_str_t *
     *    %v                        ngx_variable_value_t *
     *    %s                        null-terminated string
     *    %*s                       length and string
     *    %Z                        
唐伯虎点蚊香

前端小白,想各位学习!

Share
Published by
唐伯虎点蚊香

Recent Posts

vue:页面注入js修改input值

一般会直接这样写: let z…

14 小时 ago

聊聊vue3中的defineProps

在Vue 3中,defineP…

1 周 ago

在 Chrome 中删除、允许和管理 Cookie

您可以选择删除现有 Cooki…

2 周 ago

自定义指令:聊聊vue中的自定义指令应用法则

今天我们来聊聊vue中的自定义…

3 周 ago

聊聊Vue中@click.stop和@click.prevent

一起来学下聊聊Vue中@cli…

4 周 ago

Nginx 基本操作:启动、停止、重启命令。

我们来学习Nginx基础操作:…

1 月 ago