nginx平臺初探(100%)

http://tengine.taobao.org/book/chapter_02.htmlphp

初探nginx架構(100%)

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

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

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

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進程發送信號同樣了。android

如今,咱們知道了當咱們在操做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

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

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

有人可能要問了,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性能高效的主要緣由。apache

咱們以前說過,推薦設置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基礎概念(100%)

 

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的。好比一個請求GET    http://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,好比配置4 8k,就是表示有四個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中,鏈接與請求的基本概念,下節,咱們講基本的數據結構。

 

基本數據結構(99%)

nginx的做者爲追求極致的高效,本身實現了不少頗具特點的nginx風格的數據結構以及公共函數。好比,nginx提供了帶長度的字符串,根據編譯器選項優化過的字符串拷貝函數ngx_copy等。因此,在咱們寫nginx模塊時,應該儘可能調用nginx提供的api,儘管有些api只是對glibc的宏定義。本節,咱們介紹string、list、buffer、chain等一系列最基本的數據結構及相關api的使用技巧以及注意事項。

 

ngx_str_t(100%)

在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"); 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(100%)

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大小的內存。可是此函數分配的內存並無像上面的函數那樣進行過對齊。

 
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(100%)

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(100%)

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(100%)

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(100%)

組合類型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(100%)

你們看到在構建一個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(100%)

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(99%)

這個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配置的限制,可是會受到發送速率等其餘條件的限制。
sync:  
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_but_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(100%)

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: 每一個節點固定長度的數組的長度。
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(100%)

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的配置系統(100%)

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()函數就是這個無限循環的處理函數。在這個函數中,一個請求的簡單處理流程以下:

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

請求的處理流程

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

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

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

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

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

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

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

  1. 獲取location配置。
  2. 產生適當的響應。
  3. 發送response header。
  4. 發送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依次嘗試:

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

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

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

  1. server-side includes。
  2. XSLT filtering。
  3. 圖像縮放之類的。
  4. gzip壓縮。

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

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