一、概述

   現在大家在和Java, PHP, .net寫應用程序時,都會用到一些成熟的服務框架,所以開發效率是比較高的。而在用C/C++寫服務器程序時,用的就五花八門了,有些人用ACE, 有些人用ICE(號稱比ACE強許多),等等,這類服務器框架及庫比較豐富,但入門門檻比較高,所以更多的人是自己直接寫服務器程序,初始寫時覺得比較簡單,可時間久了,便會覺得難以擴展,性能低,容易出錯。其實,Postfix 作者為我們提供了一個高效、穩定、安全的服務器框架模型,雖然Postfix主要用作郵件系統的 mta,但其框架設計卻非常具有通用性。ACL(http://acl.sourceforge.net/) 的作者將Postfix的服務器框架模型抽取出來,形成了更加通用的服務器程序開發框架,使程序員在編寫服務器程序時可以達到事半功倍的效果。本文主要介紹了ACL中acl_master服務器程序(基于Postifx服務器程序框架)的設計及功能。

  二、框架設計圖

  如下圖所示:

協作半駐留式服務器程序開發框架 --- 基于 Postfix 服務器框架改造

  

  圖1--框架圖

     master主進程為控制進程,剛啟動時其負責監聽所有端口服務,當有新的客戶端連接到達時,master便會啟動子進程進行服務,而自己依然監控服務端口,同時監控子進程的工作狀態;而提供對外服務的子進程在master啟動時,若沒有請求任務則不會被啟動,只有當有連接或任務到達時才會被master 啟動,當該服務子進程處理完某個連接服務后并不立即退出,而是駐留在系統一段時間,等待可能的新連接到達,這樣當有新的連接到達時master就不會啟動新的子進程,因為已經有處于空閑的子進程在等待下一個連接請求;當服務子進程空閑時間達一定閥值后,就會選擇退出,將資源全部歸還操作系統(當然,也可以配置成服務子進程永不退出的模式)。因此,可以稱這種服務器框架為協作式半駐留式服務器框架,下面將會對協作式和半駐留作進一步介紹。

  三、協作方式

  Postfix服務器框架設計的非常巧妙,因為master畢竟屬于用戶空間進程,不能象操作系統那樣可以控制每個進程的運行時間片,所以master主進程必須與其服務子進程之間協作好,以處理好以下幾個過程:

  1)新連接到達時,master是該啟動新的子進程接管該連接還是由空閑子進程直接接管

  2)master何時應該啟動新的子進程

  3)新連接到達,空閑子進程池中的子進程如何競爭接管該連接

  4)子進程異常退出時,master如何處理新連接

  5)空閑子進程如何選擇退出時間(空閑時間或服務次數應決定子進程的退出)

  6)master如何知道各個子進程的工作狀態(是死了還是活著?)

  7)在不停止服務的前提下,服務子進程程序如果在線更新、如何添加新的服務、如何在線更新子進程配置

  8)如何減少所有子進程與master之間的通訊次數從而降低master的負載

  四、流程圖

  1)  master主進程流程圖

協作半駐留式服務器程序開發框架 --- 基于 Postfix 服務器框架改造

  

  圖2--master進程流程圖

  Postifx 中的 master 主進程與各個子進程之間的IPC通訊方式為管道,所以管道的個數與子進程數是成正比的。如果管道中斷,則 master 認為該管道所對應的子進程已經退出,如果是異常退出,master還需要標記該服務類子進程池以防止該類子進程異常退出頻繁而啟動也異常頻繁(如果子進程啟動過于頻繁則會給操作系統造成巨大負載);另外,如果某類服務的子進程在服務第一個連接時就異常退出,則master認為該服務有可能是不可用的,所以當有新的連接再到達時就會延遲啟動該服務子進程。

  當服務子進程池中有空閑子進程時,master便會把該服務端口的監聽權讓出,從而該服務的空閑子進程在該服務端口上接收新的連接。當某個子進程獲得新的連接后便會立即通知master其已經處于忙狀態,master便立即查找該服務的子進程進程池還有無空閑子進程,如果有則master依然不會接管該服務端口的監聽任務;如果沒有了,則master立即接管該服務端口的監聽任務,當有新的連接到達時,master先檢查有沒有該服務的空閑進程,若有便讓出該服務端口的監聽權,若沒有便會啟動新的子進程,然后讓出監聽權。

  2)  服務子進程流程圖

協作半駐留式服務器程序開發框架 --- 基于 Postfix 服務器框架改造

  

  圖3--子進程流程圖

  在master主進程剛啟動時,因為沒有任何服務請求,所以子進程是不隨master一起啟動的,此時所有服務端口的監控工作是由master統一負責,當有客戶端連接到達時,服務子進程才由master啟動,進而接收該新連接,在進一步處理客戶端請求前,子進程必須讓master進程知道它已經開始" 忙"了,好由master來決定是否再次接管該服務端口的監控任務,所以子進程首先向master發送“忙”消息,然后才開始接收并處理該客戶端請求,當子進程完成了對該客戶端的請求任務后,需向master發送“空閑”消息,以表明自己又可以繼續處理新的客戶端連接任務了。這一“忙”一“閑”兩個消息,便體現了服務子進程與master主進程的協作特點。

  當然,服務子進程可以選擇合適的退出時機:如果自己的服務次數達到配置的閥值,或自己空閑時間達到閥值,或與master主進程之間的IPC管道中斷(一般是由master停止服務要求所有服務子進程退出時或master要求所有服務子進程重讀配置時而引起的),則服務子進程便應該結束運行了。這個停止過程,一方面體現了子進程與master主進程之間的協作特點,另一方面也體現了子進程半駐留的特點,從而體現子進程進程池的半駐留特性,這一特性的最大好處就是:按需分配,當請求連接比較多時,所啟動運行的子進程就多,當請求連接任務較少時,只有少數的子進程在運行,其它的都退出了。

  3) 小結

  以上從整體上介紹了Postfix服務器框架模型中master主進程與服務子進程的邏輯流程圖,當然,其內部實現要點與細節遠比上面介紹的要復雜,只是這些復雜層面別人已經幫我們屏蔽了,我們所要做的是在此基礎上寫出自己的應用服務來,下面簡要介紹了基于Postfix的服務器框架改造抽象的ACL庫中服務器程序的開發方式,以使大家比較容易上手。

  五、五種服務器框架模板簡介

  要想使用ACL服務器框架庫開發服務器程序,首先介紹兩個個小名詞:acl_master服務器主進程與服務器模板。acl_master服務器主進程與 Postfix中的master主進程功能相似,它的主要作用是啟動并控制所有的服務子進程,這個程序不用我們寫,是ACL服務器框架中已經寫好的;所謂服務器模板,就是我們需要根據自己的需要從ACL服務器框架中選擇一種服務器模型(即服務器模板),然后在編寫服務器時按該服務器模板的方式寫即可。為什么還要選擇合適的服務器框架模板?這是因為Postfix本身就提供了三類服務器應用形式(單進程單連接進程池、單進程多連接進程池、觸發器進程池),這三類應用形式便分別使用了Postfix中的三種服務器模板,此外,ACL中又增加了兩種服務器應用形式(多線程進程池、單進程非阻塞進程池)。下面分別就這五種服務器框架模板一一做簡介:

  1) 單進程單連接進程池:由 acl_master 主進程啟動多個進程組成進程池提供某類服務,但每個進程每次只能處理一個客戶端連接請求

  2) 單進程多連接進程池:由 acl_master 主進程啟動多個進程組成進程池提供某類服務,而每個進程可以同時處理多個客戶端連接請求

  3) 觸發器進程池:由 acl_master 主進程啟動多個進程組成進程池提供定時器類服務(類似于UNIX中的cron),當某個定時器時間到達時,便由一個進程開始運行處理任務

  4) 多線程進程池:由 acl_master 主進程啟動多個進程組成進程池提供某類服務,而每個進程是由多個線程組成,每個線程處理一個客戶端連接請求

  5) 單進程非阻塞進程池:由 acl_master 主進程啟動多個進程組成進程池提供某類服務,每個進程可以并發處理多個連接(類似于Nginx, Lighttpd, Squid, Ircd),由于采用非阻塞技術,該模型服務器的并發處理能力大大提高,同時系統資源消耗也最小;當然,該模型與單進程多連接進程池采用的技術都是非阻塞技術,但該模型進行更多的應用封裝與高級處理,使編寫非阻塞程序更加容易

  以上五種服務器方式中,由于可以根據需要配置成多個進程實例,所以可以充分地利用多核的系統。其中,第5種的運行效率是最高的,當然其編程的復雜度要比其它的高,而第1種是執行效率最低的,其實它也是最安全的(在Postfix中的smtpd/smtp 等進程就屬于這一類),而相對來說,第4種在運行效率與編寫復雜度方面是一個比較好的折衷,所以在寫一般性服務器時,該服務器模型是作者推薦的方案,此外,第4種方案還有一個好處,可以做到對于空連接不必占用線程,這樣也大大提供了并發度(即線程數可以遠小于連接數)。

  六、使用簡介

  本節暫不介紹具體的編程過程,只是介紹一些配置使用過程。假設已經寫好了服務器程序:echo_server, 該程序可以采用上面的 1), 2), 4), 5) 中的任一服務器模型來寫,假設采用了第4)種;另外,還假設:acl_master等所有文件安裝的根目錄為 /opt/acl/, 主進程程序 acl_master 及 echo_server 安裝在 /opt/acl/libexec/,  acl_master 主程序配置文件安裝在 /opt/acl/conf/,echo_server 配置文件安裝在 /opt/acl/conf/service/, 日志文件目錄為 /opt/acl/var/log/, 進程號文件目錄為 /opt/acl/var/pid/。比如,你讓 echo_server 的服務端口為 6601,服務地址為 127.0.0.1, 它只是提供簡單的 echo 服務。

  你可以運行 /opt/acl/sh/start.sh 腳本來啟動 acl_master 主進程(用 ps -ef|grep acl_master 會看到 acl_master 進程存在 ,但 ps -ef|grep echo_server 卻沒有發現它的存在),然后你在一個Unix終端上 telnet 127.0.0.1 6601, 在另一個終端上 ps -ef|grep echo_server 就會發現有一個 echo_server子進程了,然后在 telnet 6601 的終端上隨便輸入一些字符串,便會立即得到回復,關閉該 telnet 連接,用 ps -ef|grep echo_server 會發現該進程依然存在,當再次 telnet 127.0.0.1 6601 時,該echo_server進程又繼續為新連接提供服務了。可以試著多開幾個終端同時 telnet 127.0.0.1 6601,看看運行效果如何?

  注意,服務子進程的配置文件格式需要與其所采用的模板類型一致;進程池中最大進程個數、進程中線程池最大線程個數、進程最大服務次數、空閑時間等都是可以以配置文件中指定的。