從最近幾次MMI設計會議討論的結果來看,嵌入式程式員對於分散式運算知之甚少。他們對分散式運算有種恐懼,所以對分布式架構極力排斥。而他們的人數又占絕對優勢,討論N次,MMI的架構還是沒有確定下來。分散式運算已經進入案頭環境,不是公司專屬應用程式的專利了,像GNOME(GNU Network Object Model Environment)的名字本身就暗示著分散式運算了。本文介紹一下分布式的基本原理,揭開分布式神秘的面紗,讓嵌入式程式員熟悉一下。
分散式運算可以分為以下幾類:
傳統的C/S模型。如HTTP/FTP/SMTP/POP/DBMS等伺服器。用戶端向伺服器發送請求,伺服器處理請求,並把結果返回給用戶端。用戶端處於主動,伺服器處於被動。這種調用是顯式的,遠程調用就是遠程調用,本地調用就是本地調用,每個細節你都要清楚,一點都含糊不得。
叢集技術。近年來PC機的計算能力飛速發展,而伺服器的計算能力,遠遠跟不上用戶端的要求。這種多對一的關係本來就不公平,人們已經認識到靠提高單台伺服器的計算能力,永遠滿足效能上的要求。一種稱叢集的技術出現了,它把多台伺服器串連起來,當成一台伺服器來用。這種技術的好處就是,不但對客戶來說是透明的,對伺服器軟體來說也是透明的,軟體不用做任何修改就可以在叢集上運行。叢集技術的應用範圍也僅限於此,只能提高同一個軟體的計算能力,而對於多個不同的軟體協同工作無能為力。
通用型分散式運算環境。如CORBA/DCOM/ RMI/ DBUS等,這些技術(規範)差不多都有具有網路透明性,被調用的方法可能在另外一個進程中,也可能在另外一台機器上。調用者基本上不用關心是本地調用還是遠程調用。當然正是這種透明性,造成了分散式運算的濫用,分散式運算用起來方便,大家以為它免費的。實際上,分散式運算的代價是可觀的,據說跨進程的調用,速度可能會降低一個數量級,跨機器的調用,速度可能降低兩個數量級。一些專家都建議減少使用分散式運算,即使要使用,也要使用粗粒度的調用,以減少調用的次數。
還其一些混合形式(SOAP?),這裡不再多說。我們主要介紹第三種分布式模型,這類分布式模型即適用於企業級應用,也適用於案頭應用。有的專註於企業級應用(如CORBA),有的專註於案頭環境(如DBUS)。它們的實現原理都差不多,基本上都基於傳統的RPC或者仿RPC實現的,下面介紹一下它們的基本原理。
我們先看一下分布式的最簡模型:
在傳統的方法中,調用一個對象的函數很簡單:建立這個對象,然後調用它的函數就行了。而在分布式的環境中,對象在另外一個進程中,完全在不同的地址空間裡,要調用它的函數可能有點困難了。
看看傳統的C/S模型的請求方式,用戶端把參數通過網路發給伺服器,伺服器根據參數要求完成相應的服務,然後把結果返回給用戶端,用戶端拿到結果了,一次請求算完成。由此看來,調用遠程對象似乎並不難,問題在於這種方式不是網路透明的,每一個細節你都要自己處理,非常複雜。
要簡化軟體的設計,當然是網路操作透明化,調用者和實現者都無需關心網路操作。要做到這一點,我們可以按下列方法:
在用戶端要引入一個代理(Proxy)對象。它全權代理實際對象,調用者甚至都不知道它是一個代理,可以像調用本機物件一樣調用這個對象。當調用者調用Proxy的函數時,Proxy並不做實際的操作,而是把這些參數打包成一個網路資料包,並把這個資料包通過網路發送給伺服器。
在伺服器引入一個樁(Stub)對象,Stub收到Proxy發送的資料包之後,把資料包解開,重新組織為參數列表,並用這些參數就調用實際對象的函數。實際對象執行相關操作,把結果返回給Stub,Stub再把結果打包成一個網路資料包,並把這個資料包通過網路發送給用戶端的Proxy。
Proxy收到結果資料包後,把資料包解開為傳回值,返回給調用者。至此,整個操作完成了。怎麼樣,簡化吧。
Proxy隱藏了用戶端的網路操作,Stub隱藏了伺服器端的網路操作,這就實現了網路透明化。你也許會說,根本沒有簡化,只是把網路操作隔離開了,仍然要去實現Proxy和Stub兩個對象,一樣的麻煩。
沒錯。不過仔細研究一下Proxy和Stub的功能,我們會發現,對於不同對象,這些操作都差不多,無非就是打包和解包而已,單調重複。單調重複的東西必然有規律可循,有規律可循就可以用代碼產生器自動產生代碼。
像DCOM和CORBA等也確實是這樣做的,先用IDL語言描述出對象的介面,然後用IDL編譯器自動產生Proxy和Stub代碼,整個過程完全不需要開發人員操心。
打包和解包的專業術語叫做marshal和unmarshal,中文常用翻譯為列集和散集。不過這兩個詞太專業了,翻譯成中文之後更加讓人不知所云。我想還是用打包和解包兩個詞更通俗一點。
在以上模型中,調用對象的方法,確實做到了網路透明化。讀者可以會問,我要訪問對象的屬性怎麼辦呢?對象的屬性就是變數,變數就一塊記憶體地區,記憶體地區在不同的進程裡完全是獨立的,這看起來確實是一個問題。還記得很多關於軟體設計書籍裡面講過的嗎:不要暴露對象屬性,調用者若要訪問對象的屬性,通過get/set方法去訪問。這樣不行了嗎,對屬性的訪問轉換為對對象方法的調用。
OK,調用對象的方法和訪問對象的屬性都解決了。還有重要的一點,如何建立對象呢。因為實際的對象並不固定在某台機器上,它的位置可能是動態。甚至Proxy本身也不知道Stub運行在哪裡。如果要讓調用者來指定,建立對象的過程仍未達到網路透明化。通常的做法是引入一個第三方中介,這個第三方中介是固定的,可以通過一定的方法找到它。第三方中介負責在用戶端的Proxy和伺服器的Stub之間穿針引線。第三方中介通常有兩種:一種是只負責幫用戶端找到伺服器,之後用戶端與伺服器直接通訊。另外一種就是不但負責找到伺服器,而且負責轉寄所有的請求。
以上的模型仍然不完整,因為現實中的對象並不是一直處理於被動的地位。而是在一定的條件下,會主動觸發一些事件,並把這些事件上報給調用者。也就是說這是一個雙向的動作,單純的C/S模型無法滿足要求,而要採用P2P的方式。原先的用戶端同時作為一個伺服器存,接受來自己伺服器的請求。像COM裡就是這樣做的,用戶端要註冊對象的事件,就要實現一個IDispatch介面,給對象反過來調用。
自己實現時還要考慮以下幾點:
l 傳輸抽象層。分布可能是跨進程也可能是跨機器。在不同的情況下,採用不同的通訊方式,效能會有所不同。做一個傳輸抽象層,在不同的情況下,可選用不同的傳輸方式,是一種好的設計。
l 文本還是二進位。把資料打包成文本還是二進位?打包成文本的好處是,可移植性好,由於人也可以看懂,調試方便。壞處是速度稍慢,打包後的資料大小會明顯變大。採用二進位的好處是,速度快,打包後的資料大小與打包前相差不大。壞處是不易調試,可移植性較差。
l 位元組順序和位元組對齊。若採用二進位方式傳輸,可移植性是個問題。因為不同的機器上,位元組順序和位元組對齊的方式都有些差異,在資料包中要加入這些說明,以提高可移植性。