文/黃忠成 EMail:code6421@pchome.com.tw
前言
DELPHI 的 3rd-Party 元件數量之多,遠超過其它的開發工具,其用途之廣可說創下前所未有的記錄。這也為 DELPHI 程式設計師省下許多重新製造輪子的時間,令系統開發速度倍增,同時減少了因實作碼增加而使錯誤率升高。可惜的是 VCL 元件似乎都有著一個通病,就是缺少完整的說明檔 ! 許多 VCL 元件甚至連範例都少的可憐,幸運的是 VCL 元件有個不成文的慣例,那就是多數都會附上完整的原始碼,這一點可以稍減其說明檔不足的現象。即便如此,說明檔不足依舊對使用者造成相當大的困擾,時間就是金錢,在設計者探索原始碼時,時間也一點點的流失了。本文所介紹的 RemObjects SDK( 以下簡稱 RO) 也不能例外,由於這套元件的開發者只有兩位,因此說明檔一直都是相當短缺,有些地方甚至還有描述錯誤的情形,但這些缺點卻無法掩蓋其嶄新的創意與高延展性的設計概念,這也是本文為何會出現在讀者眼前的主要原因, RO 是筆者看過 VCL 元件中唯一令筆者感到驚豔的,當然 ! 這只是筆者個人的感覺,對讀者不見得是如此,不過多了解一樣東西,於汝何損 ?? 因此,細細品嘗吧 !!
PS:
本文省略了討論
Web Services
的基本知識部份,如果讀者對於
Web Services
不熟悉,可參閱筆者的另幾篇文章。
參考文章
|
如何取得RO?
讀者可至
http://www.remobjects.com
取得測試版本,正式版本的定價是
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
所附的範例程式,相信對於運用這套元件應該不成問題,日後若有機會筆者會再針對其它深入的運用及細節做更詳細的介紹。