在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ Linux/ Nginx 基礎(chǔ)概念
示例: hello handler 模塊
什么是 Nginx
handler 模塊的掛載
Nginx 特點(diǎn)
handler 模塊簡介
初探 Nginx 架構(gòu)
Nginx 的模塊化體系結(jié)構(gòu)
更多 handler 模塊示例分析
Nginx 基礎(chǔ)概念
upstream 模塊簡介
Nginx 的請求處理
過濾模塊簡介
基本數(shù)據(jù)結(jié)構(gòu)
模塊的基本結(jié)構(gòu)
負(fù)載均衡模塊
過濾模塊的分析
core 模塊
handler 模塊的基本結(jié)構(gòu)
Nginx 的配置系統(tǒng)
handler 的編寫步驟
handler 模塊的編譯和使用
event 模塊

Nginx 基礎(chǔ)概念

connection

在 Nginx 中 connection 就是對 tcp 連接的封裝,其中包括連接的 socket,讀事件,寫事件。利用 Nginx 封裝的 connection,我們可以很方便的使用 Nginx 來處理與連接相關(guān)的事情,比如,建立連接,發(fā)送與接受數(shù)據(jù)等。而 Nginx 中的 http 請求的處理就是建立在 connection之上的,所以 Nginx 不僅可以作為一個(gè)web服務(wù)器,也可以作為郵件服務(wù)器。當(dāng)然,利用 Nginx 提供的 connection,我們可以與任何后端服務(wù)打交道。

結(jié)合一個(gè) tcp 連接的生命周期,我們看看 Nginx 是如何處理一個(gè)連接的。首先,Nginx 在啟動時(shí),會解析配置文件,得到需要監(jiān)聽的端口與 ip 地址,然后在 Nginx 的 master 進(jìn)程里面,先初始化好這個(gè)監(jiān)控的 socket(創(chuàng)建 socket,設(shè)置 addrreuse 等選項(xiàng),綁定到指定的 ip 地址端口,再 listen),然后再 fork 出多個(gè)子進(jìn)程出來,然后子進(jìn)程會競爭 accept 新的連接。此時(shí),客戶端就可以向 Nginx 發(fā)起連接了。當(dāng)客戶端與服務(wù)端通過三次握手建立好一個(gè)連接后,Nginx 的某一個(gè)子進(jìn)程會 accept 成功,得到這個(gè)建立好的連接的 socket,然后創(chuàng)建 Nginx 對連接的封裝,即 ngx_connection_t 結(jié)構(gòu)體。接著,設(shè)置讀寫事件處理函數(shù)并添加讀寫事件來與客戶端進(jìn)行數(shù)據(jù)的交換。最后,Nginx 或客戶端來主動關(guān)掉連接,到此,一個(gè)連接就壽終正寢了。

當(dāng)然,Nginx 也是可以作為客戶端來請求其它 server 的數(shù)據(jù)的(如 upstream 模塊),此時(shí),與其它 server 創(chuàng)建的連接,也封裝在 ngx_connection_t 中。作為客戶端,Nginx 先獲取一個(gè) ngx_connection_t 結(jié)構(gòu)體,然后創(chuàng)建 socket,并設(shè)置 socket 的屬性( 比如非阻塞)。然后再通過添加讀寫事件,調(diào)用 connect/read/write 來調(diào)用連接,最后關(guān)掉連接,并釋放 ngx_connection_t。

在 Nginx 中,每個(gè)進(jìn)程會有一個(gè)連接數(shù)的最大上限,這個(gè)上限與系統(tǒng)對 fd 的限制不一樣。在操作系統(tǒng)中,通過 ulimit -n,我們可以得到一個(gè)進(jìn)程所能夠打開的 fd 的最大數(shù),即 nofile,因?yàn)槊總€(gè) socket 連接會占用掉一個(gè) fd,所以這也會限制我們進(jìn)程的最大連接數(shù),當(dāng)然也會直接影響到我們程序所能支持的最大并發(fā)數(shù),當(dāng) fd 用完后,再創(chuàng)建 socket 時(shí),就會失敗。Nginx 通過設(shè)置 worker_connectons 來設(shè)置每個(gè)進(jìn)程支持的最大連接數(shù)。如果該值大于 nofile,那么實(shí)際的最大連接數(shù)是 nofile,Nginx 會有警告。Nginx 在實(shí)現(xiàn)時(shí),是通過一個(gè)連接池來管理的,每個(gè) worker 進(jìn)程都有一個(gè)獨(dú)立的連接池,連接池的大小是 worker_connections。這里的連接池里面保存的其實(shí)不是真實(shí)的連接,它只是一個(gè) worker_connections 大小的一個(gè) ngx_connection_t 結(jié)構(gòu)的數(shù)組。并且,Nginx 會通過一個(gè)鏈表 free_connections 來保存所有的空閑 ngx_connection_t,每次獲取一個(gè)連接時(shí),就從空閑連接鏈表中獲取一個(gè),用完后,再放回空閑連接鏈表里面。

在這里,很多人會誤解 worker_connections 這個(gè)參數(shù)的意思,認(rèn)為這個(gè)值就是 Nginx 所能建立連接的最大值。其實(shí)不然,這個(gè)值是表示每個(gè) worker 進(jìn)程所能建立連接的最大值,所以,一個(gè) Nginx 能建立的最大連接數(shù),應(yīng)該是worker_connections * worker_processes。當(dāng)然,這里說的是最大連接數(shù),對于 HTTP 請求本地資源來說,能夠支持的最大并發(fā)數(shù)量是worker_connections * worker_processes,而如果是 HTTP 作為反向代理來說,最大并發(fā)數(shù)量應(yīng)該是worker_connections * worker_processes/2。因?yàn)樽鳛榉聪虼矸?wù)器,每個(gè)并發(fā)會建立與客戶端的連接和與后端服務(wù)的連接,會占用兩個(gè)連接。

那么,我們前面有說過一個(gè)客戶端連接過來后,多個(gè)空閑的進(jìn)程,會競爭這個(gè)連接,很容易看到,這種競爭會導(dǎo)致不公平,如果某個(gè)進(jìn)程得到 accept 的機(jī)會比較多,它的空閑連接很快就用完了,如果不提前做一些控制,當(dāng) accept 到一個(gè)新的 tcp 連接后,因?yàn)闊o法得到空閑連接,而且無法將此連接轉(zhuǎn)交給其它進(jìn)程,最終會導(dǎo)致此 tcp 連接得不到處理,就中止掉了。很顯然,這是不公平的,有的進(jìn)程有空余連接,卻沒有處理機(jī)會,有的進(jìn)程因?yàn)闆]有空余連接,卻人為地丟棄連接。那么,如何解決這個(gè)問題呢?首先,Nginx 的處理得先打開 accept_mutex 選項(xiàng),此時(shí),只有獲得了 accept_mutex 的進(jìn)程才會去添加accept事件,也就是說,Nginx會控制進(jìn)程是否添加 accept 事件。Nginx 使用一個(gè)叫 ngx_accept_disabled 的變量來控制是否去競爭 accept_mutex 鎖。在第一段代碼中,計(jì)算 ngx_accept_disabled 的值,這個(gè)值是 Nginx 單進(jìn)程的所有連接總數(shù)的八分之一,減去剩下的空閑連接數(shù)量,得到的這個(gè) ngx_accept_disabled 有一個(gè)規(guī)律,當(dāng)剩余連接數(shù)小于總連接數(shù)的八分之一時(shí),其值才大于 0,而且剩余的連接數(shù)越小,這個(gè)值越大。再看第二段代碼,當(dāng) ngx_accept_disabled 大于 0 時(shí),不會去嘗試獲取 accept_mutex 鎖,并且將 ngx_accept_disabled 減 1,于是,每次執(zhí)行到此處時(shí),都會去減 1,直到小于 0。不去獲取 accept_mutex 鎖,就是等于讓出獲取連接的機(jī)會,很顯然可以看出,當(dāng)空余連接越少時(shí),ngx_accept_disable 越大,于是讓出的機(jī)會就越多,這樣其它進(jìn)程獲取鎖的機(jī)會也就越大。不去 accept,自己的連接就控制下來了,其它進(jìn)程的連接池就會得到利用,這樣,Nginx 就控制了多進(jìn)程間連接的平衡了。

    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 中連接是個(gè)什么東西就行了,而且連接是屬于比較高級的用法,在后面的模塊開發(fā)高級篇會有專門的章節(jié)來講解連接與事件的實(shí)現(xiàn)及使用。

request

這節(jié)我們講 request,在 Nginx 中我們指的是 http 請求,具體到 Nginx 中的數(shù)據(jù)結(jié)構(gòu)是ngx_http_request_t。ngx_http_request_t 是對一個(gè) http 請求的封裝。 我們知道,一個(gè) http 請求,包含請求行、請求頭、請求體、響應(yīng)行、響應(yīng)頭、響應(yīng)體。

http 請求是典型的請求-響應(yīng)類型的的網(wǎng)絡(luò)協(xié)議,而 http 是文本協(xié)議,所以我們在分析請求行與請求頭,以及輸出響應(yīng)行與響應(yīng)頭,往往是一行一行的進(jìn)行處理。如果我們自己來寫一個(gè) http 服務(wù)器,通常在一個(gè)連接建立好后,客戶端會發(fā)送請求過來。然后我們讀取一行數(shù)據(jù),分析出請求行中包含的 method、uri、http_version 信息。然后再一行一行處理請求頭,并根據(jù)請求 method 與請求頭的信息來決定是否有請求體以及請求體的長度,然后再去讀取請求體。得到請求后,我們處理請求產(chǎn)生需要輸出的數(shù)據(jù),然后再生成響應(yīng)行,響應(yīng)頭以及響應(yīng)體。在將響應(yīng)發(fā)送給客戶端之后,一個(gè)完整的請求就處理完了。當(dāng)然這是最簡單的 webserver 的處理方式,其實(shí) Nginx 也是這樣做的,只是有一些小小的區(qū)別,比如,當(dāng)請求頭讀取完成后,就開始進(jìn)行請求的處理了。Nginx 通過 ngx_http_request_t 來保存解析請求與輸出響應(yīng)相關(guān)的數(shù)據(jù)。

那接下來,簡要講講 Nginx 是如何處理一個(gè)完整的請求的。對于 Nginx 來說,一個(gè)請求是從ngx_http_init_request 開始的,在這個(gè)函數(shù)中,會設(shè)置讀事件為 ngx_http_process_request_line,也就是說,接下來的網(wǎng)絡(luò)事件,會由 ngx_http_process_request_line 來執(zhí)行。從ngx_http_process_request_line 的函數(shù)名,我們可以看到,這就是來處理請求行的,正好與之前講的,處理請求的第一件事就是處理請求行是一致的。通過 ngx_http_read_request_header 來讀取請求數(shù)據(jù)。然后調(diào)用 ngx_http_parse_request_line 函數(shù)來解析請求行。Nginx 為提高效率,采用狀態(tài)機(jī)來解析請求行,而且在進(jìn)行 method 的比較時(shí),沒有直接使用字符串比較,而是將四個(gè)字符轉(zhuǎn)換成一個(gè)整型,然后一次比較以減少 cpu 的指令數(shù),這個(gè)前面有說過。很多人可能很清楚一個(gè)請求行包含請求的方法,uri,版本,卻不知道其實(shí)在請求行中,也是可以包含有 host 的。比如一個(gè)請求 GET http://www.taobao.com/uri HTTP/1.0 這樣一個(gè)請求行也是合法的,而且 host 是 www.taobao.com,這個(gè)時(shí)候,Nginx 會忽略請求頭中的 host 域,而以請求行中的這個(gè)為準(zhǔn)來查找虛擬主機(jī)。另外,對于對于 http0.9 版來說,是不支持請求頭的,所以這里也是要特別的處理。所以,在后面解析請求頭時(shí),協(xié)議版本都是 1.0 或 1.1。整個(gè)請求行解析到的參數(shù),會保存到 ngx_http_request_t 結(jié)構(gòu)當(dāng)中。

在解析完請求行后,Nginx 會設(shè)置讀事件的 handler 為 ngx_http_process_request_headers,然后后續(xù)的請求就在 ngx_http_process_request_headers 中進(jìn)行讀取與解析。ngx_http_process_request_headers 函數(shù)用來讀取請求頭,跟請求行一樣,還是調(diào)用 ngx_http_read_request_header 來讀取請求頭,調(diào)用 ngx_http_parse_header_line 來解析一行請求頭,解析到的請求頭會保存到 ngx_http_request_t 的域 headers_in 中,headers_in 是一個(gè)鏈表結(jié)構(gòu),保存所有的請求頭。而 HTTP 中有些請求是需要特別處理的,這些請求頭與請求處理函數(shù)存放在一個(gè)映射表里面,即 ngx_http_headers_in,在初始化時(shí),會生成一個(gè) hash 表,當(dāng)每解析到一個(gè)請求頭后,就會先在這個(gè) hash 表中查找,如果有找到,則調(diào)用相應(yīng)的處理函數(shù)來處理這個(gè)請求頭。比如:Host 頭的處理函數(shù)是 ngx_http_process_host。

當(dāng) Nginx 解析到兩個(gè)回車換行符時(shí),就表示請求頭的結(jié)束,此時(shí)就會調(diào)用 ngx_http_process_request 來處理請求了。ngx_http_process_request 會設(shè)置當(dāng)前的連接的讀寫事件處理函數(shù)為 ngx_http_request_handler,然后再調(diào)用 ngx_http_handler 來真正開始處理一個(gè)完整的http請求。這里可能比較奇怪,讀寫事件處理函數(shù)都是ngx_http_request_handler,其實(shí)在這個(gè)函數(shù)中,會根據(jù)當(dāng)前事件是讀事件還是寫事件,分別調(diào)用 ngx_http_request_t 中的 read_event_handler 或者是 write_event_handler。由于此時(shí),我們的請求頭已經(jīng)讀取完成了,之前有說過,Nginx 的做法是先不讀取請求 body,所以這里面我們設(shè)置 read_event_handler 為 ngx_http_block_reading,即不讀取數(shù)據(jù)了。剛才說到,真正開始處理數(shù)據(jù),是在 ngx_http_handler 這個(gè)函數(shù)里面,這個(gè)函數(shù)會設(shè)置 write_event_handler 為 ngx_http_core_run_phases,并執(zhí)行 ngx_http_core_run_phases 函數(shù)。ngx_http_core_run_phases 這個(gè)函數(shù)將執(zhí)行多階段請求處理,Nginx 將一個(gè) http 請求的處理分為多個(gè)階段,那么這個(gè)函數(shù)就是執(zhí)行這些階段來產(chǎn)生數(shù)據(jù)。因?yàn)?ngx_http_core_run_phases 最后會產(chǎn)生數(shù)據(jù),所以我們就很容易理解,為什么設(shè)置寫事件的處理函數(shù)為 ngx_http_core_run_phases 了。在這里,我簡要說明了一下函數(shù)的調(diào)用邏輯,我們需要明白最終是調(diào)用 ngx_http_core_run_phases 來處理請求,產(chǎn)生的響應(yīng)頭會放在 ngx_http_request_t 的 headers_out 中,這一部分內(nèi)容,我會放在請求處理流程里面去講。Nginx 的各種階段會對請求進(jìn)行處理,最后會調(diào)用 filter 來過濾數(shù)據(jù),對數(shù)據(jù)進(jìn)行加工,如 truncked 傳輸、gzip 壓縮等。這里的 filter 包括 header filter 與 body filter,即對響應(yīng)頭或響應(yīng)體進(jìn)行處理。filter 是一個(gè)鏈表結(jié)構(gòu),分別有 header filter 與 body filter,先執(zhí)行 header filter 中的所有 filter,然后再執(zhí)行 body filter 中的所有 filter。在 header filter 中的最后一個(gè) filter,即 ngx_http_header_filter,這個(gè) filter 將會遍歷所有的響應(yīng)頭,最后需要輸出的響應(yīng)頭在一個(gè)連續(xù)的內(nèi)存,然后調(diào)用 ngx_http_write_filter 進(jìn)行輸出。ngx_http_write_filter 是 body filter 中的最后一個(gè),所以 Nginx 首先的 body 信息,在經(jīng)過一系列的 body filter 之后,最后也會調(diào)用 ngx_http_write_filter 來進(jìn)行輸出(有圖來說明)。

這里要注意的是,Nginx 會將整個(gè)請求頭都放在一個(gè) buffer 里面,這個(gè) buffer 的大小通過配置項(xiàng) client_header_buffer_size 來設(shè)置,如果用戶的請求頭太大,這個(gè) buffer 裝不下,那 Nginx 就會重新分配一個(gè)新的更大的 buffer 來裝請求頭,這個(gè)大 buffer 可以通過 large_client_header_buffers 來設(shè)置,這個(gè) large_buffer 這一組 buffer,比如配置 48k,就是表示有四個(gè) 8k 大小的 buffer 可以用。注意,為了保存請求行或請求頭的完整性,一個(gè)完整的請求行或請求頭,需要放在一個(gè)連續(xù)的內(nèi)存里面,所以,一個(gè)完整的請求行或請求頭,只會保存在一個(gè) buffer 里面。這樣,如果請求行大于一個(gè) buffer 的大小,就會返回 414 錯(cuò)誤,如果一個(gè)請求頭大小大于一個(gè) buffer 大小,就會返回 400 錯(cuò)誤。在了解了這些參數(shù)的值,以及 Nginx 實(shí)際的做法之后,在應(yīng)用場景,我們就需要根據(jù)實(shí)際的需求來調(diào)整這些參數(shù),來優(yōu)化我們的程序了。

處理流程圖:

http://wiki.jikexueyuan.com/project/nginx/images/chapter-2-2.png" alt="" />

以上這些,就是 Nginx 中一個(gè) http 請求的生命周期了。我們再看看與請求相關(guān)的一些概念吧。

keepalive

當(dāng)然,在 Nginx 中,對于 http1.0 與 http1.1 也是支持長連接的。什么是長連接呢?我們知道,http 請求是基于 TCP 協(xié)議之上的,那么,當(dāng)客戶端在發(fā)起請求前,需要先與服務(wù)端建立 TCP 連接,而每一次的 TCP 連接是需要三次握手來確定的,如果客戶端與服務(wù)端之間網(wǎng)絡(luò)差一點(diǎn),這三次交互消費(fèi)的時(shí)間會比較多,而且三次交互也會帶來網(wǎng)絡(luò)流量。當(dāng)然,當(dāng)連接斷開后,也會有四次的交互,當(dāng)然對用戶體驗(yàn)來說就不重要了。而 http 請求是請求應(yīng)答式的,如果我們能知道每個(gè)請求頭與響應(yīng)體的長度,那么我們是可以在一個(gè)連接上面執(zhí)行多個(gè)請求的,這就是所謂的長連接,但前提條件是我們先得確定請求頭與響應(yīng)體的長度。對于請求來說,如果當(dāng)前請求需要有body,如 POST 請求,那么 Nginx 就需要客戶端在請求頭中指定 content-length 來表明 body 的大小,否則返回 400 錯(cuò)誤。也就是說,請求體的長度是確定的,那么響應(yīng)體的長度呢?先來看看 http 協(xié)議中關(guān)于響應(yīng) body 長度的確定:

  1. 對于 http1.0 協(xié)議來說,如果響應(yīng)頭中有 content-length 頭,則以 content-length 的長度就可以知道 body 的長度了,客戶端在接收 body 時(shí),就可以依照這個(gè)長度來接收數(shù)據(jù),接收完后,就表示這個(gè)請求完成了。而如果沒有 content-length 頭,則客戶端會一直接收數(shù)據(jù),直到服務(wù)端主動斷開連接,才表示 body 接收完了。

  2. 而對于 http1.1 協(xié)議來說,如果響應(yīng)頭中的 Transfer-encoding 為 chunked 傳輸,則表示 body 是流式輸出,body 會被分成多個(gè)塊,每塊的開始會標(biāo)識出當(dāng)前塊的長度,此時(shí),body 不需要通過長度來指定。如果是非 chunked 傳輸,而且有 content-length,則按照 content-length 來接收數(shù)據(jù)。否則,如果是非 chunked,并且沒有 content-length,則客戶端接收數(shù)據(jù),直到服務(wù)端主動斷開連接。

從上面,我們可以看到,除了 http1.0 不帶 content-length 以及 http1.1 非 chunked 不帶 content-length 外,body 的長度是可知的。此時(shí),當(dāng)服務(wù)端在輸出完 body 之后,會可以考慮使用長連接。能否使用長連接,也是有條件限制的。如果客戶端的請求頭中的 connection為close,則表示客戶端需要關(guān)掉長連接,如果為 keep-alive,則客戶端需要打開長連接,如果客戶端的請求中沒有 connection 這個(gè)頭,那么根據(jù)協(xié)議,如果是 http1.0,則默認(rèn)為 close,如果是 http1.1,則默認(rèn)為 keep-alive。如果結(jié)果為 keepalive,那么,Nginx 在輸出完響應(yīng)體后,會設(shè)置當(dāng)前連接的 keepalive 屬性,然后等待客戶端下一次請求。當(dāng)然,Nginx 不可能一直等待下去,如果客戶端一直不發(fā)數(shù)據(jù)過來,豈不是一直占用這個(gè)連接?所以當(dāng) Nginx 設(shè)置了 keepalive 等待下一次的請求時(shí),同時(shí)也會設(shè)置一個(gè)最大等待時(shí)間,這個(gè)時(shí)間是通過選項(xiàng) keepalive_timeout 來配置的,如果配置為 0,則表示關(guān)掉 keepalive,此時(shí),http 版本無論是 1.1 還是 1.0,客戶端的 connection 不管是 close 還是 keepalive,都會強(qiáng)制為 close。

如果服務(wù)端最后的決定是 keepalive 打開,那么在響應(yīng)的 http 頭里面,也會包含有 connection 頭域,其值是"Keep-Alive",否則就是"Close"。如果 connection 值為 close,那么在 Nginx 響應(yīng)完數(shù)據(jù)后,會主動關(guān)掉連接。所以,對于請求量比較大的 Nginx 來說,關(guān)掉 keepalive 最后會產(chǎn)生比較多的 time-wait 狀態(tài)的 socket。一般來說,當(dāng)客戶端的一次訪問,需要多次訪問同一個(gè) server 時(shí),打開 keepalive 的優(yōu)勢非常大,比如圖片服務(wù)器,通常一個(gè)網(wǎng)頁會包含很多個(gè)圖片。打開 keepalive 也會大量減少 time-wait 的數(shù)量。

pipe

在 http1.1 中,引入了一種新的特性,即 pipeline。那么什么是 pipeline 呢?pipeline 其實(shí)就是流水線作業(yè),它可以看作為 keepalive 的一種升華,因?yàn)?pipeline 也是基于長連接的,目的就是利用一個(gè)連接做多次請求。如果客戶端要提交多個(gè)請求,對于keepalive來說,那么第二個(gè)請求,必須要等到第一個(gè)請求的響應(yīng)接收完全后,才能發(fā)起,這和 TCP 的停止等待協(xié)議是一樣的,得到兩個(gè)響應(yīng)的時(shí)間至少為2*RTT。而對 pipeline 來說,客戶端不必等到第一個(gè)請求處理完后,就可以馬上發(fā)起第二個(gè)請求。得到兩個(gè)響應(yīng)的時(shí)間可能能夠達(dá)到1*RTT。Nginx 是直接支持 pipeline 的,但是,Nginx 對 pipeline 中的多個(gè)請求的處理卻不是并行的,依然是一個(gè)請求接一個(gè)請求的處理,只是在處理第一個(gè)請求的時(shí)候,客戶端就可以發(fā)起第二個(gè)請求。這樣,Nginx 利用 pipeline 減少了處理完一個(gè)請求后,等待第二個(gè)請求的請求頭數(shù)據(jù)的時(shí)間。其實(shí) Nginx 的做法很簡單,前面說到,Nginx 在讀取數(shù)據(jù)時(shí),會將讀取的數(shù)據(jù)放到一個(gè) buffer 里面,所以,如果 Nginx 在處理完前一個(gè)請求后,如果發(fā)現(xiàn) buffer 里面還有數(shù)據(jù),就認(rèn)為剩下的數(shù)據(jù)是下一個(gè)請求的開始,然后就接下來處理下一個(gè)請求,否則就設(shè)置 keepalive。

lingering_close

lingering_close,字面意思就是延遲關(guān)閉,也就是說,當(dāng) Nginx 要關(guān)閉連接時(shí),并非立即關(guān)閉連接,而是先關(guān)閉 tcp 連接的寫,再等待一段時(shí)間后再關(guān)掉連接的讀。為什么要這樣呢?我們先來看看這樣一個(gè)場景。Nginx 在接收客戶端的請求時(shí),可能由于客戶端或服務(wù)端出錯(cuò)了,要立即響應(yīng)錯(cuò)誤信息給客戶端,而 Nginx 在響應(yīng)錯(cuò)誤信息后,大分部情況下是需要關(guān)閉當(dāng)前連接。Nginx 執(zhí)行完 write()系統(tǒng)調(diào)用把錯(cuò)誤信息發(fā)送給客戶端,write()系統(tǒng)調(diào)用返回成功并不表示數(shù)據(jù)已經(jīng)發(fā)送到客戶端,有可能還在 tcp 連接的 write buffer 里。接著如果直接執(zhí)行 close()系統(tǒng)調(diào)用關(guān)閉 tcp 連接,內(nèi)核會首先檢查 tcp 的 read buffer 里有沒有客戶端發(fā)送過來的數(shù)據(jù)留在內(nèi)核態(tài)沒有被用戶態(tài)進(jìn)程讀取,如果有則發(fā)送給客戶端 RST 報(bào)文來關(guān)閉 tcp 連接丟棄 write buffer 里的數(shù)據(jù),如果沒有則等待 write buffer 里的數(shù)據(jù)發(fā)送完畢,然后再經(jīng)過正常的 4 次分手報(bào)文斷開連接。所以,當(dāng)在某些場景下出現(xiàn) tcp write buffer 里的數(shù)據(jù)在 write()系統(tǒng)調(diào)用之后到 close()系統(tǒng)調(diào)用執(zhí)行之前沒有發(fā)送完畢,且 tcp read buffer 里面還有數(shù)據(jù)沒有讀,close()系統(tǒng)調(diào)用會導(dǎo)致客戶端收到 RST 報(bào)文且不會拿到服務(wù)端發(fā)送過來的錯(cuò)誤信息數(shù)據(jù)。那客戶端肯定會想,這服務(wù)器好霸道,動不動就 reset 我的連接,連個(gè)錯(cuò)誤信息都沒有。

在上面這個(gè)場景中,我們可以看到,關(guān)鍵點(diǎn)是服務(wù)端給客戶端發(fā)送了 RST 包,導(dǎo)致自己發(fā)送的數(shù)據(jù)在客戶端忽略掉了。所以,解決問題的重點(diǎn)是,讓服務(wù)端別發(fā) RST 包。再想想,我們發(fā)送 RST 是因?yàn)槲覀冴P(guān)掉了連接,關(guān)掉連接是因?yàn)槲覀儾幌朐偬幚泶诉B接了,也不會有任何數(shù)據(jù)產(chǎn)生了。對于全雙工的 TCP 連接來說,我們只需要關(guān)掉寫就行了,讀可以繼續(xù)進(jìn)行,我們只需要丟掉讀到的任何數(shù)據(jù)就行了,這樣的話,當(dāng)我們關(guān)掉連接后,客戶端再發(fā)過來的數(shù)據(jù),就不會再收到 RST 了。當(dāng)然最終我們還是需要關(guān)掉這個(gè)讀端的,所以我們會設(shè)置一個(gè)超時(shí)時(shí)間,在這個(gè)時(shí)間過后,就關(guān)掉讀,客戶端再發(fā)送數(shù)據(jù)來就不管了,作為服務(wù)端我會認(rèn)為,都這么長時(shí)間了,發(fā)給你的錯(cuò)誤信息也應(yīng)該讀到了,再慢就不關(guān)我事了,要怪就怪你 RP 不好了。當(dāng)然,正常的客戶端,在讀取到數(shù)據(jù)后,會關(guān)掉連接,此時(shí)服務(wù)端就會在超時(shí)時(shí)間內(nèi)關(guān)掉讀端。這些正是 lingering_close 所做的事情。協(xié)議棧提供 SO_LINGER 這個(gè)選項(xiàng),它的一種配置情況就是來處理 lingering_close 的情況的,不過 Nginx 是自己實(shí)現(xiàn)的 lingering_close。lingering_close 存在的意義就是來讀取剩下的客戶端發(fā)來的數(shù)據(jù),所以 Nginx 會有一個(gè)讀超時(shí)時(shí)間,通過 lingering_timeout 選項(xiàng)來設(shè)置,如果在 lingering_timeout 時(shí)間內(nèi)還沒有收到數(shù)據(jù),則直接關(guān)掉連接。Nginx 還支持設(shè)置一個(gè)總的讀取時(shí)間,通過 lingering_time 來設(shè)置,這個(gè)時(shí)間也就是 Nginx 在關(guān)閉寫之后,保留 socket 的時(shí)間,客戶端需要在這個(gè)時(shí)間內(nèi)發(fā)送完所有的數(shù)據(jù),否則 Nginx 在這個(gè)時(shí)間過后,會直接關(guān)掉連接。當(dāng)然,Nginx 是支持配置是否打開 lingering_close 選項(xiàng)的,通過 lingering_close 選項(xiàng)來配置。

那么,我們在實(shí)際應(yīng)用中,是否應(yīng)該打開 lingering_close 呢?這個(gè)就沒有固定的推薦值了,如 Maxim Dounin所說,lingering_close 的主要作用是保持更好的客戶端兼容性,但是卻需要消耗更多的額外資源(比如連接會一直占著)。

這節(jié),我們介紹了 Nginx 中,連接與請求的基本概念,下節(jié),我們講基本的數(shù)據(jù)結(jié)構(gòu)。