歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

Nginx源碼分析之設計之美

在這裡向nginx的作者Igor Sysoev致敬,他開發了一個如此偉大的作品。 毫不誇張的說,nginx已經展現了一個成功的項目代碼是應該如何架構的了。 本文將試圖與讀者分享這裡面的設計之美。

大千世界,任何東西都有共通之處。當我們討論一個東西時,首先要給它定義個邊界, 在這邊界裡有兩個東西:內核(Kernel)和用戶(User)。nginx作為http服務器(其實遠不止),我們給它定義邊界:實現http服務器提供服務功能。項目名稱為nginx,代號(或簡稱)為ngx,前綴為ngx_。

一、一切從命名談起
如果有人認為不具可讀性的代碼是可接受的,那他就是個'天才'。
剛提到任何東西在我們討論的邊界裡,都有兩個東西:內核(Kernel)和用戶(User)。 Kernel作為基礎設施存在,天生存在,User則是碼農自定義並創造出來的。 比如函數 printf,這個是Kernel的一部分。ngx_write_console是nginx裡自定義的一個函數。 Kernel和User的東西一定要區分,這非常有利於提高代碼的可讀性。

如何區分呢? 就是給User的東西加上項目的前綴ngx_。為什麼這樣設計呢?比如log_error,這函數能確認是C提供的,還是自己自定義的呢?但是,換成 ngx_log_error,一目了然,肯定是nginx源碼裡的一個自定義的函數。
所有的接口(全局和靜態函數,全局和靜態變量,自定義類型)應該遵守這一原則。在nginx裡,自定義結構體看起來非常舒服,比如 ngx_command_t,t是typedef的代號。struct: command vs struct: ngx_command_t,您覺得呢?好的命名應該是在頭腦裡不假思索的就直取其意,而不用再經過一次智商運算,頭腦風暴。

二、模塊化思想
nginx的整個代碼像流水線一樣工作著,這流水線上布滿著各種模塊,他們協同工作,共同完成提供服務。比如 ngx_core_module, ngx_epoll_module, ngx_http_core_module, ngx_http_static_module等,ngx_string.h(c), ngx_times.h(c),在C世界裡,文件即模塊,你可以當它是基礎設施,或工具,只是有的文件有變量,相當於文件訪問入口,比如 ngx_http_core_module.h(c)裡的ngx_http_core_module全局變量。
沒有任何獨立存在,不跟任何人打交道的模塊,因為那樣,它就沒有存在的意義,所以模塊是有依賴關系的。 比如ngx_event.h(c)依賴於 ngx_string.h,誰維護著這些關系呢,Makefile。所以能掌握一個項目的人,肯定能手寫Makefile文件。每個模塊有出場順序的,直到main函數return。nginx作者給模塊設計了一個類型成員,可以是core, event, http的一種,很明顯的會是這是 core(核心模塊) -> event(事件機制) -> http(http業務處理) 這麼一個流程。後面的依賴前面的,非常明了。

三、OO面向對象
面向對象和面向過程之爭從來沒停過。我一向認為有爭議的設計不應該融入語言裡,語言應該假設程序員能做最正確的事,而不應該去約束程序員如何犯錯誤。比如goto是否應該存在,全局變量應該怎麼樣。C以最簡潔的語法提供了程序員能秀的平台。廢話點到即止。
nginx裡有非常多的結構體,不知某大師曾說程序就是算法+數據結構,這裡的數據結構不僅是是數組,列表,隊列這些經典的,還包括用戶自定義的,或封裝的,我們曾它為抽象是不是更好呢。結構體讓某業務概念更具血肉,比如 ngx_listening_t, ngx_connection_t, ngx_event_t,非常高明的封裝和命名。讀過DDD書的,如果結合nginx源碼去看,會發現OO最強烈的表達就是抽象(封裝?多態?繼承?)。
一個結構體或類應該表達某個主題,比如ngx_connection_t抽象了連接這個業務,裡面的成員應該表達兩種屬性:顯性和隱性。很多人忽略了隱性的屬性,以 ngx_url_t 為例,url裡有 addr, port 這種顯而易見的成員,大家都會。但是應該還包括 err,這個表示,一個url解析後的結果,是不是有點像冗余字段,是的,這就是隱性的屬性,會讓整個結構體更具表達力。讀nginx源碼,從結構體出現的順序去理清是個好的方向。作者在設計時極具功力和細膩,比如 ngx_http_rewrite_module裡對rewrite的處理,ngx_http_upstream裡對upstream的處理。
結構體代表了業務概念的一個方向,nginx在行為表現方向也設計的很精致,可以細看下日志是如何處理的,其中ngx_log_t的handler和ctx兩個成員的設計。還有很明顯的責任鏈模式,讓人眼前一亮,參考ngx_http_core_run_phases

四、生命周期裡秘密
有兩個東西是一直在整個項目代碼裡游蕩的,日志和內存池。簡單的講,有3個重要概念:
cycle             : 代表了整個生命周期,只要進程還在,它就一直存活。
connection     : 代表連接的生命周期,一個客戶連接過來,它就開始誕生,連接結束,它就跟著終結。
request         : 代表請求,請求一發過來,它開始誕生,請求結束,它也就消亡。
可能你認為connection和request很像,connection比request生命周期更長,request掛了,connection不一定會掛。keepalive就是最好的證明,有了keepalive,客戶端刷新時,connection的fd還一直保持用著,服務端的socket是不會close的。
cycle有自己的pool,connection有自己的pool, request有自己的pool,除了cycle外,其余兩個在消亡前,要釋放內存。
log的表現也很活躍,從最開始的ngx_cycle有自己的log,然後設置成配置文件裡指定的error_log,然後從listen開始分支,每個listen自己復制一份log,然後listen connection用了listen對應的fd, 繼續再給 connection的兩個event: rev, wev。

五、配置文件為何存在
是先有項目代碼,還是先有配置文件呢?我覺得,配置文件是因項目代碼存在而存在的,這樣講似乎有點空白。
項目之初,代碼是可以硬編碼的,比如實現守護進程。但是呢?這樣缺少靈活性,所以用配置文件裡的配置選項控制這個行為,也正因為如此,配置選項一定要依附,掛鉤於某個模塊,然後它的值應該解析到這個模塊攜帶的配置結構體。
現在還是單一的行為,由核心行為引起的。但是到了用戶決定行為的時候,配置文件應該出現分支。你想到server, location了嗎,不同的server有不同的配置,這也是虛擬主機的實現機制。
這個分支非常的重要,原來的ngx_cycle有個void  ****conf_ctx;很酷吧,4層指針。獲取配置是這樣的:
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
其中,ngx_get_conf這樣預定義:
#define ngx_get_conf(conf_ctx, module)  conf_ctx[module.index]
但是到了分支這裡,從cycle->conf_ctx變成了r
cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
ngx_http_get_module_main_conf這樣定義:
#define ngx_http_get_module_main_conf(r, module)                             \
    (r)->main_conf[module.ctx_index]
當分析完用戶的請求行為後,又會將分析完的配置定下來,比如虛擬主機如何實現的:
ngx_http_find_virtual_server(r, r->headers_in.server.data, r->headers_in.server.len) {
    cscf = ngx_hash_find_combined(&r->virtual_names->names,
                                  ngx_hash_key(host, len), host, len);

    if (cscf) {
        r->srv_conf = cscf->ctx->srv_conf;  // 以後直接找r要src_conf獲取。
        r->loc_conf = cscf->ctx->loc_conf;
    }
}

天空行空的一口氣寫完,本文結束!

Copyright © Linux教程網 All Rights Reserved