天天看點

《深入了解Nginx:子產品開發與架構解析》一3.2 準備工作

nginx子產品需要使用c(或者c++)語言編寫代碼來實作,每個子產品都要有自己的名字。按照nginx約定俗成的命名規則,我們把第一個http子產品命名為ngx_http_mytest_module。由于第一個子產品非常簡單,一個c源檔案就可以完成,是以這裡按照官方慣例,将唯一的源代碼檔案命名為ngx_http_mytest_module.c。

實際上,我們還需要定義一個名稱,以便在編譯前的configure指令執行時顯示是否執行成功(即configure腳本執行時的ngx_addon_name變量)。為友善了解,仍然使用同一個子產品名來表示,如ngx_http_mytest_module。

為了讓http子產品正常工作,首先需要把它編譯進nginx(3.3節會探讨編譯新增子產品的兩種方式)。其次需要設定子產品如何在運作中生效,比如在圖3-1描述的典型方式中,配置檔案中的location塊決定了比對某種uri的請求将會由相應的http子產品處理,是以,運作時http架構會在接收完畢http請求的頭部後,将請求的uri與配置檔案中的所有location進行比對(事實上會優先比對虛拟主機,第11 章會詳細說明該流程),比對後再根據location {}内的配置項選擇http子產品來調用。這是一種最典型的http子產品調用方式。3.4節将解釋http子產品定義嵌入方式時用到的資料結構,3.5節将定義我們的第一個http子產品,3.6節中介紹如何使用上述子產品調用方式來處理請求。

既然有典型的調用方式,自然也有非典型的調用方式,比如ngx_http_access_module子產品,它是根據ip位址決定某個用戶端是否可以通路服務的,是以,這個子產品需要在ngx_http_access_phase階段(在第10章中會詳述http架構定義的11個階段)生效,它會比本章介紹的mytest子產品更早地介入請求的進行中,同時它的流程與圖3-1中的不同,它可以對所有請求産生作用。也就是說,任何http請求都會調用ngx_http_access_module子產品處理,隻是該子產品會根據它感興趣的配置項及所在的配置塊來決定行為方式,這與mytest子產品不同,在mytest子產品中,隻有在配置了location /uri {mytest;}後,http架構才會在某個請求比對了/uri後調用它處理請求。如果某個比對了uri請求的location中沒有配置mytest配置項,mytest子產品依然是不會被調用的。

為了做到跨平台,nginx定義、封裝了一些基本的資料結構。由于nginx對記憶體配置設定比較“吝啬”(隻有保證低記憶體消耗,才可能實作十萬甚至百萬級别的同時并發連接配接數),是以這些nginx資料結構天生都是盡可能少占用記憶體。下面介紹本章中将要用到的nginx定義的幾個基本資料結構和方法,在第7章還會介紹一些複雜的容器,讀者可以從中體會到如何才能有效地利用記憶體。

nginx使用ngx_int_t封裝有符号整型,使用ngx_uint_t封裝無符号整型。nginx各子產品的變量定義都是如此使用的,建議讀者沿用nginx的習慣,以此替代int和unsinged int。

在linux平台下,nginx對ngx_int_t和ngx_uint_t的定義如下:

在nginx的領域中,ngx_str_t結構就是字元串。ngx_str_t的定義如下:

ngx_str_t隻有兩個成員,其中data指針指向字元串起始位址,len表示字元串的有效長度。注意,ngx_str_t的data成員指向的并不是普通的字元串,因為這段字元串未必會以'0'作為結尾,是以使用時必須根據長度len來使用data成員。例如,在3.7.2節中,我們會看到r->method_name就是一個ngx_str_t類型的變量,比較method_name時必須如下這樣使用:

這裡,ngx_strncmp其實就是strncmp函數,為了跨平台nginx習慣性地對其進行了名稱上的封裝,下面看一下它的定義:

任何試圖将ngx_str_t的data成員當做字元串來使用的情況,都可能導緻記憶體越界!nginx使用ngx_str_t可以有效地降低記憶體使用量。例如,使用者請求“get /test?a=1 http/1.1rn”存儲到記憶體位址0x1d0b0110上,這時隻需要把r->method_name設定為{len = 3, data = 0x1d0b0110}就可以表示方法名“get”,而不需要單獨為method_name再配置設定記憶體備援的存儲字元串。

ngx_list_t是nginx封裝的連結清單容器,它在nginx中使用得很頻繁,例如http的頭部就是用ngx_list_t來存儲的。當然,c語言封裝的連結清單沒有c++或java等面向對象語言那麼容易了解。先看一下ngx_list_t相關成員的定義:

ngx_list_t描述整個連結清單,而ngx_list_part_t隻描述連結清單的一個元素。這裡要注意的是,ngx_list_t不是一個單純的連結清單,為了便于了解,我們姑且稱它為存儲數組的連結清單,什麼意思呢?抽象地說,就是每個連結清單元素ngx_list_part_t又是一個數組,擁有連續的記憶體,它既依賴于ngx_list_t裡的size和nalloc來表示數組的容量,同時又依靠每個ngx_list_part_t成員中的nelts來表示數組目前已使用了多少容量。是以,ngx_list_t是一個連結清單容器,而連結清單中的元素又是一個數組。事實上,ngx_list_part_t數組中的元素才是使用者想要存儲的東西,ngx_list_t連結清單能夠容納的元素數量由ngx_list_part_t數組元素的個數與每個數組所能容納的元素相乘得到。

這樣設計有什麼好處呢?

連結清單中存儲的元素是靈活的,它可以是任何一種資料結構。

連結清單元素需要占用的記憶體由ngx_list_t管理,它已經通過數組配置設定好了。

小塊的記憶體使用連結清單通路效率是低下的,使用數組通過偏移量來直接通路記憶體則要高效得多。

下面詳述每個成員的意義。

(1)ngx_list_t

part:連結清單的首個數組元素。

last:指向連結清單的最後一個數組元素。

size:前面講過,連結清單中的每個ngx_list_part_t元素都是一個數組。因為數組存儲的是某種類型的資料結構,且ngx_list_t 是非常靈活的資料結構,是以它不會限制存儲什麼樣的資料,隻是通過size限制每一個數組元素的占用的空間大小,也就是使用者要存儲的一個資料所占用的位元組數必須小于或等于size。

nalloc:連結清單的數組元素一旦配置設定後是不可更改的。nalloc表示每個ngx_list_part_t數組的容量,即最多可存儲多少個資料。

pool:連結清單中管理記憶體配置設定的記憶體池對象。使用者要存放的資料占用的記憶體都是由pool配置設定的,下文中會詳細介紹。

(2)ngx_list_part_t

elts:指向數組的起始位址。

nelts:表示數組中已經使用了多少個元素。當然,nelts必須小于ngx_list_t 結構體中的nalloc。

next:下一個連結清單元素ngx_list_part_t的位址。

事實上,ngx_list_t中的所有資料都是由ngx_pool_t類型的pool記憶體池配置設定的,它們通常都是連續的記憶體(在由一個pool記憶體池配置設定的情況下)。下面以圖3-2為例來看一下ngx_list_t的記憶體分布情況。

《深入了解Nginx:子產品開發與架構解析》一3.2 準備工作

圖3-2中是由3個ngx_list_part_t數組元素組成的ngx_list_t連結清單可能擁有的一種記憶體分布結構,讀者可以從這種較為常見的記憶體分布中看到ngx_list_t連結清單的用法。這裡,pool記憶體池為其配置設定了連續的記憶體,最前端記憶體存儲的是ngx_list_t結構中的成員,緊接着是第一個ngx_list_part_t結構占用的記憶體,然後是ngx_list_part_t結構指向的數組,它們一共占用size*nalloc位元組,表示數組中擁有nalloc個大小為size的元素。其後面是第2個ngx_list_part_t結構以及它所指向的數組,依此類推。

對于連結清單,nginx提供的接口包括:ngx_list_create接口用于建立新的連結清單,ngx_list_init接口用于初始化一個已有的連結清單,ngx_list_push接口用于添加新的元素,如下所示:

調用ngx_list_create建立元素時,pool參數是記憶體池對象(參見3.7.2節),size是每個元素的大小,n是每個連結清單數組可容納元素的個數(相當于ngx_list_t結構中的nalloc成員)。ngx_list_create傳回新建立的連結清單位址,如果建立失敗,則傳回null空指針。ngx_list_create被調用後至少會建立一個數組(不會建立空連結清單),其中包含n個大小為size位元組的連續記憶體塊,也就是ngx_list_t結構中的part成員。

下面看一個簡單的例子,我們首先建立一個連結清單,它存儲的元素是ngx_str_t,其中每個連結清單數組中存儲4個元素,代碼如下所示:

ngx_list_init的使用方法與ngx_list_create非常類似,需要注意的是,這時連結清單資料結構已經建立好了,若ngx_list_init傳回ngx_ok,則表示初始化成功,若傳回ngx_error,則表示失敗。

調用ngx_list_push表示添加新的元素,傳入的參數是ngx_list_t連結清單。正常情況下,傳回的是新配置設定的元素首位址。如果傳回null空指針,則表示添加失敗。在使用它時通常先調用ngx_list_push得到傳回的元素位址,再對傳回的位址進行指派。例如:

周遊連結清單時nginx沒有提供相應的接口,實際上也不需要。我們可以用以下方法周遊連結清單中的元素:

ngx_table_elt_t資料結構如下所示:

可以看到,ngx_table_elt_t就是一個key/value對,ngx_str_t 類型的key、value成員分别存儲的是名字、值字元串。hash成員表明ngx_table_elt_t也可以是某個散清單資料結構(ngx_hash_t類型)中的成員。ngx_uint_t 類型的hash成員可以在ngx_hash_t中更快地找到相同key的ngx_table_elt_t資料。lowcase_key指向的是全小寫的key字元串。

顯而易見,ngx_table_elt_t是為http頭部“量身訂制”的,其中key存儲頭部名稱(如content-length),value存儲對應的值(如“1024”),lowcase_key是為了忽略http頭部名稱的大小寫(例如,有些用戶端發來的http請求頭部是content-length,nginx希望它與大小寫敏感的content-length做相同處理,有了全小寫的lowcase_key成員後就可以快速達成目的了),hash用于快速檢索頭部(它的用法在3.6.3節中進行詳述)。

緩沖區ngx_buf_t是nginx處理大資料的關鍵資料結構,它既應用于記憶體資料也應用于磁盤資料。下面主要介紹ngx_buf_t結構體本身,而描述磁盤檔案的ngx_file_t結構體則在3.8.1節中說明。下面來看一下相關代碼:

關于使用ngx_buf_t的案例參見3.7.2節。ngx_buf_t是一種基本資料結構,本質上它提供的僅僅是一些指針成員和标志位。對于http子產品來說,需要注意http架構、事件架構是如何設定和使用pos、last等指針以及如何處理這些标志位的,上述說明隻是最常見的用法。(如果我們自定義一個ngx_buf_t結構體,不應當受限于上述用法,而應該根據業務需求自行定義。例如,在13.7節中用一個ngx_buf_t緩沖區轉發上下遊tcp流時,pos會指向将要發送到下遊的tcp流起始位址,而last會指向預備接收上遊tcp流的緩沖區起始位址。)

ngx_chain_t是與ngx_buf_t配合使用的連結清單資料結構,下面看一下它的定義:

buf指向目前的ngx_buf_t緩沖區,next則用來指向下一個ngx_chain_t。如果這是最後一個ngx_chain_t,則需要把next置為null。

在向使用者發送http 包體時,就要傳入ngx_chain_t連結清單對象,注意,如果是最後一個ngx_chain_t,那麼必須将next置為null,否則永遠不會發送成功,而且這個請求将一直不會結束(nginx架構的要求)。

繼續閱讀