nginx listen指令淺析之add listen

鄭爾多斯發表於2019-02-20

微信公眾號:鄭爾多斯
關注可瞭解更多的Nginx知識。任何問題或建議,請公眾號留言;
關注公眾號,有趣有內涵的文章第一時間送達!

nginx listen指令淺析之add listen

前言

我們在上篇文章中介紹了address:port的解析過程,這篇文章繼續講解解析listen指令的後續過程。
解析listen指令的函式是 ngx_http_core_listen(),這個函式的前半部分是解析address:port ,我們在上篇文章中介紹過。後面緊接著就是解析各種 default_server, recvbuf 等欄位,這些很簡單,只需要設定ngx_http_listen_opt_t結構體中的相應欄位即可,主要的功能在於下面的 ngx_http_add_listen() 函式。

函式功能

我們知道ngx_http_core_main_conf_t結構體中有一個ports欄位,這個欄位是一個ngx_http_conf_port_t型別的陣列。這個陣列儲存了當前http塊指令監聽的所有埠。ngx_http_add_listen() 函式就是把監聽的埠資訊儲存到這個陣列中。

資料結構體

牽涉到的結構體如下:
埠資料結構:

1typedef struct {
2    // 協議族型別,我們只討論IPV4,所以這裡是AF_INET
3    ngx_int_t    family;
4    // 監聽的埠號
5    in_port_t    port;
6    /* array of ngx_http_conf_addr_t */
7    ngx_array_t   addrs;     
8ngx_http_conf_port_t;
複製程式碼

address資料結構:

 1typedef struct {
2    ngx_http_listen_opt_t      opt;
3
4    ngx_hash_t                 hash;
5// 下面三個欄位都是雜湊表,用來儲存當前address:port對應的server_name
6// 其中key是server_name對應的字串
7// value 是 ngx_http_core_srv_conf_t 結構體
8    ngx_hash_wildcard_t       *wc_head;
9    ngx_hash_wildcard_t       *wc_tail;
10
11    ngx_uint_t                 nregex;
12    ngx_http_server_name_t    *regex;
13
14    /* the default server configuration for this address:port */
15    ngx_http_core_srv_conf_t  *default_server;
16     /* array of ngx_http_core_srv_conf_t */
17    ngx_array_t                servers; 
18ngx_http_conf_addr_t;
複製程式碼

原始碼分析

下面的ngx_http_add_listen()原始碼刪除了IPV6Unix Domain程式碼,只保留了IPV4相關的程式碼,如下:

 1ngx_int_t
2ngx_http_add_listen(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
3    ngx_http_listen_opt_t *lsopt)
4{
5    in_port_t                   p;
6    ngx_uint_t                  i;
7    struct sockaddr            *sa;
8    struct sockaddr_in         *sin;
9    ngx_http_conf_port_t       *port;
10    ngx_http_core_main_conf_t  *cmcf;
11
12    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
13
14// 為 ngx_http_core_main_conf_t 結構的 port 分配空間,儲存所有的埠
15    if (cmcf->ports == NULL) {
16        cmcf->ports = ngx_array_create(cf->temp_pool, 2,
17                                       sizeof(ngx_http_conf_port_t));
18    }
19
20    sa = &lsopt->u.sockaddr;
21
22    switch (sa->sa_family) {
23
24    default/* AF_INET */
25        sin = &lsopt->u.sockaddr_in;
26        p = sin->sin_port; // 拿到當前listen指令的埠號
27        break;
28    }
29
30    port = cmcf->ports->elts;
31// 遍歷所有的port,檢視該埠號是否已經存在
32    for (i = 0; i < cmcf->ports->nelts; i++) {
33     // 這裡的意思就是:只有當協議版本和埠號都相同的話才算是同一個埠號
34        if (p != port[i].port || sa->sa_family != port[i].family) {
35            continue;
36        }
37
38        /* a port is already in the port list */
39// 如果執行到這裡,說明之前已經存在了一個協議版本和埠號都相同的listen,這樣的話我們應該把當前監聽的
40// address 加入到已有的port->addrs 陣列中,統一管理當前埠對應的 address
41        return ngx_http_add_addresses(cf, cscf, &port[i], lsopt);
42    }
43
44    /* add a port to the port list */
45 // 如果當前埠不存在,那麼就向 ngx_http_core_main_conf_t->port 中新增一個元素
46// 表示一個我們監聽了一個新的埠
47    port = ngx_array_push(cmcf->ports);
48    if (port == NULL) {
49        return NGX_ERROR;
50    }
51
52    port->family = sa->sa_family;// port中儲存了協議型別,因為我們區分不同的協議
53    port->port = p;
54    port->addrs.elts = NULL;
55   // 將當前listen的address加入到port->addr中
56    return ngx_http_add_address(cf, cscf, port, lsopt);
57}
複製程式碼

其實這個函式很簡單,具體可以分為以下幾個步驟:

  • 遍歷所有的ports,查詢是否存在相同協議型別和埠的元素
  • 如果存在上述的元素,就調ngx_http_add_addresses()
  • 如果不存在上述元素,就呼叫ngx_http_add_address()

上面的兩個函式名字也很有意思,ngx_http_add_addresses()使用了複數形式的addresses(),其實也說明了相同的元素已經存在,這裡需要新增多個元素。ngx_http_add_address()使用單數形式的address,說明新增的應該是第一個元素。

埠不存在

上面我們分析過,如果埠不存在,那麼會呼叫ngx_http_add_address()函式,程式碼如下:

 1//這個函式是將一個listen的addr加入到port->addrs陣列中
2static ngx_int_t
3ngx_http_add_address(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
4    ngx_http_conf_port_t *port, ngx_http_listen_opt_t *lsopt)

5
{
6    ngx_http_conf_addr_t  *addr;
7
8    if (port->addrs.elts == NULL) {
9        if (ngx_array_init(&port->addrs, cf->temp_pool, 4,
10                           sizeof(ngx_http_conf_addr_t))
11            != NGX_OK)
12        {
13            return NGX_ERROR;
14        }
15    }
16
17// 下面的程式碼就是將listen的address新增到port->addrs陣列中.
18// 然後初始化這個元素
19    addr = ngx_array_push(&port->addrs);
20    addr->opt = *lsopt;
21    addr->hash.buckets = NULL;
22    addr->hash.size = 0;
23    addr->wc_head = NULL;
24    addr->wc_tail = NULL;
25#if (NGX_PCRE)
26    addr->nregex = 0;
27    addr->regex = NULL;
28#endif
29//先給default_server賦一個預設值,也即是當前address:port對應的ngx_http_core_srv_conf_t結構體
30// 下面的 ngx_http_add_server會根據 ngx_http_listen_opt_t 結構體的值重新給 default_server賦值.
31// 如果我們在listen後面沒有設定default_server指令,那麼所有相同 address:port 的server的第一個server就是default_server
32    addr->default_server = cscf;
33    addr->servers.elts = NULL;
34 // 下面的函式是將一個ngx_http_core_srv_conf_t 結構體新增到 addr->servers 中
35// 由於同一個 address:port 可以對應於多個server,所以這裡通過 port->addr->server欄位
36//將相同的 address:port 對應的所有虛擬主機關聯起來
37    return ngx_http_add_server(cf, cscf, addr);
38}
複製程式碼

新增server

每個address:port都可能對應多個serverngx_http_add_server函式就是serveraddress:port對應起來。

 1static ngx_int_t
2ngx_http_add_server(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
3    ngx_http_conf_addr_t *addr)

4
{
5    ngx_uint_t                  i;
6    ngx_http_core_srv_conf_t  **server;
7
8    if (addr->servers.elts == NULL) {
9        if (ngx_array_init(&addr->servers, cf->temp_pool, 4,
10                           sizeof(ngx_http_core_srv_conf_t *))
11            != NGX_OK)
12        {
13            return NGX_ERROR;
14        }
15
16    } else {
17        server = addr->servers.elts;
18        for (i = 0; i < addr->servers.nelts; i++) {
19            if (server[i] == cscf) {
20// 這一部分的意思就是:防止同一個server塊內有兩個listen指令
21                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
22                                   "a duplicate listen %s", addr->opt.addr);
23                return NGX_ERROR;
24            }
25        }
26    }
27
28// 下面的程式碼實在是沒有什麼意思,就是將 listen 指令所在的 ngx_http_core_srv_conf_t 
29// 結構體新增到 addr->servers 陣列中
30    server = ngx_array_push(&addr->servers);
31    *server = cscf;
32
33    return NGX_OK;
34}
複製程式碼

埠已存在

上面我們說過,如果相同的埠已經存在,那麼就會呼叫ngx_http_add_addresses()函式將當前的埠新增到相應的陣列元素中。我們下面看一下這個函式。

再次重申:執行ngx_http_add_addresses的時候,表示當前listenport已經存在。

 1static ngx_int_t
2ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
3    ngx_http_conf_port_t *port, ngx_http_listen_opt_t *lsopt)

4
{
5    u_char                *p;
6    size_t                 len, off;
7    ngx_uint_t             i, default_server;
8    struct sockaddr       *sa;
9    ngx_http_conf_addr_t  *addr;
10
11    /*
12     * we cannot compare whole sockaddr struct's as kernel
13     * may fill some fields in inherited sockaddr struct's.
14* 不能比較整個sockaddr結構體,因為核心可能在sockaddr中填充了其他的欄位。
15* 我的理解:不同版本的核心,sockaddr結構體中可能包含不同的欄位,所以不能比較整個結構體,只能比較一些必然會存在的欄位.
16* 這應該也是為了相容性
17     */

18
19    sa = &lsopt->u.sockaddr;
20
21    switch (sa->sa_family) {
22
23    default/* AF_INET */
24// off 欄位指向了 sockaddr_in 中 sin_addr 的起始地址
25// len 欄位說明了 sin_addr 的長度
26// 下面我們要比較相同的address是否已經存在了
27        off = offsetof(struct sockaddr_in, sin_addr);
28        len = 4;// sin_addr的長度
29        break;
30    }
31
32    p = lsopt->u.sockaddr_data + off;
33
34    addr = port->addrs.elts;
35
36    for (i = 0; i < port->addrs.nelts; i++) {
37 // 遍歷當前port所有的addrs,查詢是否存在相同 address 
38        if (ngx_memcmp(p, addr[i].opt.u.sockaddr_data + off, len) != 0) {
39            continue;
40        }
41
42        /* the address is already in the address list */
43 // 如果address已經存在,也就是說 address 和 port 全部相同
44// 那麼我們就只需把 ngx_http_core_srv_conf_t 新增到 addrs->servers
45// 陣列中即可
46        if (ngx_http_add_server(cf, cscf, &addr[i]) != NGX_OK) {
47            return NGX_ERROR;
48        }
49
50        /* preserve default_server bit during listen options overwriting */
51// 看一下當前遍歷到的listen指令是否設定了 default_server 
52        default_server = addr[i].opt.default_server;
53
54        if (lsopt->set) {
55// 這裡的作用我們在 關於listen指令中的bind和set.note 文章中已經說過了
56            if (addr[i].opt.set) {
57                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
58                        "duplicate listen options for %s", addr[i].opt.addr);
59                return NGX_ERROR;
60            }
61
62            addr[i].opt = *lsopt;
63        }
64
65        /* check the duplicate "default" server for this address:port */
66
67        if (lsopt->default_server) {
68
69            if (default_server) {
70                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
71                        "a duplicate default server for %s", addr[i].opt.addr);
72                return NGX_ERROR;
73            }
74
75            default_server = 1;
76            addr[i].default_server = cscf;
77        }
78
79        addr[i].opt.default_server = default_server;
80        return NGX_OK;
81    }
82
83    /* add the address to the addresses list that bound to this port */
84    // 如果執行到這裡,說明該address尚不存在,所以新建一個address
85    return ngx_http_add_address(cf, cscf, port, lsopt);
86}
複製程式碼

函式的關係

上面的幾個函式錯綜複雜,容易弄混,我對這幾個函式的功能進行了一些總結。

ngx_http_add_addresses()函式的功能呢:

遍歷當前埠addrs陣列的所有元素, 檢視是否有addrs元素和當前的address相同
1、如果不存在相同的address元素,則呼叫ngx_http_add_address()完成如下功能:

  • 將當前listen指令的address作為一個新元素新增到port-&gt;addrs陣列中
  • 呼叫 ngx_http_add_server() 函式完成如下功能
    如果呼叫 ngx_http_add_server() 則說明 addressport都是相同的,那麼我們應該將當前 address:port 對應的 ngx_http_core_srv_conf_t 結構體新增到 port-&gt;addrs-&gt;servers 陣列中。

2、如果存在相同的address元素,那麼就呼叫 ngx_http_add_server()函式完成功能。

測試用例

該配置檔案僅用於測試listen指令

 1user root;
2daemon on;
3worker_processes  1;
4master_process on;
5
6events {
7    use epoll;
8    worker_connections  1024;
9    multi_accept on;
10    accept_mutex on;
11}
12
13
14http{
15
16      # server_1,和server_2監聽的 ip:port 相同
17    server {
18       listen  127.0.0.1:8088;
19       server_name first;
20       location / {
21            root   first;
22        }
23    }
24
25    # server_2,和server_1監聽的 ip:port 相同
26    server {
27        listen  127.0.0.1:8088;
28        server_name second;
29        location / {
30            root   second;
31        }
32    }
33
34    # 先配置一個虛擬ip: ifconfig eth0:1 192.168.10.5 netmask 255.255.255.0
35    # server_3,和server_1監聽的 port 相同,但是ip不同
36    server {
37        listen  192.168.10.5:8088;
38        server_name third;
39        location / {
40            root   third;
41        }
42
43    }
44
45     # server_4,和server_3監聽的 ip:port 相同
46    server {
47       listen  192.168.10.5:8088;
48       server_name fourth;
49       location / {
50                root  fourth;
51       }
52
53    }
54
55
56    # server_5
57    server {
58        listen  127.0.0.1:8089;
59        server_name fiveth;
60        location /{
61            root   fiveth;
62        }
63
64    }
65
66    # server_6 與server_5監聽的埠相同,ip不同
67    server {
68        listen  192.168.10.5:8089;
69        server_name sixth;
70        location /{
71            root sixth;
72        }
73
74    }
75
76
77   # server_7 與server_5監聽的埠相同,但是監聽全部ip
78   server {
79        listen  8089;
80        server_name seventh;
81        location / {
82                root   seventh;
83        }
84   }
85}
複製程式碼

測試結果

listen記憶體佈局
listen記憶體佈局

喜歡本文的朋友們,歡迎長按下圖關注訂閱號鄭爾多斯,更多精彩內容第一時間送達

鄭爾多斯
鄭爾多斯

相關文章