初探nginx架構

初探nginx架構(100%)

衆所周知,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啓動後,如果我們要操作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基礎概念(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長度的確定:

對於http1.0協議來說,如果響應頭中有content-length頭,則以content-length的長度就可以知道body的長度 了,客戶端在接收body時,就可以依照這個長度來接收數據,接收完後,就表示這個請求完成了。而如果沒有content-length頭,則客戶端會一 直接收數據,直到服務端主動斷開連接,才表示body接收完了。
而對於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的實現又有其幾個顯著的特點:

ngx_hash_t不像其他的hash表的實現,可以插入刪除元素,它只能一次初始化,就構建起整個hash表以後,既不能再刪除,也不能在插入元素了。
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()函數就是這個無限循環的處理函數。在這個函數中,一個請求的簡單處理流程如下:

操作系統提供的機制(例如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處理。

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