Nginx 入門指南

                                       Nginx 入門指南php

                                     

簡介:html

       Nginx 是一款輕量級的 Web 服務器/反向代理服務器及電子郵件(IMAP/POP3)代理服務器,其特色是佔有內存少,併發能力強。本教程根據淘寶核心系統服務器平臺組的成員的平常工做總結而成,主要介紹了 Nginx 平臺的特色及模塊開發,幫助讀者更好的構建和維護 Nginx 服務器.前端

 

 

適用人羣:高性能 Web 服務器維護人員,對互聯網服務器感興趣的程序開發者。 linux

一.什麼是 Nginxandroid

 

Nginx 是俄羅斯人編寫的十分輕量級的 HTTP 服務器,Nginx,它的發音爲「engine X」,是一個高性能的HTTP和反向代理服務器,同時也是一個 IMAP/POP3/SMTP 代理服務器。Nginx 是由俄羅斯人 Igor Sysoev 爲俄羅斯訪問量第二的 Rambler.ru 站點開發的,它已經在該站點運行超過兩年半了。Igor Sysoev 在創建的項目時,使用基於 BSD 許可。
英文主頁: http://nginx.net 。
到 2013 年,目前有不少國內網站採用 Nginx 做爲 Web 服務器,如國內知名的新浪、16三、騰訊、Discuz、豆瓣等。據 netcraft 統計,Nginx 排名第 3,約佔 15% 的份額(參見: http://news.netcraft.com/archives/category/web-server-survey/ )
Nginx 以事件驅動的方式編寫,因此有很是好的性能,同時也是一個很是高效的反向代理、負載平衡。其擁有匹配 Lighttpd 的性能,同時尚未 Lighttpd 的內存泄漏問題,並且 Lighttpd 的 mod_proxy 也有一些問題而且好久沒有更新。
如今,Igor 將源代碼以類 BSD 許可證的形式發佈。Nginx 由於它的穩定性、豐富的模塊庫、靈活的配置和低系統資源的消耗而聞名.業界一致認爲它是 Apache2.2+mod_proxy_balancer 的輕量級代替者,不只是由於響應靜態頁面的速度很是快,並且它的模塊數量達到 Apache 的近 2/3。對 proxy 和 rewrite 模塊的支持很完全,還支持 mod_fcgi、ssl、vhosts ,適合用來作 mongrel clusters 的前端 HTTP 響應。

二.Nginx 特色

 

Nginx 作爲 HTTP 服務器,有如下幾項基本特性:nginx

 

  • 處理靜態文件,索引文件以及自動索引;打開文件描述符緩衝.程序員

  • 無緩存的反向代理加速,簡單的負載均衡和容錯.web

  • FastCGI,簡單的負載均衡和容錯.算法

  • 模塊化的結構。包括 gzipping, byte ranges, chunked responses,以及 SSI-filter 等 filter。若是由 FastCGI 或其它代理服務器處理單頁中存在的多個 SSI,則這項處理能夠並行運行,而不須要相互等待。apache

  • 支持 SSL 和 TLSSNI.

 

Nginx 專爲性能優化而開發,性能是其最重要的考量,實現上很是注重效率 。它支持內核 Poll 模型,能經受高負載的考驗,有報告代表能支持高達 50,000 個併發鏈接數。

 

Nginx 具備很高的穩定性。其它 HTTP 服務器,當遇到訪問的峯值,或者有人惡意發起慢速鏈接時,也極可能會致使服務器物理內存耗盡頻繁交換,失去響應,只能重啓服務器。例如當前 apache 一旦上到 200 個以上進程,web響應速度就明顯很是緩慢了。而 Nginx 採起了分階段資源分配技術,使得它的 CPU 與內存佔用率很是低。Nginx 官方表示保持 10,000 個沒有活動的鏈接,它只佔 2.5M 內存,因此相似 DOS 這樣的攻擊對 Nginx 來講基本上是毫無用處的。就穩定性而言,Nginx 比 lighthttpd 更勝一籌。

 

Nginx 支持熱部署。它的啓動特別容易, 而且幾乎能夠作到 7*24 不間斷運行,即便運行數個月也不須要從新啓動。你還可以在不間斷服務的狀況下,對軟件版本進行進行升級。

 

Nginx 採用 master-slave 模型,可以充分利用 SMP 的優點,且可以減小工做進程在磁盤 I/O 的阻塞延遲。當採用 select()/poll() 調用時,還能夠限制每一個進程的鏈接數。

 

Nginx 代碼質量很是高,代碼很規範,手法成熟,模塊擴展也很容易。特別值得一提的是強大的 Upstream 與 Filter 鏈。Upstream 爲諸如 reverse proxy,與其餘服務器通訊模塊的編寫奠基了很好的基礎。而 Filter 鏈最酷的部分就是各個 filter 沒必要等待前一個 filter 執行完畢。它能夠把前一個 filter 的輸出作爲當前 filter 的輸入,這有點像 Unix 的管線。這意味着,一個模塊能夠開始壓縮從後端服務器發送過來的請求,且能夠在模塊接收完後端服務器的整個請求以前把壓縮流轉向客戶端。

 

Nginx 採用了一些 os 提供的最新特性如對 sendfile (Linux2.2+),accept-filter (FreeBSD4.1+),TCP_DEFER_ACCEPT (Linux 2.4+)的支持,從而大大提升了性能。

 

固然,Nginx 還很年輕,多多少少存在一些問題,好比:Nginx 是俄羅斯人建立,雖然前幾年文檔比較少,可是目前文檔方面比較全面,英文資料居多,中文的資料也比較多,並且有專門的書籍和資料可供查找。

 

Nginx 的做者和社區都在不斷的努力完善,咱們有理由相信 Nginx 將繼續以高速的增加率來分享輕量級 HTTP 服務器市場,會有一個更美好的將來。

 

3.初探 Nginx 架構

衆所周知,Nginx 性能高,而 Nginx 的高性能與其架構是分不開的。那麼 Nginx 到底是怎麼樣的呢?這一節咱們先來初識一下 Nginx 框架吧。

Nginx 在啓動後,在 unix 系統中會以 daemon 的方式在後臺運行,後臺進程包含一個 master 進程和多個 worker 進程。咱們也能夠手動地關掉後臺模式,讓 Nginx 在前臺運行,而且經過配置讓 Nginx 取消 master 進程,從而能夠使 Nginx 以單進程方式運行。很顯然,生產環境下咱們確定不會這麼作,因此關閉後臺模式,通常是用來調試用的,在後面的章節裏面,咱們會詳細地講解如何調試 Nginx。因此,咱們能夠看到,Nginx 是以多進程的方式來工做的,固然 Nginx 也是支持多線程的方式的,只是咱們主流的方式仍是多進程的方式,也是 Nginx 的默認方式。Nginx 採用多進程的方式有諸多好處,因此我就主要講解 Nginx 的多進程模式吧。

剛纔講到,Nginx 在啓動後,會有一個 master 進程和多個 worker 進程。master 進程主要用來管理 worker 進程,包含:接收來自外界的信號,向各 worker 進程發送信號,監控 worker 進程的運行狀態,當 worker 進程退出後(異常狀況下),會自動從新啓動新的 worker 進程。而基本的網絡事件,則是放在 worker 進程中來處理了。多個 worker 進程之間是對等的,他們同等競爭來自客戶端的請求,各進程互相之間是獨立的。一個請求,只可能在一個 worker 進程中處理,一個 worker 進程,不可能處理其它進程的請求。worker 進程的個數是能夠設置的,通常咱們會設置與機器cpu核數一致,這裏面的緣由與 Nginx 的進程模型以及事件處理模型是分不開的。Nginx 的進程模型,能夠由下圖來表示:

在 Nginx 啓動後,若是咱們要操做 Nginx,要怎麼作呢?從上文中咱們能夠看到,master 來管理 worker 進程,因此咱們只須要與 master 進程通訊就好了。master 進程會接收來自外界發來的信號,再根據信號作不一樣的事情。因此咱們要控制 Nginx,只須要經過 kill 向 master 進程發送信號就好了。好比kill -HUP pid,則是告訴 Nginx,從容地重啓 Nginx,咱們通常用這個信號來重啓 Nginx,或從新加載配置,由於是從容地重啓,所以服務是不中斷的。master 進程在接收到 HUP 信號後是怎麼作的呢?首先 master 進程在接到信號後,會先從新加載配置文件,而後再啓動新的 worker 進程,並向全部老的 worker 進程發送信號,告訴他們能夠光榮退休了。新的 worker 在啓動後,就開始接收新的請求,而老的 worker 在收到來自 master 的信號後,就再也不接收新的請求,而且在當前進程中的全部未處理完的請求處理完成後,再退出。固然,直接給 master 進程發送信號,這是比較老的操做方式,Nginx 在 0.8 版本以後,引入了一系列命令行參數,來方便咱們管理。好比,./nginx -s reload,就是來重啓 Nginx,./nginx -s stop,就是來中止 Nginx 的運行。如何作到的呢?咱們仍是拿 reload 來講,咱們看到,執行命令時,咱們是啓動一個新的 Nginx 進程,而新的 Nginx 進程在解析到 reload 參數後,就知道咱們的目的是控制 Nginx 來從新加載配置文件了,它會向 master 進程發送信號,而後接下來的動做,就和咱們直接向 master 進程發送信號同樣了。

如今,咱們知道了當咱們在操做 Nginx 的時候,Nginx 內部作了些什麼事情,那麼,worker 進程又是如何處理請求的呢?咱們前面有提到,worker 進程之間是平等的,每一個進程,處理請求的機會也是同樣的。當咱們提供 80 端口的 http 服務時,一個鏈接請求過來,每一個進程都有可能處理這個鏈接,怎麼作到的呢?首先,每一個 worker 進程都是從 master 進程 fork 過來,在 master 進程裏面,先創建好須要 listen 的 socket(listenfd)以後,而後再 fork 出多個 worker 進程。全部 worker 進程的 listenfd 會在新鏈接到來時變得可讀,爲保證只有一個進程處理該鏈接,全部 worker 進程在註冊 listenfd 讀事件前搶 accept_mutex,搶到互斥鎖的那個進程註冊 listenfd 讀事件,在讀事件裏調用 accept 接受該鏈接。當一個 worker 進程在 accept 這個鏈接以後,就開始讀取請求,解析請求,處理請求,產生數據後,再返回給客戶端,最後才斷開鏈接,這樣一個完整的請求就是這樣的了。咱們能夠看到,一個請求,徹底由 worker 進程來處理,並且只在一個 worker 進程中處理。

那麼,Nginx 採用這種進程模型有什麼好處呢?固然,好處確定會不少了。首先,對於每一個 worker 進程來講,獨立的進程,不須要加鎖,因此省掉了鎖帶來的開銷,同時在編程以及問題查找時,也會方便不少。其次,採用獨立的進程,可讓互相之間不會影響,一個進程退出後,其它進程還在工做,服務不會中斷,master 進程則很快啓動新的 worker 進程。固然,worker 進程的異常退出,確定是程序有 bug 了,異常退出,會致使當前 worker 上的全部請求失敗,不過不會影響到全部請求,因此下降了風險。固然,好處還有不少,你們能夠慢慢體會。

上面講了不少關於 Nginx 的進程模型,接下來,咱們來看看 Nginx 是如何處理事件的。

有人可能要問了,Nginx 採用多 worker 的方式來處理請求,每一個 worker 裏面只有一個主線程,那可以處理的併發數頗有限啊,多少個 worker 就能處理多少個併發,何來高併發呢?非也,這就是 Nginx 的高明之處,Nginx 採用了異步非阻塞的方式來處理請求,也就是說,Nginx 是能夠同時處理成千上萬個請求的。想一想 apache 的經常使用工做方式(apache 也有異步非阻塞版本,但因其與自帶某些模塊衝突,因此不經常使用),每一個請求會獨佔一個工做線程,當併發數上到幾千時,就同時有幾千的線程在處理請求了。這對操做系統來講,是個不小的挑戰,線程帶來的內存佔用很是大,線程的上下文切換帶來的 cpu 開銷很大,天然性能就上不去了,而這些開銷徹底是沒有意義的。

爲何 Nginx 能夠採用異步非阻塞的方式來處理呢,或者異步非阻塞究竟是怎麼回事呢?咱們先回到原點,看看一個請求的完整過程。首先,請求過來,要創建鏈接,而後再接收數據,接收數據後,再發送數據。具體到系統底層,就是讀寫事件,而當讀寫事件沒有準備好時,必然不可操做,若是不用非阻塞的方式來調用,那就得阻塞調用了,事件沒有準備好,那就只能等了,等事件準備好了,你再繼續吧。阻塞調用會進入內核等待,cpu 就會讓出去給別人用了,對單線程的 worker 來講,顯然不合適,當網絡事件越多時,你們都在等待呢,cpu 空閒下來沒人用,cpu利用率天然上不去了,更別談高併發了。好吧,你說加進程數,這跟apache的線程模型有什麼區別,注意,別增長無謂的上下文切換。因此,在 Nginx 裏面,最忌諱阻塞的系統調用了。不要阻塞,那就非阻塞嘍。非阻塞就是,事件沒有準備好,立刻返回 EAGAIN,告訴你,事件還沒準備好呢,你慌什麼,過會再來吧。好吧,你過一會,再來檢查一下事件,直到事件準備好了爲止,在這期間,你就能夠先去作其它事情,而後再來看看事件好了沒。雖然不阻塞了,但你得不時地過來檢查一下事件的狀態,你能夠作更多的事情了,但帶來的開銷也是不小的。因此,纔會有了異步非阻塞的事件處理機制,具體到系統調用就是像 select/poll/epoll/kqueue 這樣的系統調用。它們提供了一種機制,讓你能夠同時監控多個事件,調用他們是阻塞的,但能夠設置超時時間,在超時時間以內,若是有事件準備好了,就返回。這種機制正好解決了咱們上面的兩個問題,拿 epoll 爲例(在後面的例子中,咱們多以 epoll 爲例子,以表明這一類函數),當事件沒準備好時,放到 epoll 裏面,事件準備好了,咱們就去讀寫,當讀寫返回 EAGAIN 時,咱們將它再次加入到 epoll 裏面。這樣,只要有事件準備好了,咱們就去處理它,只有當全部事件都沒準備好時,纔在 epoll 裏面等着。這樣,咱們就能夠併發處理大量的併發了,固然,這裏的併發請求,是指未處理完的請求,線程只有一個,因此同時能處理的請求固然只有一個了,只是在請求間進行不斷地切換而已,切換也是由於異步事件未準備好,而主動讓出的。這裏的切換是沒有任何代價,你能夠理解爲循環處理多個準備好的事件,事實上就是這樣的。與多線程相比,這種事件處理方式是有很大的優點的,不須要建立線程,每一個請求佔用的內存也不多,沒有上下文切換,事件處理很是的輕量級。併發數再多也不會致使無謂的資源浪費(上下文切換)。更多的併發數,只是會佔用更多的內存而已。 我以前有對鏈接數進行過測試,在 24G 內存的機器上,處理的併發請求數達到過 200 萬。如今的網絡服務器基本都採用這種方式,這也是nginx性能高效的主要緣由。

咱們以前說過,推薦設置 worker 的個數爲 cpu 的核數,在這裏就很容易理解了,更多的 worker 數,只會致使進程來競爭 cpu 資源了,從而帶來沒必要要的上下文切換。並且,nginx爲了更好的利用多核特性,提供了 cpu 親緣性的綁定選項,咱們能夠將某一個進程綁定在某一個核上,這樣就不會由於進程的切換帶來 cache 的失效。像這種小的優化在 Nginx 中很是常見,同時也說明了 Nginx 做者的苦心孤詣。好比,Nginx 在作 4 個字節的字符串比較時,會將 4 個字符轉換成一個 int 型,再做比較,以減小 cpu 的指令數等等。

如今,知道了 Nginx 爲何會選擇這樣的進程模型與事件模型了。對於一個基本的 Web 服務器來講,事件一般有三種類型,網絡事件、信號、定時器。從上面的講解中知道,網絡事件經過異步非阻塞能夠很好的解決掉。如何處理信號與定時器?

首先,信號的處理。對 Nginx 來講,有一些特定的信號,表明着特定的意義。信號會中斷掉程序當前的運行,在改變狀態後,繼續執行。若是是系統調用,則可能會致使系統調用的失敗,須要重入。關於信號的處理,你們能夠學習一些專業書籍,這裏很少說。對於 Nginx 來講,若是nginx正在等待事件(epoll_wait 時),若是程序收到信號,在信號處理函數處理完後,epoll_wait 會返回錯誤,而後程序可再次進入 epoll_wait 調用。

另外,再來看看定時器。因爲 epoll_wait 等函數在調用的時候是能夠設置一個超時時間的,因此 Nginx 藉助這個超時時間來實現定時器。nginx裏面的定時器事件是放在一顆維護定時器的紅黑樹裏面,每次在進入 epoll_wait前,先從該紅黑樹裏面拿到全部定時器事件的最小時間,在計算出 epoll_wait 的超時時間後進入 epoll_wait。因此,當沒有事件產生,也沒有中斷信號時,epoll_wait 會超時,也就是說,定時器事件到了。這時,nginx會檢查全部的超時事件,將他們的狀態設置爲超時,而後再去處理網絡事件。由此能夠看出,當咱們寫 Nginx 代碼時,在處理網絡事件的回調函數時,一般作的第一個事情就是判斷超時,而後再去處理網絡事件。

咱們能夠用一段僞代碼來總結一下 Nginx 的事件處理模型:

while (true) {
        for t in run_tasks:
            t.handler();
        update_time(&now);
        timeout = ETERNITY;
        for t in wait_tasks: /* sorted already */
            if (t.time <= now) {
                t.timeout_handler();
            } else {
                timeout = t.time - now;
                break;
            }
        nevents = poll_function(events, timeout);
        for i in nevents:
            task t;
            if (events[i].type == READ) {
                t.handler = read_handler;
            } else { /* events[i].type == WRITE */
                t.handler = write_handler;
            }
            run_tasks_add(t);
    }

好,本節咱們講了進程模型,事件模型,包括網絡事件,信號,定時器事件。

 

四.Nginx 基礎概念

connection

在 Nginx 中 connection 就是對 tcp 鏈接的封裝,其中包括鏈接的 socket,讀事件,寫事件。利用 Nginx 封裝的 connection,咱們能夠很方便的使用 Nginx 來處理與鏈接相關的事情,好比,創建鏈接,發送與接受數據等。而 Nginx 中的 http 請求的處理就是創建在 connection之上的,因此 Nginx 不只能夠做爲一個web服務器,也能夠做爲郵件服務器。固然,利用 Nginx 提供的 connection,咱們能夠與任何後端服務打交道。

結合一個 tcp 鏈接的生命週期,咱們看看 Nginx 是如何處理一個鏈接的。首先,Nginx 在啓動時,會解析配置文件,獲得須要監聽的端口與 ip 地址,而後在 Nginx 的 master 進程裏面,先初始化好這個監控的 socket(建立 socket,設置 addrreuse 等選項,綁定到指定的 ip 地址端口,再 listen),而後再 fork 出多個子進程出來,而後子進程會競爭 accept 新的鏈接。此時,客戶端就能夠向 Nginx 發起鏈接了。當客戶端與服務端經過三次握手創建好一個鏈接後,Nginx 的某一個子進程會 accept 成功,獲得這個創建好的鏈接的 socket,而後建立 Nginx 對鏈接的封裝,即 ngx_connection_t 結構體。接着,設置讀寫事件處理函數並添加讀寫事件來與客戶端進行數據的交換。最後,Nginx 或客戶端來主動關掉鏈接,到此,一個鏈接就壽終正寢了。

固然,Nginx 也是能夠做爲客戶端來請求其它 server 的數據的(如 upstream 模塊),此時,與其它 server 建立的鏈接,也封裝在 ngx_connection_t 中。做爲客戶端,Nginx 先獲取一個 ngx_connection_t 結構體,而後建立 socket,並設置 socket 的屬性( 好比非阻塞)。而後再經過添加讀寫事件,調用 connect/read/write 來調用鏈接,最後關掉鏈接,並釋放 ngx_connection_t。

在 Nginx 中,每一個進程會有一個鏈接數的最大上限,這個上限與系統對 fd 的限制不同。在操做系統中,經過 ulimit -n,咱們能夠獲得一個進程所可以打開的 fd 的最大數,即 nofile,由於每一個 socket 鏈接會佔用掉一個 fd,因此這也會限制咱們進程的最大鏈接數,固然也會直接影響到咱們程序所能支持的最大併發數,當 fd 用完後,再建立 socket 時,就會失敗。Nginx 經過設置 worker_connectons 來設置每一個進程支持的最大鏈接數。若是該值大於 nofile,那麼實際的最大鏈接數是 nofile,Nginx 會有警告。Nginx 在實現時,是經過一個鏈接池來管理的,每一個 worker 進程都有一個獨立的鏈接池,鏈接池的大小是 worker_connections。這裏的鏈接池裏面保存的其實不是真實的鏈接,它只是一個 worker_connections 大小的一個 ngx_connection_t 結構的數組。而且,Nginx 會經過一個鏈表 free_connections 來保存全部的空閒 ngx_connection_t,每次獲取一個鏈接時,就從空閒鏈接鏈表中獲取一個,用完後,再放回空閒鏈接鏈表裏面。

在這裏,不少人會誤解 worker_connections 這個參數的意思,認爲這個值就是 Nginx 所能創建鏈接的最大值。其實否則,這個值是表示每一個 worker 進程所能創建鏈接的最大值,因此,一個 Nginx 能創建的最大鏈接數,應該是worker_connections * worker_processes。固然,這裏說的是最大鏈接數,對於 HTTP 請求本地資源來講,可以支持的最大併發數量是worker_connections * worker_processes,而若是是 HTTP 做爲反向代理來講,最大併發數量應該是worker_connections * worker_processes/2。由於做爲反向代理服務器,每一個併發會創建與客戶端的鏈接和與後端服務的鏈接,會佔用兩個鏈接。

那麼,咱們前面有說過一個客戶端鏈接過來後,多個空閒的進程,會競爭這個鏈接,很容易看到,這種競爭會致使不公平,若是某個進程獲得 accept 的機會比較多,它的空閒鏈接很快就用完了,若是不提早作一些控制,當 accept 到一個新的 tcp 鏈接後,由於沒法獲得空閒鏈接,並且沒法將此鏈接轉交給其它進程,最終會致使此 tcp 鏈接得不處處理,就停止掉了。很顯然,這是不公平的,有的進程有空餘鏈接,卻沒有處理機會,有的進程由於沒有空餘鏈接,卻人爲地丟棄鏈接。那麼,如何解決這個問題呢?首先,Nginx 的處理得先打開 accept_mutex 選項,此時,只有得到了 accept_mutex 的進程纔會去添加accept事件,也就是說,Nginx會控制進程是否添加 accept 事件。Nginx 使用一個叫 ngx_accept_disabled 的變量來控制是否去競爭 accept_mutex 鎖。在第一段代碼中,計算 ngx_accept_disabled 的值,這個值是 Nginx 單進程的全部鏈接總數的八分之一,減去剩下的空閒鏈接數量,獲得的這個 ngx_accept_disabled 有一個規律,當剩餘鏈接數小於總鏈接數的八分之一時,其值才大於 0,並且剩餘的鏈接數越小,這個值越大。再看第二段代碼,當 ngx_accept_disabled 大於 0 時,不會去嘗試獲取 accept_mutex 鎖,而且將 ngx_accept_disabled 減 1,因而,每次執行到此處時,都會去減 1,直到小於 0。不去獲取 accept_mutex 鎖,就是等於讓出獲取鏈接的機會,很顯然能夠看出,當空餘鏈接越少時,ngx_accept_disable 越大,因而讓出的機會就越多,這樣其它進程獲取鎖的機會也就越大。不去 accept,本身的鏈接就控制下來了,其它進程的鏈接池就會獲得利用,這樣,Nginx 就控制了多進程間鏈接的平衡了。

ngx_accept_disabled = ngx_cycle->connection_n / 8
        - ngx_cycle->free_connection_n;

    if (ngx_accept_disabled > 0) {
        ngx_accept_disabled--;

    } else {
        if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
            return;
        }

        if (ngx_accept_mutex_held) {
            flags |= NGX_POST_EVENTS;

        } else {
            if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
            {
                timer = ngx_accept_mutex_delay;
            }
        }
    }

好了,鏈接就先介紹到這,本章的目的是介紹基本概念,知道在 Nginx 中鏈接是個什麼東西就好了,並且鏈接是屬於比較高級的用法,在後面的模塊開發高級篇會有專門的章節來說解鏈接與事件的實現及使用。

request

這節咱們講 request,在 Nginx 中咱們指的是 http 請求,具體到 Nginx 中的數據結構是ngx_http_request_t。ngx_http_request_t 是對一個 http 請求的封裝。 咱們知道,一個 http 請求,包含請求行、請求頭、請求體、響應行、響應頭、響應體。

http 請求是典型的請求-響應類型的的網絡協議,而 http 是文本協議,因此咱們在分析請求行與請求頭,以及輸出響應行與響應頭,每每是一行一行的進行處理。若是咱們本身來寫一個 http 服務器,一般在一個鏈接創建好後,客戶端會發送請求過來。而後咱們讀取一行數據,分析出請求行中包含的 method、uri、http_version 信息。而後再一行一行處理請求頭,並根據請求 method 與請求頭的信息來決定是否有請求體以及請求體的長度,而後再去讀取請求體。獲得請求後,咱們處理請求產生須要輸出的數據,而後再生成響應行,響應頭以及響應體。在將響應發送給客戶端以後,一個完整的請求就處理完了。固然這是最簡單的 webserver 的處理方式,其實 Nginx 也是這樣作的,只是有一些小小的區別,好比,當請求頭讀取完成後,就開始進行請求的處理了。Nginx 經過 ngx_http_request_t 來保存解析請求與輸出響應相關的數據。

那接下來,簡要講講 Nginx 是如何處理一個完整的請求的。對於 Nginx 來講,一個請求是從ngx_http_init_request 開始的,在這個函數中,會設置讀事件爲 ngx_http_process_request_line,也就是說,接下來的網絡事件,會由 ngx_http_process_request_line 來執行。從ngx_http_process_request_line 的函數名,咱們能夠看到,這就是來處理請求行的,正好與以前講的,處理請求的第一件事就是處理請求行是一致的。經過 ngx_http_read_request_header 來讀取請求數據。而後調用 ngx_http_parse_request_line 函數來解析請求行。Nginx 爲提升效率,採用狀態機來解析請求行,並且在進行 method 的比較時,沒有直接使用字符串比較,而是將四個字符轉換成一個整型,而後一次比較以減小 cpu 的指令數,這個前面有說過。不少人可能很清楚一個請求行包含請求的方法,uri,版本,殊不知道其實在請求行中,也是能夠包含有 host 的。好比一個請求 GEThttp://www.taobao.com/uri HTTP/1.0 這樣一個請求行也是合法的,並且 host 是 www.taobao.com,這個時候,Nginx 會忽略請求頭中的 host 域,而以請求行中的這個爲準來查找虛擬主機。另外,對於對於 http0.9 版來講,是不支持請求頭的,因此這裏也是要特別的處理。因此,在後面解析請求頭時,協議版本都是 1.0 或 1.1。整個請求行解析到的參數,會保存到 ngx_http_request_t 結構當中。

在解析完請求行後,Nginx 會設置讀事件的 handler 爲 ngx_http_process_request_headers,而後後續的請求就在 ngx_http_process_request_headers 中進行讀取與解析。ngx_http_process_request_headers 函數用來讀取請求頭,跟請求行同樣,仍是調用 ngx_http_read_request_header 來讀取請求頭,調用 ngx_http_parse_header_line 來解析一行請求頭,解析到的請求頭會保存到 ngx_http_request_t 的域 headers_in 中,headers_in 是一個鏈表結構,保存全部的請求頭。而 HTTP 中有些請求是須要特別處理的,這些請求頭與請求處理函數存放在一個映射表裏面,即 ngx_http_headers_in,在初始化時,會生成一個 hash 表,當每解析到一個請求頭後,就會先在這個 hash 表中查找,若是有找到,則調用相應的處理函數來處理這個請求頭。好比:Host 頭的處理函數是 ngx_http_process_host。

當 Nginx 解析到兩個回車換行符時,就表示請求頭的結束,此時就會調用 ngx_http_process_request 來處理請求了。ngx_http_process_request 會設置當前的鏈接的讀寫事件處理函數爲 ngx_http_request_handler,而後再調用 ngx_http_handler 來真正開始處理一個完整的http請求。這裏可能比較奇怪,讀寫事件處理函數都是ngx_http_request_handler,其實在這個函數中,會根據當前事件是讀事件仍是寫事件,分別調用 ngx_http_request_t 中的 read_event_handler 或者是 write_event_handler。因爲此時,咱們的請求頭已經讀取完成了,以前有說過,Nginx 的作法是先不讀取請求 body,因此這裏面咱們設置 read_event_handler 爲 ngx_http_block_reading,即不讀取數據了。剛纔說到,真正開始處理數據,是在 ngx_http_handler 這個函數裏面,這個函數會設置 write_event_handler 爲 ngx_http_core_run_phases,並執行 ngx_http_core_run_phases 函數。ngx_http_core_run_phases 這個函數將執行多階段請求處理,Nginx 將一個 http 請求的處理分爲多個階段,那麼這個函數就是執行這些階段來產生數據。由於 ngx_http_core_run_phases 最後會產生數據,因此咱們就很容易理解,爲何設置寫事件的處理函數爲 ngx_http_core_run_phases 了。在這裏,我簡要說明了一下函數的調用邏輯,咱們須要明白最終是調用 ngx_http_core_run_phases 來處理請求,產生的響應頭會放在 ngx_http_request_t 的 headers_out 中,這一部份內容,我會放在請求處理流程裏面去講。Nginx 的各類階段會對請求進行處理,最後會調用 filter 來過濾數據,對數據進行加工,如 truncked 傳輸、gzip 壓縮等。這裏的 filter 包括 header filter 與 body filter,即對響應頭或響應體進行處理。filter 是一個鏈表結構,分別有 header filter 與 body filter,先執行 header filter 中的全部 filter,而後再執行 body filter 中的全部 filter。在 header filter 中的最後一個 filter,即 ngx_http_header_filter,這個 filter 將會遍歷全部的響應頭,最後須要輸出的響應頭在一個連續的內存,而後調用 ngx_http_write_filter 進行輸出。ngx_http_write_filter 是 body filter 中的最後一個,因此 Nginx 首先的 body 信息,在通過一系列的 body filter 以後,最後也會調用 ngx_http_write_filter 來進行輸出(有圖來講明)。

這裏要注意的是,Nginx 會將整個請求頭都放在一個 buffer 裏面,這個 buffer 的大小經過配置項 client_header_buffer_size 來設置,若是用戶的請求頭太大,這個 buffer 裝不下,那 Nginx 就會從新分配一個新的更大的 buffer 來裝請求頭,這個大 buffer 能夠經過 large_client_header_buffers 來設置,這個 large_buffer 這一組 buffer,好比配置 48k,就是表示有四個 8k 大小的 buffer 能夠用。注意,爲了保存請求行或請求頭的完整性,一個完整的請求行或請求頭,須要放在一個連續的內存裏面,因此,一個完整的請求行或請求頭,只會保存在一個 buffer 裏面。這樣,若是請求行大於一個 buffer 的大小,就會返回 414 錯誤,若是一個請求頭大小大於一個 buffer 大小,就會返回 400 錯誤。在瞭解了這些參數的值,以及 Nginx 實際的作法以後,在應用場景,咱們就須要根據實際的需求來調整這些參數,來優化咱們的程序了。

處理流程圖:

以上這些,就是 Nginx 中一個 http 請求的生命週期了。咱們再看看與請求相關的一些概念吧。

keepalive

固然,在 Nginx 中,對於 http1.0 與 http1.1 也是支持長鏈接的。什麼是長鏈接呢?咱們知道,http 請求是基於 TCP 協議之上的,那麼,當客戶端在發起請求前,須要先與服務端創建 TCP 鏈接,而每一次的 TCP 鏈接是須要三次握手來肯定的,若是客戶端與服務端之間網絡差一點,這三次交互消費的時間會比較多,並且三次交互也會帶來網絡流量。固然,當鏈接斷開後,也會有四次的交互,固然對用戶體驗來講就不重要了。而 http 請求是請求應答式的,若是咱們能知道每一個請求頭與響應體的長度,那麼咱們是能夠在一個鏈接上面執行多個請求的,這就是所謂的長鏈接,但前提條件是咱們先得肯定請求頭與響應體的長度。對於請求來講,若是當前請求須要有body,如 POST 請求,那麼 Nginx 就須要客戶端在請求頭中指定 content-length 來代表 body 的大小,不然返回 400 錯誤。也就是說,請求體的長度是肯定的,那麼響應體的長度呢?先來看看 http 協議中關於響應 body 長度的肯定:

  1. 對於 http1.0 協議來講,若是響應頭中有 content-length 頭,則以 content-length 的長度就能夠知道 body 的長度了,客戶端在接收 body 時,就能夠依照這個長度來接收數據,接收完後,就表示這個請求完成了。而若是沒有 content-length 頭,則客戶端會一直接收數據,直到服務端主動斷開鏈接,才表示 body 接收完了。

  2. 而對於 http1.1 協議來講,若是響應頭中的 Transfer-encoding 爲 chunked 傳輸,則表示 body 是流式輸出,body 會被分紅多個塊,每塊的開始會標識出當前塊的長度,此時,body 不須要經過長度來指定。若是是非 chunked 傳輸,並且有 content-length,則按照 content-length 來接收數據。不然,若是是非 chunked,而且沒有 content-length,則客戶端接收數據,直到服務端主動斷開鏈接。

從上面,咱們能夠看到,除了 http1.0 不帶 content-length 以及 http1.1 非 chunked 不帶 content-length 外,body 的長度是可知的。此時,當服務端在輸出完 body 以後,會能夠考慮使用長鏈接。可否使用長鏈接,也是有條件限制的。若是客戶端的請求頭中的 connection爲close,則表示客戶端須要關掉長鏈接,若是爲 keep-alive,則客戶端須要打開長鏈接,若是客戶端的請求中沒有 connection 這個頭,那麼根據協議,若是是 http1.0,則默認爲 close,若是是 http1.1,則默認爲 keep-alive。若是結果爲 keepalive,那麼,Nginx 在輸出完響應體後,會設置當前鏈接的 keepalive 屬性,而後等待客戶端下一次請求。固然,Nginx 不可能一直等待下去,若是客戶端一直不發數據過來,豈不是一直佔用這個鏈接?因此當 Nginx 設置了 keepalive 等待下一次的請求時,同時也會設置一個最大等待時間,這個時間是經過選項 keepalive_timeout 來配置的,若是配置爲 0,則表示關掉 keepalive,此時,http 版本不管是 1.1 仍是 1.0,客戶端的 connection 無論是 close 仍是 keepalive,都會強制爲 close。

若是服務端最後的決定是 keepalive 打開,那麼在響應的 http 頭裏面,也會包含有 connection 頭域,其值是"Keep-Alive",不然就是"Close"。若是 connection 值爲 close,那麼在 Nginx 響應完數據後,會主動關掉鏈接。因此,對於請求量比較大的 Nginx 來講,關掉 keepalive 最後會產生比較多的 time-wait 狀態的 socket。通常來講,當客戶端的一次訪問,須要屢次訪問同一個 server 時,打開 keepalive 的優點很是大,好比圖片服務器,一般一個網頁會包含不少個圖片。打開 keepalive 也會大量減小 time-wait 的數量。

pipe

在 http1.1 中,引入了一種新的特性,即 pipeline。那麼什麼是 pipeline 呢?pipeline 其實就是流水線做業,它能夠看做爲 keepalive 的一種昇華,由於 pipeline 也是基於長鏈接的,目的就是利用一個鏈接作屢次請求。若是客戶端要提交多個請求,對於keepalive來講,那麼第二個請求,必需要等到第一個請求的響應接收徹底後,才能發起,這和 TCP 的中止等待協議是同樣的,獲得兩個響應的時間至少爲2*RTT。而對 pipeline 來講,客戶端沒必要等到第一個請求處理完後,就能夠立刻發起第二個請求。獲得兩個響應的時間可能可以達到1*RTT。Nginx 是直接支持 pipeline 的,可是,Nginx 對 pipeline 中的多個請求的處理卻不是並行的,依然是一個請求接一個請求的處理,只是在處理第一個請求的時候,客戶端就能夠發起第二個請求。這樣,Nginx 利用 pipeline 減小了處理完一個請求後,等待第二個請求的請求頭數據的時間。其實 Nginx 的作法很簡單,前面說到,Nginx 在讀取數據時,會將讀取的數據放到一個 buffer 裏面,因此,若是 Nginx 在處理完前一個請求後,若是發現 buffer 裏面還有數據,就認爲剩下的數據是下一個請求的開始,而後就接下來處理下一個請求,不然就設置 keepalive。

lingering_close

lingering_close,字面意思就是延遲關閉,也就是說,當 Nginx 要關閉鏈接時,並不是當即關閉鏈接,而是先關閉 tcp 鏈接的寫,再等待一段時間後再關掉鏈接的讀。爲何要這樣呢?咱們先來看看這樣一個場景。Nginx 在接收客戶端的請求時,可能因爲客戶端或服務端出錯了,要當即響應錯誤信息給客戶端,而 Nginx 在響應錯誤信息後,大分部狀況下是須要關閉當前鏈接。Nginx 執行完 write()系統調用把錯誤信息發送給客戶端,write()系統調用返回成功並不表示數據已經發送到客戶端,有可能還在 tcp 鏈接的 write buffer 裏。接着若是直接執行 close()系統調用關閉 tcp 鏈接,內核會首先檢查 tcp 的 read buffer 裏有沒有客戶端發送過來的數據留在內核態沒有被用戶態進程讀取,若是有則發送給客戶端 RST 報文來關閉 tcp 鏈接丟棄 write buffer 裏的數據,若是沒有則等待 write buffer 裏的數據發送完畢,而後再通過正常的 4 次分手報文斷開鏈接。因此,當在某些場景下出現 tcp write buffer 裏的數據在 write()系統調用以後到 close()系統調用執行以前沒有發送完畢,且 tcp read buffer 裏面還有數據沒有讀,close()系統調用會致使客戶端收到 RST 報文且不會拿到服務端發送過來的錯誤信息數據。那客戶端確定會想,這服務器好霸道,動不動就 reset 個人鏈接,連個錯誤信息都沒有。

在上面這個場景中,咱們能夠看到,關鍵點是服務端給客戶端發送了 RST 包,致使本身發送的數據在客戶端忽略掉了。因此,解決問題的重點是,讓服務端別發 RST 包。再想一想,咱們發送 RST 是由於咱們關掉了鏈接,關掉鏈接是由於咱們不想再處理此鏈接了,也不會有任何數據產生了。對於全雙工的 TCP 鏈接來講,咱們只須要關掉寫就好了,讀能夠繼續進行,咱們只須要丟掉讀到的任何數據就好了,這樣的話,當咱們關掉鏈接後,客戶端再發過來的數據,就不會再收到 RST 了。固然最終咱們仍是須要關掉這個讀端的,因此咱們會設置一個超時時間,在這個時間事後,就關掉讀,客戶端再發送數據來就無論了,做爲服務端我會認爲,都這麼長時間了,發給你的錯誤信息也應該讀到了,再慢就不關我事了,要怪就怪你 RP 很差了。固然,正常的客戶端,在讀取到數據後,會關掉鏈接,此時服務端就會在超時時間內關掉讀端。這些正是 lingering_close 所作的事情。協議棧提供 SO_LINGER 這個選項,它的一種配置狀況就是來處理 lingering_close 的狀況的,不過 Nginx 是本身實現的 lingering_close。lingering_close 存在的意義就是來讀取剩下的客戶端發來的數據,因此 Nginx 會有一個讀超時時間,經過 lingering_timeout 選項來設置,若是在 lingering_timeout 時間內尚未收到數據,則直接關掉鏈接。Nginx 還支持設置一個總的讀取時間,經過 lingering_time 來設置,這個時間也就是 Nginx 在關閉寫以後,保留 socket 的時間,客戶端須要在這個時間內發送完全部的數據,不然 Nginx 在這個時間事後,會直接關掉鏈接。固然,Nginx 是支持配置是否打開 lingering_close 選項的,經過 lingering_close 選項來配置。

那麼,咱們在實際應用中,是否應該打開 lingering_close 呢?這個就沒有固定的推薦值了,如 Maxim Dounin所說,lingering_close 的主要做用是保持更好的客戶端兼容性,可是卻須要消耗更多的額外資源(好比鏈接會一直佔着)。

這節,咱們介紹了 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                        '\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");
    u_char buffer[1024];
    ngx_snprintf(buffer, 1024, "%V", &str);    // 注意,str取地址
void ngx_encode_base64(ngx_str_t *dst, ngx_str_t *src);
    ngx_int_t ngx_decode_base64(ngx_str_t *dst, ngx_str_t *src);

這兩個函數用於對 str 進行 base64 編碼與解碼,調用前,須要保證 dst 中有足夠的空間來存放結果,若是不知道具體大小,可先調用 ngx_base64_encoded_length 與 ngx_base64_decoded_length 來預估最大佔用空間。

uintptr_t ngx_escape_uri(u_char *dst, u_char *src, size_t size,
        ngx_uint_t type);

對 src 進行編碼,根據 type 來按不一樣的方式進行編碼,若是 dst 爲 NULL,則返回須要轉義的字符的數量,由此可獲得須要的空間大小。type 的類型能夠是:

#define NGX_ESCAPE_URI         0
    #define NGX_ESCAPE_ARGS        1
    #define NGX_ESCAPE_HTML        2
    #define NGX_ESCAPE_REFRESH     3
    #define NGX_ESCAPE_MEMCACHED   4
    #define NGX_ESCAPE_MAIL_AUTH   5
void ngx_unescape_uri(u_char **dst, u_char **src, size_t size, ngx_uint_t type);

對 src 進行反編碼,type 能夠是 0、NGX_UNESCAPE_URI、NGX_UNESCAPE_REDIRECT 這三個值。若是是 0,則表示 src 中的全部字符都要進行轉碼。若是是 NGX_UNESCAPE_URI 與 NGX_UNESCAPE_REDIRECT,則遇到'?'後就結束了,後面的字符就無論了。而 NGX_UNESCAPE_URI 與 NGX_UNESCAPE_REDIRECT 之間的區別是 NGX_UNESCAPE_URI 對於遇到的須要轉碼的字符,都會轉碼,而 NGX_UNESCAPE_REDIRECT 則只會對非可見字符進行轉碼。

uintptr_t ngx_escape_html(u_char *dst, u_char *src, size_t size);

對 html 標籤進行編碼。

固然,我這裏只介紹了一些經常使用的 api 的使用,你們能夠先熟悉一下,在實際使用過程當中,遇到不明白的,最快最直接的方法就是去看源碼,看 api 的實現或看 Nginx 自身調用 api 的地方是怎麼作的,代碼就是最好的文檔。

ngx_pool_t

ngx_pool_t是一個很是重要的數據結構,在不少重要的場合都有使用,不少重要的數據結構也都在使用它。那麼它到底是一個什麼東西呢?簡單的說,它提供了一種機制,幫助管理一系列的資源(如內存,文件等),使得對這些資源的使用和釋放統一進行,免除了使用過程當中考慮到對各類各樣資源的何時釋放,是否遺漏了釋放的擔憂。

例如對於內存的管理,若是咱們須要使用內存,那麼老是從一個 ngx_pool_t 的對象中獲取內存,在最終的某個時刻,咱們銷燬這個 ngx_pool_t 對象,全部這些內存都被釋放了。這樣咱們就沒必要要對對這些內存進行 malloc 和 free 的操做,不用擔憂是否某塊被malloc出來的內存沒有被釋放。由於當 ngx_pool_t 對象被銷燬的時候,全部從這個對象中分配出來的內存都會被統一釋放掉。

再好比咱們要使用一系列的文件,可是咱們打開之後,最終須要都關閉,那麼咱們就把這些文件統一登記到一個 ngx_pool_t 對象中,當這個 ngx_pool_t 對象被銷燬的時候,全部這些文件都將會被關閉。

從上面舉的兩個例子中咱們能夠看出,使用 ngx_pool_t 這個數據結構的時候,全部的資源的釋放都在這個對象被銷燬的時刻,統一進行了釋放,那麼就會帶來一個問題,就是這些資源的生存週期(或者說被佔用的時間)是跟 ngx_pool_t 的生存週期基本一致(ngx_pool_t 也提供了少許操做能夠提早釋放資源)。從最高效的角度來講,這並非最好的。好比,咱們須要依次使用 A,B,C 三個資源,且使用完 B 的時候,A 就不會再被使用了,使用C的時候 A 和 B 都不會被使用到。若是不使用 ngx_pool_t 來管理這三個資源,那咱們可能從系統裏面申請 A,使用 A,而後在釋放 A。接着申請 B,使用 B,再釋放 B。最後申請 C,使用 C,而後釋放 C。可是當咱們使用一個 ngx_pool_t 對象來管理這三個資源的時候,A,B 和 C 的釋放是在最後一塊兒發生的,也就是在使用完 C 之後。誠然,這在客觀上增長了程序在一段時間的資源使用量。可是這也減輕了程序員分別管理三個資源的生命週期的工做。這也就是有所得,必有所失的道理。其實是一個取捨的問題,要看在具體的狀況下,你更在意的是哪一個。

能夠看一下在 Nginx 裏面一個典型的使用 ngx_pool_t 的場景,對於 Nginx 處理的每一個 http request, Nginx 會生成一個 ngx_pool_t 對象與這個 http request 關聯,全部處理過程當中須要申請的資源都從這個 ngx_pool_t 對象中獲取,當這個 http request 處理完成之後,全部在處理過程當中申請的資源,都將隨着這個關聯的 ngx_pool_t 對象的銷燬而釋放。

ngx_pool_t 相關結構及操做被定義在文件src/core/ngx_palloc.h|c中。

typedef struct ngx_pool_s        ngx_pool_t; 

    struct ngx_pool_s {
        ngx_pool_data_t       d;
        size_t                max;
        ngx_pool_t           *current;
        ngx_chain_t          *chain;
        ngx_pool_large_t     *large;
        ngx_pool_cleanup_t   *cleanup;
        ngx_log_t            *log;
    };

從 ngx_pool_t 的通常使用者的角度來講,可不用關注 ngx_pool_t 結構中各字段做用。因此這裏也不會進行詳細的解釋,固然在說明某些操做函數的使用的時候,若有必要,會進行說明。

下面咱們來分別解釋下 ngx_pool_t 的相關操做。

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);

建立一個初始節點大小爲 size 的 pool,log 爲後續在該 pool 上進行操做時輸出日誌的對象。 須要說明的是 size 的選擇,size 的大小必須小於等於 NGX_MAX_ALLOC_FROM_POOL,且必須大於 sizeof(ngx_pool_t)。

選擇大於 NGX_MAX_ALLOC_FROM_POOL 的值會形成浪費,由於大於該限制的空間不會被用到(只是說在第一個由 ngx_pool_t 對象管理的內存塊上的內存,後續的分配若是第一個內存塊上的空閒部分已用完,會再分配的)。

選擇小於 sizeof(ngx_pool_t)的值會形成程序崩潰。因爲初始大小的內存塊中要用一部分來存儲 ngx_pool_t 這個信息自己。

當一個 ngx_pool_t 對象被建立之後,該對象的 max 字段被賦值爲 size-sizeof(ngx_pool_t)和 NGX_MAX_ALLOC_FROM_POOL 這二者中比較小的。後續的從這個 pool 中分配的內存塊,在第一塊內存使用完成之後,若是要繼續分配的話,就須要繼續從操做系統申請內存。當內存的大小小於等於 max 字段的時候,則分配新的內存塊,連接在 d 這個字段(其實是 d.next 字段)管理的一條鏈表上。當要分配的內存塊是比 max 大的,那麼從系統中申請的內存是被掛接在 large 字段管理的一條鏈表上。咱們暫且把這個稱之爲大塊內存鏈和小塊內存鏈。

void *ngx_palloc(ngx_pool_t *pool, size_t size);

從這個 pool 中分配一塊爲 size 大小的內存。注意,此函數分配的內存的起始地址按照 NGX_ALIGNMENT 進行了對齊。對齊操做會提升系統處理的速度,但會形成少許內存的浪費。

void *ngx_pnalloc(ngx_pool_t *pool, size_t size);

從這個 pool 中分配一塊爲 size 大小的內存。可是此函數分配的內存並無像上面的函數那樣進行過對齊。

.. code:: c

void *ngx_pcalloc(ngx_pool_t *pool, size_t size);

該函數也是分配size大小的內存,而且對分配的內存塊進行了清零。內部其實是轉調用ngx_palloc實現的。

void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);

按照指定對齊大小 alignment 來申請一塊大小爲 size 的內存。此處獲取的內存無論大小都將被置於大內存塊鏈中管理。

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);

對於被置於大塊內存鏈,也就是被 large 字段管理的一列內存中的某塊進行釋放。該函數的實現是順序遍歷 large 管理的大塊內存鏈表。因此效率比較低下。若是在這個鏈表中找到了這塊內存,則釋放,並返回 NGX_OK。不然返回 NGX_DECLINED。

因爲這個操做效率比較低下,除非必要,也就是說這塊內存很是大,確應及時釋放,不然通常不須要調用。反正內存在這個 pool 被銷燬的時候,總歸會都釋放掉的嘛!

ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);

ngx_pool_t 中的 cleanup 字段管理着一個特殊的鏈表,該鏈表的每一項都記錄着一個特殊的須要釋放的資源。對於這個鏈表中每一個節點所包含的資源如何去釋放,是自說明的。這也就提供了很是大的靈活性。意味着,ngx_pool_t 不只僅能夠管理內存,經過這個機制,也能夠管理任何須要釋放的資源,例如,關閉文件,或者刪除文件等等。下面咱們看一下這個鏈表每一個節點的類型:

typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;
    typedef void (*ngx_pool_cleanup_pt)(void *data);

    struct ngx_pool_cleanup_s {
        ngx_pool_cleanup_pt   handler;
        void                 *data;
        ngx_pool_cleanup_t   *next;
    };
  • data: 指明瞭該節點所對應的資源。

  • handler: 是一個函數指針,指向一個能夠釋放 data 所對應資源的函數。該函數只有一個參數,就是 data。

  • next: 指向該鏈表中下一個元素。

看到這裏,ngx_pool_cleanup_add 這個函數的用法,我相信你們都應該有一些明白了。可是這個參數 size 是起什麼做用的呢?這個 size 就是要存儲這個 data 字段所指向的資源的大小,該函數會爲 data 分配 size 大小的空間。

好比咱們須要最後刪除一個文件。那咱們在調用這個函數的時候,把 size 指定爲存儲文件名的字符串的大小,而後調用這個函數給 cleanup 鏈表中增長一項。該函數會返回新添加的這個節點。咱們而後把這個節點中的 data 字段拷貝爲文件名。把 hander 字段賦值爲一個刪除文件的函數(固然該函數的原型要按照 void (\*ngx_pool_cleanup_pt)(void \*data))。

void ngx_destroy_pool(ngx_pool_t *pool);

該函數就是釋放 pool 中持有的全部內存,以及依次調用 cleanup 字段所管理的鏈表中每一個元素的 handler 字段所指向的函數,來釋放掉全部該 pool 管理的資源。而且把 pool 指向的 ngx_pool_t 也釋放掉了,徹底不可用了。

void ngx_reset_pool(ngx_pool_t *pool);

該函數釋放 pool 中全部大塊內存鏈表上的內存,小塊內存鏈上的內存塊都修改成可用。可是不會去處理 cleanup鏈表上的項目。

ngx_array_t

ngx_array_t 是 Nginx 內部使用的數組結構。Nginx 的數組結構在存儲上與你們認知的 C 語言內置的數組有類似性,好比實際上存儲數據的區域也是一大塊連續的內存。可是數組除了存儲數據的內存之外還包含一些元信息來描述相關的一些信息。下面咱們從數組的定義上來詳細的瞭解一下。ngx_array_t 的定義位於src/core/ngx_array.c|h裏面。

typedef struct ngx_array_s       ngx_array_t;
    struct ngx_array_s {
        void        *elts;
        ngx_uint_t   nelts;
        size_t       size;
        ngx_uint_t   nalloc;
        ngx_pool_t  *pool;
    };
  • elts: 指向實際的數據存儲區域。

  • nelts: 數組實際元素個數。

  • size: 數組單個元素的大小,單位是字節。

  • nalloc: 數組的容量。表示該數組在不引起擴容的前提下,能夠最多存儲的元素的個數。當 nelts 增加到達 nalloc 時,若是再往此數組中存儲元素,則會引起數組的擴容。數組的容量將會擴展到原有容量的 2 倍大小。其實是分配新的一塊內存,新的一塊內存的大小是原有內存大小的 2 倍。原有的數據會被拷貝到新的一塊內存中。

  • pool: 該數組用來分配內存的內存池。

下面介紹 ngx_array_t 相關操做函數。

ngx_array_t *ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size);

建立一個新的數組對象,並返回這個對象。

  • p: 數組分配內存使用的內存池;
  • n: 數組的初始容量大小,即在不擴容的狀況下最多能夠容納的元素個數。
  • size: 單個元素的大小,單位是字節。
void ngx_array_destroy(ngx_array_t *a);

銷燬該數組對象,並釋放其分配的內存回內存池。

void *ngx_array_push(ngx_array_t *a);

在數組 a 上新追加一個元素,並返回指向新元素的指針。須要把返回的指針使用類型轉換,轉換爲具體的類型,而後再給新元素自己或者是各字段(若是數組的元素是複雜類型)賦值。

void *ngx_array_push_n(ngx_array_t *a, ngx_uint_t n);

在數組 a 上追加 n 個元素,並返回指向這些追加元素的首個元素的位置的指針。

static ngx_inline ngx_int_t ngx_array_init(ngx_array_t *array, ngx_pool_t *pool, ngx_uint_t n, size_t size);

若是一個數組對象是被分配在堆上的,那麼當調用 ngx_array_destroy 銷燬之後,若是想再次使用,就能夠調用此函數。

若是一個數組對象是被分配在棧上的,那麼就須要調用此函數,進行初始化的工做之後,才能夠使用。

注意事項 因爲使用 ngx_palloc 分配內存,數組在擴容時,舊的內存不會被釋放,會形成內存的浪費。所以,最好能提早規劃好數組的容量,在建立或者初始化的時候一次搞定,避免屢次擴容,形成內存浪費。

ngx_hash_t

ngx_hash_t 是 Nginx 本身的 hash 表的實現。定義和實現位於src/core/ngx_hash.h|c中。ngx_hash_t 的實現也與數據結構教科書上所描述的 hash 表的實現是大同小異。對於經常使用的解決衝突的方法有線性探測,二次探測和開鏈法等。ngx_hash_t 使用的是最經常使用的一種,也就是開鏈法,這也是 STL 中的 hash 表使用的方法。

可是 ngx_hash_t 的實現又有其幾個顯著的特色:

  1. ngx_hash_t 不像其餘的 hash 表的實現,能夠插入刪除元素,它只能一次初始化,就構建起整個 hash 表之後,既不能再刪除,也不能在插入元素了。
  2. ngx_hash_t 的開鏈並非真的開了一個鏈表,其實是開了一段連續的存儲空間,幾乎能夠看作是一個數組。這是由於 ngx_hash_t 在初始化的時候,會經歷一次預計算的過程,提早把每一個桶裏面會有多少元素放進去給計算出來,這樣就提早知道每一個桶的大小了。那麼就不須要使用鏈表,一段連續的存儲空間就足夠了。這也從必定程度上節省了內存的使用。

從上面的描述,咱們能夠看出來,這個值越大,越形成內存的浪費。就兩步,首先是初始化,而後就能夠在裏面進行查找了。下面咱們詳細來看一下。

ngx_hash_t 的初始化。

ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
 ngx_uint_t nelts);

首先咱們來看一下初始化函數。該函數的第一個參數 hinit 是初始化的一些參數的一個集合。 names 是初始化一個 ngx_hash_t 所須要的全部 key 的一個數組。而 nelts 就是 key 的個數。下面先看一下 ngx_hash_init_t 類型,該類型提供了初始化一個 hash 表所須要的一些基本信息。

typedef struct {
        ngx_hash_t       *hash;
        ngx_hash_key_pt   key;

        ngx_uint_t        max_size;
        ngx_uint_t        bucket_size;

        char             *name;
        ngx_pool_t       *pool;
        ngx_pool_t       *temp_pool;
    } ngx_hash_init_t;
  • hash: 該字段若是爲 NULL,那麼調用完初始化函數後,該字段指向新建立出來的 hash 表。若是該字段不爲 NULL,那麼在初始的時候,全部的數據被插入了這個字段所指的 hash 表中。

  • key: 指向從字符串生成 hash 值的 hash 函數。Nginx 的源代碼中提供了默認的實現函數 ngx_hash_key_lc。

  • max_size: hash 表中的桶的個數。該字段越大,元素存儲時衝突的可能性越小,每一個桶中存儲的元素會更少,則查詢起來的速度更快。固然,這個值越大,越形成內存的浪費也越大,(實際上也浪費不了多少)。
:bucket_size: 每一個桶的最大限制大小,單位是字節。若是在初始化一個 hash 表的時候,發現某個桶裏面沒法存的下全部屬於該桶的元素,則 hash 表初始化失敗。

name: 該 hash 表的名字。

pool: 該 hash 表分配內存使用的 pool。

temp_pool: 該 hash 表使用的臨時 pool,在初始化完成之後,該 pool 能夠被釋放和銷燬掉。

下面來看一下存儲 hash 表 key 的數組的結構。

typedef struct {
        ngx_str_t         key;
        ngx_uint_t        key_hash;
        void             *value;
    } ngx_hash_key_t;

key 和 value 的含義顯而易見,就不用解釋了。key_hash 是對 key 使用 hash 函數計算出來的值。

對這兩個結構分析完成之後,我想你們應該都已經明白這個函數應該是如何使用了吧。該函數成功初始化一個 hash 表之後,返回 NGX_OK,不然返回 NGX_ERROR。

void *ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len);

在 hash 裏面查找 key 對應的 value。實際上這裏的 key 是對真正的 key(也就是 name)計算出的 hash 值。len 是 name 的長度。

若是查找成功,則返回指向 value 的指針,不然返回 NULL。

ngx_hash_wildcard_t

Nginx 爲了處理帶有通配符的域名的匹配問題,實現了 ngx_hash_wildcard_t 這樣的 hash 表。他能夠支持兩種類型的帶有通配符的域名。一種是通配符在前的,例如:\*.abc.com,也能夠省略掉星號,直接寫成.abc.com。這樣的 key,能夠匹配 www.abc.com,qqq.www.abc.com 之類的。另一種是通配符在末尾的,例如:mail.xxx.\*,請特別注意通配符在末尾的不像位於開始的通配符能夠被省略掉。這樣的通配符,能夠匹配 mail.xxx.com、mail.xxx.com.cn、mail.xxx.net 之類的域名。

有一點必須說明,就是一個 ngx_hash_wildcard_t 類型的 hash 表只能包含通配符在前的key或者是通配符在後的key。不能同時包含兩種類型的通配符的 key。ngx_hash_wildcard_t 類型變量的構建是經過函數 ngx_hash_wildcard_init 完成的,而查詢是經過函數 ngx_hash_find_wc_head 或者 ngx_hash_find_wc_tail 來作的。ngx_hash_find_wc_head 查詢包含通配符在前的 key 的 hash 表的,而 ngx_hash_find_wc_tail 是查詢包含通配符在後的 key 的 hash 表的。

下面詳細說明這幾個函數的用法。

ngx_int_t ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
        ngx_uint_t nelts);

該函數用來構建一個能夠包含通配符 key 的 hash 表。

  • hinit: 構造一個通配符 hash 表的一些參數的一個集合。關於該參數對應的類型的說明,請參見 ngx_hash_t 類型中 ngx_hash_init 函數的說明。

  • names: 構造此 hash 表的全部的通配符 key 的數組。特別要注意的是這裏的 key 已經都是被預處理過的。例如:\*.abc.com或者.abc.com被預處理完成之後,變成了com.abc.。而mail.xxx.\*則被預處理爲mail.xxx.。爲何會被處理這樣?這裏不得不簡單地描述一下通配符 hash 表的實現原理。當構造此類型的 hash 表的時候,其實是構造了一個 hash 表的一個「鏈表」,是經過 hash 表中的 key 「連接」起來的。好比:對於\*.abc.com將會構造出 2 個 hash 表,第一個 hash 表中有一個 key 爲 com 的表項,該表項的 value 包含有指向第二個 hash 表的指針,而第二個 hash 表中有一個表項 abc,該表項的 value 包含有指向\*.abc.com對應的 value 的指針。那麼查詢的時候,好比查詢 www.abc.com 的時候,先查 com,經過查 com 能夠找到第二級的 hash 表,在第二級 hash 表中,再查找 abc,依次類推,直到在某一級的 hash 表中查到的表項對應的 value 對應一個真正的值而非一個指向下一級 hash 表的指針的時候,查詢過程結束。這裏有一點須要特別注意的,就是 names 數組中元素的 value 值低兩位 bit 必須爲 0(有特殊用途)。若是不知足這個條件,這個 hash 表查詢不出正確結果。

  • nelts: names 數組元素的個數。

該函數執行成功返回 NGX_OK,不然 NGX_ERROR。

void *ngx_hash_find_wc_head(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);

該函數查詢包含通配符在前的 key 的 hash 表的。

  • hwc: hash 表對象的指針。
  • name: 須要查詢的域名,例如: www.abc.com。
  • len: name 的長度。

該函數返回匹配的通配符對應 value。若是沒有查到,返回 NULL。

void *ngx_hash_find_wc_tail(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);

該函數查詢包含通配符在末尾的 key 的 hash 表的。

參數及返回值請參加上個函數的說明。

ngx_hash_combined_t

組合類型 hash 表,該 hash 表的定義以下:

typedef struct {
        ngx_hash_t            hash;
        ngx_hash_wildcard_t  *wc_head;
        ngx_hash_wildcard_t  *wc_tail;
    } ngx_hash_combined_t;

從其定義顯見,該類型實際上包含了三個 hash 表,一個普通 hash 表,一個包含前向通配符的 hash 表和一個包含後向通配符的 hash 表。

Nginx 提供該類型的做用,在於提供一個方便的容器包含三個類型的 hash 表,當有包含通配符的和不包含通配符的一組 key 構建 hash 表之後,以一種方便的方式來查詢,你不須要再考慮一個 key 究竟是應該到哪一個類型的 hash 表裏去查了。

構造這樣一組合 hash 表的時候,首先定義一個該類型的變量,再分別構造其包含的三個子 hash 表便可。

對於該類型 hash 表的查詢,Nginx 提供了一個方便的函數 ngx_hash_find_combined。

void *ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key,
    u_char *name, size_t len);

該函數在此組合 hash 表中,依次查詢其三個子 hash 表,看是否匹配,一旦找到,當即返回查找結果,也就是說若是有多個可能匹配,則只返回第一個匹配的結果。

  • hash: 此組合 hash 表對象。
  • key: 根據 name 計算出的 hash 值。
  • name: key 的具體內容。
  • len: name 的長度。

返回查詢的結果,未查到則返回 NULL。

ngx_hash_keys_arrays_t

你們看到在構建一個 ngx_hash_wildcard_t 的時候,須要對通配符的哪些 key 進行預處理。這個處理起來比較麻煩。而當有一組 key,這些裏面既有無通配符的 key,也有包含通配符的 key 的時候。咱們就須要構建三個 hash 表,一個包含普通的 key 的 hash 表,一個包含前向通配符的 hash 表,一個包含後向通配符的 hash 表(或者也能夠把這三個 hash 表組合成一個 ngx_hash_combined_t)。在這種狀況下,爲了讓你們方便的構造這些 hash 表,Nginx 提供給了此輔助類型。

該類型以及相關的操做函數也定義在src/core/ngx_hash.h|c裏。咱們先來看一下該類型的定義。

typedef struct {
        ngx_uint_t        hsize;

        ngx_pool_t       *pool;
        ngx_pool_t       *temp_pool;

        ngx_array_t       keys;
        ngx_array_t      *keys_hash;

        ngx_array_t       dns_wc_head;
        ngx_array_t      *dns_wc_head_hash;

        ngx_array_t       dns_wc_tail;
        ngx_array_t      *dns_wc_tail_hash;
    } ngx_hash_keys_arrays_t;
  • hsize: 將要構建的 hash 表的桶的個數。對於使用這個結構中包含的信息構建的三種類型的 hash 表都會使用此參數。

  • pool: 構建這些 hash 表使用的 pool。

  • temp_pool: 在構建這個類型以及最終的三個 hash 表過程當中可能用到臨時 pool。該 temp_pool 能夠在構建完成之後,被銷燬掉。這裏只是存放臨時的一些內存消耗。

  • keys: 存放全部非通配符 key 的數組。

  • keys_hash: 這是個二維數組,第一個維度表明的是 bucket 的編號,那麼 keys_hash[i] 中存放的是全部的 key 算出來的 hash 值對 hsize 取模之後的值爲 i 的 key。假設有 3 個 key,分別是 key1,key2 和 key3 假設 hash 值算出來之後對 hsize 取模的值都是 i,那麼這三個 key 的值就順序存放在keys_hash[i][0],keys_hash[i][1],keys_hash[i][2]。該值在調用的過程當中用來保存和檢測是否有衝突的 key 值,也就是是否有重複。

  • dns_wc_head: 放前向通配符 key 被處理完成之後的值。好比:\*.abc.com 被處理完成之後,變成 「com.abc.」 被存放在此數組中。

  • dns_wc_tail: 存放後向通配符 key 被處理完成之後的值。好比:mail.xxx.\* 被處理完成之後,變成 「mail.xxx.」 被存放在此數組中。

  • dns_wc_head_hash: 該值在調用的過程當中用來保存和檢測是否有衝突的前向通配符的 key 值,也就是是否有重複。

  • dns_wc_tail_hash: 該值在調用的過程當中用來保存和檢測是否有衝突的後向通配符的 key 值,也就是是否有重複。

在定義一個這個類型的變量,並對字段 pool 和 temp_pool 賦值之後,就能夠調用函數 ngx_hash_add_key 把全部的 key 加入到這個結構中了,該函數會自動實現普通 key,帶前向通配符的 key 和帶後向通配符的 key 的分類和檢查,並將這個些值存放到對應的字段中去,而後就能夠經過檢查這個結構體中的 keys、dns_wc_head、dns_wc_tail 三個數組是否爲空,來決定是否構建普通 hash 表,前向通配符 hash 表和後向通配符 hash 表了(在構建這三個類型的 hash 表的時候,能夠分別使用 keys、dns_wc_head、dns_wc_tail三個數組)。

構建出這三個 hash 表之後,能夠組合在一個 ngx_hash_combined_t 對象中,使用 ngx_hash_find_combined 進行查找。或者是仍然保持三個獨立的變量對應這三個 hash 表,本身決定什麼時候以及在哪一個 hash 表中進行查詢。

ngx_int_t ngx_hash_keys_array_init(ngx_hash_keys_arrays_t *ha, ngx_uint_t type);

初始化這個結構,主要是對這個結構中的 ngx_array_t 類型的字段進行初始化,成功返回 NGX_OK。

  • ha: 該結構的對象指針。

  • type: 該字段有 2 個值可選擇,即 NGX_HASH_SMALL 和 NGX_HASH_LARGE。用來指明將要創建的 hash 表的類型,若是是 NGX_HASH_SMALL,則有比較小的桶的個數和數組元素大小。NGX_HASH_LARGE 則相反。
ngx_int_t ngx_hash_add_key(ngx_hash_keys_arrays_t *ha, ngx_str_t *key,
    void *value, ngx_uint_t flags);

通常是循環調用這個函數,把一組鍵值對加入到這個結構體中。返回 NGX_OK 是加入成功。返回 NGX_BUSY 意味着key值重複。

  • ha: 該結構的對象指針。

  • key: 參數名自解釋了。

  • value: 參數名自解釋了。

  • flags: 有兩個標誌位能夠設置,NGX_HASH_WILDCARD_KEY 和 NGX_HASH_READONLY_KEY。同時要設置的使用邏輯與操做符就能夠了。NGX_HASH_READONLY_KEY 被設置的時候,在計算 hash 值的時候,key 的值不會被轉成小寫字符,不然會。NGX_HASH_WILDCARD_KEY 被設置的時候,說明 key 裏面可能含有通配符,會進行相應的處理。若是兩個標誌位都不設置,傳 0。

有關於這個數據結構的使用,能夠參考src/http/ngx_http.c中的 ngx_http_server_names 函數。

ngx_chain_t

Nginx 的 filter 模塊在處理從別的 filter 模塊或者是 handler 模塊傳遞過來的數據(實際上就是須要發送給客戶端的 http response)。這個傳遞過來的數據是以一個鏈表的形式(ngx_chain_t)。並且數據可能被分屢次傳遞過來。也就是屢次調用 filter 的處理函數,以不一樣的 ngx_chain_t。

該結構被定義在src/core/ngx_buf.h|c。下面咱們來看一下 ngx_chain_t 的定義。

typedef struct ngx_chain_s       ngx_chain_t;

    struct ngx_chain_s {
        ngx_buf_t    *buf;
        ngx_chain_t  *next;
    };

就 2 個字段,next 指向這個鏈表的下個節點。buf 指向實際的數據。因此在這個鏈表上追加節點也是很是容易,只要把末尾元素的 next 指針指向新的節點,把新節點的 next 賦值爲 NULL 便可。

ngx_chain_t *ngx_alloc_chain_link(ngx_pool_t *pool);

該函數建立一個 ngx_chain_t 的對象,並返回指向對象的指針,失敗返回 NULL。

#define ngx_free_chain(pool, cl)                                             \
        cl->next = pool->chain;                                                  \
    pool->chain = cl

該宏釋放一個 ngx_chain_t 類型的對象。若是要釋放整個 chain,則迭代此鏈表,對每一個節點使用此宏便可。

注意: 對 ngx_chaint_t 類型的釋放,並非真的釋放了內存,而僅僅是把這個對象掛在了這個 pool 對象的一個叫作 chain 的字段對應的 chain 上,以供下次從這個 pool 上分配 ngx_chain_t 類型對象的時候,快速的從這個 pool->chain上 取下鏈首元素就返回了,固然,若是這個鏈是空的,纔會真的在這個 pool 上使用 ngx_palloc 函數進行分配。

ngx_buf_t

這個 ngx_buf_t 就是這個 ngx_chain_t 鏈表的每一個節點的實際數據。該結構其實是一種抽象的數據結構,它表明某種具體的數據。這個數據多是指向內存中的某個緩衝區,也可能指向一個文件的某一部分,也多是一些純元數據(元數據的做用在於指示這個鏈表的讀取者對讀取的數據進行不一樣的處理)。

該數據結構位於src/core/ngx_buf.h|c文件中。咱們來看一下它的定義。

struct ngx_buf_s {
        u_char          *pos;
        u_char          *last;
        off_t            file_pos;
        off_t            file_last;

        u_char          *start;         /* start of buffer */
        u_char          *end;           /* end of buffer */
        ngx_buf_tag_t    tag;
        ngx_file_t      *file;
        ngx_buf_t       *shadow;

        /* the buf's content could be changed */
        unsigned         temporary:1;

        /*
         * the buf's content is in a memory cache or in a read only memory
         * and must not be changed
         */
        unsigned         memory:1;

        /* the buf's content is mmap()ed and must not be changed */
        unsigned         mmap:1;

        unsigned         recycled:1;
        unsigned         in_file:1;
        unsigned         flush:1;
        unsigned         sync:1;
        unsigned         last_buf:1;
        unsigned         last_in_chain:1;

        unsigned         last_shadow:1;
        unsigned         temp_file:1;

        /* STUB */ int   num;
    };
  • pos:當 buf 所指向的數據在內存裏的時候,pos 指向的是這段數據開始的位置。

  • last:當 buf 所指向的數據在內存裏的時候,last 指向的是這段數據結束的位置。

  • file_pos:當 buf 所指向的數據是在文件裏的時候,file_pos 指向的是這段數據的開始位置在文件中的偏移量。

  • file_last:當 buf 所指向的數據是在文件裏的時候,file_last 指向的是這段數據的結束位置在文件中的偏移量。

  • start:當 buf 所指向的數據在內存裏的時候,這一整塊內存包含的內容可能被包含在多個 buf 中(好比在某段數據中間插入了其餘的數據,這一塊數據就須要被拆分開)。那麼這些 buf 中的 start 和 end 都指向這一塊內存的開始地址和結束地址。而 pos 和 last 指向本 buf 所實際包含的數據的開始和結尾。

  • end:解釋參見 start。

  • tag:其實是一個void *類型的指針,使用者能夠關聯任意的對象上去,只要對使用者有意義。

  • file:當 buf 所包含的內容在文件中時,file字段指向對應的文件對象。

  • shadow:當這個 buf 完整 copy 了另一個 buf 的全部字段的時候,那麼這兩個 buf 指向的其實是同一塊內存,或者是同一個文件的同一部分,此時這兩個 buf 的 shadow 字段都是指向對方的。那麼對於這樣的兩個 buf,在釋放的時候,就須要使用者特別當心,具體是由哪裏釋放,要提早考慮好,若是形成資源的屢次釋放,可能會形成程序崩潰!

  • temporary:爲 1 時表示該 buf 所包含的內容是在一個用戶建立的內存塊中,而且能夠被在 filter 處理的過程當中進行變動,而不會形成問題。

  • memory:爲 1 時表示該 buf 所包含的內容是在內存中,可是這些內容卻不能被進行處理的 filter 進行變動。

  • mmap:爲 1 時表示該 buf 所包含的內容是在內存中, 是經過 mmap 使用內存映射從文件中映射到內存中的,這些內容卻不能被進行處理的 filter 進行變動。

  • recycled:能夠回收的。也就是這個 buf 是能夠被釋放的。這個字段一般是配合 shadow 字段一塊兒使用的,對於使用 ngx_create_temp_buf 函數建立的 buf,而且是另一個 buf 的 shadow,那麼能夠使用這個字段來標示這個buf是能夠被釋放的。

  • in_file:爲 1 時表示該 buf 所包含的內容是在文件中。

  • flush:遇到有 flush 字段被設置爲 1 的 buf 的 chain,則該 chain 的數據即使不是最後結束的數據(last_buf被設置,標誌全部要輸出的內容都完了),也會進行輸出,不會受 postpone_output 配置的限制,可是會受到發送速率等其餘條件的限制。

  • last_buf:數據被以多個 chain 傳遞給了過濾器,此字段爲 1 代表這是最後一個 buf。

  • last_in_chain:在當前的 chain 裏面,此 buf 是最後一個。特別要注意的是 last_in_chain 的 buf 不必定是last_buf,可是 last_buf 的 buf 必定是 last_in_chain 的。這是由於數據會被以多個 chain 傳遞給某 個filter 模塊。

  • last_shadow:在建立一個 buf 的 shadow 的時候,一般將新建立的一個 buf 的 last_shadow 置爲 1。

  • temp_file:因爲受到內存使用的限制,有時候一些 buf 的內容須要被寫到磁盤上的臨時文件中去,那麼這時,就設置此標誌。

對於此對象的建立,能夠直接在某個 ngx_pool_t 上分配,而後根據須要,給對應的字段賦值。也能夠使用定義好的 2 個宏:

#define ngx_alloc_buf(pool)  ngx_palloc(pool, sizeof(ngx_buf_t))
    #define ngx_calloc_buf(pool) ngx_pcalloc(pool, sizeof(ngx_buf_t))

這兩個宏使用相似函數,也是不說自明的。

對於建立 temporary 字段爲 1 的 buf(就是其內容能夠被後續的 filter 模塊進行修改),能夠直接使用函數 ngx_create_temp_buf 進行建立。

ngx_buf_t *ngx_create_temp_buf(ngx_pool_t *pool, size_t size);

該函數建立一個 ngx_buf_t 類型的對象,並返回指向這個對象的指針,建立失敗返回 NULL。

對於建立的這個對象,它的 start 和 end 指向新分配內存開始和結束的地方。pos 和 last 都指向這塊新分配內存的開始處,這樣,後續的操做能夠在這塊新分配的內存上存入數據。

  • pool: 分配該 buf 和 buf 使用的內存所使用的 pool。
  • size: 該 buf 使用的內存的大小。

爲了配合對 ngx_buf_t 的使用,Nginx 定義瞭如下的宏方便操做。

#define ngx_buf_in_memory(b)        (b->temporary || b->memory || b->mmap)

返回這個 buf 裏面的內容是否在內存裏。

#define ngx_buf_in_memory_only(b)   (ngx_buf_in_memory(b) && !b->in_file)

返回這個 buf 裏面的內容是否僅僅在內存裏,而且沒有在文件裏。

#define ngx_buf_special(b)                                                   \
        ((b->flush || b->last_buf || b->sync)                                    \
         && !ngx_buf_in_memory(b) && !b->in_file)

返回該 buf 是不是一個特殊的 buf,只含有特殊的標誌和沒有包含真正的數據。

#define ngx_buf_sync_only(b)                                                 \
        (b->sync                                                                 \
         && !ngx_buf_in_memory(b) && !b->in_file && !b->flush && !b->last_buf)

返回該 buf 是不是一個只包含 sync 標誌而不包含真正數據的特殊 buf。

#define ngx_buf_size(b)                                                      \
        (ngx_buf_in_memory(b) ? (off_t) (b->last - b->pos):                      \
                                (b->file_last - b->file_pos))

返回該 buf 所含數據的大小,無論這個數據是在文件裏仍是在內存裏。

ngx_list_t

ngx_list_t 顧名思義,看起來好像是一個 list 的數據結構。這樣的說法,算對也不算對。由於它符合 list 類型數據結構的一些特色,好比能夠添加元素,實現自增加,不會像數組類型的數據結構,受到初始設定的數組容量的限制,而且它跟咱們常見的 list 型數據結構也是同樣的,內部實現使用了一個鏈表。

那麼它跟咱們常見的鏈表實現的 list 有什麼不一樣呢?不一樣點就在於它的節點,它的節點不像咱們常見的 list 的節點,只能存放一個元素,ngx_list_t 的節點其實是一個固定大小的數組。

在初始化的時候,咱們須要設定元素須要佔用的空間大小,每一個節點數組的容量大小。在添加元素到這個 list 裏面的時候,會在最尾部的節點裏的數組上添加元素,若是這個節點的數組存滿了,就再增長一個新的節點到這個 list 裏面去。

好了,看到這裏,你們應該基本上明白這個 list 結構了吧?還不明白也沒有關係,下面咱們來具體看一下它的定義,這些定義和相關的操做函數定義在src/core/ngx_list.h|c文件中。

typedef struct {
        ngx_list_part_t  *last;
        ngx_list_part_t   part;
        size_t            size;
        ngx_uint_t        nalloc;
        ngx_pool_t       *pool;
    } ngx_list_t;
  • last: 指向該鏈表的最後一個節點。
  • part: 該鏈表的首個存放具體元素的節點。
  • size: 鏈表中存放的具體元素所需內存大小。
  • nalloc: 每一個節點所含的固定大小的數組的容量。
  • pool: 該 list 使用的分配內存的 pool。

好,咱們在看一下每一個節點的定義。

typedef struct ngx_list_part_s  ngx_list_part_t;
    struct ngx_list_part_s {
        void             *elts;
        ngx_uint_t        nelts;
        ngx_list_part_t  *next;
    };
  • elts: 節點中存放具體元素的內存的開始地址。

  • nelts: 節點中已有元素個數。這個值是不能大於鏈表頭節點 ngx_list_t 類型中的 nalloc 字段的。

  • next: 指向下一個節點。

咱們來看一下提供的一個操做的函數。

ngx_list_t *ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size);

該函數建立一個 ngx_list_t 類型的對象,並對該 list 的第一個節點分配存放元素的內存空間。

  • pool: 分配內存使用的 pool。

  • n: 每一個節點(ngx_list_part_t)固定長度的數組的長度,即最多能夠存放的元素個數。

  • size: 每一個元素所佔用的內存大小。

  • 返回值: 成功返回指向建立的 ngx_list_t 對象的指針,失敗返回 NULL。
void *ngx_list_push(ngx_list_t *list);

該函數在給定的 list 的尾部追加一個元素,並返回指向新元素存放空間的指針。若是追加失敗,則返回 NULL。

static ngx_inline ngx_int_t
    ngx_list_init(ngx_list_t *list, ngx_pool_t *pool, ngx_uint_t n, size_t size);

該函數是用於 ngx_list_t 類型的對象已經存在,可是其第一個節點存放元素的內存空間還未分配的狀況下,能夠調用此函數來給這個 list 的首節點來分配存放元素的內存空間。

那麼何時會出現已經有了 ngx_list_t 類型的對象,而其首節點存放元素的內存還沒有分配的狀況呢?那就是這個 ngx_list_t 類型的變量並非經過調用 ngx_list_create 函數建立的。例如:若是某個結構體的一個成員變量是 ngx_list_t 類型的,那麼當這個結構體類型的對象被建立出來的時候,這個成員變量也被建立出來了,可是它的首節點的存放元素的內存並未被分配。

總之,若是這個 ngx_list_t 類型的變量,若是不是你經過調用函數 ngx_list_create 建立的,那麼就必須調用此函數去初始化,不然,你往這個 list 裏追加元素就可能引起不可預知的行爲,亦或程序會崩潰!

ngx_queue_t

ngx_queue_t 是 Nginx 中的雙向鏈表,在 Nginx 源碼目錄src/core下面的ngx_queue.h|c裏面。它的原型以下:

typedef struct ngx_queue_s ngx_queue_t;

    struct ngx_queue_s {
        ngx_queue_t  *prev;
        ngx_queue_t  *next;
    };

不一樣於教科書中將鏈表節點的數據成員聲明在鏈表節點的結構體中,ngx_queue_t 只是聲明瞭前向和後向指針。在使用的時候,咱們首先須要定義一個哨兵節點(對於後續具體存放數據的節點,咱們稱之爲數據節點),好比:

ngx_queue_t free;

接下來須要進行初始化,經過宏 ngx_queue_init()來實現:

ngx_queue_init(&free);

ngx_queue_init()的宏定義以下:

#define ngx_queue_init(q)     \
        (q)->prev = q;            \
        (q)->next = q

可見初始的時候哨兵節點的 prev 和 next 都指向本身,所以實際上是一個空鏈表。ngx_queue_empty()能夠用來判斷一個鏈表是否爲空,其實現也很簡單,就是:

#define ngx_queue_empty(h)    \
        (h == (h)->prev)

那麼如何聲明一個具備數據元素的鏈表節點呢?只要在相應的結構體中加上一個 ngx_queue_t 的成員就好了。好比 ngx_http_upstream_keepalive_module 中的 ngx_http_upstream_keepalive_cache_t:

typedef struct {
        ngx_http_upstream_keepalive_srv_conf_t  *conf;

        ngx_queue_t                        queue;
        ngx_connection_t                  *connection;

        socklen_t                          socklen;
        u_char                             sockaddr[NGX_SOCKADDRLEN];
    } ngx_http_upstream_keepalive_cache_t;

對於每個這樣的數據節點,能夠經過 ngx_queue_insert_head()來添加到鏈表中,第一個參數是哨兵節點,第二個參數是數據節點,好比:

ngx_http_upstream_keepalive_cache_t cache;
    ngx_queue_insert_head(&free, &cache.queue);

相應的幾個宏定義以下:

#define ngx_queue_insert_head(h, x)                         \
        (x)->next = (h)->next;                                  \
        (x)->next->prev = x;                                    \
        (x)->prev = h;                                          \
        (h)->next = x

    #define ngx_queue_insert_after   ngx_queue_insert_head

    #define ngx_queue_insert_tail(h, x)                          \
        (x)->prev = (h)->prev;                                   \
        (x)->prev->next = x;                                     \
        (x)->next = h;                                           \
        (h)->prev = x

ngx_queue_insert_head() 和 ngx_queue_insert_after() 都是往頭部添加節點,ngx_queue_insert_tail() 是往尾部添加節點。從代碼能夠看出哨兵節點的 prev 指向鏈表的尾數據節點,next 指向鏈表的頭數據節點。另外 ngx_queue_head() 和 ngx_queue_last() 這兩個宏分別能夠獲得頭節點和尾節點。

那假如如今有一個 ngx_queue_t *q 指向的是鏈表中的數據節點的 queue 成員,如何獲得ngx_http_upstream_keepalive_cache_t 的數據呢? Nginx 提供了 ngx_queue_data() 宏來獲得ngx_http_upstream_keepalive_cache_t 的指針,例如:

ngx_http_upstream_keepalive_cache_t *cache = ngx_queue_data(q,
                              ngx_http_upstream_keepalive_cache_t,
                                                     queue);

也許您已經能夠猜到 ngx_queue_data 是經過地址相減來獲得的:

#define ngx_queue_data(q, type, link)                        \
        (type *) ((u_char *) q - offsetof(type, link))

另外 Nginx 也提供了 ngx_queue_remove()宏來從鏈表中刪除一個數據節點,以及 ngx_queue_add() 用來將一個鏈表添加到另外一個鏈表。

 
 

六.Nginx 的配置系統

Nginx 的配置系統由一個主配置文件和其餘一些輔助的配置文件構成。這些配置文件均是純文本文件,所有位於Nginx 安裝目錄下的 conf 目錄下。

配置文件中以#開始的行,或者是前面有若干空格或者 TAB,而後再跟#的行,都被認爲是註釋,也就是隻對編輯查看文件的用戶有意義,程序在讀取這些註釋行的時候,其實際的內容是被忽略的。

因爲除主配置文件 nginx.conf 之外的文件都是在某些狀況下才使用的,而只有主配置文件是在任何狀況下都被使用的。因此在這裏咱們就以主配置文件爲例,來解釋 Nginx 的配置系統。

在 nginx.conf 中,包含若干配置項。每一個配置項由配置指令和指令參數 2 個部分構成。指令參數也就是配置指令對應的配置值。

指令概述

配置指令是一個字符串,能夠用單引號或者雙引號括起來,也能夠不括。可是若是配置指令包含空格,必定要引發來。

指令參數

指令的參數使用一個或者多個空格或者 TAB 字符與指令分開。指令的參數有一個或者多個 TOKEN 串組成。TOKEN 串之間由空格或者 TAB 鍵分隔。

TOKEN 串分爲簡單字符串或者是複合配置塊。複合配置塊便是由大括號括起來的一堆內容。一個複合配置塊中可能包含若干其餘的配置指令。

若是一個配置指令的參數所有由簡單字符串構成,也就是不包含複合配置塊,那麼咱們就說這個配置指令是一個簡單配置項,不然稱之爲複雜配置項。例以下面這個是一個簡單配置項:

error_page   500 502 503 504  /50x.html;

對於簡單配置,配置項的結尾使用分號結束。對於複雜配置項,包含多個 TOKEN 串的,通常都是簡單 TOKEN 串放在前面,複合配置塊通常位於最後,並且其結尾,並不須要再添加分號。例以下面這個複雜配置項:

location / {
            root   /home/jizhao/nginx-book/build/html;
            index  index.html index.htm;
        }

指令上下文

nginx.conf 中的配置信息,根據其邏輯上的意義,對它們進行了分類,也就是分紅了多個做用域,或者稱之爲配置指令上下文。不一樣的做用域含有一個或者多個配置項。

當前 Nginx 支持的幾個指令上下文:

  • main: Nginx 在運行時與具體業務功能(好比http服務或者email服務代理)無關的一些參數,好比工做進程數,運行的身份等。
  • http: 與提供 http 服務相關的一些配置參數。例如:是否使用 keepalive 啊,是否使用gzip進行壓縮等。
  • server: http 服務上支持若干虛擬主機。每一個虛擬主機一個對應的 server 配置項,配置項裏面包含該虛擬主機相關的配置。在提供 mail 服務的代理時,也能夠創建若干 server,每一個 server 經過監聽的地址來區分。
  • location: http 服務中,某些特定的URL對應的一系列配置項。
  • mail: 實現 email 相關的 SMTP/IMAP/POP3 代理時,共享的一些配置項(由於可能實現多個代理,工做在多個監聽地址上)。

指令上下文,可能有包含的狀況出現。例如:一般 http 上下文和 mail 上下文必定是出如今 main 上下文裏的。在一個上下文裏,可能包含另一種類型的上下文屢次。例如:若是 http 服務,支持了多個虛擬主機,那麼在 http 上下文裏,就會出現多個 server 上下文。

咱們來看一個示例配置:

user  nobody;
    worker_processes  1;
    error_log  logs/error.log  info;

    events {
        worker_connections  1024;
    }

    http {  
        server {  
            listen          80;  
            server_name     www.linuxidc.com;  
            access_log      logs/linuxidc.access.log main;  
            location / {  
                index index.html;  
                root  /var/www/linuxidc.com/htdocs;  
            }  
        }  

        server {  
            listen          80;  
            server_name     www.Androidj.com;  
            access_log      logs/androidj.access.log main;  
            location / {  
                index index.html;  
                root  /var/www/androidj.com/htdocs;  
            }  
        }  
    }

    mail {
        auth_http  127.0.0.1:80/auth.php;
        pop3_capabilities  "TOP"  "USER";
        imap_capabilities  "IMAP4rev1"  "UIDPLUS";

        server {
            listen     110;
            protocol   pop3;
            proxy      on;
        }
        server {
            listen      25;
            protocol    smtp;
            proxy       on;
            smtp_auth   login plain;
            xclient     off;
        }
    }

在這個配置中,上面提到個五種配置指令上下文都存在。

存在於 main 上下文中的配置指令以下:

  • user
  • worker_processes
  • error_log
  • events
  • http
  • mail

存在於 http 上下文中的指令以下:

  • server

存在於 mail 上下文中的指令以下:

  • server
  • auth_http
  • imap_capabilities

存在於 server 上下文中的配置指令以下:

  • listen
  • server_name
  • access_log
  • location
  • protocol
  • proxy
  • smtp_auth
  • xclient

存在於 location 上下文中的指令以下:

  • index
  • root

固然,這裏只是一些示例。具體有哪些配置指令,以及這些配置指令能夠出如今什麼樣的上下文中,須要參考 Nginx 的使用文檔。

 

七.Nginx 的模塊化體系結構

Nginx 的內部結構是由核心部分和一系列的功能模塊所組成。這樣劃分是爲了使得每一個模塊的功能相對簡單,便於開發,同時也便於對系統進行功能擴展。爲了便於描述,下文中咱們將使用 Nginx core 來稱呼 Nginx 的核心功能部分。

Nginx 提供了 Web 服務器的基礎功能,同時提供了 Web 服務反向代理,Email 服務反向代理功能。Nginx core實現了底層的通信協議,爲其餘模塊和 Nginx 進程構建了基本的運行時環境,而且構建了其餘各模塊的協做基礎。除此以外,或者說大部分與協議相關的,或者應用相關的功能都是在這些模塊中所實現的。

模塊概述

Nginx 將各功能模塊組織成一條鏈,當有請求到達的時候,請求依次通過這條鏈上的部分或者所有模塊,進行處理。每一個模塊實現特定的功能。例如,實現對請求解壓縮的模塊,實現 SSI 的模塊,實現與上游服務器進行通信的模塊,實現與 FastCGI 服務進行通信的模塊。

有兩個模塊比較特殊,他們居於 Nginx core 和各功能模塊的中間。這兩個模塊就是 http 模塊和 mail 模塊。這 2 個模塊在 Nginx core 之上實現了另一層抽象,處理與 HTTP 協議和 Email 相關協議(SMTP/POP3/IMAP)有關的事件,而且確保這些事件能被以正確的順序調用其餘的一些功能模塊。

目前 HTTP 協議是被實如今 http 模塊中的,可是有可能未來被剝離到一個單獨的模塊中,以擴展 Nginx 支持 SPDY 協議。

模塊的分類

Nginx 的模塊根據其功能基本上能夠分爲如下幾種類型:

  • event module: 搭建了獨立於操做系統的事件處理機制的框架,及提供了各具體事件的處理。包括 ngx_events_module, ngx_event_core_module和ngx_epoll_module 等。Nginx 具體使用何種事件處理模塊,這依賴於具體的操做系統和編譯選項。

  • phase handler: 此類型的模塊也被直接稱爲 handler 模塊。主要負責處理客戶端請求併產生待響應內容,好比 ngx_http_static_module 模塊,負責客戶端的靜態頁面請求處理並將對應的磁盤文件準備爲響應內容輸出。

  • output filter: 也稱爲 filter 模塊,主要是負責對輸出的內容進行處理,能夠對輸出進行修改。例如,能夠實現對輸出的全部 html 頁面增長預約義的 footbar 一類的工做,或者對輸出的圖片的 URL 進行替換之類的工做。

  • upstream: upstream 模塊實現反向代理的功能,將真正的請求轉發到後端服務器上,並從後端服務器上讀取響應,發回客戶端。upstream 模塊是一種特殊的 handler,只不過響應內容不是真正由本身產生的,而是從後端服務器上讀取的。

  • load-balancer: 負載均衡模塊,實現特定的算法,在衆多的後端服務器中,選擇一個服務器出來做爲某個請求的轉發服務器。

 

八.Nginx 的請求處理

Nginx 使用一個多進程模型來對外提供服務,其中一個 master 進程,多個 worker 進程。master 進程負責管理 Nginx 自己和其餘 worker 進程。

全部實際上的業務處理邏輯都在 worker 進程。worker 進程中有一個函數,執行無限循環,不斷處理收到的來自客戶端的請求,並進行處理,直到整個 Nginx 服務被中止。

worker 進程中,ngx_worker_process_cycle()函數就是這個無限循環的處理函數。在這個函數中,一個請求的簡單處理流程以下:

  • 操做系統提供的機制(例如 epoll, kqueue 等)產生相關的事件。
  • 接收和處理這些事件,如是接受到數據,則產生更高層的 request 對象。
  • 處理 request 的 header 和 body。
  • 產生響應,併發送回客戶端。
  • 完成 request 的處理。
  • 從新初始化定時器及其餘事件。

請求的處理流程

爲了讓你們更好的瞭解 Nginx 中請求處理過程,咱們以 HTTP Request 爲例,來作一下詳細地說明。

從 Nginx 的內部來看,一個 HTTP Request 的處理過程涉及到如下幾個階段。

  • 初始化 HTTP Request(讀取來自客戶端的數據,生成 HTTP Request 對象,該對象含有該請求全部的信息)。
  • 處理請求頭。
  • 處理請求體。
  • 若是有的話,調用與此請求(URL 或者 Location)關聯的 handler。
  • 依次調用各 phase handler 進行處理。

在這裏,咱們須要瞭解一下 phase handler 這個概念。phase 字面的意思,就是階段。因此 phase handlers 也就好理解了,就是包含若干個處理階段的一些 handler。

在每個階段,包含有若干個 handler,再處理到某個階段的時候,依次調用該階段的 handler 對 HTTP Request 進行處理。

一般狀況下,一個 phase handler 對這個 request 進行處理,併產生一些輸出。一般 phase handler 是與定義在配置文件中的某個 location 相關聯的。

一個 phase handler 一般執行如下幾項任務:

  • 獲取 location 配置。
  • 產生適當的響應。
  • 發送 response header。
  • 發送 response body。

當 Nginx 讀取到一個 HTTP Request 的 header 的時候,Nginx 首先查找與這個請求關聯的虛擬主機的配置。若是找到了這個虛擬主機的配置,那麼一般狀況下,這個 HTTP Request 將會通過如下幾個階段的處理(phase handlers):

  • NGX_HTTP_POST_READ_PHASE: 讀取請求內容階段
  • NGX_HTTP_SERVER_REWRITE_PHASE: Server 請求地址重寫階段
  • NGX_HTTP_FIND_CONFIG_PHASE: 配置查找階段:
  • NGX_HTTP_REWRITE_PHASE: Location請求地址重寫階段
  • NGX_HTTP_POST_REWRITE_PHASE: 請求地址重寫提交階段
  • NGX_HTTP_PREACCESS_PHASE: 訪問權限檢查準備階段
  • NGX_HTTP_ACCESS_PHASE: 訪問權限檢查階段
  • NGX_HTTP_POST_ACCESS_PHASE: 訪問權限檢查提交階段
  • NGX_HTTP_TRY_FILES_PHASE: 配置項 try_files 處理階段
  • NGX_HTTP_CONTENT_PHASE: 內容產生階段
  • NGX_HTTP_LOG_PHASE: 日誌模塊處理階段

在內容產生階段,爲了給一個 request 產生正確的響應,Nginx 必須把這個 request 交給一個合適的 content handler 去處理。若是這個 request 對應的 location 在配置文件中被明確指定了一個 content handler,那麼Nginx 就能夠經過對 location 的匹配,直接找到這個對應的 handler,並把這個 request 交給這個 content handler 去處理。這樣的配置指令包括像,perl,flv,proxy_pass,mp4等。

若是一個 request 對應的 location 並無直接有配置的 content handler,那麼 Nginx 依次嘗試:

  • 若是一個 location 裏面有配置 random_index on,那麼隨機選擇一個文件,發送給客戶端。
  • 若是一個 location 裏面有配置 index 指令,那麼發送 index 指令指明的文件,給客戶端。
  • 若是一個 location 裏面有配置 autoindex on,那麼就發送請求地址對應的服務端路徑下的文件列表給客戶端。
  • 若是這個 request 對應的 location 上有設置 gzip_static on,那麼就查找是否有對應的.gz文件存在,有的話,就發送這個給客戶端(客戶端支持 gzip 的狀況下)。
  • 請求的 URI 若是對應一個靜態文件,static module 就發送靜態文件的內容到客戶端。

內容產生階段完成之後,生成的輸出會被傳遞到 filter 模塊去進行處理。filter 模塊也是與 location 相關的。全部的 fiter 模塊都被組織成一條鏈。輸出會依次穿越全部的 filter,直到有一個 filter 模塊的返回值代表已經處理完成。

這裏列舉幾個常見的 filter 模塊,例如:

  • server-side includes。
  • XSLT filtering。
  • 圖像縮放之類的。
  • gzip 壓縮。

在全部的 filter 中,有幾個 filter 模塊須要關注一下。按照調用的順序依次說明以下:

  • write: 寫輸出到客戶端,其實是寫到鏈接對應的 socket 上。
  • postpone: 這個 filter 是負責 subrequest 的,也就是子請求的。
  • copy: 將一些須要複製的 buf(文件或者內存)從新複製一份而後交給剩餘的 body filter 處理。

 

九.handler 模塊簡介

相信你們在看了前一章的模塊概述之後,都對 Nginx 的模塊有了一個基本的認識。基本上做爲第三方開發者最可能開發的就是三種類型的模塊,即 handler,filter 和 load-balancer。Handler 模塊就是接受來自客戶端的請求併產生輸出的模塊。有些地方說 upstream 模塊實際上也是一種 handler 模塊,只不過它產生的內容來自於從後端服務器獲取的,而非在本機產生的。

在上一章提到,配置文件中使用 location 指令能夠配置 content handler 模塊,當 Nginx 系統啓動的時候,每一個 handler 模塊都有一次機會把本身關聯到對應的 location上。若是有多個 handler 模塊都關聯了同一個 location,那麼實際上只有一個 handler 模塊真正會起做用。固然大多數狀況下,模塊開發人員都會避免出現這種狀況。

handler 模塊處理的結果一般有三種狀況: 處理成功,處理失敗(處理的時候發生了錯誤)或者是拒絕去處理。在拒絕處理的狀況下,這個 location 的處理就會由默認的 handler 模塊來進行處理。例如,當請求一個靜態文件的時候,若是關聯到這個 location 上的一個 handler 模塊拒絕處理,就會由默認的 ngx_http_static_module 模塊進行處理,該模塊是一個典型的 handler 模塊。

本章主要講述的是如何編寫 handler 模塊,在研究 handler 模塊編寫以前先來了解一下模塊的一些基本數據結構。

 

十.模塊的基本結構

在這一節咱們將會對一般的模塊開發過程當中,每一個模塊所包含的一些經常使用的部分進行說明。這些部分有些是必須的,有些不是必須的。同時這裏所列出的這些東西對於其餘類型的模塊,例如 filter 模塊等也都是相同的。

模塊配置結構

基本上每一個模塊都會提供一些配置指令,以便於用戶能夠經過配置來控制該模塊的行爲。那麼這些配置信息怎麼存儲呢?那就須要定義該模塊的配置結構來進行存儲。

你們都知道 Nginx 的配置信息分紅了幾個做用域(scope,有時也稱做上下文),這就是 main,server 以及 location。一樣的每一個模塊提供的配置指令也能夠出如今這幾個做用域裏。那對於這三個做用域的配置信息,每一個模塊就須要定義三個不一樣的數據結構去進行存儲。固然,不是每一個模塊都會在這三個做用域都提供配置指令的。那麼也就不必定每一個模塊都須要定義三個數據結構去存儲這些配置信息了。視模塊的實現而言,須要幾個就定義幾個。

有一點須要特別注意的就是,在模塊的開發過程當中,咱們最好使用 Nginx 原有的命名習慣。這樣跟原代碼的契合度更高,看起來也更舒服。

對於模塊配置信息的定義,命名習慣是ngx_http_<module name>_(main|srv|loc)_conf_t。這裏有個例子,就是從咱們後面將要展現給你們的 hello module 中截取的。

typedef struct
    {
        ngx_str_t hello_string;
        ngx_int_t hello_counter;
    }ngx_http_hello_loc_conf_t;

模塊配置指令

一個模塊的配置指令是定義在一個靜態數組中的。一樣地,咱們來看一下從 hello module 中截取的模塊配置指令的定義。

static ngx_command_t ngx_http_hello_commands[] = {
       { 
            ngx_string("hello_string"),
            NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS|NGX_CONF_TAKE1,
            ngx_http_hello_string,
            NGX_HTTP_LOC_CONF_OFFSET,
            offsetof(ngx_http_hello_loc_conf_t, hello_string),
            NULL },

        { 
            ngx_string("hello_counter"),
            NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
            ngx_http_hello_counter,
            NGX_HTTP_LOC_CONF_OFFSET,
            offsetof(ngx_http_hello_loc_conf_t, hello_counter),
            NULL },               

        ngx_null_command
    };

其實看這個定義,就基本能看出來一些信息。例如,咱們是定義了兩個配置指令,一個是叫 hello_string,能夠接受一個參數,或者是沒有參數。另一個命令是 hello_counter,接受一個 NGX_CONF_FLAG 類型的參數。除此以外,彷佛看起來有點迷惑。沒有關係,咱們來詳細看一下 ngx_command_t,一旦咱們瞭解這個結構的詳細信息,那麼我相信上述這個定義所表達的全部信息就不言自明瞭。

ngx_command_t 的定義,位於src/core/ngx_conf_file.h中。

struct ngx_command_s {
        ngx_str_t             name;
        ngx_uint_t            type;
        char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
        ngx_uint_t            conf;
        ngx_uint_t            offset;
        void                 *post;
    };

name: 配置指令的名稱。

type: 該配置的類型,其實更準確一點說,是該配置指令屬性的集合。Nginx 提供了不少預約義的屬性值(一些宏定義),經過邏輯或運算符可組合在一塊兒,造成對這個配置指令的詳細的說明。下面列出可在這裏使用的預約義屬性值及說明。

  • NGX_CONF_NOARGS:配置指令不接受任何參數。
  • NGX_CONF_TAKE1:配置指令接受 1 個參數。
  • NGX_CONF_TAKE2:配置指令接受 2 個參數。
  • NGX_CONF_TAKE3:配置指令接受 3 個參數。
  • NGX_CONF_TAKE4:配置指令接受 4 個參數。
  • NGX_CONF_TAKE5:配置指令接受 5 個參數。
  • NGX_CONF_TAKE6:配置指令接受 6 個參數。
  • NGX_CONF_TAKE7:配置指令接受 7 個參數。

能夠組合多個屬性,好比一個指令便可以不填參數,也能夠接受1個或者2個參數。那麼就是NGX_CONF_NOARGS|NGX_CONF_TAKE1|NGX_CONF_TAKE2。若是寫上面三個屬性在一塊兒,你以爲麻煩,那麼沒有關係,Nginx 提供了一些定義,使用起來更簡潔。

  • NGX_CONF_TAKE12:配置指令接受 1 個或者 2 個參數。
  • NGX_CONF_TAKE13:配置指令接受 1 個或者 3 個參數。
  • NGX_CONF_TAKE23:配置指令接受 2 個或者 3 個參數。
  • NGX_CONF_TAKE123:配置指令接受 1 個或者 2 個或者 3 參數。
  • NGX_CONF_TAKE1234:配置指令接受 1 個或者 2 個或者 3 個或者 4 個參數。
  • NGX_CONF_1MORE:配置指令接受至少一個參數。
  • NGX_CONF_2MORE:配置指令接受至少兩個參數。
  • NGX_CONF_MULTI: 配置指令能夠接受多個參數,即個數不定。
  • NGX_CONF_BLOCK:配置指令能夠接受的值是一個配置信息塊。也就是一對大括號括起來的內容。裏面能夠再包括不少的配置指令。好比常見的 server 指令就是這個屬性的。
  • NGX_CONF_FLAG:配置指令能夠接受的值是"on"或者"off",最終會被轉成 bool 值。
  • NGX_CONF_ANY:配置指令能夠接受的任意的參數值。一個或者多個,或者"on"或者"off",或者是配置塊。

最後要說明的是,不管如何,Nginx 的配置指令的參數個數不能夠超過 NGX_CONF_MAX_ARGS 個。目前這個值被定義爲 8,也就是不能超過 8 個參數值。

下面介紹一組說明配置指令能夠出現的位置的屬性。

  • NGX_DIRECT_CONF:能夠出如今配置文件中最外層。例如已經提供的配置指令 daemon,master_process 等。
  • NGX_MAIN_CONF: http、mail、events、error_log 等。
  • NGX_ANY_CONF: 該配置指令能夠出如今任意配置級別上。

對於咱們編寫的大多數模塊而言,都是在處理http相關的事情,也就是所謂的都是NGX_HTTP_MODULE,對於這樣類型的模塊,其配置可能出現的位置也是分爲直接出如今http裏面,以及其餘位置。

  • NGX_HTTP_MAIN_CONF: 能夠直接出如今 http 配置指令裏。
  • NGX_HTTP_SRV_CONF: 能夠出如今 http 裏面的 server 配置指令裏。
  • NGX_HTTP_LOC_CONF: 能夠出如今 http server 塊裏面的 location 配置指令裏。
  • NGX_HTTP_UPS_CONF: 能夠出如今 http 裏面的 upstream 配置指令裏。
  • NGX_HTTP_SIF_CONF: 能夠出如今 http 裏面的 server 配置指令裏的 if 語句所在的 block 中。
  • NGX_HTTP_LMT_CONF: 能夠出如今 http 裏面的 limit_except 指令的 block 中。
  • NGX_HTTP_LIF_CONF: 能夠出如今 http server 塊裏面的 location 配置指令裏的 if 語句所在的 block 中。

set: 這是一個函數指針,當 Nginx 在解析配置的時候,若是遇到這個配置指令,將會把讀取到的值傳遞給這個函數進行分解處理。由於具體每一個配置指令的值如何處理,只有定義這個配置指令的人是最清楚的。來看一下這個函數指針要求的函數原型。

char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

先看該函數的返回值,處理成功時,返回 NGX_OK,不然返回 NGX_CONF_ERROR 或者是一個自定義的錯誤信息的字符串。

再看一下這個函數被調用的時候,傳入的三個參數。

  • cf: 該參數裏面保存從配置文件讀取到的原始字符串以及相關的一些信息。特別注意的是這個參數的args字段是一個 ngx_str_t類型的數組,該數組的首個元素是這個配置指令自己,第二個元素是指令的第一個參數,第三個元素是第二個參數,依次類推。

  • cmd: 這個配置指令對應的 ngx_command_t 結構。

  • conf: 就是定義的存儲這個配置值的結構體,好比在上面展現的那個 ngx_http_hello_loc_conf_t。當解析這個 hello_string 變量的時候,傳入的 conf 就指向一個 ngx_http_hello_loc_conf_t 類型的變量。用戶在處理的時候能夠使用類型轉換,轉換成本身知道的類型,再進行字段的賦值。

爲了更加方便的實現對配置指令參數的讀取,Nginx 已經默認提供了對一些標準類型的參數進行讀取的函數,能夠直接賦值給 set 字段使用。下面來看一下這些已經實現的 set 類型函數。

  • ngx_conf_set_flag_slot: 讀取 NGX_CONF_FLAG 類型的參數。
  • ngx_conf_set_str_slot:讀取字符串類型的參數。
  • ngx_conf_set_str_array_slot: 讀取字符串數組類型的參數。
  • ngx_conf_set_keyval_slot: 讀取鍵值對類型的參數。
  • ngx_conf_set_num_slot: 讀取整數類型(有符號整數 ngx_int_t)的參數。
  • ngx_conf_set_size_slot:讀取 size_t 類型的參數,也就是無符號數。
  • ngx_conf_set_off_slot: 讀取 off_t 類型的參數。
  • ngx_conf_set_msec_slot: 讀取毫秒值類型的參數。
  • ngx_conf_set_sec_slot: 讀取秒值類型的參數。
  • ngx_conf_set_bufs_slot: 讀取的參數值是 2 個,一個是 buf 的個數,一個是 buf 的大小。例如: output_buffers 1 128k;
  • ngx_conf_set_enum_slot: 讀取枚舉類型的參數,將其轉換成整數 ngx_uint_t 類型。
  • ngx_conf_set_bitmask_slot: 讀取參數的值,並將這些參數的值以 bit 位的形式存儲。例如:HttpDavModule 模塊的 dav_methods 指令。

conf: 該字段被 NGX_HTTP_MODULE 類型模塊所用 (咱們編寫的基本上都是 NGX_HTTP_MOUDLE,只有一些 Nginx 核心模塊是非 NGX_HTTP_MODULE),該字段指定當前配置項存儲的內存位置。其實是使用哪一個內存池的問題。由於 http 模塊對全部 http 模塊所要保存的配置信息,劃分了 main, server 和 location 三個地方進行存儲,每一個地方都有一個內存池用來分配存儲這些信息的內存。這裏可能的值爲 NGX_HTTP_MAIN_CONF_OFFSET、NGX_HTTP_SRV_CONF_OFFSET 或 NGX_HTTP_LOC_CONF_OFFSET。固然也能夠直接置爲 0,就是 NGX_HTTP_MAIN_CONF_OFFSET。

offset: 指定該配置項值的精確存放位置,通常指定爲某一個結構體變量的字段偏移。由於對於配置信息的存儲,通常咱們都是定義個結構體來存儲的。那麼好比咱們定義了一個結構體 A,該項配置的值須要存儲到該結構體的 b 字段。那麼在這裏就能夠填寫爲 offsetof(A, b)。對於有些配置項,它的值不須要保存或者是須要保存到更爲複雜的結構中時,這裏能夠設置爲 0。

post: 該字段存儲一個指針。能夠指向任何一個在讀取配置過程當中須要的數據,以便於進行配置讀取的處理。大多數時候,都不須要,因此簡單地設爲 0 便可。

看到這裏,應該就比較清楚了。ngx_http_hello_commands 這個數組每 5 個元素爲一組,用來描述一個配置項的全部狀況。那麼若是有多個配置項,只要按照須要再增長 5 個對應的元素對新的配置項進行說明。

須要注意的是,就是在ngx_http_hello_commands這個數組定義的最後,都要加一個ngx_null_command做爲結尾。

模塊上下文結構

這是一個 ngx_http_module_t 類型的靜態變量。這個變量其實是提供一組回調函數指針,這些函數有在建立存儲配置信息的對象的函數,也有在建立前和建立後會調用的函數。這些函數都將被 Nginx 在合適的時間進行調用。

typedef struct {
        ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);
        ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);

        void       *(*create_main_conf)(ngx_conf_t *cf);
        char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);

        void       *(*create_srv_conf)(ngx_conf_t *cf);
        char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

        void       *(*create_loc_conf)(ngx_conf_t *cf);
        char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
    } ngx_http_module_t;
  • preconfiguration: 在建立和讀取該模塊的配置信息以前被調用。

  • postconfiguration: 在建立和讀取該模塊的配置信息以後被調用。

  • create_main_conf: 調用該函數建立本模塊位於 http block 的配置信息存儲結構。該函數成功的時候,返回建立的配置對象。失敗的話,返回 NULL。

  • init_main_conf: 調用該函數初始化本模塊位於 http block 的配置信息存儲結構。該函數成功的時候,返回 NGX_CONF_OK。失敗的話,返回 NGX_CONF_ERROR 或錯誤字符串。

  • create_srv_conf: 調用該函數建立本模塊位於 http server block 的配置信息存儲結構,每一個 server block 會建立一個。該函數成功的時候,返回建立的配置對象。失敗的話,返回 NULL。

  • merge_srv_conf: 由於有些配置指令既能夠出如今 http block,也能夠出如今 http server block 中。那麼遇到這種狀況,每一個 server 都會有本身存儲結構來存儲該 server 的配置,可是在這種狀況下 http block 中的配置與 server block 中的配置信息發生衝突的時候,就須要調用此函數進行合併,該函數並不是必須提供,當預計到絕對不會發生須要合併的狀況的時候,就無需提供。固然爲了安全起見仍是建議提供。該函數執行成功的時候,返回 NGX_CONF_OK。失敗的話,返回 NGX_CONF_ERROR 或錯誤字符串。

  • create_loc_conf: 調用該函數建立本模塊位於 location block 的配置信息存儲結構。每一個在配置中指明的 location 建立一個。該函數執行成功,返回建立的配置對象。失敗的話,返回 NULL。

  • merge_loc_conf: 與 merge_srv_conf 相似,這個也是進行配置值合併的地方。該函數成功的時候,返回 NGX_CONF_OK。失敗的話,返回 NGX_CONF_ERROR 或錯誤字符串。

Nginx 裏面的配置信息都是上下一層層的嵌套的,對於具體某個 location 的話,對於同一個配置,若是當前層次沒有定義,那麼就使用上層的配置,不然使用當前層次的配置。

這些配置信息通常默認都應該設爲一個未初始化的值,針對這個需求,Nginx 定義了一系列的宏定義來表明各類配置所對應數據類型的未初始化值,以下:

#define NGX_CONF_UNSET       -1
    #define NGX_CONF_UNSET_UINT  (ngx_uint_t) -1
    #define NGX_CONF_UNSET_PTR   (void *) -1
    #define NGX_CONF_UNSET_SIZE  (size_t) -1
    #define NGX_CONF_UNSET_MSEC  (ngx_msec_t) -1

又由於對於配置項的合併,邏輯都相似,也就是前面已經說過的,若是在本層次已經配置了,也就是配置項的值已經被讀取進來了(那麼這些配置項的值就不會等於上面已經定義的那些 UNSET 的值),就使用本層次的值做爲定義合併的結果,不然,使用上層的值,若是上層的值也是這些UNSET類的值,那就賦值爲默認值,不然就使用上層的值做爲合併的結果。對於這樣相似的操做,Nginx 定義了一些宏操做來作這些事情,咱們來看其中一個的定義。

#define ngx_conf_merge_uint_value(conf, prev, default) \
        if (conf == NGX_CONF_UNSET_UINT) {      \
            conf = (prev == NGX_CONF_UNSET_UINT) ? default : prev; \
        }

顯而易見,這個邏輯確實比較簡單,因此其它的宏定義也相似,咱們就列具其中的一部分吧。

ngx_conf_merge_value
    ngx_conf_merge_ptr_value
    ngx_conf_merge_uint_value
    ngx_conf_merge_msec_value
    ngx_conf_merge_sec_value

等等。

下面來看一下 hello 模塊的模塊上下文的定義,加深一下印象。

static ngx_http_module_t ngx_http_hello_module_ctx = {
        NULL,                          /* preconfiguration */
        ngx_http_hello_init,           /* postconfiguration */

        NULL,                          /* create main configuration */
        NULL,                          /* init main configuration */

        NULL,                          /* create server configuration */
        NULL,                          /* merge server configuration */

        ngx_http_hello_create_loc_conf, /* create location configuration */
        NULL                        /* merge location configuration */
    };

注意:這裏並無提供 merge_loc_conf 函數,由於咱們這個模塊的配置指令已經肯定只出如今 NGX_HTTP_LOC_CONF 中這一個層次上,不會發生須要合併的狀況。

模塊的定義

對於開發一個模塊來講,咱們都須要定義一個 ngx_module_t 類型的變量來講明這個模塊自己的信息,從某種意義上來講,這是這個模塊最重要的一個信息,它告訴了 Nginx 這個模塊的一些信息,上面定義的配置信息,還有模塊上下文信息,都是經過這個結構來告訴 Nginx 系統的,也就是加載模塊的上層代碼,都須要經過定義的這個結構,來獲取這些信息。

咱們先來看下 ngx_module_t 的定義

typedef struct ngx_module_s      ngx_module_t;
    struct ngx_module_s {
        ngx_uint_t            ctx_index;
        ngx_uint_t            index;
        ngx_uint_t            spare0;
        ngx_uint_t            spare1;
        ngx_uint_t            abi_compatibility;
        ngx_uint_t            major_version;
        ngx_uint_t            minor_version;
        void                 *ctx;
        ngx_command_t        *commands;
        ngx_uint_t            type;
        ngx_int_t           (*init_master)(ngx_log_t *log);
        ngx_int_t           (*init_module)(ngx_cycle_t *cycle);
        ngx_int_t           (*init_process)(ngx_cycle_t *cycle);
        ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);
        void                (*exit_thread)(ngx_cycle_t *cycle);
        void                (*exit_process)(ngx_cycle_t *cycle);
        void                (*exit_master)(ngx_cycle_t *cycle);
        uintptr_t             spare_hook0;
        uintptr_t             spare_hook1;
        uintptr_t             spare_hook2;
        uintptr_t             spare_hook3;
        uintptr_t             spare_hook4;
        uintptr_t             spare_hook5;
        uintptr_t             spare_hook6;
        uintptr_t             spare_hook7;
    };

    #define NGX_NUMBER_MAJOR  3
    #define NGX_NUMBER_MINOR  1
    #define NGX_MODULE_V1          0, 0, 0, 0,                              \
        NGX_DSO_ABI_COMPATIBILITY, NGX_NUMBER_MAJOR, NGX_NUMBER_MINOR
    #define NGX_MODULE_V1_PADDING  0, 0, 0, 0, 0, 0, 0, 0

再看一下 hello 模塊的模塊定義。

ngx_module_t ngx_http_hello_module = {
        NGX_MODULE_V1,
        &ngx_http_hello_module_ctx,    /* module context */
        ngx_http_hello_commands,       /* module directives */
        NGX_HTTP_MODULE,               /* module type */
        NULL,                          /* init master */
        NULL,                          /* init module */
        NULL,                          /* init process */
        NULL,                          /* init thread */
        NULL,                          /* exit thread */
        NULL,                          /* exit process */
        NULL,                          /* exit master */
        NGX_MODULE_V1_PADDING
    };

模塊能夠提供一些回調函數給 Nginx,當 Nginx 在建立進程線程或者結束進程線程時進行調用。但大多數模塊在這些時刻並不須要作什麼,因此都簡單賦值爲 NULL。

 

十一.handler 模塊的基本結構

除了上一節介紹的模塊的基本結構之外,handler 模塊必須提供一個真正的處理函數,這個函數負責對來自客戶端請求的真正處理。這個函數的處理,既能夠選擇本身直接生成內容,也能夠選擇拒絕處理,由後續的 handler 去進行處理,或者是選擇丟給後續的 filter 進行處理。來看一下這個函數的原型申明。

typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);

r 是 http 請求。裏面包含請求全部的信息,這裏不詳細說明了,能夠參考別的章節的介紹。 該函數處理成功返回 NGX_OK,處理髮生錯誤返回 NGX_ERROR,拒絕處理(留給後續的 handler 進行處理)返回 NGX_DECLINE。 返回 NGX_OK 也就表明給客戶端的響應已經生成好了,不然返回 NGX_ERROR 就發生錯誤了。

 

十二.handler 模塊的掛載

handler 模塊真正的處理函數經過兩種方式掛載處處理過程當中,一種方式就是按處理階段掛載;另一種掛載方式就是按需掛載。

按處理階段掛載

爲了更精細地控制對於客戶端請求的處理過程,Nginx 把這個處理過程劃分紅了 11 個階段。他們從前到後,依次列舉以下:

  • NGX_HTTP_POST_READ_PHASE: 讀取請求內容階段
  • NGX_HTTP_SERVER_REWRITE_PHASE: Server 請求地址重寫階段
  • NGX_HTTP_FIND_CONFIG_PHASE: 配置查找階段:
  • NGX_HTTP_REWRITE_PHASE: Location 請求地址重寫階段
  • NGX_HTTP_POST_REWRITE_PHASE: 請求地址重寫提交階段
  • NGX_HTTP_PREACCESS_PHASE: 訪問權限檢查準備階段
  • NGX_HTTP_ACCESS_PHASE: 訪問權限檢查階段
  • NGX_HTTP_POST_ACCESS_PHASE: 訪問權限檢查提交階段
  • NGX_HTTP_TRY_FILES_PHASE: 配置項 try_files 處理階段
  • NGX_HTTP_CONTENT_PHASE: 內容產生階段
  • NGX_HTTP_LOG_PHASE: 日誌模塊處理階段

通常狀況下,咱們自定義的模塊,大多數是掛載在 NGX_HTTP_CONTENT_PHASE 階段的。掛載的動做通常是在模塊上下文調用的 postconfiguration 函數中。

注意:有幾個階段是特例,它不調用掛載地任何的handler,也就是你就不用掛載到這幾個階段了:

  • NGX_HTTP_FIND_CONFIG_PHASE
  • NGX_HTTP_POST_ACCESS_PHASE
  • NGX_HTTP_POST_REWRITE_PHASE
  • NGX_HTTP_TRY_FILES_PHASE

因此其實真正是有 7 個 phase 你能夠去掛載 handler。

掛載的代碼以下(摘自 hello module):

static ngx_int_t
    ngx_http_hello_init(ngx_conf_t *cf)
    {
        ngx_http_handler_pt        *h;
        ngx_http_core_main_conf_t  *cmcf;

        cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

        h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
        if (h == NULL) {
            return NGX_ERROR;
        }

        *h = ngx_http_hello_handler;

        return NGX_OK;
    }

使用這種方式掛載的 handler 也被稱爲 content phase handlers。

按需掛載

以這種方式掛載的 handler 也被稱爲 content handler。

當一個請求進來之後,Nginx 從 NGX_HTTP_POST_READ_PHASE 階段開始依次執行每一個階段中全部 handler。執行到 NGX_HTTP_CONTENT_PHASE 階段的時候,若是這個 location 有一個對應的 content handler 模塊,那麼就去執行這個 content handler 模塊真正的處理函數。不然繼續依次執行 NGX_HTTP_CONTENT_PHASE 階段中全部 content phase handlers,直到某個函數處理返回 NGX_OK 或者 NGX_ERROR。

換句話說,當某個 location 處理到 NGX_HTTP_CONTENT_PHASE 階段時,若是有 content handler 模塊,那麼 NGX_HTTP_CONTENT_PHASE 掛載的全部 content phase handlers 都不會被執行了。

可是使用這個方法掛載上去的 handler 有一個特色是必須在 NGX_HTTP_CONTENT_PHASE 階段才能執行到。若是你想本身的 handler 在更早的階段執行,那就不要使用這種掛載方式。

那麼在什麼狀況會使用這種方式來掛載呢?通常狀況下,某個模塊對某個 location 進行了處理之後,發現符合本身處理的邏輯,並且也沒有必要再調用 NGX_HTTP_CONTENT_PHASE 階段的其它 handler 進行處理的時候,就動態掛載上這個 handler。

下面來看一下使用這種掛載方式的具體例子(摘自 Emiller's Guide To Nginx Module Development)。



static char * ngx_http_circle_gif(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_circle_gif_handler; return NGX_CONF_OK; }

十三.handler 的編寫步驟

好,到了這裏,讓咱們稍微整理一下思路,回顧一下實現一個 handler 的步驟:

  1. 編寫模塊基本結構。包括模塊的定義,模塊上下文結構,模塊的配置結構等。
  2. 實現 handler 的掛載函數。根據模塊的需求選擇正確的掛載方式。
  3. 編寫 handler 處理函數。模塊的功能主要經過這個函數來完成。

看起來不是那麼難,對吧?仍是那句老話,世上無難事,只怕有心人! 如今咱們來完整的分析前面提到的 hello handler module 示例的功能和代碼。

 

十四.示例: hello handler 模塊

在前面已經看到了這個 hello handler module 的部分重要的結構。該模塊提供了 2 個配置指令,僅能夠出如今 location 指令的做用域中。這兩個指令是 hello_string, 該指令接受一個參數來設置顯示的字符串。若是沒有跟參數,那麼就使用默認的字符串做爲響應字符串。

另外一個指令是 hello_counter,若是設置爲 on,則會在響應的字符串後面追加 Visited Times:的字樣,以統計請求的次數。

這裏有兩點注意一下:

  1. 對於 flag 類型的配置指令,當值爲 off 的時候,使用 ngx_conf_set_flag_slot 函數,會轉化爲 0,爲on,則轉化爲非 0。
  2. 另一個是,我提供了 merge_loc_conf 函數,可是卻沒有設置到模塊的上下文定義中。這樣有一個缺點,就是若是一個指令沒有出如今配置文件中的時候,配置信息中的值,將永遠會保持在 create_loc_conf 中的初始化的值。那若是,在相似 create_loc_conf 這樣的函數中,對建立出來的配置信息的值,沒有設置爲合理的值的話,後面用戶又沒有配置,就會出現問題。

下面來完整的給出 ngx_http_hello_module 模塊的完整代碼。

#include <ngx_config.h>
    #include <ngx_core.h>
    #include <ngx_http.h>

    typedef struct
    {
        ngx_str_t hello_string;
        ngx_int_t hello_counter;
    }ngx_http_hello_loc_conf_t;

    static ngx_int_t ngx_http_hello_init(ngx_conf_t *cf);

    static void *ngx_http_hello_create_loc_conf(ngx_conf_t *cf);

    static char *ngx_http_hello_string(ngx_conf_t *cf, ngx_command_t *cmd,
        void *conf);
    static char *ngx_http_hello_counter(ngx_conf_t *cf, ngx_command_t *cmd,
        void *conf);

    static ngx_command_t ngx_http_hello_commands[] = {
       { 
            ngx_string("hello_string"),
            NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS|NGX_CONF_TAKE1,
            ngx_http_hello_string,
            NGX_HTTP_LOC_CONF_OFFSET,
            offsetof(ngx_http_hello_loc_conf_t, hello_string),
            NULL },

        { 
            ngx_string("hello_counter"),
            NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
            ngx_http_hello_counter,
            NGX_HTTP_LOC_CONF_OFFSET,
            offsetof(ngx_http_hello_loc_conf_t, hello_counter),
            NULL },               

        ngx_null_command
    };

    /* 
    static u_char ngx_hello_default_string[] = "Default String: Hello, world!";
    */
    static int ngx_hello_visited_times = 0; 

    static ngx_http_module_t ngx_http_hello_module_ctx = {
        NULL,                          /* preconfiguration */
        ngx_http_hello_init,           /* postconfiguration */

        NULL,                          /* create main configuration */
        NULL,                          /* init main configuration */

        NULL,                          /* create server configuration */
        NULL,                          /* merge server configuration */

        ngx_http_hello_create_loc_conf, /* create location configuration */
        NULL                            /* merge location configuration */
    };

    ngx_module_t ngx_http_hello_module = {
        NGX_MODULE_V1,
        &ngx_http_hello_module_ctx,    /* module context */
        ngx_http_hello_commands,       /* module directives */
        NGX_HTTP_MODULE,               /* module type */
        NULL,                          /* init master */
        NULL,                          /* init module */
        NULL,                          /* init process */
        NULL,                          /* init thread */
        NULL,                          /* exit thread */
        NULL,                          /* exit process */
        NULL,                          /* exit master */
        NGX_MODULE_V1_PADDING
    };

    static ngx_int_t
    ngx_http_hello_handler(ngx_http_request_t *r)
    {
        ngx_int_t    rc;
        ngx_buf_t   *b;
        ngx_chain_t  out;
        ngx_http_hello_loc_conf_t* my_conf;
        u_char ngx_hello_string[1024] = {0};
        ngx_uint_t content_length = 0;

        ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "ngx_http_hello_handler is called!");

        my_conf = ngx_http_get_module_loc_conf(r, ngx_http_hello_module);
        if (my_conf->hello_string.len == 0 )
        {
            ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "hello_string is empty!");
            return NGX_DECLINED;
        }

        if (my_conf->hello_counter == NGX_CONF_UNSET
            || my_conf->hello_counter == 0)
        {
            ngx_sprintf(ngx_hello_string, "%s", my_conf->hello_string.data);
        }
        else
        {
            ngx_sprintf(ngx_hello_string, "%s Visited Times:%d", my_conf->hello_string.data, 
                ++ngx_hello_visited_times);
        }
        ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "hello_string:%s", ngx_hello_string);
        content_length = ngx_strlen(ngx_hello_string);

        /* we response to 'GET' and 'HEAD' requests only */
        if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
            return NGX_HTTP_NOT_ALLOWED;
        }

        /* discard request body, since we don't need it here */
        rc = ngx_http_discard_request_body(r);

        if (rc != NGX_OK) {
            return rc;
        }

        /* set the 'Content-type' header */
        /*
         *r->headers_out.content_type.len = sizeof("text/html") - 1;
         *r->headers_out.content_type.data = (u_char *)"text/html";
                 */
        ngx_str_set(&r->headers_out.content_type, "text/html");

        /* send the header only, if the request type is http 'HEAD' */
        if (r->method == NGX_HTTP_HEAD) {
            r->headers_out.status = NGX_HTTP_OK;
            r->headers_out.content_length_n = content_length;

            return ngx_http_send_header(r);
        }

        /* allocate a buffer for your response body */
        b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
        if (b == NULL) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        /* attach this buffer to the buffer chain */
        out.buf = b;
        out.next = NULL;

        /* adjust the pointers of the buffer */
        b->pos = ngx_hello_string;
        b->last = ngx_hello_string + content_length;
        b->memory = 1;    /* this buffer is in memory */
        b->last_buf = 1;  /* this is the last buffer in the buffer chain */

        /* set the status line */
        r->headers_out.status = NGX_HTTP_OK;
        r->headers_out.content_length_n = content_length;

        /* send the headers of your response */
        rc = ngx_http_send_header(r);

        if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
            return rc;
        }

        /* send the buffer chain of your response */
        return ngx_http_output_filter(r, &out);
    }

    static void *ngx_http_hello_create_loc_conf(ngx_conf_t *cf)
    {
        ngx_http_hello_loc_conf_t* local_conf = NULL;
        local_conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_hello_loc_conf_t));
        if (local_conf == NULL)
        {
            return NULL;
        }

        ngx_str_null(&local_conf->hello_string);
        local_conf->hello_counter = NGX_CONF_UNSET;

        return local_conf;
    } 

    /*
    static char *ngx_http_hello_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
    {
        ngx_http_hello_loc_conf_t* prev = parent;
        ngx_http_hello_loc_conf_t* conf = child;

        ngx_conf_merge_str_value(conf->hello_string, prev->hello_string, ngx_hello_default_string);
        ngx_conf_merge_value(conf->hello_counter, prev->hello_counter, 0);

        return NGX_CONF_OK;
    }*/

    static char *
    ngx_http_hello_string(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
    {

        ngx_http_hello_loc_conf_t* local_conf;

        local_conf = conf;
        char* rv = ngx_conf_set_str_slot(cf, cmd, conf);

        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "hello_string:%s", local_conf->hello_string.data);

        return rv;
    }

    static char *ngx_http_hello_counter(ngx_conf_t *cf, ngx_command_t *cmd,
        void *conf)
    {
        ngx_http_hello_loc_conf_t* local_conf;

        local_conf = conf;

        char* rv = NULL;

        rv = ngx_conf_set_flag_slot(cf, cmd, conf);

        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "hello_counter:%d", local_conf->hello_counter);
        return rv;    
    }

    static ngx_int_t
    ngx_http_hello_init(ngx_conf_t *cf)
    {
        ngx_http_handler_pt        *h;
        ngx_http_core_main_conf_t  *cmcf;

        cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

        h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
        if (h == NULL) {
            return NGX_ERROR;
        }

        *h = ngx_http_hello_handler;

        return NGX_OK;
    }

經過上面一些介紹,我相信你們都能對整個示例模塊有一個比較好的理解。惟一可能感受有些理解困難的地方在於ngx_http_hello_handler 函數裏面產生和設置輸出。但其實你們在本書的前面的相關章節均可以看到對 ngx_buf_t 和 request 等相關數據結構的說明。若是仔細看了這些地方的說明的話,應該對這裏代碼的實現就比較容易理解了。所以,這裏再也不贅述解釋。

 

十五.handler 模塊的編譯和使用

模塊的功能開發完了以後,模塊的使用還須要編譯纔可以執行,下面咱們來看下模塊的編譯和使用。

config 文件的編寫

對於開發一個模塊,咱們是須要把這個模塊的 C 代碼組織到一個目錄裏,同時須要編寫一個 config 文件。這個 config 文件的內容就是告訴 Nginx 的編譯腳本,該如何進行編譯。咱們來看一下 hello handler module 的 config 文件的內容,而後再作解釋。

ngx_addon_name=ngx_http_hello_module
    HTTP_MODULES="$HTTP_MODULES ngx_http_hello_module"
    NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_hello_module.c"

其實文件很簡單,幾乎不須要作什麼解釋。你們一看都懂了。惟一須要說明的是,若是這個模塊的實現有多個源文件,那麼都在 NGX_ADDON_SRCS 這個變量裏,依次寫進去就能夠。

編譯

對於模塊的編譯,Nginx 並不像 apache 同樣,提供了單獨的編譯工具,能夠在沒有 apache 源代碼的狀況下來單獨編譯一個模塊的代碼。Nginx 必須去到 Nginx 的源代碼目錄裏,經過 configure 指令的參數,來進行編譯。下面看一下 hello module 的 configure 指令:

./configure --prefix=/usr/local/nginx-1.3.1 --add-module=/home/jizhao/open_source/book_module

我寫的這個示例模塊的代碼和 config 文件都放在/home/jizhao/open_source/book_module這個目錄下。因此一切都很明瞭,也沒什麼好說的了。

使用

使用一個模塊須要根據這個模塊定義的配置指令來作。好比咱們這個簡單的 hello handler module 的使用就很簡單。在個人測試服務器的配置文件裏,就是在 http 裏面的默認的 server 裏面加入以下的配置:

location /test {
            hello_string jizhao;
            hello_counter on;
    }

當咱們訪問這個地址的時候, lynx http://127.0.0.1/test 的時候,就能夠看到返回的結果。

jizhao Visited Times:1

固然你訪問屢次,這個次數是會增長的。

 

十六.更多 handler 模塊示例分析

http access module

該模塊的代碼位於src/http/modules/ngx_http_access_module.c中。該模塊的做用是提供對於特定 host 的客戶端的訪問控制。能夠限定特定 host 的客戶端對於服務端所有,或者某個 server,或者是某個 location 的訪問。

該模塊的實現很是簡單,總共也就只有幾個函數。

static ngx_int_t ngx_http_access_handler(ngx_http_request_t *r);
    static ngx_int_t ngx_http_access_inet(ngx_http_request_t *r,
        ngx_http_access_loc_conf_t *alcf, in_addr_t addr);
    #if (NGX_HAVE_INET6)
    static ngx_int_t ngx_http_access_inet6(ngx_http_request_t *r,
        ngx_http_access_loc_conf_t *alcf, u_char *p);
    #endif
    static ngx_int_t ngx_http_access_found(ngx_http_request_t *r, ngx_uint_t deny);
    static char *ngx_http_access_rule(ngx_conf_t *cf, ngx_command_t *cmd,
        void *conf);
    static void *ngx_http_access_create_loc_conf(ngx_conf_t *cf);
    static char *ngx_http_access_merge_loc_conf(ngx_conf_t *cf,
        void *parent, void *child);
    static ngx_int_t ngx_http_access_init(ngx_conf_t *cf);

對於與配置相關的幾個函數都不須要作解釋了,須要提一下的是函數 ngx_http_access_init,該函數在實現上把本模塊掛載到了 NGX_HTTP_ACCESS_PHASE 階段的 handler 上,從而使本身的被調用時機發生在了 NGX_HTTP_CONTENT_PHASE 等階段前。由於進行客戶端地址的限制檢查,根本不須要等到這麼後面。

另外看一下這個模塊的主處理函數 ngx_http_access_handler。這個函數的邏輯也很是簡單,主要是根據客戶端地址的類型,來分別選擇 ipv4 類型的處理函數 ngx_http_access_inet 仍是 ipv6 類型的處理函數 ngx_http_access_inet6。

而這個兩個處理函數內部也很是簡單,就是循環檢查每一個規則,檢查是否有匹配的規則,若是有就返回匹配的結果,若是都沒有匹配,就默認拒絕。

http static module

從某種程度上來講,此模塊能夠算的上是「最正宗的」,「最古老」的 content handler。由於本模塊的做用就是讀取磁盤上的靜態文件,並把文件內容做爲產生的輸出。在Web技術發展的早期,只有靜態頁面,沒有服務端腳原本動態生成 HTML 的時候。恐怕開發個 Web 服務器的時候,第一個要開發就是這樣一個 content handler。

http static module 的代碼位於src/http/modules/ngx_http_static_module.c中,總共只有兩百多行近三百行。能夠說是很是短小。

咱們首先來看一下該模塊的模塊上下文的定義。

ngx_http_module_t  ngx_http_static_module_ctx = {
        NULL,                                  /* preconfiguration */
        ngx_http_static_init,                  /* postconfiguration */

        NULL,                                  /* create main configuration */
        NULL,                                  /* init main configuration */

        NULL,                                  /* create server configuration */
        NULL,                                  /* merge server configuration */

        NULL,                                  /* create location configuration */
        NULL                                   /* merge location configuration */
    };

是很是的簡潔吧,連任何與配置相關的函數都沒有。對了,由於該模塊沒有提供任何配置指令。你們想一想也就知道了,這個模塊作的事情實在是太簡單了,也確實沒什麼好配置的。惟一須要調用的函數是一個 ngx_http_static_init 函數。好了,來看一下這個函數都幹了寫什麼。

static ngx_int_t
    ngx_http_static_init(ngx_conf_t *cf)
    {
        ngx_http_handler_pt        *h;
        ngx_http_core_main_conf_t  *cmcf;

        cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

        h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
        if (h == NULL) {
            return NGX_ERROR;
        }

        *h = ngx_http_static_handler;

        return NGX_OK;
    }

僅僅是掛載這個 handler 到 NGX_HTTP_CONTENT_PHASE 處理階段。簡單吧?

下面咱們就看一下這個模塊最核心的處理邏輯所在的 ngx_http_static_handler 函數。該函數大概佔了這個模塊代碼量的百分之八九十。

static ngx_int_t
    ngx_http_static_handler(ngx_http_request_t *r)
    {
        u_char                    *last, *location;
        size_t                     root, len;
        ngx_str_t                  path;
        ngx_int_t                  rc;
        ngx_uint_t                 level;
        ngx_log_t                 *log;
        ngx_buf_t                 *b;
        ngx_chain_t                out;
        ngx_open_file_info_t       of;
        ngx_http_core_loc_conf_t  *clcf;

        if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD|NGX_HTTP_POST))) {
            return NGX_HTTP_NOT_ALLOWED;
        }

        if (r->uri.data[r->uri.len - 1] == '/') {
            return NGX_DECLINED;
        }

        log = r->connection->log;

        /*
         * ngx_http_map_uri_to_path() allocates memory for terminating '\0'
         * so we do not need to reserve memory for '/' for possible redirect
         */

        last = ngx_http_map_uri_to_path(r, &path, &root, 0);
        if (last == NULL) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        path.len = last - path.data;

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0,
                       "http filename: \"%s\"", path.data);

        clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

        ngx_memzero(&of, sizeof(ngx_open_file_info_t));

        of.read_ahead = clcf->read_ahead;
        of.directio = clcf->directio;
        of.valid = clcf->open_file_cache_valid;
        of.min_uses = clcf->open_file_cache_min_uses;
        of.errors = clcf->open_file_cache_errors;
        of.events = clcf->open_file_cache_events;

        if (ngx_http_set_disable_symlinks(r, clcf, &path, &of) != NGX_OK) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        if (ngx_open_cached_file(clcf->open_file_cache, &path, &of, r->pool)
            != NGX_OK)
        {
            switch (of.err) {

            case 0:
                return NGX_HTTP_INTERNAL_SERVER_ERROR;

            case NGX_ENOENT:
            case NGX_ENOTDIR:
            case NGX_ENAMETOOLONG:

                level = NGX_LOG_ERR;
                rc = NGX_HTTP_NOT_FOUND;
                break;

            case NGX_EACCES:
    #if (NGX_HAVE_OPENAT)
            case NGX_EMLINK:
            case NGX_ELOOP:
    #endif

                level = NGX_LOG_ERR;
                rc = NGX_HTTP_FORBIDDEN;
                break;

            default:

                level = NGX_LOG_CRIT;
                rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
                break;
            }

            if (rc != NGX_HTTP_NOT_FOUND || clcf->log_not_found) {
                ngx_log_error(level, log, of.err,
                              "%s \"%s\" failed", of.failed, path.data);
            }

            return rc;
        }

        r->root_tested = !r->error_page;

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, "http static fd: %d", of.fd);

        if (of.is_dir) {

            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, log, 0, "http dir");

            ngx_http_clear_location(r);

            r->headers_out.location = ngx_palloc(r->pool, sizeof(ngx_table_elt_t));
            if (r->headers_out.location == NULL) {
                return NGX_HTTP_INTERNAL_SERVER_ERROR;
            }

            len = r->uri.len + 1;

            if (!clcf->alias && clcf->root_lengths == NULL && r->args.len == 0) {
                location = path.data + clcf->root.len;

                *last = '/';

            } else {
                if (r->args.len) {
                    len += r->args.len + 1;
                }

                location = ngx_pnalloc(r->pool, len);
                if (location == NULL) {
                    return NGX_HTTP_INTERNAL_SERVER_ERROR;
                }

                last = ngx_copy(location, r->uri.data, r->uri.len);

                *last = '/';

                if (r->args.len) {
                    *++last = '?';
                    ngx_memcpy(++last, r->args.data, r->args.len);
                }
            }

            /*
             * we do not need to set the r->headers_out.location->hash and
             * r->headers_out.location->key fields
             */

            r->headers_out.location->value.len = len;
            r->headers_out.location->value.data = location;

            return NGX_HTTP_MOVED_PERMANENTLY;
        }

    #if !(NGX_WIN32) /* the not regular files are probably Unix specific */

        if (!of.is_file) {
            ngx_log_error(NGX_LOG_CRIT, log, 0,
                          "\"%s\" is not a regular file", path.data);

            return NGX_HTTP_NOT_FOUND;
        }

    #endif

        if (r->method & NGX_HTTP_POST) {
            return NGX_HTTP_NOT_ALLOWED;
        }

        rc = ngx_http_discard_request_body(r);

        if (rc != NGX_OK) {
            return rc;
        }

        log->action = "sending response to client";

        r->headers_out.status = NGX_HTTP_OK;
        r->headers_out.content_length_n = of.size;
        r->headers_out.last_modified_time = of.mtime;

        if (ngx_http_set_content_type(r) != NGX_OK) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        if (r != r->main && of.size == 0) {
            return ngx_http_send_header(r);
        }

        r->allow_ranges = 1;

        /* we need to allocate all before the header would be sent */

        b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
        if (b == NULL) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t));
        if (b->file == NULL) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        rc = ngx_http_send_header(r);

        if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
            return rc;
        }

        b->file_pos = 0;
        b->file_last = of.size;

        b->in_file = b->file_last ? 1: 0;
        b->last_buf = (r == r->main) ? 1: 0;
        b->last_in_chain = 1;

        b->file->fd = of.fd;
        b->file->name = path;
        b->file->log = log;
        b->file->directio = of.is_directio;

        out.buf = b;
        out.next = NULL;

        return ngx_http_output_filter(r, &out);
    }

首先是檢查客戶端的 http 請求類型(r->method),若是請求類型爲NGX_HTTP_GET|NGX_HTTP_HEAD|NGX_HTTP_POST,則繼續進行處理,不然一概返回 NGX_HTTP_NOT_ALLOWED 從而拒絕客戶端的發起的請求。

其次是檢查請求的 url 的結尾字符是否是斜槓/,若是是說明請求的不是一個文件,給後續的 handler 去處理,好比後續的 ngx_http_autoindex_handler(若是是請求的是一個目錄下面,能夠列出這個目錄的文件),或者是 ngx_http_index_handler(若是請求的路徑下面有個默認的 index 文件,直接返回 index 文件的內容)。

而後接下來調用了一個 ngx_http_map_uri_to_path 函數,該函數的做用是把請求的 http 協議的路徑轉化成一個文件系統的路徑。

而後根據轉化出來的具體路徑,去打開文件,打開文件的時候作了 2 種檢查,一種是,若是請求的文件是個 symbol link,根據配置,是否容許符號連接,不容許返回錯誤。還有一個檢查是,若是請求的是一個名稱,是一個目錄的名字,也返回錯誤。若是都沒有錯誤,就讀取文件,返回內容。其實說返回內容可能不是特別準確,比較準確的說法是,把產生的內容傳遞給後續的 filter 去處理。

http log module

該模塊提供了對於每個 http 請求進行記錄的功能,也就是咱們見到的 access.log。固然這個模塊對於 log 提供了一些配置指令,使得能夠比較方便的定製 access.log。

這個模塊的代碼位於src/http/modules/ngx_http_log_module.c,雖然這個模塊的代碼有接近 1400 行,可是主要的邏輯在於對日誌自己格式啊,等細節的處理。咱們在這裏進行分析主要是關注,如何編寫一個 log handler 的問題。

因爲 log handler 的時候,拿到的參數也是 request 這個東西,那麼也就意味着咱們若是須要,能夠好好研究下這個結構,把咱們須要的全部信息都記錄下來。

對於 log handler,有一點特別須要注意的就是,log handler 是不管如何都會被調用的,就是隻要服務端接受到了一個客戶端的請求,也就是產生了一個 request 對象,那麼這些個 log handler 的處理函數都會被調用的,就是在釋放 request 的時候被調用的(ngx_http_free_request函數)。

那麼固然絕對不能忘記的就是 log handler 最好,也是建議被掛載在 NGX_HTTP_LOG_PHASE 階段。由於掛載在其餘階段,有可能在某些狀況下被跳過,而沒有執行到,致使你的 log 模塊記錄的信息不全。

還有一點要說明的是,因爲 Nginx 是容許在某個階段有多個 handler 模塊存在的,根據其處理結果,肯定是否要調用下一個 handler。可是對於掛載在 NGX_HTTP_LOG_PHASE 階段的 handler,則根本不關注這裏 handler 的具體處理函數的返回值,全部的都被調用。以下,位於src/http/ngx_http_request.c中的 ngx_http_log_request 函數。

static void
    ngx_http_log_request(ngx_http_request_t *r)
    {
        ngx_uint_t                  i, n;
        ngx_http_handler_pt        *log_handler;
        ngx_http_core_main_conf_t  *cmcf;

        cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);

        log_handler = cmcf->phases[NGX_HTTP_LOG_PHASE].handlers.elts;
        n = cmcf->phases[NGX_HTTP_LOG_PHASE].handlers.nelts;

        for (i = 0; i < n; i++) {
            log_handler[i](r);
        }
    }

 

十七.過濾模塊簡介

執行時間和內容

過濾(filter)模塊是過濾響應頭和內容的模塊,能夠對回覆的頭和內容進行處理。它的處理時間在獲取回覆內容以後,向用戶發送響應以前。它的處理過程分爲兩個階段,過濾 HTTP 回覆的頭部和主體,在這兩個階段能夠分別對頭部和主體進行修改。

在代碼中有相似的函數:

ngx_http_top_header_filter(r);
ngx_http_top_body_filter(r, in);

就是分別對頭部和主體進行過濾的函數。全部模塊的響應內容要返回給客戶端,都必須調用這兩個接口。

執行順序

過濾模塊的調用是有順序的,它的順序在編譯的時候就決定了。控制編譯的腳本位於 auto/modules 中,當你編譯完 Nginx 之後,能夠在 objs 目錄下面看到一個 ngx_modules.c 的文件。打開這個文件,有相似的代碼:

ngx_module_t *ngx_modules[] = {
            ...
            &ngx_http_write_filter_module,
            &ngx_http_header_filter_module,
            &ngx_http_chunked_filter_module,
            &ngx_http_range_header_filter_module,
            &ngx_http_gzip_filter_module,
            &ngx_http_postpone_filter_module,
            &ngx_http_ssi_filter_module,
            &ngx_http_charset_filter_module,
            &ngx_http_userid_filter_module,
            &ngx_http_headers_filter_module,
            &ngx_http_copy_filter_module,
            &ngx_http_range_body_filter_module,
            &ngx_http_not_modified_filter_module,
            NULL
        };

從 write_filter 到 not_modified_filter,模塊的執行順序是反向的。也就是說最先執行的是 not_modified_filter,而後各個模塊依次執行。通常狀況下,第三方過濾模塊的 config 文件會將模塊名追加到變量 HTTP_AUX_FILTER_MODULES 中,此時該模塊只能加入到 copy_filter 和 headers_filter 模塊之間執行。

Nginx 執行的時候是怎麼按照次序依次來執行各個過濾模塊呢?它採用了一種很隱晦的方法,即經過局部的全局變量。好比,在每一個 filter 模塊,極可能看到以下代碼:

static ngx_http_output_header_filter_pt  ngx_http_next_header_filter;
        static ngx_http_output_body_filter_pt    ngx_http_next_body_filter;

        ...

        ngx_http_next_header_filter = ngx_http_top_header_filter;
        ngx_http_top_header_filter = ngx_http_example_header_filter;

        ngx_http_next_body_filter = ngx_http_top_body_filter;
        ngx_http_top_body_filter = ngx_http_example_body_filter;

ngx_http_top_header_filter 是一個全局變量。當編譯進一個 filter 模塊的時候,就被賦值爲當前 filter 模塊的處理函數。而 ngx_http_next_header_filter 是一個局部全局變量,它保存了編譯前上一個 filter 模塊的處理函數。因此總體看來,就像用全局變量組成的一條單向鏈表。

每一個模塊想執行下一個過濾函數,只要調用一下 ngx_http_next_header_filter 這個局部變量。而整個過濾模塊鏈的入口,須要調用 ngx_http_top_header_filter 這個全局變量。ngx_http_top_body_filter 的行爲與 header fitler 相似。

響應頭和響應體過濾函數的執行順序以下所示:

這圖只表示了 head_filter 和 body_filter 之間的執行順序,在 header_filter 和 body_filter 處理函數之間,在 body_filter 處理函數之間,可能還有其餘執行代碼。

模塊編譯

Nginx 能夠方便的加入第三方的過濾模塊。在過濾模塊的目錄裏,首先須要加入 config 文件,文件的內容以下:

ngx_addon_name=ngx_http_example_filter_module
HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES ngx_http_example_filter_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_example_filter_module.c"

說明把這個名爲 ngx_http_example_filter_module 的過濾模塊加入,ngx_http_example_filter_module.c 是該模塊的源代碼。

注意 HTTP_AUX_FILTER_MODULES 這個變量與通常的內容處理模塊不一樣。

 

十八.過濾模塊的分析

相關結構體

ngx_chain_t 結構很是簡單,是一個單向鏈表:

typedef struct ngx_chain_s ngx_chain_t;

        struct ngx_chain_s {
            ngx_buf_t    *buf;
            ngx_chain_t  *next;
        };

在過濾模塊中,全部輸出的內容都是經過一條單向鏈表所組成。這種單向鏈表的設計,正好應和了 Nginx 流式的輸出模式。每次 Nginx 都是讀到一部分的內容,就放到鏈表,而後輸出出去。這種設計的好處是簡單,非阻塞,可是相應的問題就是跨鏈表的內容操做很是麻煩,若是須要跨鏈表,不少時候都只能緩存鏈表的內容。

單鏈表負載的就是 ngx_buf_t,這個結構體使用很是普遍,先讓咱們看下該結構體的代碼:

struct ngx_buf_s {
            u_char          *pos;       /* 當前buffer真實內容的起始位置 */
            u_char          *last;      /* 當前buffer真實內容的結束位置 */
            off_t            file_pos;  /* 在文件中真實內容的起始位置   */
            off_t            file_last; /* 在文件中真實內容的結束位置   */

            u_char          *start;    /* buffer內存的開始分配的位置 */
            u_char          *end;      /* buffer內存的結束分配的位置 */
            ngx_buf_tag_t    tag;      /* buffer屬於哪一個模塊的標誌 */
            ngx_file_t      *file;     /* buffer所引用的文件 */

            /* 用來引用替換事後的buffer,以便當全部buffer輸出之後,
             * 這個影子buffer能夠被釋放。
             */
            ngx_buf_t       *shadow; 

            /* the buf's content could be changed */
            unsigned         temporary:1;

            /*
             * the buf's content is in a memory cache or in a read only memory
             * and must not be changed
             */
            unsigned         memory:1;

            /* the buf's content is mmap()ed and must not be changed */
            unsigned         mmap:1;

            unsigned         recycled:1; /* 內存能夠被輸出並回收 */
            unsigned         in_file:1;  /* buffer的內容在文件中 */
            /* 立刻所有輸出buffer的內容, gzip模塊裏面用得比較多 */
            unsigned         flush:1;
            /* 基本上是一段輸出鏈的最後一個buffer帶的標誌,標示能夠輸出,
             * 有些零長度的buffer也能夠置該標誌
             */
            unsigned         sync:1;
            /* 全部請求裏面最後一塊buffer,包含子請求 */
            unsigned         last_buf:1;
            /* 當前請求輸出鏈的最後一塊buffer         */
            unsigned         last_in_chain:1;
            /* shadow鏈裏面的最後buffer,能夠釋放buffer了 */
            unsigned         last_shadow:1;
            /* 是不是暫存文件 */
            unsigned         temp_file:1;

            /* 統計用,表示使用次數 */
            /* STUB */ int   num;
        };

通常 buffer 結構體能夠表示一塊內存,內存的起始和結束地址分別用 start 和 end 表示,pos 和 last 表示實際的內容。若是內容已經處理過了,pos 的位置就能夠日後移動。若是讀取到新的內容,last 的位置就會日後移動。因此 buffer 能夠在屢次調用過程當中使用。若是 last 等於 end,就說明這塊內存已經用完了。若是 pos 等於 last,說明內存已經處理完了。下面是一個簡單的示意圖,說明 buffer 中指針的用法:

響應頭過濾函數

響應頭過濾函數主要的用處就是處理 HTTP 響應的頭,能夠根據實際狀況對於響應頭進行修改或者添加刪除。響應頭過濾函數先於響應體過濾函數,並且只調用一次,因此通常可做過濾模塊的初始化工做。

響應頭過濾函數的入口只有一個:

ngx_int_t
        ngx_http_send_header(ngx_http_request_t *r)
        {
            ...

            return ngx_http_top_header_filter(r);
        }

該函數向客戶端發送回覆的時候調用,而後按前一節所述的執行順序。該函數的返回值通常是 NGX_OK,NGX_ERROR 和 NGX_AGAIN,分別表示處理成功,失敗和未完成。

你能夠把 HTTP 響應頭的存儲方式想象成一個 hash 表,在 Nginx 內部能夠很方便地查找和修改各個響應頭部,ngx_http_header_filter_module 過濾模塊把全部的 HTTP 頭組合成一個完整的 buffer,最終 ngx_http_write_filter_module 過濾模塊把 buffer 輸出。

按照前一節過濾模塊的順序,依次講解以下:

filter module description
ngx_http_not_modified_filter_module 默認打開,若是請求的 if-modified-since 等於回覆的 last-modified 間值,說明回覆沒有變化,清空全部回覆的內容,返回 304。
ngx_http_range_body_filter_module 默認打開,只是響應體過濾函數,支持 range 功能,若是請求包含range請求,那就只發送range請求的一段內容。
ngx_http_copy_filter_module 始終打開,只是響應體過濾函數, 主要工做是把文件中內容讀到內存中,以便進行處理。
ngx_http_headers_filter_module 始終打開,能夠設置 expire 和 Cache-control 頭,能夠添加任意名稱的頭
ngx_http_userid_filter_module 默認關閉,能夠添加統計用的識別用戶的 cookie。
ngx_http_charset_filter_module 默認關閉,能夠添加 charset,也能夠將內容從一種字符集轉換到另一種字符集,不支持多字節字符集。
ngx_http_ssi_filter_module 默認關閉,過濾 SSI 請求,能夠發起子請求,去獲取include進來的文件
ngx_http_postpone_filter_module 始終打開,用來將子請求和主請求的輸出鏈合併
ngx_http_gzip_filter_module 默認關閉,支持流式的壓縮內容
ngx_http_range_header_filter_module 默認打開,只是響應頭過濾函數,用來解析range頭,併產生range響應的頭。
ngx_http_chunked_filter_module 默認打開,對於 HTTP/1.1 和缺乏 content-length 的回覆自動打開。
ngx_http_header_filter_module 始終打開,用來將全部 header 組成一個完整的 HTTP 頭。
ngx_http_write_filter_module 始終打開,將輸出鏈拷貝到 r->out中,而後輸出內容。

響應體過濾函數

響應體過濾函數是過濾響應主體的函數。ngx_http_top_body_filter 這個函數每一個請求可能會被執行屢次,它的入口函數是 ngx_http_output_filter,好比:

ngx_int_t
        ngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in)
        {
            ngx_int_t          rc;
            ngx_connection_t  *c;

            c = r->connection;

            rc = ngx_http_top_body_filter(r, in);

            if (rc == NGX_ERROR) {
                /* NGX_ERROR may be returned by any filter */
                c->error = 1;
            }

            return rc;
        }

ngx_http_output_filter 能夠被通常的靜態處理模塊調用,也有多是在 upstream 模塊裏面被調用,對於整個請求的處理階段來講,他們處於的用處都是同樣的,就是把響應內容過濾,而後發給客戶端。

具體模塊的響應體過濾函數的格式相似這樣:

static int 
        ngx_http_example_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
        {
            ...

            return ngx_http_next_body_filter(r, in);
        }

該函數的返回值通常是 NGX_OK,NGX_ERROR 和 NGX_AGAIN,分別表示處理成功,失敗和未完成。

主要功能介紹

響應的主體內容就存於單鏈表 in,鏈表通常不會太長,有時 in 參數可能爲 NULL。in中存有buf結構體中,對於靜態文件,這個buf大小默認是 32K;對於反向代理的應用,這個buf多是4k或者8k。爲了保持內存的低消耗,Nginx通常不會分配過大的內存,處理的原則是收到必定的數據,就發送出去。一個簡單的例子,能夠看看Nginx的chunked_filter模塊,在沒有 content-length 的狀況下,chunk 模塊能夠流式(stream)的加上長度,方便瀏覽器接收和顯示內容。

在響應體過濾模塊中,尤爲要注意的是 buf 的標誌位,完整描述能夠在「相關結構體」這個節中看到。若是 buf 中包含 last 標誌,說明是最後一塊 buf,能夠直接輸出並結束請求了。若是有 flush 標誌,說明這塊 buf 須要立刻輸出,不能緩存。若是整塊 buffer 通過處理完之後,沒有數據了,你能夠把 buffer 的 sync 標誌置上,表示只是同步的用處。

當全部的過濾模塊都處理完畢時,在最後的 write_fitler 模塊中,Nginx 會將 in 輸出鏈拷貝到 r->out 輸出鏈的末尾,而後調用 sendfile 或者 writev 接口輸出。因爲 Nginx 是非阻塞的 socket 接口,寫操做並不必定會成功,可能會有部分數據還殘存在 r->out。在下次的調用中,Nginx 會繼續嘗試發送,直至成功。

發出子請求

Nginx 過濾模塊一大特點就是能夠發出子請求,也就是在過濾響應內容的時候,你能夠發送新的請求,Nginx 會根據你調用的前後順序,將多個回覆的內容拼接成正常的響應主體。一個簡單的例子能夠參考 addition 模塊。

Nginx 是如何保證父請求和子請求的順序呢?當 Nginx 發出子請求時,就會調用 ngx_http_subrequest 函數,將子請求插入父請求的 r->postponed 鏈表中。子請求會在主請求執行完畢時得到依次調用。子請求一樣會有一個請求全部的生存期和處理過程,也會進入過濾模塊流程。

關鍵點是在 postpone_filter 模塊中,它會拼接主請求和子請求的響應內容。r->postponed 按次序保存有父請求和子請求,它是一個鏈表,若是前面一個請求未完成,那後一個請求內容就不會輸出。當前一個請求完成時並輸出時,後一個請求才可輸出,當全部的子請求都完成時,全部的響應內容也就輸出完畢了。

一些優化措施

Nginx 過濾模塊涉及到的結構體,主要就是 chain 和 buf,很是簡單。在平常的過濾模塊中,這兩類結構使用很是頻繁,Nginx採用相似 freelist 重複利用的原則,將使用完畢的 chain 或者 buf 結構體,放置到一個固定的空閒鏈表裏,以待下次使用。

好比,在通用內存池結構體中,pool->chain 變量裏面就保存着釋放的 chain。而通常的 buf 結構體,沒有模塊間公用的空閒鏈表池,都是保存在各模塊的緩存空閒鏈表池裏面。對於 buf 結構體,還有一種 busy 鏈表,表示該鏈表中的 buf 都處於輸出狀態,若是 buf 輸出完畢,這些 buf 就能夠釋放並重複利用了。

功能 函數名
chain 分配 ngx_alloc_chain_link
chain 釋放 ngx_free_chain
buf 分配 ngx_chain_get_free_buf
buf 釋放 ngx_chain_update_chains

過濾內容的緩存

因爲 Nginx 設計流式的輸出結構,當咱們須要對響應內容做全文過濾的時候,必須緩存部分的 buf 內容。該類過濾模塊每每比較複雜,好比 sub,ssi,gzip 等模塊。這類模塊的設計很是靈活,我簡單講一下設計原則:

  1. 輸入鏈 in 須要拷貝操做,通過緩存的過濾模塊,輸入輸出鏈每每已經徹底不同了,因此須要拷貝,經過 ngx_chain_add_copy 函數完成。

  2. 通常有本身的 free 和 busy 緩存鏈表池,能夠提升 buf 分配效率。

  3. 若是須要分配大塊內容,通常分配固定大小的內存卡,並設置 recycled 標誌,表示能夠重複利用。

  4. 原有的輸入 buf 被替換緩存時,必須將其 buf->pos 設爲 buf->last,代表原有的 buf 已經被輸出完畢。或者在新創建的 buf,將 buf->shadow 指向舊的 buf,以便輸出完畢時及時釋放舊的 buf。

 

 

十八.upstream 模塊簡介

 

Nginx 模塊通常被分紅三大類:handler、filter 和 upstream。前面的章節中,讀者已經瞭解了 handler、filter。利用這兩類模塊,能夠使 Nginx 輕鬆完成任何單機工做。而本章介紹的 upstream 模塊,將使 Nginx 跨越單機的限制,完成網絡數據的接收、處理和轉發。

數據轉發功能,爲 Nginx 提供了跨越單機的橫向處理能力,使 Nginx 擺脫只能爲終端節點提供單一功能的限制,而使它具有了網路應用級別的拆分、封裝和整合的戰略功能。在雲模型大行其道的今天,數據轉發是 Nginx 有能力構建一個網絡應用的關鍵組件。固然,鑑於開發成本的問題,一個網絡應用的關鍵組件一開始每每會採用高級編程語言開發。可是當系統到達必定規模,而且須要更重視性能的時候,爲了達到所要求的性能目標,高級語言開發出的組件必須進行結構化修改。此時,對於修改代價而言,Nginx 的 upstream 模塊呈現出極大的吸引力,由於它天生就快。做爲附帶,Nginx 的配置系統提供的層次化和鬆耦合使得系統的擴展性也達到比較高的程度。

言歸正傳,下面介紹 upstream 的寫法。

upstream 模塊接口

從本質上說,upstream 屬於 handler,只是他不產生本身的內容,而是經過請求後端服務器獲得內容,因此才稱爲 upstream(上游)。請求並取得響應內容的整個過程已經被封裝到 Nginx 內部,因此 upstream 模塊只須要開發若干回調函數,完成構造請求和解析響應等具體的工做。

這些回調函數以下表所示:

SN 描述
create_request 生成發送到後端服務器的請求緩衝(緩衝鏈),在初始化 upstream 時使用。
reinit_request 在某臺後端服務器出錯的狀況,Nginx會嘗試另外一臺後端服務器。Nginx 選定新的服務器之後,會先調用此函數,以從新初始化 upstream 模塊的工做狀態,而後再次進行 upstream 鏈接。
process_header 處理後端服務器返回的信息頭部。所謂頭部是與 upstreamserver 通訊的協議規定的,好比 HTTP 協議的 header 部分,或者 memcached 協議的響應狀態部分。
abort_request 在客戶端放棄請求時被調用。不須要在函數中實現關閉後端服務器鏈接的功能,系統會自動完成關閉鏈接的步驟,因此通常此函數不會進行任何具體工做。
finalize_request 正常完成與後端服務器的請求後調用該函數,與 abort_request 相同,通常也不會進行任何具體工做。
input_filter 處理後端服務器返回的響應正文。Nginx 默認的 input_filter 會將收到的內容封裝成爲緩衝區鏈 ngx_chain。該鏈由 upstream 的 out_bufs 指針域定位,因此開發人員能夠在模塊之外經過該指針 獲得後端服務器返回的正文數據。memcached 模塊實現了本身的 input_filter,在後面會具體分析這個模塊。
input_filter_init 初始化 input filter 的上下文。Nginx 默認的 input_filter_init 直接返回。

memcached 模塊分析

memcache 是一款高性能的分佈式 cache 系統,獲得了很是普遍的應用。memcache 定義了一套私有通訊協議,使得不能經過 HTTP 請求來訪問 memcache。但協議自己簡單高效,並且 memcache 使用普遍,因此大部分現代開發語言和平臺都提供了 memcache 支持,方便開發者使用 memcache。

Nginx 提供了 ngx_http_memcached 模塊,提供從 memcache 讀取數據的功能,而不提供向 memcache 寫數據的功能。做爲 Web 服務器,這種設計是能夠接受的。

下面,咱們開始分析 ngx_http_memcached 模塊,一窺 upstream 的奧祕。

Handler 模塊?

初看 memcached 模塊,你們可能以爲並沒有特別之處。若是稍微細看,甚至以爲有點像 handler 模塊,當你們看到這段代碼之後,一定疑惑爲何會跟 handler 模塊如出一轍。

clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
        clcf->handler = ngx_http_memcached_handler;

由於 upstream 模塊使用的就是 handler 模塊的接入方式。同時,upstream 模塊的指令系統的設計也是遵循 handler 模塊的基本規則:配置該模塊纔會執行該模塊。

{ ngx_string("memcached_pass"),
          NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_CONF_TAKE1,
          ngx_http_memcached_pass,
          NGX_HTTP_LOC_CONF_OFFSET,
          0,
          NULL }

因此你們以爲眼熟是好事,說明你們對 Handler 的寫法已經很熟悉了。

Upstream 模塊

那麼,upstream 模塊的特別之處究竟在哪裏呢?答案是就在模塊處理函數的實現中。upstream 模塊的處理函數進行的操做都包含一個固定的流程。在 memcached 的例子中,能夠觀察 ngx_http_memcached_handler 的代碼,能夠發現,這個固定的操做流程是:

  1. 建立 upstream 數據結構。
if (ngx_http_upstream_create(r) != NGX_OK) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }
  1. 設置模塊的 tag 和 schema。schema 如今只會用於日誌,tag 會用於 buf_chain 管理。
u = r->upstream;

        ngx_str_set(&u->schema, "memcached://");
        u->output.tag = (ngx_buf_tag_t) &ngx_http_memcached_module;
  1. 設置 upstream 的後端服務器列表數據結構。
mlcf = ngx_http_get_module_loc_conf(r, ngx_http_memcached_module);
        u->conf = &mlcf->upstream;
  1. 設置 upstream 回調函數。在這裏列出的代碼稍稍調整了代碼順序。
u->create_request = ngx_http_memcached_create_request;
        u->reinit_request = ngx_http_memcached_reinit_request;
        u->process_header = ngx_http_memcached_process_header;
        u->abort_request = ngx_http_memcached_abort_request;
        u->finalize_request = ngx_http_memcached_finalize_request;
        u->input_filter_init = ngx_http_memcached_filter_init;
        u->input_filter = ngx_http_memcached_filter;
  1. 建立並設置 upstream 環境數據結構。
ctx = ngx_palloc(r->pool, sizeof(ngx_http_memcached_ctx_t));
        if (ctx == NULL) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        ctx->rest = NGX_HTTP_MEMCACHED_END;
        ctx->request = r;

        ngx_http_set_ctx(r, ctx, ngx_http_memcached_module);

        u->input_filter_ctx = ctx;
  1. 完成 upstream 初始化並進行收尾工做。
r->main->count++;
        ngx_http_upstream_init(r);
        return NGX_DONE;

任何 upstream 模塊,簡單如 memcached,複雜如 proxy、fastcgi 都是如此。不一樣的 upstream 模塊在這 6 步中的最大差異會出如今第 二、三、四、5 上。其中第 二、4 兩步很容易理解,不一樣的模塊設置的標誌和使用的回調函數確定不一樣。第 5 步也不難理解,只有第3步是最爲晦澀的,不一樣的模塊在取得後端服務器列表時,策略的差別很是大,有如 memcached 這樣簡單明瞭的,也有如 proxy 那樣邏輯複雜的。這個問題先記下來,等把memcached剖析清楚了,再單獨討論。

第 6 步是一個常態。將 count 加 1,而後返回 NGX_DONE。Nginx 遇到這種狀況,雖然會認爲當前請求的處理已經結束,可是不會釋放請求使用的內存資源,也不會關閉與客戶端的鏈接。之因此須要這樣,是由於 Nginx 創建了 upstream 請求和客戶端請求之間一對一的關係,在後續使用 ngx_event_pipe 將 upstream 響應發送回客戶端時,還要使用到這些保存着客戶端信息的數據結構。這部分會在後面的原理篇作具體介紹,這裏再也不展開。

將 upstream 請求和客戶端請求進行一對一綁定,這個設計有優點也有缺陷。優點就是簡化模塊開發,能夠將精力集中在模塊邏輯上,而缺陷一樣明顯,一對一的設計不少時候都不能知足複雜邏輯的須要。對於這一點,將會在後面的原理篇來闡述。

回調函數

前面剖析了 memcached 模塊的骨架,如今開始逐個解決每一個回調函數。

  • ngx_http_memcached_create_request:很簡單的按照設置的內容生成一個 key,接着生成一個「get $key」的請求,放在 r->upstream->request_bufs 裏面。

  • ngx_http_memcached_reinit_request:無需初始化。

  • ngx_http_memcached_abort_request:無需額外操做。

  • ngx_http_memcached_finalize_request:無需額外操做。

  • ngx_http_memcached_process_header:模塊的業務重點函數。memcache 協議的頭部信息被定義爲第一行文本,能夠找到這段代碼證實:
for (p = u->buffer.pos; p < u->buffer.last; p++) {
            if ( * p == LF) {
            goto found;
        }

若是在已讀入緩衝的數據中沒有發現 LF('\n')字符,函數返回 NGX_AGAIN,表示頭部未徹底讀入,須要繼續讀取數據。Nginx 在收到新的數據之後會再次調用該函數。

Nginx 處理後端服務器的響應頭時只會使用一塊緩存,全部數據都在這塊緩存中,因此解析頭部信息時不須要考慮頭部信息跨越多塊緩存的狀況。而若是頭部過大,不能保存在這塊緩存中,Nginx 會返回錯誤信息給客戶端,並記錄 error log,提示緩存不夠大。

process_header 的重要職責是將後端服務器返回的狀態翻譯成返回給客戶端的狀態。例如,在 ngx_http_memcached_process_header 中,有這樣幾段代碼:

r->headers_out.content_length_n = ngx_atoof(len, p - len - 1);

        u->headers_in.status_n = 200;
        u->state->status = 200;

        u->headers_in.status_n = 404;
        u->state->status = 404;

u->state 用於計算 upstream 相關的變量。好比 u->state->status 將被用於計算變量「upstream_status」的值。u->headers_in 將被做爲返回給客戶端的響應返回狀態碼。而第一行則是設置返回給客戶端的響應的長度。

在這個函數中不能忘記的一件事情是處理完頭部信息之後須要將讀指針 pos 後移,不然這段數據也將被複制到返回給客戶端的響應的正文中,進而致使正文內容不正確。

u->buffer.pos = p + 1;

process_header 函數完成響應頭的正確處理,應該返回 NGX_OK。若是返回 NGX_AGAIN,表示未讀取完整數據,須要從後端服務器繼續讀取數據。返回 NGX_DECLINED 無心義,其餘任何返回值都被認爲是出錯狀態,Nginx 將結束 upstream 請求並返回錯誤信息。

  • ngx_http_memcached_filter_init:修正從後端服務器收到的內容長度。由於在處理 header 時沒有加上這部分長度。

  • ngx_http_memcached_filter:memcached 模塊是少有的帶有處理正文的回調函數的模塊。由於 memcached 模塊須要過濾正文末尾 CRLF "END" CRLF,因此實現了本身的 filter 回調函數。處理正文的實際意義是將從後端服務器收到的正文有效內容封裝成 ngx_chain_t,並加在 u->out_bufs 末尾。Nginx 並不進行數據拷貝,而是創建 ngx_buf_t 數據結構指向這些數據內存區,而後由 ngx_chain_t 組織這些 buf。這種實現避免了內存大量搬遷,也是 Nginx 高效的奧祕之一。

本節回顧

這一節介紹了 upstream 模塊的基本組成。upstream 模塊是從 handler 模塊發展而來,指令系統和模塊生效方式與 handler 模塊無異。不一樣之處在於,upstream 模塊在 handler 函數中設置衆多回調函數。實際工做都是由這些回調函數完成的。每一個回調函數都是在 upstream 的某個固定階段執行,各司其職,大部分回調函數通常不會真正用到。upstream 最重要的回調函數是 create_request、process_header 和 input_filter,他們共同實現了與後端服務器的協議的解析部分。

 

十九.負載均衡模塊

 

負載均衡模塊用於從upstream指令定義的後端主機列表中選取一臺主機。Nginx 先使用負載均衡模塊找到一臺主機,再使用 upstream 模塊實現與這臺主機的交互。爲了方便介紹負載均衡模塊,作到言之有物,如下選取 Nginx 內置的 ip hash 模塊做爲實際例子進行分析。

配置

要了解負載均衡模塊的開發方法,首先須要瞭解負載均衡模塊的使用方法。由於負載均衡模塊與以前書中提到的模塊差異比較大,因此咱們從配置入手比較容易理解。

在配置文件中,咱們若是須要使用 ip hash 的負載均衡算法。咱們須要寫一個相似下面的配置:

upstream test {
            ip_hash;

            server 192.168.0.1;
            server 192.168.0.2;
        }

從配置咱們能夠看出負載均衡模塊的使用場景:

  1. 核心指令ip_hash只能在 upstream {}中使用。這條指令用於通知 Nginx 使用 ip hash 負載均衡算法。若是沒加這條指令,Nginx 會使用默認的 round robin 負載均衡模塊。請各位讀者對比 handler 模塊的配置,是否是有共同點?
  2. upstream {}中的指令可能出如今server指令前,可能出如今server指令後,也可能出如今兩條server指令之間。各位讀者可能會有疑問,有什麼差異麼?那麼請各位讀者嘗試下面這個配置:
upstream test {
            server 192.168.0.1 weight=5;
            ip_hash;
            server 192.168.0.2 weight=7;
        }

神奇的事情出現了:

nginx: [emerg] invalid parameter "weight=7" in nginx.conf:103
        configuration file nginx.conf test failed

可見 ip_hash 指令的確能影響到配置的解析。

指令

配置決定指令系統,如今就來看 ip_hash 的指令定義:

static ngx_command_t  ngx_http_upstream_ip_hash_commands[] = {

        { ngx_string("ip_hash"),
          NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS,
          ngx_http_upstream_ip_hash,
          0,
          0,
          NULL },

        ngx_null_command
    };

沒有特別的東西,除了指令屬性是 NGX_HTTP_UPS_CONF。這個屬性表示該指令的適用範圍是 upstream{}。

鉤子

以從前面的章節獲得的經驗,你們應該知道這裏就是模塊的切入點了。負載均衡模塊的鉤子代碼都是有規律的,這裏經過 ip_hash 模塊來分析這個規律。

static char *
    ngx_http_upstream_ip_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
    {
        ngx_http_upstream_srv_conf_t  *uscf;

        uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);

        uscf->peer.init_upstream = ngx_http_upstream_init_ip_hash;

        uscf->flags = NGX_HTTP_UPSTREAM_CREATE
                    |NGX_HTTP_UPSTREAM_MAX_FAILS
                    |NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
                    |NGX_HTTP_UPSTREAM_DOWN;

        return NGX_CONF_OK;
    }

這段代碼中有兩點值得咱們注意。一個是 uscf->flags 的設置,另外一個是設置 init_upstream 回調。

設置 uscf->flags

  1. NGX_HTTP_UPSTREAM_CREATE:建立標誌,若是含有建立標誌的話,Nginx 會檢查重複建立,以及必要參數是否填寫;

  2. NGX_HTTP_UPSTREAM_MAX_FAILS:能夠在 server 中使用 max_fails 屬性;

  3. NGX_HTTP_UPSTREAM_FAIL_TIMEOUT:能夠在 server 中使用 fail_timeout 屬性;

  4. NGX_HTTP_UPSTREAM_DOWN:能夠在 server 中使用 down 屬性;

  5. NGX_HTTP_UPSTREAM_WEIGHT:能夠在 server 中使用 weight 屬性;

  6. NGX_HTTP_UPSTREAM_BACKUP:能夠在 server 中使用 backup 屬性。

聰明的讀者若是聯想到剛剛遇到的那個神奇的配置錯誤,能夠得出一個結論:在負載均衡模塊的指令處理函數中能夠設置並修改 upstream{} 中server指令支持的屬性。這是一個很重要的性質,由於不一樣的負載均衡模塊對各類屬性的支持狀況都是不同的,那麼就須要在解析配置文件的時候檢測出是否使用了不支持的負載均衡屬性並給出錯誤提示,這對於提高系統維護性是頗有意義的。可是,這種機制也存在缺陷,正如前面的例子所示,沒有機制可以追加檢查在更新支持屬性以前已經配置了不支持屬性的server指令。

設置 init_upstream 回調

Nginx 初始化 upstream 時,會在 ngx_http_upstream_init_main_conf 函數中調用設置的回調函數初始化負載均衡模塊。這裏不太好理解的是 uscf 的具體位置。經過下面的示意圖,說明 upstream 負載均衡模塊的配置的內存佈局。

從圖上能夠看出,MAIN_CONF 中 ngx_upstream_module 模塊的配置項中有一個指針數組 upstreams,數組中的每一個元素對應就是配置文件中每個 upstream{}的信息。更具體的將會在後面的原理篇討論。

初始化配置

init_upstream 回調函數執行時須要初始化負載均衡模塊的配置,還要設置一個新鉤子,這個鉤子函數會在 Nginx 處理每一個請求時做爲初始化函數調用,關於這個新鉤子函數的功能,後面會有詳細的描述。這裏,咱們先分析 IP hash 模塊初始化配置的代碼:

ngx_http_upstream_init_round_robin(cf, us);
    us->peer.init = ngx_http_upstream_init_ip_hash_peer;

這段代碼很是簡單:IP hash 模塊首先調用另外一個負載均衡模塊 Round Robin 的初始化函數,而後再設置本身的處理請求階段初始化鉤子。實際上幾個負載均衡模塊能夠組成一條鏈表,每次都是從鏈首的模塊開始進行處理。若是模塊決定不處理,能夠將處理權交給鏈表中的下一個模塊。這裏,IP hash 模塊指定 Round Robin 模塊做爲本身的後繼負載均衡模塊,因此在本身的初始化配置函數中也對 Round Robin 模塊進行初始化。

初始化請求

Nginx 收到一個請求之後,若是發現須要訪問 upstream,就會執行對應的 peer.init 函數。這是在初始化配置時設置的回調函數。這個函數最重要的做用是構造一張表,當前請求能夠使用的 upstream 服務器被依次添加到這張表中。之因此須要這張表,最重要的緣由是若是 upstream 服務器出現異常,不能提供服務時,能夠從這張表中取得其餘服務器進行重試操做。此外,這張表也能夠用於負載均衡的計算。之因此構造這張表的行爲放在這裏而不是在前面初始化配置的階段,是由於upstream須要爲每個請求提供獨立隔離的環境。

爲了討論 peer.init 的核心,咱們仍是看 IP hash 模塊的實現:

r->upstream->peer.data = &iphp->rrp;

    ngx_http_upstream_init_round_robin_peer(r, us);

    r->upstream->peer.get = ngx_http_upstream_get_ip_hash_peer;

第一行是設置數據指針,這個指針就是指向前面提到的那張表;

第二行是調用 Round Robin 模塊的回調函數對該模塊進行請求初始化。面前已經提到,一個負載均衡模塊能夠調用其餘負載均衡模塊以提供功能的補充。

第三行是設置一個新的回調函數get。該函數負責從表中取出某個服務器。除了 get 回調函數,還有另外一個r->upstream->peer.free的回調函數。該函數在 upstream 請求完成後調用,負責作一些善後工做。好比咱們須要維護一個 upstream 服務器訪問計數器,那麼能夠在 get 函數中對其加 1,在 free 中對其減 1。若是是 SSL 的話,Nginx 還提供兩個回調函數 peer.set_session 和 peer.save_session。通常來講,有兩個切入點實現負載均衡算法,其一是在這裏,其二是在 get 回調函數中。

peer.get 和 peer.free 回調函數

這兩個函數是負載均衡模塊最底層的函數,負責實際獲取一個鏈接和回收一個鏈接的預備操做。之因此說是預備操做,是由於在這兩個函數中,並不實際進行創建鏈接或者釋放鏈接的動做,而只是執行獲取鏈接的地址或維護鏈接狀態的操做。須要理解的清楚一點,在 peer.get 函數中獲取鏈接的地址信息,並不表明這時鏈接必定沒有被創建,相反的,經過 get 函數的返回值,Nginx 能夠了解是否存在可用鏈接,鏈接是否已經創建。這些返回值總結以下:

返回值 說明 Nginx 後續動做
NGX_DONE 獲得了鏈接地址信息,而且鏈接已經創建。 直接使用鏈接,發送數據。
NGX_OK 獲得了鏈接地址信息,但鏈接並未創建。 創建鏈接,如鏈接不能當即創建,設置事件,
    暫停執行本請求,執行別的請求。
NGX_BUSY 全部鏈接均不可用。 返回502錯誤至客戶端。

各位讀者看到上面這張表,可能會有幾個問題浮現出來:

Q: 何時鏈接是已經創建的?

A: 使用後端 keepalive 鏈接的時候,鏈接在使用完之後並不關閉,而是存放在一個隊列中,新的請求只須要從隊列中取出鏈接,這些鏈接都是已經準備好的。

Q: 什麼叫全部鏈接均不可用?

A: 初始化請求的過程當中,創建了一張表,get 函數負責每次從這張表中不重複的取出一個鏈接,當沒法從表中取得一個新的鏈接時,即全部鏈接均不可用。

Q: 對於一個請求,peer.get 函數可能被調用屢次麼?

A: 正式如此。當某次 peer.get 函數獲得的鏈接地址鏈接不上,或者請求對應的服務器獲得異常響應,Nginx 會執行 ngx_http_upstream_next,而後可能再次調用 peer.get 函數嘗試別的鏈接。upstream 總體流程以下:

本節回顧

這一節介紹了負載均衡模塊的基本組成。負載均衡模塊的配置區集中在 upstream{}塊中。負載均衡模塊的回調函數體系是以 init_upstream 爲起點,經歷 init_peer,最終到達 peer.get 和 peer.free。其中 init_peer 負責創建每一個請求使用的 server 列表,peer.get 負責從 server 列表中選擇某個 server(通常是不重複選擇),而 peer.free 負責 server 釋放前的資源釋放工做。最後,這一節經過一張圖將 upstream 模塊和負載均衡模塊在請求處理過程當中的相互關係展示出來。

 

二十.core 模塊

 

Nginx 的啓動模塊

啓動模塊從啓動 Nginx 進程開始,作了一系列的初始化工做,源代碼位於src/core/nginx.c,從 main 函數開始:

  • 時間、正則、錯誤日誌、ssl 等初始化
  • 讀入命令行參數
  • OS 相關初始化
  • 讀入並解析配置
  • 核心模塊初始化
  • 建立各類暫時文件和目錄
  • 建立共享內存
  • 打開 listen 的端口
  • 全部模塊初始化
  • 啓動 worker 進程

 

二十一.event 模塊

 

event 的類型和功能

Nginx 是以 event(事件)處理模型爲基礎的模塊。它爲了支持跨平臺,抽象出了 event 模塊。它支持的 event 處理類型有:AIO(異步IO),/dev/poll(Solaris 和 Unix 特有),epoll(Linux 特有),eventport(Solaris 10 特有),kqueue(BSD 特有),poll,rtsig(實時信號),select 等。

event 模塊的主要功能就是,監聽 accept 後創建的鏈接,對讀寫事件進行添加刪除。事件處理模型和 Nginx 的非阻塞 IO 模型結合在一塊兒使用。當 IO 可讀可寫的時候,相應的讀寫事件就會被喚醒,此時就會去處理事件的回調函數。

特別對於 Linux,Nginx 大部分 event 採用 epoll EPOLLET(邊沿觸發)的方法來觸發事件,只有 listen 端口的讀事件是 EPOLLLT(水平觸發)。對於邊沿觸發,若是出現了可讀事件,必須及時處理,不然可能會出現讀事件再也不觸發,鏈接餓死的狀況。

typedef struct {
        /* 添加刪除事件 */
        ngx_int_t  (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
        ngx_int_t  (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);

        ngx_int_t  (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
        ngx_int_t  (*disable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);

        /* 添加刪除鏈接,會同時監聽讀寫事件 */
        ngx_int_t  (*add_conn)(ngx_connection_t *c);
        ngx_int_t  (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);

        ngx_int_t  (*process_changes)(ngx_cycle_t *cycle, ngx_uint_t nowait);
        /* 處理事件的函數 */
        ngx_int_t  (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer,
                                   ngx_uint_t flags);

        ngx_int_t  (*init)(ngx_cycle_t *cycle, ngx_msec_t timer);
        void       (*done)(ngx_cycle_t *cycle);
} ngx_event_actions_t;

上述是 event 處理抽象出來的關鍵結構體,能夠看到,每一個 event 處理模型,都須要實現部分功能。最關鍵的是 add 和 del 功能,就是最基本的添加和刪除事件的函數。

accept 鎖

Nginx 是多進程程序,80 端口是各進程所共享的,多進程同時 listen 80 端口,勢必會產生競爭,也產生了所謂的「驚羣」效應。當內核 accept 一個鏈接時,會喚醒全部等待中的進程,但實際上只有一個進程能獲取鏈接,其餘的進程都是被無效喚醒的。因此 Nginx 採用了自有的一套 accept 加鎖機制,避免多個進程同時調用 accept。Nginx 多進程的鎖在底層默認是經過 CPU 自旋鎖來實現。若是操做系統不支持自旋鎖,就採用文件鎖。

Nginx 事件處理的入口函數是 ngx_process_events_and_timers(),下面是部分代碼,能夠看到其加鎖的過程:

if (ngx_use_accept_mutex) {
        if (ngx_accept_disabled > 0) {
                ngx_accept_disabled--;

        } else {
                if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                        return;
                }

                if (ngx_accept_mutex_held) {
                        flags |= NGX_POST_EVENTS;

                } else {
                        if (timer == NGX_TIMER_INFINITE
                                || timer > ngx_accept_mutex_delay)
                        {
                                timer = ngx_accept_mutex_delay;
                        }
                }
        }
}

在 ngx_trylock_accept_mutex()函數裏面,若是拿到了鎖,Nginx 會把 listen 的端口讀事件加入 event 處理,該進程在有新鏈接進來時就能夠進行 accept 了。注意 accept 操做是一個普通的讀事件。下面的代碼說明了這點:

(void) ngx_process_events(cycle, timer, flags);

if (ngx_posted_accept_events) {
        ngx_event_process_posted(cycle, &ngx_posted_accept_events);
}

if (ngx_accept_mutex_held) {
        ngx_shmtx_unlock(&ngx_accept_mutex);
}

ngx_process_events()函數是全部事件處理的入口,它會遍歷全部的事件。搶到了 accept 鎖的進程跟通常進程稍微不一樣的是,它被加上了 NGX_POST_EVENTS 標誌,也就是說在 ngx_process_events() 函數裏面只接受而不處理事件,並加入 post_events 的隊列裏面。直到 ngx_accept_mutex 鎖去掉之後纔去處理具體的事件。爲何這樣?由於 ngx_accept_mutex 是全局鎖,這樣作能夠儘可能減小該進程搶到鎖之後,從 accept 開始到結束的時間,以便其餘進程繼續接收新的鏈接,提升吞吐量。

ngx_posted_accept_events 和 ngx_posted_events 就分別是 accept 延遲事件隊列和普通延遲事件隊列。能夠看到 ngx_posted_accept_events 仍是放到 ngx_accept_mutex 鎖裏面處理的。該隊列裏面處理的都是 accept 事件,它會一口氣把內核 backlog 裏等待的鏈接都 accept 進來,註冊到讀寫事件裏。

而 ngx_posted_events 是普通的延遲事件隊列。通常狀況下,什麼樣的事件會放到這個普通延遲隊列裏面呢?個人理解是,那些 CPU 耗時比較多的均可以放進去。由於 Nginx 事件處理都是根據觸發順序在一個大循環裏依次處理的,由於 Nginx 一個進程同時只能處理一個事件,因此有些耗時多的事件會把後面全部事件的處理都耽擱了。

除了加鎖,Nginx 也對各進程的請求處理的均衡性做了優化,也就是說,若是在負載高的時候,進程搶到的鎖過多,會致使這個進程被禁止接受請求一段時間。

好比,在 ngx_event_accept 函數中,有相似代碼:

ngx_accept_disabled = ngx_cycle->connection_n / 8
              - ngx_cycle->free_connection_n;

ngx_cycle->connection_n 是進程能夠分配的鏈接總數,ngx_cycle->free_connection_n 是空閒的進程數。上述等式說明了,當前進程的空閒進程數小於 1/8 的話,就會被禁止 accept 一段時間。

定時器

Nginx 在須要用到超時的時候,都會用到定時器機制。好比,創建鏈接之後的那些讀寫超時。Nginx 使用紅黑樹來構造按期器,紅黑樹是一種有序的二叉平衡樹,其查找插入和刪除的複雜度都爲 O(logn),因此是一種比較理想的二叉樹。

定時器的機制就是,二叉樹的值是其超時時間,每次查找二叉樹的最小值,若是最小值已通過期,就刪除該節點,而後繼續查找,直到全部超時節點都被刪除。

 

 

 
 
本文內容來自:http://wiki.jikexueyuan.com/project/nginx/
相關文章
相關標籤/搜索