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取地址

還有其他一些編碼解碼字符串的函數,如有興趣可以自己看看源碼~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章