nginx源码初读(4)--让烦恼从数据结构开始(ngx_str)

nginx作者定义的ngx_str_t中,字符串并不是以传统c的’\0’结尾的,使用了长度len配合data来表示一个数据段。所以我们要尽量使用nginx自带的api进行操作,如果没有自带,那就自己写一个api来进行相关操作,而不要随便使用c自带的字符串处理函数,那样很可能会导致内存越界,而且从原则上来说,这是违规行为。

看看ngx_str_t被定义成了什么:

typedef struct {
    size_t      len;          // 字符数据的长度
    u_char     *data;         // 存储的字符数据
} ngx_str_t;

这样做自然是有道理的,尤其在这个内存占用十分“小气”的nginx里。首先,通过长度来表示字符串长度,降低了长度计算次数。其次,nginx可以重复引用一段字符串内存,data可以指向任意内存,长度表示结束,而不用去copy一份自己的字符串(因为如果要以’\0’结束,而不能更改原字符串,所以势必要copy一段字符串)。这样做减少了很多不必要的内存分配和占用,有效的降低了内存使用量和长度的计算次数。
例如,如果用户请求”GET /test?a=1 http/1.1\r\n”存储在内存0x1d0b0000,这时只需要把r->method_name设置为{ len=3, data=0x1d0b0000}就可以表示方法名”GET”,而不需要单独为method_name再分配内存冗余的存储字符串。

由以上特性可知,我们在nginx中,必须谨慎的修改字符串,需要认真考虑修改后是否会对其它引用造成影响。
如果非要使用libc的函数处理字符串,有两个方案:
1. copy到一个新的buffer里,加上’\0’
2. 把要使用的部分后一位改成0,使用完再改回来(要确定改了后面的东西不会有不良反应0.0)

赋值和初始化操作:

#define ngx_string(str)     { sizeof(str) - 1, (u_char *) str }
/* 使用sizeof将一个传统字符串转换为nginx专用形式,因为使用了sizeof,所以str必须为常量
 * 在传统c标准中只能用于做初始化时的赋值操作,普通的赋值是编译错误的(结构体的赋值操作)
 * 在C99标准中,可以这样来赋值:str=(ngx_str_t)ngx_string("hello world")
 */

#define ngx_null_string     { 0, NULL }
/* 用法同ngx_string,用于将字符串初始化为空 */

#define ngx_str_set(str, text)                                     \
    (str)->len = sizeof(text) - 1; (str)->data = (u_char *) text
#define ngx_str_null(str)   (str)->len = 0; (str)->data = NULL
/* 这两个函数就是直接调用的,用来给str赋值和重置的,str类型要求是指针,text必须为常量 
 * 这两个函数有一点要注意,因为它们是两个语句,并且没有用括号括住,所以在if等其中要用时要括住
   其实保持良好的习惯就ok,碰到if-else不管是不是单句都括住
 */

大小写转换函数:

#define ngx_tolower(c)      (u_char) ((c >= 'A' && c <= 'Z') ? (c | 0x20) : c)
#define ngx_toupper(c)      (u_char) ((c >= 'a' && c <= 'z') ? (c & ~0x20) : c)
/* 通过三元表达式实现字母的大小写转换 */

void
ngx_strlow(u_char *dst, u_char *src, size_t n)
{
    /* 将src的前n个字符转换成小写存放在dst字符串当中,需保证dst指向空间大于等于n且可写。*/
    while (n) {
        *dst = ngx_tolower(*src);
        dst++;
        src++;
        n--;
    }
}
/* 如果想要改变原字符串前n个字母为小写,可以ngx_strlow(str, str, n) */

字符串比较函数:

#define ngx_strncmp(s1, s2, n)  strncmp((const char *) s1, (const char *) s2, n)
/* 字符串比较前n个字符,调用了libc的函数,因为只要指定了n就没问题,注意参数类型是char*不是ngx_str */
#define ngx_strcmp(s1, s2)  strcmp((const char *) s1, (const char *) s2)
/* if (ngx_strcmp(var[i].data, "TZ") == 0
               || ngx_strncmp(var[i].data, "TZ=", 3) == 0) {
       goto tz_found;
   }
 */

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); 
/* 不区分大小写的字符串比较 */

ngx_int_t ngx_rstrncmp(u_char *s1, u_char *s2, size_t n);
ngx_int_t ngx_rstrncasecmp(u_char *s1, u_char *s2, size_t n);
/* 从n-1开始往前比较字符串,加case的是不区分大小写的比较 */

#define ngx_memcmp(s1, s2, n)  memcmp((const char *) s1, (const char *) s2, n)
ngx_int_t ngx_memn2cmp(u_char *s1, u_char *s2, size_t n1, size_t n2);
/* 分别是define的前n个字符比较和自定义的两个带长度的字符串比较 */

ngx_int_t ngx_dns_strcmp(u_char *s1, u_char *s2);
ngx_int_t ngx_filename_cmp(u_char *s1, u_char *s2, size_t n);
/* 分别为自定义的dns和filename比较函数,在其中分别令'.'和'/'成为了最小的字符 */

/* 学到的经典字符串比较函数:
    ngx_int_t
    ngx_dns_strcmp(u_char *s1, u_char *s2)
    {
        ngx_uint_t  c1, c2;

        for ( ;; ) {
            c1 = (ngx_uint_t) *s1++;
            c2 = (ngx_uint_t) *s2++;

            c1 = (c1 >= 'A' && c1 <= 'Z') ? (c1 | 0x20) : c1;   // 大小写转换
            c2 = (c2 >= 'A' && c2 <= 'Z') ? (c2 | 0x20) : c2;   // 可以用宏定义ngx_tolower(c)

            if (c1 == c2) {
                if (c1) {                 // 当前字符相当,如果还有后续字符,继续比较
                    continue;
                }
                return 0;                 // 当前两个字符串都到尾了,返回0
            }

            // in ASCII '.' > '-', but we need '.' to be the lowest character 
            c1 = (c1 == '.') ? ' ' : c1;
            c2 = (c2 == '.') ? ' ' : c2;

            return c1 - c2;
        }
    }

查找函数:

#define ngx_strstr(s1, s2)  strstr((const char *) s1, (const char *) s2)
#define ngx_strlen(s)       strlen((const char *) s)
#define ngx_strchr(s1, c)   strchr((const char *) s1, (int) c)
/* 封装的原libc中的三个字符串处理函数 */

static ngx_inline u_char*   ngx_strlchr(u_char *p, u_char *last, u_char c)
/* 自己实现的在选定字段内查找c */

u_char* ngx_strnstr(u_char *s1, char *s2, size_t len)
{
    u_char  c1, c2;
    size_t  n;

    c2 = *(u_char *) s2++;
    n = ngx_strlen(s2);
    do {
        do {
            if (len-- == 0) {              // 第一个退出条件,s1找到len的位置了
                return NULL;
            }
            c1 = *s1++;
            if (c1 == 0) {                 // 第二个退出条件,s1找到结尾了(0)
                return NULL;
            }
        } while (c1 != c2);
        if (n > len) {                     // 找到相同字符,长度肯定不匹配了,放外层循环内
            return NULL;
        }
    } while (ngx_strncmp(s1, (u_char *) s2, n) != 0);

    return --s1;
}
/* 感觉这段代码写的挺好,没有什么可以挑剔的地方,性能逻辑都很棒,在s1的前len个位置查找s2 */

u_char *ngx_strcasestrn(u_char *s1, char *s2, size_t n);
/* 在s1中查找s2的前n个字符,调用了strncasecmp,不区分大小写 */
u_char *ngx_strlcasestrn(u_char *s1, u_char *last, u_char *s2, size_t n);
/* 在范围内实现上面函数的功能,不区分大小写 */

字符串设置函数:

#define ngx_memzero(buf, n)       (void) memset(buf, 0, n)
#define ngx_memset(buf, c, n)     (void) memset(buf, c, n)
/* 封装的memset函数 */

字符串复制函数:

#define ngx_memcpy(dst, src, n)   (void) memcpy(dst, src, n)
#define ngx_cpymem(dst, src, n)   (((u_char *) memcpy(dst, src, n)) + (n))
/* 封装了一个cpymem用来持续的给dst中复制信息,每次赋值完返回结尾的位置 */

#if ( __INTEL_COMPILER >= 800 )
/*
 * the simple inline cycle copies the variable length strings up to 16
 * bytes faster than icc8 autodetecting _intel_fast_memcpy()
 */
static ngx_inline/*inline*/ u_char *
ngx_copy(u_char *dst, u_char *src, size_t len)
{
    if (len < 17) {
        while (len) {
            *dst++ = *src++;
            len--;
        }
        return dst;
    } else {
        return ngx_cpymem(dst, src, len);
    }
}
#else
#define ngx_copy                  ngx_cpymem
#endif
/* 编译优化,自定义ngx_copy替代cpymem,使用它的时候会根据编译环境和str长度进行最优选择(是否调用memcpy)*/

#define ngx_memmove(dst, src, n)   (void) memmove(dst, src, n)
#define ngx_movemem(dst, src, n)   (((u_char *) memmove(dst, src, n)) + (n))
/* 会处理内存重叠情况的memcpy */

u_char *ngx_cpystrn(u_char *dst, u_char *src, size_t n);
/* 从src中复制最多n个字符(遇0结束)到dst,并给dst后补一个0,然后返回0的这个位置 */

u_char *ngx_pstrdup(ngx_pool_t *pool, ngx_str_t *src);
/* 在pool中给src中的data申请一片内存存储,返回存储地址,失败返回NULL */

字符串转换函数:

ngx_int_t ngx_atoi(u_char *line, size_t n)
{
    ngx_int_t  value, cutoff, cutlim;
    if (n == 0) {
        return NGX_ERROR;
    }
    cutoff = NGX_MAX_INT_T_VALUE / 10;
    cutlim = NGX_MAX_INT_T_VALUE % 10;
    for (value = 0; n--; line++) {
        // 出现了异常字符
        if (*line < '0' || *line > '9') {
            return NGX_ERROR;
        }
        // 发生越界的条件处理
        if (value >= cutoff && (value > cutoff || *line - '0' > cutlim)) {
            return NGX_ERROR;
        }
        value = value * 10 + (*line - '0');
    }
    return value;
}
/* 将line的前n个字符转成一个整形,越界处理代码很喜欢,收藏一下 */

ngx_int_t ngx_atofp(u_char *line, size_t n, size_t point)
{
    ngx_int_t   value, cutoff, cutlim;
    ngx_uint_t  dot;
    if (n == 0) {
        return NGX_ERROR;
    }
    cutoff = NGX_MAX_INT_T_VALUE / 10;
    cutlim = NGX_MAX_INT_T_VALUE % 10;
    dot = 0;
    for (value = 0; n--; line++) {
        if (point == 0) {
            return NGX_ERROR;
        }
        if (*line == '.') {
            // 如果出现两个小数点,报错!
            if (dot) {
                return NGX_ERROR;
            }
            dot = 1;
            continue;
        }
        if (*line < '0' || *line > '9') {
            return NGX_ERROR;
        }
        if (value >= cutoff && (value > cutoff || *line - '0' > cutlim)) {
            return NGX_ERROR;
        }
        value = value * 10 + (*line - '0');
        point -= dot;                   // 把小数点后的数直接加在value里,相当于point--
    }
    while (point--) {
        if (value > cutoff) {
            return NGX_ERROR;
        }
       value = value * 10;
    }
    return value;
}
/* 将line前n个字符指向的定点数转为整形并扩大point*10倍 */

ssize_t ngx_atosz(u_char *line, size_t n);
off_t ngx_atoof(u_char *line, size_t n);
time_t ngx_atotm(u_char *line, size_t n);
/* 将line转换为一个ssize_t/off_t/time_t类型的值,虽然都是整型但还是会有一些不同,增强nginx移植性,架构好 */

ngx_int_t ngx_hextoi(u_char *line, size_t n);
/* 将16进制的值转换为10进制 */

字符串格式化函数:

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函数还是比较危险的,容易产生缓冲区溢出漏洞。*/

/* case 'V':
       v = va_arg(args, ngx_str_t *);
       len = ngx_min(((size_t) (last - buf)), v->len);     // 长度限制,防止溢出
       buf = ngx_cpymem(buf, v->data, len);                // 调用安全的cpymem
       fmt++;
       continue;
 */

#define ngx_vsnprintf(buf, max, fmt, args)                                   \
    ngx_vslprintf(buf, buf + (max), fmt, args)
/* 宏定义的函数调用,max标识buf的长度 */

u_char *ngx_vslprintf(u_char *buf, u_char *last, const char *fmt, va_list args);
/* 所有的sprintf都是调用这个接口实现的,buf~last标注buf的范围,fmt为格式化字符串,args为解析出的参数 */

在这一系列函数中,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                        '\0'
 *    %N                        '\n'
 *    %c                        char
 *    %%                        %
 *
 *  reserved:
 *    %t                        ptrdiff_t
 *    %S                        null-terminated wchar string
 *    %C                        wchar
 */

这里特别要提醒的是,我们最常用于格式化ngx_str_t结构,其对应的转义符是%V,传给函数的一定要是指针类型,否则程序就会coredump掉。这也是我们最容易犯的错。比如:

ngx_str_t str = ngx_string("hello world");
char buffer[1024];
ngx_snprintf(buffer, 1024, "%V", &str);    // 注意,str取地址

还有其他一些编码解码字符串的函数,如有兴趣可以自己看看源码~~

你可能感兴趣的:(nginx学习)