Nginx源码解读-字符串

前言

Nginx的字符串表示方法和C语言中的字符串表示方法不太一样,在C语言中一个字符串是从某一个地址开始,然后往后遍历直到遇到\0才表示这个字符串结束了。但是在Nginx中,根据实际的使用场景,在C语言的基础上对其稍加封装了一下,字符串的结束位置不再以\0作为结束标志,而是给出一个长度来计算结束位置。总的来说C语言中的字符串是通过起始位置和结束位置来确认,Nginx中则是通过起始位置和长度来确认。

详细说明可参照源码中src/ngx_string.h|.c文件。

储备知识

数据结构定义

1
2
3
4
typedef struct {
size_t len; //字符串长度
u_char *data; //字符串的起始位置
} ngx_str_t;

字符串在Nginx中是一个名为ngx_str_t的结构体,在这个结构体当中我们可以看到包含了一个指示字符串长度的len和一个指向具体内容的指针变量data。这种表示方法和C语言中的字符串表示方法有点不太一样,熟悉C语言的应该知道字符串的表示只有一个字符指针来指向具体内容的开始位置,往后一直遇到\0为止。造成这种差异的主要原因是因为在Nginx涉及到字符串计算的地方,会经常性的遇到要求某一个字符串长度的情况。在这种应用场景之下,这种表示方法能够更方便快捷的得到结果。PHP的字符串实现上也是采用的这种方式,有兴趣的可以去看一下PHP的相关源码。

Nginx中采用这种方法来表示字符串还有一个原因是共用内存,减小开销。比如在Nginx解析http请求的时候,会把请求的内容放一个字符串中。在这个字符串中,某一部分是Content-Length,另一部分是User-Agent这样等等。如果采用C语言的字符串表示的话,那么解析的过程中就需要去申请多块内存,每一块内存中分别去写入不同属性的内容。如果采用Nginx的这种表示方法,只需要把表示各种属性字符串的data指针指向请求的不同位置就好了,用len来表示结束的位置,这样能极大的减少内存的开销。

由于Nginx的字符串和C语言的字符串的表示差异,一些C语言的自带的字符串函数就不能直接作用到Nginx当中了,因此Nginx实现一批适应于ngx_str_t字符串操作的字符串函数,我们可以在ngx_string.h中可以找到。

主要函数

ngx_string

通过C标准的字符串来构造一个ngx_str_t格式的字符串,由于在ngx_str_t中不需要保存\0,因此len的值应该减1

使用这个宏定义的时候参数一定要是一个常量字符串,而不应该是一个字符指针,否则sizeof(str)的计算结果会大不一样

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

ngx_null_string

构造一个空的ngx_str_t格式的字符串

1
#define ngx_null_string     { 0, NULL }

ngx_str_set

通过C标准的字符串,给ngx_str_t格式的字符串赋值

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

ngx_str_null

置空ngx_str_t字符串

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

字符大小写转换

1
2
3
#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)

为什么要把这个字符大小写单独拿出来说那,因为Nginx对字符的大小写转换的实现有点不太一样。我们都知道的同一个字母的大写和小写的Ascall码值差了32,从大写到小写就加上32,从小写到大写就减去32,很简单,一般大部分的人都会写出下面的这种方式:

1
2
3
#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)

对比一下可以看出Nginx采用了位操作的办法,这是因为A-Z的Ascall二进制表示为1000001-1011010,a-z的Ascall二进制表示为1100001-1111010,差值32的二进制为0100000。A-Z的二进制从右向左数第6位是0,a-z的二进制从右向左数第6位是1。第6位代表的就是32,因此在转换的时候,我们只要去变换一位就可以了。

ngx_strlow

该方法就是使用上面的ngx_tolower对一个字符串的n个字符进行小写转换,感觉循环里面的前三行可以用一行代码去搞定,这样的操作在其他的文件中经常能够看见,不知道是因为写这个文件的人的风格还是别的原因。

1
2
3
4
5
6
7
8
9
10
void
ngx_strlow(u_char *dst, u_char *src, size_t n)
{
while (n) {
*dst = ngx_tolower(*src);
dst++;
src++;
n--;
}
}

字符串拷贝相关宏定义

下面的这些宏定义就是把一些C语言字符串操作的方法换了一个名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#define ngx_strncmp(s1, s2, n)  strncmp((const char *) s1, (const char *) s2, n)

/*
* 从一个字符串拷贝n个字符到另一个字符串
*/

#define ngx_strcmp(s1, s2) strcmp((const char *) s1, (const char *) s2)

/*
* 从一个字符串拷贝所有字符到另一个字符串
*/

#define ngx_strstr(s1, s2) strstr((const char *) s1, (const char *) s2)

/*
* 在s1中搜索s2出现的位置
*/

#define ngx_strlen(s) strlen((const char *) s)

/*
* 求字符串的长度
*/

#define ngx_strchr(s1, c) strchr((const char *) s1, (int) c)

/*
* 在s1中搜索字符c出现的首次位置
*/

ngx_strlchr

在一个字符串[p,last]范围内查找字符c在左侧出现的首个位置,找到的话返回相应的指针位置,否则返回NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static ngx_inline u_char *
ngx_strlchr(u_char *p, u_char *last, u_char c)
{
while (p < last) {

if (*p == c) {
return p;
}

p++;
}

return NULL;
}

ngx_memcpy & ngx_cpymem

这两个函数都是拷贝n个字符到另一个字符,根据NGX_MEMCPY_LIMIT的不同来分别定义这两个函数的实现

ngx_memcpy:从src拷贝n个字符到dst
ngx_cpymem:从src拷贝n个字符到dst并返回拷贝后的指针地址,此时指针指向第n+1的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#if (NGX_MEMCPY_LIMIT)

/* 拷贝限制内存的情况下 */

void *ngx_memcpy(void *dst, const void *src, size_t n);
#define ngx_cpymem(dst, src, n) (((u_char *) ngx_memcpy(dst, src, n)) + (n))

#else

/* 拷贝不限制内存的情况下 */

#define ngx_memcpy(dst, src, n) (void) memcpy(dst, src, n)
#define ngx_cpymem(dst, src, n) (((u_char *) memcpy(dst, src, n)) + (n))

#endif

ngx_copy

从src拷贝len长度的字符到dst,这个函数根据编译器的类型不同实现的具体方法也不同。在第一种方式里面可以看到如果拷贝的长度在17个字符以内,采用的是一个循环去拷贝,官方注释里面说是这样的情况下效率更高,这里就不深究了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#if ( __INTEL_COMPILER >= 800 )

static ngx_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_memmove

从src拷贝n个字节到dst,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中

1
#define ngx_memmove(dst, src, n)   (void) memmove(dst, src, n)

ngx_movemem

在ngx_memmove功能的基础上,返回指向n+1字节的指针

1
#define ngx_movemem(dst, src, n)   (((u_char *) memmove(dst, src, n)) + (n))

ngx_memcmp

比较s1,s2两块内存的前n个字节

1
#define ngx_memcmp(s1, s2, n)  memcmp((const char *) s1, (const char *) s2, n)

ngx_cpystrn

从src字符串中拷贝n个字符到dst字符串中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
u_char *
ngx_cpystrn(u_char *dst, u_char *src, size_t n)
{
if (n == 0) {
return dst;
}

while (--n) {
*dst = *src;

if (*dst == '\0') {
return dst;
}

dst++;
src++;
}

*dst = '\0';

return dst;
}

ngx_pstrdup

从内存池里面申请一块内存,并用src里面的内容填充,并返回该内存的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
u_char *
ngx_pstrdup(ngx_pool_t *pool, ngx_str_t *src)
{
u_char *dst;

dst = ngx_pnalloc(pool, src->len); /* 从内存池里申请内存 */
if (dst == NULL) {
return NULL;
}

ngx_memcpy(dst, src->data, src->len); /* 拷贝内存里的内容 */

return dst;
}

ngx_strcasecmp

比较两个字符串,忽略大小写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ngx_int_t
ngx_strcasecmp(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;

if (c1 == c2) {

if (c1) {
continue;
}

return 0;
}

return c1 - c2;
}
}

ngx_strncasecmp

比较两个字符串前n个字符,忽略大小写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ngx_int_t
ngx_strncasecmp(u_char *s1, u_char *s2, size_t n)
{
ngx_uint_t c1, c2;

while (n) {
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;

if (c1 == c2) {

/* 字符串没有结束,没有遇到\0 */
if (c1) {
n--;
continue;
}

return 0;
}

return c1 - c2;
}

return 0;
}

ngx_strnstr

在字符串中查找另一个子串(在第一个字符串的len长度内)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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 {

/* 匹配s1和s2首字母相同的位置 */
do {
if (len-- == 0) {
return NULL;
}

c1 = *s1++;

if (c1 == 0) {
return NULL;
}

} while (c1 != c2);

/* 如果s2的长度 大于s1剩余还未遍历的长度 直接返回 */
if (n > len) {
return NULL;
}

/* 当首s1某个位置的字母和s2首字母相同的话 比较前n个字符 */
/* 这里需要注意的一点是 此时的s1,s2是指向的下一个字符 此时应该比较的字符数量应该是小于n的 但是在ngx_strncmp中判断了字符串是否截止了 这里用n也就没什么影响了 */
} while (ngx_strncmp(s1, (u_char *) s2, n) != 0);

/* 由于s1指向的是当前比较字符的下一个 因此要进行--操作 */
return --s1;
}

ngx_strstrn

在字符串中去查找另一个子串(字符串是一个以\0结尾的静态字符串)

注意
这里n为s2的长度减一 因为代码中是在确定了s1中s2首字母出现的位置后,从下一个字符开始比较的,具体看下面代码中的注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
u_char *
ngx_strstrn(u_char *s1, char *s2, size_t n)
{
u_char c1, c2;

/* 这里可以看到当c2获取了s2的首字母后 s2指向的是下一个字符 也就是参数n为何是s2的长度减一的缘故 */
c2 = *(u_char *) s2++;

do {
do {
c1 = *s1++;

if (c1 == 0) {
return NULL;
}

} while (c1 != c2);

} while (ngx_strncmp(s1, (u_char *) s2, n) != 0);

/* 由于s1指向的是当前比较字符的下一个 因此要进行--操作 */
return --s1;
}

ngx_strcasestrn

ngx_strstrn的功能一样都是在字符串中查找子串,只是这个函数里面是忽略了大小写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
u_char *
ngx_strcasestrn(u_char *s1, char *s2, size_t n)
{
ngx_uint_t c1, c2;

c2 = (ngx_uint_t) *s2++;

/* 转成小写 */
c2 = (c2 >= 'A' && c2 <= 'Z') ? (c2 | 0x20) : c2;

do
/* 确定s1中和s2首字母相同的位置 */
do {
c1 = (ngx_uint_t) *s1++;

if (c1 == 0) {
return NULL;
}

/* 转成小写 */
c1 = (c1 >= 'A' && c1 <= 'Z') ? (c1 | 0x20) : c1;

} while (c1 != c2);

} while (ngx_strncasecmp(s1, (u_char *) s2, n) != 0);

return --s1;
}

ngx_strlcasestrn

ngx_strcasestrn的基础上对被搜索的字符串加上了一个范围限制,即在[s1,last]范围内去查找子串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
u_char *
ngx_strlcasestrn(u_char *s1, u_char *last, u_char *s2, size_t n)
{
ngx_uint_t c1, c2;

c2 = (ngx_uint_t) *s2++;
c2 = (c2 >= 'A' && c2 <= 'Z') ? (c2 | 0x20) : c2;
last -= n;

do {
do {
if (s1 >= last) {
return NULL;
}

c1 = (ngx_uint_t) *s1++;

c1 = (c1 >= 'A' && c1 <= 'Z') ? (c1 | 0x20) : c1;

} while (c1 != c2);

} while (ngx_strncasecmp(s1, s2, n) != 0);

return --s1;
}

ngx_rstrncmp

从右开始比较两个字符串的n个字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ngx_int_t
ngx_rstrncmp(u_char *s1, u_char *s2, size_t n)
{
if (n == 0) {
return 0;
}

n--;

for ( ;; ) {
if (s1[n] != s2[n]) {
return s1[n] - s2[n];
}

if (n == 0) {
return 0;
}

n--;
}
}

ngx_rstrncasecmp

从右开始比较两个字符串的n个字符,忽略大小写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ngx_int_t
ngx_rstrncasecmp(u_char *s1, u_char *s2, size_t n)
{
u_char c1, c2;

if (n == 0) {
return 0;
}

n--;

for ( ;; ) {
c1 = s1[n];
if (c1 >= 'a' && c1 <= 'z') {
c1 -= 'a' - 'A';
}

c2 = s2[n];
if (c2 >= 'a' && c2 <= 'z') {
c2 -= 'a' - 'A';
}

if (c1 != c2) {
return c1 - c2;
}

if (n == 0) {
return 0;
}

n--;
}
}