nginx的請求接收流程(一)


今年我們組計划寫一本nginx模塊開發以及原理解析方面的書,整本書是以open book的形式在網上會定時的更新,網址為http://tengine.taobao.org/book/index.html。本書分析的nginx源碼版本為1.2.0,環境為linux,事件處理模型為epoll,大部分分析流程都基於以上假設。我會負責其中一些章節的編寫,所以打算在這里寫一系列我負責章節內容相關的文章(主要包括nginx各phase模塊的開發,nginx請求的處理流程等)。本篇文章主要會介紹nginx中請求的接收流程,包括請求頭的解析和請求體的讀取流程。

 

首先介紹一下rfc2616中定義的http請求基本格式:

 

[cpp]  view plain copy
 
  1. <span style="font-size:18px;">Request = Request-Line   
  2.       *(( general-header           
  3.            | request-header            
  4.            | entity-header ) CRLF)    
  5.           CRLF  
  6.           [ message-body ]   </span>  

 

 

第一行是請求行(request line),用來說明請求方法,要訪問的資源以及所使用的HTTP版本:

 

[cpp]  view plain copy
 
  1. <span style="font-size:18px;">Request-Line   = Method SP Request-URI SP HTTP-Version CRLF</span>  

 

 

請求方法(Method)的定義如下,其中最常用的是GET,POST方法:

 

[cpp]  view plain copy
 
  1. <span style="font-size:18px;">Method = "OPTIONS"   
  2. "GET"   
  3. "HEAD"   
  4. "POST"   
  5. "PUT"   
  6. "DELETE"   
  7. "TRACE"   
  8. "CONNECT"   
  9. | extension-method   
  10. extension-method = token</span>  

 

 

要訪問的資源由統一資源地位符URI(Uniform Resource Identifier)確定,它的一個比較通用的組成格式(rfc2396)如下:

 

[cpp]  view plain copy
 
  1. <span style="font-size:18px;"><scheme>://<authority><path>?<query> </span>  

 

 

一般來說根據請求方法(Method)的不同,請求URI的格式會有所不同,通常只需寫出path和query部分。

http版本(version)定義如下,現在用的一般為1.0和1.1版本:

 

[cpp]  view plain copy
 
  1. HTTP/<major>.<minor>  

 

 

請求行的下一行則是請求頭,rfc2616中定義了3種不同類型的請求頭,分別為general-header,request-header和entity-header,每種類型rfc中都定義了一些通用的頭,其中entity-header類型可以包含自定義的頭。


現在開始介紹nginx中請求頭的解析,nginx的請求處理流程中,會涉及到2個非常重要的數據結構,ngx_connection_t和ngx_http_request_t,分別用來表示連接和請求,這2個數據結構在本書的前篇中已經做了比較詳細的介紹,沒有印象的讀者可以翻回去復習一下,整個請求處理流程從頭到尾,對應着這2個數據結構的分配,初始化,使用,重用和銷毀。


nginx在初始化階段,具體是在init process階段的ngx_event_process_init函數中會為每一個監聽套接字分配一個連接結構(ngx_connection_t),並將該連接結構的讀事件成員(read)的事件處理函數設置為ngx_event_accept,並且如果沒有使用accept互斥鎖的話,在這個函數中會將該讀事件掛載到nginx的事件處理模型上(poll或者epoll等),反之則會等到init process階段結束,在工作進程的事件處理循環中,某個進程搶到了accept鎖才能掛載該讀事件。

[cpp]  view plain copy
 
  1. <span style="font-size:18px;">static ngx_int_t  
  2. ngx_event_process_init(ngx_cycle_t *cycle)  
  3. {  
  4.     ...  
  5.   
  6.   
  7.     /* 初始化用來管理所有定時器的紅黑樹 */  
  8.     if (ngx_event_timer_init(cycle->log) == NGX_ERROR) {  
  9.         return NGX_ERROR;  
  10.     }  
  11.     /* 初始化事件模型 */  
  12.     for (m = 0; ngx_modules[m]; m++) {  
  13.         if (ngx_modules[m]->type != NGX_EVENT_MODULE) {  
  14.             continue;  
  15.         }  
  16.   
  17.   
  18.         if (ngx_modules[m]->ctx_index != ecf->use) {  
  19.             continue;  
  20.         }  
  21.   
  22.   
  23.         module = ngx_modules[m]->ctx;  
  24.   
  25.   
  26.         if (module->actions.init(cycle, ngx_timer_resolution) != NGX_OK) {  
  27.             /* fatal */  
  28.             exit(2);  
  29.         }  
  30.   
  31.   
  32.         break;  
  33.     }  
  34.   
  35.   
  36.     ...  
  37.   
  38.   
  39.     /* for each listening socket */  
  40.     /* 為每個監聽套接字分配一個連接結構 */  
  41.     ls = cycle->listening.elts;  
  42.     for (i = 0; i < cycle->listening.nelts; i++) {  
  43.   
  44.   
  45.         c = ngx_get_connection(ls[i].fd, cycle->log);  
  46.   
  47.   
  48.         if (c == NULL) {  
  49.             return NGX_ERROR;  
  50.         }  
  51.   
  52.   
  53.         c->log = &ls[i].log;  
  54.   
  55.   
  56.         c->listening = &ls[i];  
  57.         ls[i].connection = c;  
  58.   
  59.   
  60.         rev = c->read;  
  61.   
  62.   
  63.         rev->log = c->log;  
  64.         /* 標識此讀事件為新請求連接事件 */  
  65.         rev->accept = 1;  
  66.   
  67.   
  68.         ...  
  69.   
  70.   
  71. #if (NGX_WIN32)  
  72.   
  73.   
  74.         /* windows環境下不做分析,但原理類似 */  
  75.   
  76.   
  77. #else  
  78.         /* 將讀事件結構的處理函數設置為ngx_event_accept */  
  79.         rev->handler = ngx_event_accept;  
  80.         /* 如果使用accept鎖的話,要在后面搶到鎖才能將監聽句柄掛載上事件處理模型上 */  
  81.         if (ngx_use_accept_mutex) {  
  82.             continue;  
  83.         }  
  84.         /* 否則,將該監聽句柄直接掛載上事件處理模型 */  
  85.         if (ngx_event_flags & NGX_USE_RTSIG_EVENT) {  
  86.             if (ngx_add_conn(c) == NGX_ERROR) {  
  87.                 return NGX_ERROR;  
  88.             }  
  89.   
  90.   
  91.         } else {  
  92.             if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {  
  93.                 return NGX_ERROR;  
  94.             }  
  95.         }  
  96.   
  97.   
  98. #endif  
  99.   
  100.   
  101.     }  
  102.   
  103.   
  104.     return NGX_OK;  
  105. }</span>  


當一個工作進程在某個時刻將監聽事件掛載上事件處理模型之后,nginx就可以正式的接收並處理客戶端過來的請求了。這時如果有一個用戶在瀏覽器的地址欄內輸入一個域名,並且域名解析服務器將該域名解析到一台由nginx監聽的服務器上,nginx的事件處理模型接收到這個讀事件之后,會速度交由之前注冊好的事件處理函數ngx_event_accept來處理。


在ngx_event_accept函數中,nginx調用accept函數,從已連接隊列得到一個連接以及對應的套接字,接着分配一個連接結構(ngx_connection_t),並將新得到的套接字保存在該連接結構中,這里還會做一些基本的連接初始化工作:
首先給該連接分配一個內存池,初始大小默認為256字節,可通過connection_pool_size指令設置;
分配日志結構,並保存在其中,以便后續的日志系統使用;
初始化連接相應的io收發函數,具體的io收發函數和使用的事件模型及操作系統相關;
分配一個套接口地址(sockaddr),並將accept得到的對端地址拷貝在其中,保存在sockaddr字段;
將本地套接口地址保存在local_sockaddr字段,因為這個值是從監聽結構ngx_listening_t中可得,而監聽結構中保存的只是配置文件中設置的監聽地址,但是配置的監聽地址可能是通配符*,即監聽在所有的地址上,所以連接中保存的這個值最終可能還會變動,會被確定為真正的接收地址;
將連接的寫事件設置為已就緒,即設置ready為1,nginx默認連接第一次為可寫;
如果監聽套接字設置了TCP_DEFER_ACCEPT屬性,則表示該連接上已經有數據包過來,於是設置讀事件為就緒;
將sockaddr字段保存的對端地址格式化為可讀字符串,並保存在addr_text字段;
最后調用ngx_http_init_connection函數初始化該連接結構的其他部分。


ngx_http_init_connection函數最重要的工作是初始化讀寫事件的處理函數:將該連接結構的寫事件的處理函數設置為ngx_http_empty_handler,這個事件處理函數不會做任何操作,實際上nginx默認連接第一次可寫,不會掛載寫事件,如果有數據需要發送,nginx會直接寫到這個連接,只有在發生一次寫不完的情況下,才會掛載寫事件到事件模型上,並設置真正的寫事件處理函數,這里后面的章節還會做詳細介紹;讀事件的處理函數設置為ngx_http_init_request,此時如果該連接上已經有數據過來(設置了deferred accept),則會直接調用ngx_http_init_request函數來處理該請求,反之則設置一個定時器並在事件處理模型上掛載一個讀事件,等待數據到來或者超時。當然這里不管是已經有數據到來,或者需要等待數據到來,又或者等待超時,最終都會進入讀事件的處理函數-ngx_http_init_request。
 

 


ngx_http_init_request函數主要工作即是初始化請求,由於它是一個事件處理函數,它只有唯一一個ngx_event_t *類型的參數,ngx_event_t 結構在nginx中表示一個事件,事件處理的上下文類似於一個中斷處理的上下文,為了在這個上下文得到相關的信息,nginx中一般會將連接結構的引用保存在事件結構的data字段,請求結構的引用則保存在連接結構的data字段,這樣在事件處理函數中可以方便的得到對應的連接結構和請求結構。進入函數內部看一下,首先判斷該事件是否是超時事件,如果是的話直接關閉連接並返回;反之則是指之前accept的連接上有請求過來需要處理,ngx_http_init_request函數首先在連接的內存池中為該請求分配一個ngx_http_request_t結構,這個結構將用來保存該請求所有的信息。分配完之后,這個結構的引用會被包存在連接的hc成員的request字段,以便於在長連接或pipelined請求中復用該請求結構。在這個函數中,nginx根據該請求的接收端口和地址找到一個默認虛擬服務器配置(listen指令的default_server屬性用來標識一個默認虛擬服務器,否則監聽在相同端口和地址的多個虛擬服務器,其中第一個定義的則為默認),因為在nginx配置文件中可以設置多個監聽在不同端口和地址的虛擬服務器(每個server塊對應一個虛擬服務器),另外還根據域名(server_name指令可以配置該虛擬服務器對應的域名)來區分監聽在相同端口和地址的虛擬服務器,每個虛擬服務器可以擁有不同的配置內容,而這些配置內容決定了nginx在接收到一個請求之后如何處理該請求。找到之后,相應的配置被保存在該請求對應的ngx_http_request_t結構中。注意這里根據端口和地址找到的默認配置只是臨時使用一下,最終nginx會根據域名找到真正的虛擬服務器配置,隨后的初始化工作還包括:

將連接的讀事件的處理函數設置為ngx_http_process_request_line函數,這個函數用來解析請求行,將請求的read_event_handler設置為ngx_http_block_reading函數,這個函數實際上什么都不做(當然在事件模型設置為水平觸發時,唯一做的事情就是將事件從事件模型監聽列表中刪除,防止該事件一直被觸發),后面會說到這里為什么會將read_event_handler設置為此函數;
為這個請求分配一個緩沖區用來保存它的請求頭,地址保存在header_in字段,默認大小為1024個字節,可以使用client_header_buffer_size指令修改,這里需要注意一下,nginx用來保存請求頭的緩沖區是在該請求所在連接的內存池中分配,而且會將地址保存一份在連接的buffer字段中,這樣做的目的也是為了給該連接的下一次請求重用這個緩沖區,另外如果客戶端發過來的請求頭大於1024個字節,nginx會重新分配更大的緩存區,默認用於大請求的頭的緩沖區最大為8K,最多4個,這2個值可以用large_client_header_buffers指令設置,后面還會說到請求行和一個請求頭都不能超過一個最大緩沖區的大小;
同樣的nginx會為這個請求分配一個內存池,后續所有與該請求相關的內存分配一般都會使用該內存池,默認大小為4096個字節,可以使用request_pool_size指令修改;
為這個請求分配響應頭鏈表,初始大小為20;
創建所有模塊的上下文ctx指針數組,變量數據;
將該請求的main字段設置為它本身,表示這是一個主請求,nginx中對應的還有子請求概念,后面的章節會做詳細的介紹;
將該請求的count字段設置為1,count字段表示請求的引用計數;
將當前時間保持在start_sec和start_msec字段,這個時間是該請求的起始時刻,將被用來計算一個請求的處理時間(request time),nginx使用的這個起始點和apache略有差別,nginx中請求的起始點是接收到客戶端的第一個數據包開始,而apache則是接收到客戶端的整個request line后開始算起;
初始化請求的其他字段,比如將uri_changes設置為11,表示最多可以將該請求的uri改寫10次,subrequests被設置為201,表示一個請求最多可以發起200個子請求;
做完所有這些初始化工作之后,ngx_http_init_request函數會調用讀事件的處理函數來真正的解析客戶端發過來的數據,也就是會進入ngx_http_process_request_line函數中處理。


ngx_http_process_request_line函數的主要作用即是解析請求行,同樣由於涉及到網絡IO操作,即使是很短的一行請求行可能也不能被一次讀完,所以在之前的ngx_http_init_request函數中,ngx_http_process_request_line函數被設置為讀事件的處理函數,它也只擁有一個唯一的ngx_event_t *類型參數,並且在函數的開頭,同樣需要判斷是否是超時事件,如果是的話,則關閉這個請求和連接;否則開始正常的解析流程。先調用ngx_http_read_request_header函數讀取數據。


由於可能多次進入ngx_http_process_request_line函數,ngx_http_read_request_header函數首先檢查請求的header_in指向的緩沖區內是否有數據,有的話直接返回;否則從連接讀取數據並保存在請求的header_in指向的緩存區,而且只要緩沖區有空間的話,會一次盡可能多的讀數據,讀到多少返回多少;如果客戶端暫時沒有發任何數據過來,並返回NGX_AGAIN,返回之前會做2件事情:1,設置一個定時器,時長默認為60s,可以通過指令client_header_timeout設置,如果定時事件到達之前沒有任何可讀事件,nginx將會關閉此請求;2,調用ngx_handle_read_event函數處理一下讀事件-如果該連接尚未在事件處理模型上掛載讀事件,則將其掛載上;如果客戶端提前關閉了連接或者讀取數據發生了其他錯誤,則給客戶端返回一個400錯誤(當然這里並不保證客戶端能夠接收到響應數據,因為客戶端可能都已經關閉了連接),最后函數返回NGX_ERROR;


如果ngx_http_read_request_header函數正常的讀取到了數據,ngx_http_process_request_line函數將調用ngx_http_parse_request_line函數來解析,這個函數根據http協議規范中對請求行的定義實現了一個有限狀態機,經過這個狀態機,nginx會記錄請求行中的請求方法(Method),請求uri以及http協議版本在緩沖區中的起始位置,在解析過程中還會記錄一些其他有用的信息,以便后面的處理過程中使用。如果解析請求行的過程中沒有產生任何問題,該函數會返回NGX_OK;如果請求行不滿足協議規范,該函數會立即終止解析過程,並返回相應錯誤號;如果緩沖區數據不夠,該函數返回NGX_AGAIN。在整個解析http請求的狀態機中始終遵循着兩條重要的原則:減少內存拷貝和回溯。內存拷貝是一個相對比較昂貴的操作,大量的內存拷貝會帶來較低的運行時效率。nginx在需要做內存拷貝的地方盡量只拷貝內存的起始和結束地址而不是內存本身,這樣做的話僅僅只需要兩個賦值操作而已,大大降低了開銷,當然這樣帶來的影響是后續的操作不能修改內存本身,如果修改的話,會影響到所有引用到該內存區間的地方,所以必須很小心的管理,必要的時候需要拷貝一份。這里不得不提到nginx中最能體現這一思想的數據結構,ngx_buf_t,它用來表示nginx中的緩存,在很多情況下,只需要將一塊內存的起始地址和結束地址分別保存在它的pos和last成員中,再將它的memory標志置1,即可表示一塊不能修改的內存區間,在另外的需要一塊能夠修改的緩存的情形中,則必須分配一塊所需大小的內存並保存其起始地址,再將ngx_bug_t的temprary標志置1,表示這是一塊能夠被修改的內存區域。


再回到ngx_http_process_request_line函數中,如果ngx_http_parse_request_line函數返回了錯誤,則直接給客戶端返回400錯誤;
如果返回NGX_AGAIN,則需要判斷一下是否是由於緩沖區空間不夠,還是已讀數據不夠。如果是緩沖區大小不夠了,nginx會調用ngx_http_alloc_large_header_buffer函數來分配另一塊大緩沖區,如果大緩沖區還不夠裝下整個請求行,nginx則會返回414錯誤給客戶端,否則分配了更大的緩沖區並拷貝之前的數據之后,繼續調用ngx_http_read_request_header函數讀取數據來進入請求行自動機處理,直到請求行解析結束;
如果返回了NGX_OK,則表示請求行被正確的解析出來了,這時先記錄好請求行的起始地址以及長度,並將請求uri的path和參數部分保存在請求結構的uri字段,請求方法起始位置和長度保存在method_name字段,http版本起始位置和長度記錄在http_protocol字段。還要從uri中解析出參數以及請求資源的拓展名,分別保存在args和exten字段。接下來將要解析請求頭,我將在下一篇文章中接着介紹。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM