天天看點

《深入了解Nginx:子產品開發與架構解析》一3.6 處理使用者請求

本節介紹如何處理一個實際的http請求。回顧一下上文,在出現mytest配置項時,ngx_http_mytest方法會被調用,這時将ngx_http_core_loc_conf_t結構的handler成員指定為ngx_http_mytest_handler, 另外,http架構在接收完http請求的頭部後,會調用handler指向的方法。下面看一下handler成員的原型ngx_http_handler_pt:

typedef ngx_int_t (ngx_http_handler_pt)(ngx_http_request_t r);

從上面這段代碼可以看出,實際處理請求的方法ngx_http_mytest_handler将接收一個ngx_http_request_t類型的參數r,傳回一個ngx_int_t(參見3.2.1節)類型的結果。下面先探讨一下ngx_http_mytest_handler方法可以傳回什麼,再看一下參數r包含了哪些nginx已經解析完的使用者請求資訊。

這個傳回值可以是http中響應包的傳回碼,其中包括了http架構已經在/src/http/ngx_http_request.h檔案中定義好的宏,如下所示。

注意 以上傳回值除了rfc2616規範中定義的傳回碼外,還有nginx自身定義的http傳回碼。例如,ngx_http_close就是用于要求http架構直接關閉使用者連接配接的。

在ngx_http_mytest_handler的傳回值中,如果是正常的http傳回碼,nginx就會按照規範構造合法的響應包發送給使用者。例如,假設對于put方法暫不支援,那麼,在處理方法中發現方法名是put時,傳回ngx_http_not_allowed,這樣nginx也就會構造類似下面的響應包給使用者。

在處理方法中除了傳回http響應碼外,還可以傳回nginx全局定義的幾個錯誤碼,包括:

這些錯誤碼對于nginx自身提供的大部分方法來說都是通用的。是以,當我們最後調用ngx_http_output_filter(參見3.7節)向使用者發送響應包時,可以将ngx_http_output_filter的傳回值作為ngx_http_mytest_handler方法的傳回值使用。例如:

當然,直接傳回以上7個通用值也是可以的。在不同的場景下,這7個通用傳回值代表的含義不盡相同。在mytest的例子中,http架構在ngx_http_content_phase階段調用ngx_http_mytest_handler後,會将ngx_http_mytest_handler的傳回值作為參數傳給ngx_http_finalize_request方法,如下所示。

上面的r->content_handler會指向ngx_http_mytest_handler處理方法。也就是說,事實上ngx_http_finalize_request決定了ngx_http_mytest_handler如何起作用。本章不探讨ngx_http_finalize_request的實作(詳見11.10節),隻簡單地說明一下4個通用傳回碼,另外,在11.10節中介紹這4個傳回碼引發的nginx一系列動作。

ngx_ok:表示成功。nginx将會繼續執行該請求的後續動作(如執行subrequest或撤銷這個請求)。

ngx_declined:繼續在ngx_http_content_phase階段尋找下一個對于該請求感興趣的http子產品來再次處理這個請求。

ngx_done:表示到此為止,同時http架構将暫時不再繼續執行這個請求的後續部分。事實上,這時會檢查連接配接的類型,如果是keepalive類型的使用者請求,就會保持住http連接配接,然後把控制權交給nginx。這個傳回碼很有用,考慮以下場景:在一個請求中我們必須通路一個耗時極長的操作(比如某個網絡調用),這樣會阻塞住nginx,又因為我們沒有把控制權交還給nginx,而是在ngx_http_mytest_handler中讓nginx worker程序休眠了(如等待網絡的回包),是以,這就會導緻nginx出現性能問題,該程序上的其他使用者請求也得不到響應。可如果我們把這個耗時極長的操作分為上下兩個部分(就像linux核心中對中斷處理的劃分),上半部分和下半部分都是無阻塞的(耗時很少的操作),這樣,在ngx_http_mytest_handler進入時調用上半部分,然後傳回ngx_done,把控制交還給nginx,進而讓nginx繼續處理其他請求。在下半部分被觸發時(這裡不探讨具體的實作方式,事實上使用upstream方式做反向代理時用的就是這種思想),再回調下半部分處理方法,這樣就可以保證nginx的高性能特性了。如果需要徹底了解ngx_done的意義,那麼必須學習第11章内容,其中還涉及請求的引用計數内容。

ngx_error:表示錯誤。這時會調用ngx_http_terminate_request終止請求。如果還有post子請求,那麼将會在執行完post請求後再終止本次請求。

請求的所有資訊(如方法、uri、協定版本号和頭部等)都可以在傳入的ngx_http_request_t類型參數r中取得。ngx_http_request_t結構體的内容很多,本節不會探讨ngx_http_request_t中所有成員的意義(ngx_http_request_t結構體中的許多成員隻有http架構才感興趣,在11.3.1節會更詳細的說明),隻介紹一下擷取uri和參數的方法,這非常簡單,因為nginx提供了多種方法得到這些資訊。下面先介紹相關成員的定義。

在對一個使用者請求行進行解析時,可以得到下列4類資訊。

(1)方法名

method的類型是ngx_uint_t(無符号整型),它是nginx忽略大小寫等情形時解析完使用者請求後得到的方法類型,其取值範圍如下所示。

當需要了解使用者請求中的http方法時,應該使用r->method這個整型成員與以上15個宏進行比較,這樣速度是最快的(如果使用method_name成員與字元串做比較,那麼效率會差很多),大部分情況下推薦使用這種方式。除此之外,還可以用method_name取得使用者請求中的方法名字元串,或者聯合request_start與method_end指針取得方法名。method_name是ngx_str_t類型,按照3.2.2節中介紹的方法使用即可。

request_start與method_end的用法也很簡單,其中request_start指向使用者請求的首位址,同時也是方法名的位址,method_end指向方法名的最後一個字元(注意,這點與其他xxx_end指針不同)。擷取方法名時可以從request_start開始向後周遊,直到位址與method_end相同為止,這段記憶體存儲着方法名。

注意 nginx中對記憶體的控制相當嚴格,為了避免不必要的記憶體開銷,許多需要用到的成員都不是重新配置設定記憶體後存儲的,而是直接指向使用者請求中的相應位址。例如,method_name.data、request_start這兩個指針實際指向的都是同一個位址。而且,因為它們是簡單的記憶體指針,不是指向字元串的指針,是以,在大部分情況下,都不能将這些u_char*指針當做字元串使用。

(2)uri

ngx_str_t類型的uri成員指向使用者請求中的uri。同理,u_char類型的uri_start和uri_end也與request_start、method_end的用法相似,唯一不同的是,method_end指向方法名的最後一個字元,而uri_end指向uri結束後的下一個位址,也就是最後一個字元的下一個字元位址(http架構的行為),這是大部分u_char類型指針對“xxx_start”和“xxx_end”變量的用法。

ngx_str_t類型的extern成員指向使用者請求的檔案擴充名。例如,在通路“get /a.txt http/1.1”時,extern的值是{len = 3, data = "txt"},而在通路“get /a http/1.1”時,extern的值為空,也就是{len = 0, data = 0x0}。

uri_ext指針指向的位址與extern.data相同。

unparsed_uri表示沒有進行url解碼的原始請求。例如,當uri為“/a b”時,unparsed_uri是“/a%20b”(空格字元做完編碼後是%20)。

(3)url參數

arg指向使用者請求中的url參數。

args_start指向url參數的起始位址,配合uri_end使用也可以獲得url參數。

(4)協定版本

http_protocol指向使用者請求中http的起始位址。

http_version是nginx解析過的協定版本,它的取值範圍如下:

建議使用http_version分析http的協定版本。

最後,使用request_start和request_end可以擷取原始的使用者請求行。

在ngx_http_request_t* r中就可以取到請求中的http頭部,比如使用下面的成員:

其中,header_in指向nginx收到的未經解析的http頭部,這裡暫不關注它(在第11章中可以看到,header_in就是接收http頭部的緩沖區)。ngx_http_headers_in_t 類型的headers_in則存儲已經解析過的http頭部。下面介紹ngx_http_headers_in_t結構體中的成員。

typedef struct {

/所有解析過的http頭部都在headers連結清單中,可以使用3.2.3節中介紹的周遊連結清單的方法來擷取所有的http頭部。注意,這裡headers連結清單的每一個元素都是3.2.4節介紹過的ngx_table_elt_t成員/

/以下每個ngx_table_elt_t成員都是rfc1616規範中定義的http頭部, 它們實際都指向headers連結清單中的相應成員。注意,當它們為null空指針時,表示沒有解析到相應的http頭部/

/user和passwd是隻有ngx_http_auth_basic_module才會用到的成員,這裡可以忽略/

/cookies是以ngx_array_t數組存儲的,本章先不介紹這個資料結構,感興趣的話可以直接跳到7.3節了解ngx_array_t的相關用法/

/http連接配接類型,它的取值範圍是0、ngx_http_connection_close或者ngx_http_connection_keep_alive/

/以下7個标志位是http架構根據浏覽器傳來的“useragent”頭部,它們可用來判斷浏覽器的類型,值為1時表示是相應的浏覽器發來的請求,值為0時則相反/

擷取http頭部時,直接使用r->headers_in的相應成員就可以了。這裡舉例說明一下如何通過周遊headers連結清單擷取非rfc2616标準的http頭部,讀者可以先回顧一下ngx_list_t連結清單和ngx_table_elt_t結構體的用法。前面3.2.3節中已經介紹過,headers是一個ngx_list_t連結清單,它存儲着解析過的所有http頭部,連結清單中的元素都是ngx_table_elt_t類型。下面嘗試在一個使用者請求中找到“rpc-description”頭部,首先判斷其值是否為“uploadfile”,再決定後續的伺服器行為,代碼如下。

//開始周遊連結清單

/判斷目前的頭部是否是“rpc-description”。如果想要忽略大小寫,則應該先用header[i].lowcase_key代替header[i].key.data,然後比較字元串/

對于常見的http頭部,直接擷取r->headers_in中已經由http架構解析過的成員即可,而對于不常見的http頭部,需要周遊r->headers_in.headers連結清單才能獲得。

http包體的長度有可能非常大,如果試圖一次性調用并讀取完所有的包體,那麼多半會阻塞nginx程序。http架構提供了一種方法來異步地接收包體:

ngx_int_t ngx_http_read_client_request_body(ngx_http_request_t *r, ngx_http_client_body_handler_pt post_handler);

ngx_http_read_client_request_body是一個異步方法,調用它隻是說明要求nginx開始接收請求的包體,并不表示是否已經接收完,當接收完所有的包體内容後,post_handler指向的回調方法會被調用。是以,即使在調用了ngx_http_read_client_request_body方法後它已經傳回,也無法确定這時是否已經調用過post_handler指向的方法。換句話說,ngx_http_read_client_request_body傳回時既有可能已經接收完請求中所有的包體(假如包體的長度很小),也有可能還沒開始接收包體。如果ngx_http_read_client_request_body是在ngx_http_mytest_handler處理方法中調用的,那麼後者一般要傳回ngx_done,因為下一步就是将它的傳回值作為參數傳給ngx_http_finalize_request。ngx_done的意義在3.6.1節中已經介紹過,這裡不再贅述。

下面看一下包體接收完畢後的回調方法原型ngx_http_client_body_handler_pt是如何定義的:

其中,有參數ngx_http_request_t r,這個請求的資訊都可以從r中獲得。這樣可以定義一個方法void func(ngx_http_request_t r),在nginx接收完包體`

時調用它,另外,後續的流程也都會寫在這個方法中,例如:

注意 ngx_http_mytest_body_handler的傳回類型是void,nginx不會根據傳回值做一些收尾工作,是以,我們在該方法裡處理完請求時必須要主動調用ngx_http_finalize_request方法來結束請求。

接收包體時可以這樣寫:

nginx異步接收http請求的包體的内容将在11.8節中詳述。

如果不想處理請求中的包體,那麼可以調用ngx_http_discard_request_body方法将接收自用戶端的http包體丢棄掉。例如:

ngx_int_t rc = ngx_http_discard_request_body(r);

ngx_http_discard_request_body隻是丢棄包體,不處理包體不就行了嗎?何必還要調用ngx_http_discard_request_body方法呢?其實這一步非常有意義,因為有些用戶端可能會一直試圖發送包體,而如果http子產品不接收發來的tcp流,有可能造成用戶端發送逾時。

接收完請求的包體後,可以在r->request_body->temp_file->file中擷取臨時檔案(假定将r->request_body_in_file_only标志位設為1,那就一定可以在這個變量擷取到包體。更複雜的接收包體的方式本節暫不讨論)。file是一個ngx_file_t類型,在3.8節會詳細介紹它的用法。這裡,我們可以從r->request_body->temp_file->file.name中擷取nginx接收到的請求包體所在檔案的名稱(包括路徑)。

繼續閱讀