文/黃忠成 EMail:code6421@pchome.com.tw
前言
DELPHI 的3rd-Party 元件數量之多,遠超過其它的開發工具,其用途之廣可說創下前所未有的記錄。這也為DELPHI 程式設計師省下許多重新製造輪子的時間,令系統開發速度倍增,同時減少了因實作碼增加而使錯誤率升高。可惜的是VCL元件似乎都有著一個通病,就是缺少完整的說明檔!許多VCL 元件甚至連範例都少的可憐,幸運的是VCL 元件有個不成文的慣例,那就是多數都會附上完整的原始碼,這一點可以稍減其說明檔不足的現象。即便如此,說明檔不足依舊對使用者造成相當大的困擾,時間就是金錢,在設計者探索原始碼時,時間也一點點的流失了。本文所介紹的RemObjects SDK(以下簡稱RO) 也不能例外,由於這套元件的開發者只有兩位,因此說明檔一直都是相當短缺,有些地方甚至還有描述錯誤的情形,但這些缺點卻無法掩蓋其嶄新的創意與高延展性的設計概念,這也是本文為何會出現在讀者眼前的主要原因,RO是筆者看過VCL 元件中唯一令筆者感到驚豔的,當然! 這只是筆者個人的感覺,對讀者不見得是如此,不過多了解一樣東西,於汝何損?? 因此,細細品嘗吧!!
PS:本文省略了討論Web Services的基本知識部份,如果讀者對於Web Services不熟悉,可參閱筆者的另幾篇文章。
參考文章 |
元 |
|
|
如何取得RO?
讀者可至 取得測試版本,正式版本的定價是229 EUR,未來的Enterprise 版本的定價是603.90 EUR,這兩個版本都附上了完整的原始碼,目前RemObjects Enterprise SDK版本尚在Beta中,此版本擁有許多新功能,除了加強的RO 2.0之外還有抽象化資料存取的Data Abstract元件組,協助除錯的Debug Server工具,以及完全使用C# 寫成的RO.NET Client SDK。
PS:測試版本僅能運行於DELPHI IDE中,讀者可利用Project Group來輔助運行Server端與Client端的程式。
PS2: RO 1.x支援DELPHI 5、6、7 Professional(DataSnap 部份需Enterprise),Kylix 3 for DELPHI。
What’s RemObjects SDK
隨著各家廠商的強力背書與推銷,Web Services 儼然成為未來分散式系統開發的主流架構,但是Web Services 至今仍然存在一些問題,其中有些是屬於規格的問題,有些則是先天上的限制,許多使用Web Services 開發系統的人都會有一個困擾,那就是效率不高,其原因很簡單,XML 本身屬於純文字型態,加上必須依賴XML Parser 剖析XML 文件,在傳輸與解譯上都是造成效率不彰的原因,這是Web Services 的先天限制,也是為了相容性所付出的代價。當然! 如果網路頻寬夠大,電腦速度夠快,這些都不是問題。但事實是目前的頻寬與電腦速度還不足以勝任,這使得Web Services 的應用面縮減不少,因此許多的Web Servcies開發工具都會提供將SOAP訊息壓縮的解決方案,藉此減少網路傳輸時間。另一個問題則是Web Services 必須依賴網路通訊協定,以現今的情況來看是以HTTP或TCP兩種網路通訊協定為主流,假如客戶想將系統安裝於一台電腦上(不管是何理由,或許是因為節省金錢),Web Services 還是需要一個佔用Port,就實務上來看這並不是什麼大問題,但如果可以不佔用Port豈不更好?? RO 就是這樣一套元件,首先! RO 支援兩種訊息標準,一個是SOAP(也就是Web Services)、另一個則是Binary(二進位訊息),支援SOAP 可讓其它支援Web Services 的開發工具經由SOAP連上RO Server,支援Binary 可以讓RO Client以更快的速度與RO Server 溝通,這比起將SOAP壓縮後傳遞的效率高上許多,更令人興奮的是RO允許設計者混用這兩種訊息協定,也就是說只須撰寫一個Server並放上這兩個訊息元件,這一個Server 就可以同時服務使用SOAP 與 Binary 訊息的Client 端。有趣嗎??更有趣的事情還在後面,RO 支援HTTP、TCP、Windows Message、DLL、UDP(2.0)、MSMQ(RO Enterprise) 多種通訊協定,並且允許設計者混用這些協定(DLL 是例外),簡單的說! 就是寫一個Server 同時允許Client 端以HTTP、TCP、Windows Message、UDP、MSMQ 方式連結,再加上之前所提的兩種訊息標準,這個Server是不是更有趣了呢??呵!還沒講完呢,RO 不但具備這些特色,同時也允許設計者撰寫自己的訊息協定與通訊協定,其步驟也不複雜,這些都是RO出色的主要原因。另外RO 也支援Kylix 3 for DELPHI,這代表著使用RO 可撰寫Linux Server/Client,Windows Server/Client,日後的RO Client SDK.NET支援.NET Framework、Mono、Ractor,及Compact Framework,你能想像這種情況嗎??
PS: TCP 與 Windows Message同時只能支援一種訊息格式,如SOAP 或是 Binary,原因是這兩種協定並沒有類似URL的概念,HTTP則無此限制,另外RO Enterprise SDK將會支援.NET Binary(.NET Remoting) 與RO Binary 兩種格式。
初試RemObjects SDK
談了這麼多空話,現在是時候試試RO的能力了,這一節中以一個簡單的計算機為範例(唔!!這是RO 送的,不要都不行….),在安裝完RO 後元件盤上會出現RemObjects SDK 頁,如下圖所示:
其中分為五類,見下表:
元件 | 功能 | 類別 |
TROBinMessagw,TROSOAPMessage | 訊息元件,用來處理訊息。 | 訊息類 |
TROIndyHTTPServer, TROIndyTCPServer, TROBPDXHTTPServer, TROBPDXTCPServer, TROWinMessageServer | Server端元件,用來接收訊息,支援HTTP、TCP、Window Message與DLL(DLL 不需要元件,只需export 一個function即可) | Server類 |
TROIndyHTTPChannel,TROIndyTCPChannel, TROBPDXHTTPChannel, TROBPDXTCPChannel, TROWinInetHTTPChannel, TRODLLChannel | Client 端元件,用來送出訊息到Server端,支援HTTP,TCP,Windows Message與DLL。 | Client類 |
TRODataSnapConnection,TRODataSnapProviderPublisher | 支援DataSnap運作的元件,是的,RemObjects SDK允許使用DataSnap運行於其上。 | DataSnap支援 |
TROWebBrokerServer | Web Broker 支援,允許任何架構於Web Broker之上的網頁程式直接掛載RemObjects SDK Server。 | Web 支援 |
表中所提及的元件除BPDX(這是一組Internet 元件,名為DXSock,與Indy 有相同功能,但在效率與穩定性上都比Indy強,不過在易用性上卻遠不及Indy,而且她屬於商業型元件,不過當讀者購買RO後不須額外付費就可使用這套元件)、WebBrokerServer、DLLxxx之外,其它都會在本文中運用到。
在對這些元件有一個概略的認識後,現在就可以開始撰寫一個簡單的程式了,首先請開啟New Dialogs 對話盒,切換到RemObjects SDK 這一頁,其內有幾個Wizard可協助設計者快速的產生骨架程式:
下表是這幾個Wizard的簡單說明:
Wizard | 說明 |
Apache 2 Shared Module Server Project | 建立Apache 2 Shared Module的Server程式。 |
Apache Shared Module Server Project | 建立 Apache 1.x Shared Module的Server程式。 |
DLL Server Project | 建立DLL 的Server程式。 |
ISAPI/NSAPI Server Project | 建立ISAPI、NSAPI 的Server程式。 |
RemObjects DataSnap Server Module | 建立一個支援DataSnap操作的TRODataSnapDataModule。 |
Windows Executable Server Project | 建立一個可獨立執行的Server。 |
表中除了RemObjects DataSnap Server Module之外,其它都是用來建立一個新RO Server專案,這裡請選擇最簡單的Windows Executable Server Project,按下OK後會開啟下面這個視窗:
下表是這個視窗的欄位說明:
欄位 | 說明 |
Project Name | 專案名稱。 |
Service Library Name | Library 名稱,在RO 1.x 中這個參數的用途不大。 |
Service Name | Web Service 的名稱。 |
Server Class | 通訊協定,見Server類元件。 |
Message Class | 訊息協定,見Client 類元件。 |
Project Directory | 專案存放的目錄。 |
輸入資訊後按下OK就完成了骨架程式的建立工作了,這個程式已包含所有必須
用到的元件,接下來只需啟動位於主選單上的RemObjects->Service Builder 工具定義Web Services 的方法即可完成Server端的程式:
RO 預設會幫使用者產生兩個方法,一個是Sum、另一個是GetServerTime(不要都不行…),為求簡單! 這裡直接運用這兩個方法,不做任何的變動。請將Service Builder關閉後編譯這個專案,此時RO 會跳出一個對話窗詢問是否產生Service的定義與實作檔案,請選擇是,並且在產生後切換到CalcService_Impl.pas加入這兩個方法的實作碼:
function TCalcService.Sum(const A: Integer; const B: Integer): Integer; begin Result:=A+B; end;
function TCalcService.GetServerTime: DateTime; begin Result:=Now; end; |
最後將TROIndyHTTPServer(她被命名為ROServer)的Active設為Ture 就完成了Server端的程式了(假如選擇的是BPDX類元件,那麼這個動作必須寫於程式中,因為BPDX不允許在設計時期啟動)。
在撰寫Client端程式之前必須先將Server程式執行起來,因為Client端必須由Server取得WSDL(事實上也可以直接由RODL(RO 的定義檔)產生呼叫端的程式,後面會介紹這一部份),接著請開立一個新的專案,並且在其FORM 上放入TROWinInetHTTPChannel、TROSOAPMessage 兩個元件,然後啟動位於主選單上的RemObjects->Import Service 來讀入WSDL定義(Import WSDL):
8099是TROIndyHTTPChannel 預設的Port,讀者可經由TROIndyHTTPChannel.Port設定可更改其Port位址。
按下Import後會開出Service Builder,其中可以看到CalcService的定義,將Service Builder關閉後RO會要求輸入檔案名稱,請將她命名為CalcService_Intf.pas。最後有個不方便的地方,由於Service Builder 的一個Bug,這個檔案必須做一些修改才能正常運作:
//CalcService_EndPointURI = 'http://localhost/soap'; CalcService_EndPointURI = 'http://localhost:8099/soap'; //CalcService_DefaultURN = 'urn-NewLibrary:CalcService'; CalcService_DefaultURN = 'CalcService'; |
註解部份是原來的程式碼,粗體字部份是修改後的程式碼,這個Bug 已經被確認在下一個版本就會修正。
最後加上一些UI介面在FORM上,並加上呼叫Web Services 的程式碼就完成了Client端程式了,下圖是UI 介面:
接著是呼叫Web Services的程式碼:
…………… uses CalcService_Intf;
procedure TfmMain.btnCalcClick(Sender: TObject); var vService:CalcService; begin vService:= CoCalcService.Create(ROSOAPMessage1,ROWinInetHTTPChannel1); try lblResult.Caption:=IntToStr(vService.Sum(StrToInt(edtValue1.Text),StrToInt(edtValue2.Text))); finally vService:=Nil; end; end;
procedure TfmMain.Button1Click(Sender: TObject); var vService:CalcService; begin vService:= CoCalcService.Create(ROSOAPMessage1,ROWinInetHTTPChannel1); try lblServerTime.Caption:=DateTimeToStr(vService.GetServerTime); finally vService:=Nil; end; end; |
下圖是這個程式的執行畫面:
很簡單是吧?? 唯一美中不足的是那個Bug….
範例程式中附上了BizSnap、.NET、Java 三個Client端的原始碼,有興趣的讀者可自行觀看,這裡就不再說明了。
非同步呼叫模式
RO 支援非同步呼叫模式,簡單的說就是建立一個執行緒來呼叫遠端的Web Services,這樣在呼叫Web Services的期間主執行緒就可以做其它的事,不會因為長時間的呼叫而導致程式無回應的狀況出現。由於目前版本的Service Builder並不會自動產生非同步呼叫的Unit,因此使用者必須自行使用命令列的RODL2.EXE 來產生這個Unit,這個工具位於RemObjects SDK\Bin 目錄下,只要鍵入下面這個命令就可產生出這個Unit:
RODL2 /rodl:CalcLibrary.rodl /language:pascal /type:async |
CalcLibrary.rodl 是由Service Builder所產生出來的,通常位於Server所在目錄下,/language:pascal則是指定產生pascal語言的程式碼(RO未來支援Pascal、C# 兩種語言),/type:async則是表示產生出非同步呼叫的Unit,完成後會產生一個名為CalcLibrary_Async.pas的程式檔,將它加入Client專案中,並修改呼叫Web Services部份的程式碼:
uses CalcLibrary_Async;
procedure TfmMain.btnCalcClick(Sender: TObject); var vService:CalcService_Async; begin ROWinInetHTTPChannel1.TargetURL:='http://localhost:8099/soap'; vService:=CoCalcService_Async.Create(ROSOAPMessage1,ROWinInetHTTPChannel1); try vService.Invoke_Sum(StrToInt(edtValue1.Text),StrToInt(edtValue2.Text)); Sleep(3000); lblResult.Caption:=IntToStr(vService.Retrieve_Sum); finally vService:=Nil; end; end;
|
由於產生的CalcLibrary_Async.pas有一些小Bug,編譯時uses 區段會有錯誤,請將uses CalcLibrary_Intf 改為CalcService_Intf,這樣就可以正常編譯並執行了。
多種訊息標準
前面的章節曾經提過,RO 支援混用多種通訊協定與訊息標準,這個功能使得Server的延展性大幅提升,要完成這個功能的步驟相當簡單,請開啟前一節的Server端專案,並在FORM上放上一個TROBINMessage元件,接著雙按TROIndyHTTPServer.Dispatcher屬性開出下面這個對話窗:
請按下Add的按紐,接著在Message處選擇ROBinMessage1,這樣就算完成了訊息標準的設定了,讀者也可以藉由更改Path Info屬性來變更這個訊息所對應的URL,除此之外,TROBinMessage支援將資料壓縮後再傳送,這個選項預設是開啟的。現在Server已經可以支援SOAP、Binary兩種訊息格式了,編譯後執行程式後開啟Client端的專案,並在FORM上放入一個TROBinMessage與切換使用訊息的RadioGroup元件:
最後修改呼叫端的程式碼:
function TfmMain.GetService:CalcService; begin Result:=Nil; case RadioGroup1.ItemIndex of 0: begin ROWinInetHTTPChannel1.TargetURL:='http://localhost:8099/soap'; Result:=CoCalcService.Create(ROSOAPMessage1,ROWinInetHTTPChannel1); end; 1: begin ROWinInetHTTPChannel1.TargetURL:='http://localhost:8099/bin'; Result:=CoCalcService.Create(ROBINMessage1,ROWinInetHTTPChannel1); end; end; end;
procedure TfmMain.btnCalcClick(Sender: TObject); var vService:CalcService; begin vService:=GetService; try lblResult.Caption:=IntToStr(vService.Sum(StrToInt(edtValue1.Text),StrToInt(edtValue2.Text))); finally vService:=Nil; end; end;
procedure TfmMain.Button1Click(Sender: TObject); var vService:CalcService; begin vService:=GetService; try lblServerTime.Caption:=DateTimeToStr(vService.GetServerTime); finally vService:=Nil; end; end; |
粗體字是變動後的程式碼,主要在於切換不同的訊息標準。完成後執行程式,這時Client已經可以使用SOAP、Binary與Server端溝通了,當然! 兩個不一樣的Client可以同時使用兩種不同的訊息標準與Server溝通。
要了解SOAP 與Binary 的差別,只需使用TCPTrace 這個工具來觀察Client與Server的訊息流動狀態:
SOAP
BINARY
兩種訊息有著近10 倍的差距。
訊息編碼
除了支援SOAP與BINARY訊息格式之外,RO 也允許將訊息編碼,這對需要較高安全性的Multi-Tier系統來說相當有用,要為系統加入這個功能,只須要設定ROServer的Encryption中的幾個屬性即可,首先選取欲使用的編碼演算法:
接著再設定EncryptionRecvKey、EncryptionSendKey就可以了,這兩個特性還支援多種演算法,這可以防堵有能力的破解高手經由反組譯取得加密與解密的Key:
如有需要,也可以將Encryption.UseCompression設為True,這樣更增加訊息在傳送期間的安全性。
多種通訊協定
上一節中實作了支援多種訊息的Server、Client端,這一節將繼續實作支援多種通訊協定的Server端與使用不同通訊協定的Client端。首先開啟Server端的專案,在FORM上放入TROIndyTCPServer、TROWinMessageServer,接著設定TROWinMessageServer.ServerID 為”ROCalcServer”,完成後請雙按TROIndyTCPServer.Dispatcher與TROWinMessageServer.Dispatcher來設定其支援的訊息元件,由於這兩種Server只允許一個訊息元件存在,在本例中選擇Binary,最後將這兩個元件的Active設成Ture就完成了,下面兩個程式是使用TCP與Windows Message的Client端程式碼,完整的程式可於範例目錄中找到:
TCPChannel
procedure TfmMain.btnCalcClick(Sender: TObject); var vService:CalcService; begin vService:=CoCalcService.Create(ROBINMessage1,ROIndyTCPChannel1); try lblResult.Caption:=IntToStr(vService.Sum(StrToInt(edtValue1.Text),StrToInt(edtValue2.Text))); finally vService:=Nil; end; end;
procedure TfmMain.Button1Click(Sender: TObject); var vService:CalcService; begin vService:=CoCalcService.Create(ROBINMessage1,ROIndyTCPChannel1); try lblServerTime.Caption:=DateTimeToStr(vService.GetServerTime); finally vService:=Nil; end; end; |
Window Message Channel
procedure TfmMain.btnCalcClick(Sender: TObject); var vService:CalcService; begin vService:=CoCalcService.Create(ROBINMessage1,ROWinMessageChannel1); try lblResult.Caption:=IntToStr(vService.Sum(StrToInt(edtValue1.Text),StrToInt(edtValue2.Text))); finally vService:=Nil; end; end;
procedure TfmMain.Button1Click(Sender: TObject); var vService:CalcService; begin vService:=CoCalcService.Create(ROBINMessage1,ROWinMessageChannel1); try lblServerTime.Caption:=DateTimeToStr(vService.GetServerTime); finally vService:=Nil; end; end; |
執行畫面與之前的程式大同小異,這裡就不再貼上來了。
複雜資料型態(Complex Type)
Web Services 除了可以表示簡單的資料型態之外,也可以表示複雜的資料型態,這一部份在RO中稱之為Struct(結構),這一節中將介紹如何在RO中定義這種資料型態。
其實步驟相當簡單,只需在Service Builder中按下Struct按紐新增一個Struct 資料型態,並在其下方定義內部的結構就可以了,如下圖:
然後定義一個方法,將其傳回值設成這個Struct 即可;
下面是這個方法的實作碼:
function TComplexTypeService.GetPerson: Person; begin Result:=Person.Create; Result.Name:='黃忠成'; Result.Age:=18; end; |
Client 端部份只需以Import WSDL 或是Import RODL(選擇位於Server專案目錄下的那個ComplexTypeService.rodl)即可產生Invoke 部份的程式碼,最後只要加入呼叫Web Services的程式碼就算完成Client端了:
Procedure TfmMain.Button1Click(Sender: TObject); var vService:ComplexTypeService; vData:Person; begin vService:=CoComplexTypeService.Create(ROSOAPMessage1,ROWinInetHTTPChannel1); try vData:=vService.GetPerson; try edtName.Text:=vData.Name; edtAge.Text:=IntToStr(vData.Age); finally vData.Free; end; finally vService:=Nil; end; end; |
完整的程式請參照範例,除了Struct之外,眼尖的讀者應該也看到了RO 支援Enum(列舉)、Array(陣列)型態,這些型態的使用方式大同小異,日後2.x 會增加Set與Exception的型態定義。
另外有一點必須注意,當Client端是.NET時RO會產生中文亂碼的問題,這一點目前筆者與RO Team正在討論中,下一版本就可解決。
DataSnap 支援
前面曾經提過,RO 允許DataSnap運行於其上,這給了程式設計師一個相當大的禮物,以往DataSnap只能運行於CORBA、DCOM、Socket、SOAP 之上,前面三個通訊協定都屬於封閉型的,而SOAP又會遭遇效率低落的問題,使用RO的話不但可以使用HTTP、TCP、Windows Message、DLL,又可以混用SOAP與Binary兩種訊息格式,同時具備了延展性與效率,其建構步驟也相當簡單,這一節中將撰寫一個簡單的RO DataSnap Server與Client端。首先請建立一個新專案,並如往常般放置TROIndyHTTPServer、TROSOAPMessage、TROBinMessage至FORM上,接著開啟New Dialogs 來加入一個RO DataSnap DataModule:
在其上放置TDatabase、TSession、TQuery、TDataSetProvider等元件:
接著照以往設定Remote DataModule 般設定這幾個元件的關聯,最後雙按DataModule.Providers 加入欲開放給Client端的DataSetProvider 就完成了Server端程式了。
Client端程式也很簡單,只需放置需要的UI元件及TROWinInetHTTPChannel、TROBinMessage、TRODataSnapConnection 再做一些設定即可,這部份有一點需注意,由於Indy 的Bug,因此無法在Design Time 混用TRODataSnapConnection與TROIndyHTTPChannel,因此請先利用TROWinInetHTTPChannel,待設定好各ClientDataSet的ProviderName後再將TROWinInetHTTPChannel換成TROIndyHTTPChannel(如果需要的話)。
現在設定TROWinInetHTTPChannel.TargetURL 為http://localhost:8099/bin,並將其Connected設成True,接著切換至TRODataSnapConnection設定Message、Channel這兩個特性值分別為TROWinInetHTTPChannel 及 TROBinMesage後將Connect 設成True,現在放置一個TClientDataSet至Form上,並設定其RemoteServer為TRODataSnapConnection,完成後應該就能在ProvideName的下拉盒中找到DataSetProvider1了,最後放置一個DataSource與相關的UI元件就完成了:
一個簡單但完整的3-Tier 程式就完成了。
PacketRecords
DataSnap的PacketRecords 一直都是多數人的最愛,RO 也支援這個功能,但事實上這只是RO 眾多物件建立模式的一種,下面的章節中會介紹RO 的物件模式與設計概念,這裡只需修改DataSnap Server 的Data Module 程式碼就可支援這個功能:
Initialization { To adjust scalability of the DataSnapModule and avoid recreation of instances for each roundtrip, you might want to choose a different ClassFactory type.
Make sure to check the following url for more information: http://www.remobjects.com/page.asp?id=C771266D-6D99-4301-B77D-D7B92D3BCD4D }
//TROPooledClassFactory.Create('IAppServer', Create_DataSnapModule, TIAppServer_Invoker, 25); //TROClassFactory.Create('IAppServer', Create_DataSnapModule, TIAppServer_Invoker); TROPerClientClassFactory.Create('IAppServer', Create_DataSnapModule, TIAppServer_Invoker, 20*60 { seconds Session timeout }); |
TROPerClientClassFactory 會為每一個Client建立一個專有的Service 物件,因此運用她就可使PacketRecords 功能正常運作。
DataSnap Server 與 Method
以往在撰寫DataSnap Server時常常會定義一些供Client端呼叫的方法,在RO中一樣可以這樣做,不過因為RO目前版本的一些限制,所以無法很便利的定義這些方法,這點日後會改善(事實上,在RO 2.x Beta中這一點已經解決了)。
RemObjects SDK 的Server 與Client運作模式
相信許多讀者一定很好奇,RO 內部到底是如何處理Client端的呼叫,Server又是如何將SOAP轉換為呼叫動作執行目的物件中的方法呢?? 事實上,在這一點上RO有非常獨到的見解,在詳細介紹其運作模式之前,筆者先就目前幾種主流的Web Services 開發工具的運作模式做一簡單的介紹,其中包括BizSnap、.NET Web Services、.NET Remoting與Java。
BizSnap與.NET Remoting 的Server端運作模式
BizSnap與.NET Remoting有著非常相似的處理模式,甚至可以說除了實作的語言與編譯技術不同之外,其主要的概念是完全相同的,下圖是其運作模式的流程圖:
當Client 將Request 送達Server端後,會經過一個Message Dispatcher機制,這個機制大多是幾個重要的元件合作完成,主要在於解出Request中對於所要求物件的描述,以及欲呼叫的方法等資訊,有了這些資訊後Dispatcher就可以找到對應的物件與方法,接著就開始了呼叫動作,由於Request 是SOAP訊息格式,並不能直接用來呼叫物件的方法,因此得先將SOAP訊息轉化為Stack(堆疊),完成這個轉換動作後就到了這種處理模式中的核心概念了,也就是建立起目的物件並呼叫對應的方法,這個動作非常依賴前面的Message To Stack程序,因為這個程序會將SOAP訊息轉化為Stack,有了Stack之後Push Stack and Call Method 動作才能正確的執行,那麼如何呼叫目的方法呢?很簡單,只要利用該語言所提供的RTTI資訊(.NET 中則是MetaData),就可取得該方法的記憶體位址,接著只須以低階的ASM 或IL 所提供的CALL 指令即可呼叫該方法,由於已將SOAP訊息轉為Stack,因此傳入參數就不是問題了。在呼叫結束後,Stack 中已經有了傳回的參數,接著只須將Stack轉回SOAP 訊息傳回給Client端就可以了。
BizSnap、.NET Remoting 的Client端運作模式
上一節所提的是Server端接到要求後的處理流程,這一節將介紹Client端的呼叫動作,
下圖是大略的流程:
不管是BizSnap或是.NET Remoting,當Client端欲呼叫Web Services時都會經過一個Proxy Object,於BizSnap中這個物件就是THTTPRIO,.NET Remoting中這個物件就是RealProxy,由於這個物件屬於靜態的,因此在使用之前必需將其轉型回目的物件的型別,當Client端下達轉型動作後整個魔法就開始運行了,首先Proxy Object會利用RTTI或是MetaData資訊取得欲轉型的類別資訊,並依照這些資訊建立起一個相容於該類別的物件(Transparent Proxy Object),接著將這個物件中的所有方法位址替換為Stub Method,Stub Method 做的事情很單純,只是將Stack轉為SOAP Message後送出,當Server端回應後再將SOAP Message轉換為Stack 後返回,這樣整個Client端呼叫動作就完成了,下次再呼叫時只需由Cache中取出這個已建立好的Transparent Proxy Object,就可以直接進行呼叫,這可以避免因反覆以RTTI或是MetaData建立Transparent Proxy Object而失去效率。
BizSnap、.NET Remoting 的處理模式屬於較低階的方法,這種方法的壞處大於好處,
好處是設計者可以完全不了解其內部運作,以傳統方式來撰寫程式,壞處是過度依賴編譯器及平台,增加日後移植到其它語言或平台上的難度,另外使用動態產生物件類別的低階技術很容易引發效率及記憶體管理的問題。
.NET Web Services 與Java
.NET Web Services 與Java 的處理模式與.NET Remoting、BizSnap大同小異,其間最大的不同之處在於這種模式利用了其語言的特性,採動態呼叫的方式來執行呼叫的動作,而非如先前所提的模式在Stack與Message之間進行轉換,這種模式簡單的在Client端與Server端之間插入一個預先編譯好的Proxy Object,這個Object是由WSDL所產生的,其中定義了所有目標物件的方法,在這些方法中簡單的將傳入的參數轉換為SOAP Message,將傳回的訊息轉回參數,其間的運作完全屬於高階型態:
Client 端的呼叫
Server端的處理
由上面兩個圖上可看出,這種模式講求簡單,Client端的Stub Method 由原本的一個變成每個方法一個,Server端則由原本的低階CALL命令轉為語言本身所提供的動態呼叫功能。這樣的簡化帶來了效率,由於Client端不再經過動態轉型與建立中介物件的動作,因此在效率上有顯著的提升,也因為少了這些低階的動作,記憶體管理上顯得容易多了。但這種方式卻有著另外的幾個缺點,由於Proxy Object的程式碼增加,相對的程式所佔用的記憶體也隨之變大,另外Server採用動態呼叫的方式來喚起方法,這種方式通常效率不高。
RemObjects SDK
前面所提的兩種模式皆有其優缺點,RO 在這方面則提出了另一個嶄新的處理模式,
下圖是RO 的Server端處理模式:
上圖中大約與前面所提及的模式相同,其中不同之處在於Invoke Object,這是一個預先編譯好的物件,其作用與.NET Web Services的Proxy Object相同,這個物件中所有方法都是Stub Method,將SOAP訊息轉為參數後呼叫Real Object(Implement Object)的方法,完成後將參數轉回訊息後返回Client端。那麼這種模式有何獨到之處呢??答案是效率,整個動作之中看不到低階的Stack或是動態呼叫,沒有這些動作的加入,當然速度上也就加快不少。
RO 的Client端處理方式與Server端大同小異,因此結論是! RO 沒有用到任何的中介技術,也沒有用到任何語言獨有的功能,這也是RO .NET 為何能在短短的幾個月內就能完成的主要原因。下面是CalcService 的xxx_Intf.pas、xxx_Invk.pas、xxx_Impl.pas的原始碼,交替著看應可讓讀者更清楚這種模式的運作。
unit CalcLibrary_Invk;
{----------------------------------------------------------------------------} { This unit was automatically generated by the RemObjects SDK after reading } { the RODL file associated with this project . } { } { Do not modify this unit manually, or your changes will be lost when this } { unit will regenerated the next time you compile the project. } {----------------------------------------------------------------------------}
interface
uses {vcl:} Classes, {RemObjects:} uROServer, uROServerIntf, uROTypes, uROClientIntf, {Generated:} CalcLibrary_Intf;
type TCalcService_Invoker = class(TROInvoker) private protected published procedure Invoke_Sum(const __Instance:IInterface; const __Message:IROMessage; const __Transport:IROTransport); procedure Invoke_GetServerTime(const __Instance:IInterface; const __Message:IROMessage; const __Transport:IROTransport); end;
implementation
uses {RemObjects:} uRORes;
{ TCalcService_Invoker }
procedure TCalcService_Invoker.Invoke_Sum(const __Instance:IInterface; const __Message:IROMessage; const __Transport:IROTransport); { function Sum(const A: Integer; const B: Integer): Integer; } var A: Integer; B: Integer; Result: Integer; begin try __Message.Read('A', TypeInfo(Integer), A, []); __Message.Read('B', TypeInfo(Integer), B, []);
Result := (__Instance as CalcService).Sum(A, B);
__Message.Initialize(__Transport, 'CalcLibrary', 'CalcService', 'SumResponse'); __Message.Write('Result', TypeInfo(Integer), Result, []); __Message.Finalize;
finally end; end;
procedure TCalcService_Invoker.Invoke_GetServerTime(const __Instance:IInterface; const __Message:IROMessage; const __Transport:IROTransport); { function GetServerTime: DateTime; } var Result: DateTime; begin try
Result := (__Instance as CalcService).GetServerTime;
__Message.Initialize(__Transport, 'CalcLibrary', 'CalcService', 'GetServerTimeResponse'); __Message.Write('Result', TypeInfo(DateTime), Result, [paIsDateTime]); __Message.Finalize;
finally end; end;
end. |
unit CalcLibrary_Intf;
{----------------------------------------------------------------------------} { This unit was automatically generated by the RemObjects SDK after reading } { the RODL file associated with this project . } { } { Do not modify this unit manually, or your changes will be lost when this } { unit will regenerated the next time you compile the project. } {----------------------------------------------------------------------------}
interface
uses {vcl:} Classes, TypInfo, {RemObjects:} uROProxy, uROTypes, uROClientIntf;
const LibraryUID = '{D62FF60B-5567-43E3-8386-161869372E27}';
type { Forward declarations } CalcService = interface;
{ CalcService }
{ Description: Service CalcService. This service has been automatically generated using the RODL template you can find in the Templates directory. } CalcService = interface ['{D62FF60B-5567-43E3-8386-161869372E27}'] function Sum(const A: Integer; const B: Integer): Integer; function GetServerTime: DateTime; end;
{ CoCalcService } CoCalcService = class class function Create(const aMessage : IROMessage; aTransportChannel : IROTransportChannel) : CalcService; end;
implementation
uses {vcl:} SysUtils, {RemObjects:} uRORes;
type TCalcService_Proxy = class(TROProxy, CalcService) private protected function Sum(const A: Integer; const B: Integer): Integer; function GetServerTime: DateTime; end;
{ CoCalcService }
class function CoCalcService.Create(const aMessage : IROMessage; aTransportChannel : IROTransportChannel) : CalcService; begin result := TCalcService_Proxy.Create(aMessage, aTransportChannel); end;
{ TCalcService_Proxy }
function TCalcService_Proxy.Sum(const A: Integer; const B: Integer): Integer; var __request, __response : TMemoryStream; begin __request := TMemoryStream.Create; __response := TMemoryStream.Create;
try __Message.Initialize(__TransportChannel, 'CalcLibrary', 'CalcService', 'Sum'); __Message.Write('A', TypeInfo(Integer), A, []); __Message.Write('B', TypeInfo(Integer), B, []); __Message.Finalize;
__Message.WriteToStream(__request); __TransportChannel.Dispatch(__request, __response); __Message.ReadFromStream(__response);
__Message.Read('Result', TypeInfo(Integer), result, []); finally __request.Free; __response.Free; end end;
function TCalcService_Proxy.GetServerTime: DateTime; var __request, __response : TMemoryStream; begin __request := TMemoryStream.Create; __response := TMemoryStream.Create;
try __Message.Initialize(__TransportChannel, 'CalcLibrary', 'CalcService', 'GetServerTime'); __Message.Finalize;
__Message.WriteToStream(__request); __TransportChannel.Dispatch(__request, __response); __Message.ReadFromStream(__response);
__Message.Read('Result', TypeInfo(DateTime), result, [paIsDateTime]); finally __request.Free; __response.Free; end end;
initialization end. |
unit CalcService_Impl;
{----------------------------------------------------------------------------} { This unit was automatically generated by the RemObjects SDK after reading } { the RODL file associated with this project . } { } { This is where you are supposed to code the implementation of your objects. } {----------------------------------------------------------------------------}
interface
uses {vcl:} Classes,SysUtils, {RemObjects:} uROClientIntf, uROTypes, uROServer, uROServerIntf, uRORemoteDataModule, {Generated:} CalcLibrary_Intf;
type TCalcService = class(TRORemoteDataModule, CalcService) private protected function Sum(const A: Integer; const B: Integer): Integer; function GetServerTime: DateTime; end;
implementation
{$R *.DFM} uses {Generated:} CalcLibrary_Invk;
procedure Create_CalcService(out anInstance : IUnknown); begin anInstance := TCalcService.Create(NIL); end;
function TCalcService.Sum(const A: Integer; const B: Integer): Integer; begin Result:=A+B; end;
function TCalcService.GetServerTime: DateTime; begin Result:=Now; end;
initialization TROClassFactory.Create('CalcService', Create_CalcService, TCalcService_Invoker);
finalization
end. |
RemObjects SDK 的Class Factory
還記得前面在DataSnap中所提到的TROPerClientClassFactory嗎?? 她是RO眾多ClassFactroy的一種,預設情況下,當使用Service Builder建立一個新的Service 之後,RO會產生一個以TROClassFactroy建立的Service物件,這些ClassFactory可以主宰物件的建立與釋放方式,這在Multi-Tier 程式中是極重要的課題,挑選一個適合的ClassFactroy會直接影響Server的效率與資源耗損量,下表分別介紹RO所提供的幾個ClassFactorys。
ClassFactory | 說明 | 建立方式 |
TROClassFactory | 預設產生的ClassFactory,會在收到Client端要求後建立目標物件,完成呼叫動作後釋放該物件,簡單的說就是Single-Call模式。 | TROClassFactory.Create('CalcService', Create_CalcService, TCalcService_Invoker); |
TROSingletonClassFactory | Singleton模式,不管Client端有多少個,只使用一個物件來服務所有Client端,使用這種模式必須注意多執行緒下的關鍵區段控制。 | TROSingletonClassFactory.Create('SingletonService', Create_SingletonService, TSingletonService_Invoker); |
TROSynchronizedSingletonClassFactory | 與TROSingletonClassFactroy相同,唯一不同之處是此ClassFactory會自行處理多執行緒下的關鍵區段控制,不需設計者擔心。 | TROSynchronizedSingletonClassFactory.Create('SingletonService', Create_SingletonService, TSingletonService_Invoker); |
TROPooledClassFactory | 使用Pooling 機制來建立物件,簡單的說是預先建立一定數量的物件,不管Client端有多少,Server端就只有一定數量的物件服務所有Client端,假如Client端大於Server端物件數量,有些Client端會面臨排隊等待或是失敗(取決於第五個參數)。Server端的物件不會自行釋放。 | TROPooledClassFactory.Create('PooledService', Create_PooledService, TPooledService_Invoker, 2,[pbCreateAdditional],false); 最後三個參數分別代表著Pool的數量,也就是最大物件數,pbCreateAdditional 則是告知當Client端大於Pool物件數量時,是否建立新物件來因應,這個參數可以是pbWait,要Client端等待,或是pbFail,直接傳回錯誤訊息,pbCreateAdditional則是直接建立新的物件服務Client端,這個新物件將以Single-Call模式執行,第三個參數則是控制是否在程式啟動後就將Pool數量的物件建立好,如果這個參數是否的話,那麼物件的建立會被延後到第一個Client端連上後再執行。 |
TROPerClientClassFactory | 使用Session機制來建立物件,簡單的說就是每一個Client端都會有專用的Server端物件,也又是具狀態的模式。 | TROPerClientClassFactory.Create('SessionSample', Create_SessionSample, TSessionSample_Invoker, 20*60 { seconds Session timeout }); 最後一個參數是設定這個物件在建立後經過多少時間沒動作之後就釋放掉。 |
這些ClassFactory適用於大多數的Multi-Tier 程式,這也是RO 獨有的特色之一,唯一較可惜的是其它開發工具無法完全應用到這些功能,只有RO Client才能完全享受這些ClassFactory所帶來的便利。
TROWebBrokerServer
RO 除了允許使用者撰寫獨立的Server之外,同時也允許將Web Services加入到既有的Web Broker 專案中,這是一個非常特殊的模式,使用者只要在Web Module上放入TROWebBrokerServer元件,並將其Active設成Ture後執行主選單上的RemObjects->Turn xxxx to RemObject Project選項,即可將這個Web Broker專案轉換為RO 的專案,並允許執行Service Builder來定義Service,有趣的是! 這些Service 可以直接使用原本定義於這個專案中的所有Data Module與物件,這形成了一個相當特殊的運行模式,由於這牽扯到了Web Broker技術,因此本文不會詳細的敘述這個用法,日後有機會再與讀者分享這種特殊模式的運用。
PS:記得在TWebDispatcher元件中加入一個/soap的Action。
Call Back Support?
在Multi-Tier 程式中Call Back 的支援是相當重要的,RO在1.x 中並沒有提供這個功能,其中牽扯到了一些設計上的概念問題,不過2.x 中預計會提供這個功能,並且採用了可穿越防火牆與NAT 的雙向溝通技術。
後記
本文介紹了RemObject SDK大多數的功能,配合上RO所附的範例程式,相信對於運用這套元件應該不成問題,日後若有機會筆者會再針對其它深入的運用及細節做更詳細的介紹。