(連接篇)
Delphi 的MIDAS出來了這么多年終于有改進的版本了,COM-FREE的DataSnap 2009真是清爽了很多,DataSnap 2009 除了不支持回調(diào)和Intercept組件以外 其它的該有的都有了 而且還有很多強大的特性;
第一篇就先 寫點DataSnap 2009連接方面可能要用到的東西,以后再繼續(xù)寫寫關于生命周期的管理 對象池的應用 以及遠程管理 遠程方法調(diào)用等方面的東西吧。
首先 建立個DataSnap 2009的服務器工程,一共用到三個組件:
DSServer 服務配置組件 用于綁定其它的組件;
DSServerClass 可以看作是一個類的工廠 用于導出需要遠程調(diào)用的服務端模塊;
DSTCPServerTransport 傳輸組件 這里使用的是indy的tcpserver;
將DSServerClass和DSTCPServerTransport 的Server設置成DSServer就可以了,客戶端連接和斷開連接時會觸發(fā)DSServer的兩個事件OnConnect和OnDisConnect,參數(shù)為 TDSConnectEventObject。
我們看下該類的定義
- TDSConnectEventObject = class(TDSEventObject)
- public
- constructor Create(const ADbxContext: TDBXContext; const AServer: TDSCustomServer; const ATransport: TDSServerTransport; const AChannelInfo: TDBXChannelInfo; const ADbxConnection: TDBXConnection; const AConnectProperties: TDBXProperties);
- private
- FConnectProperties: TDBXProperties;
- FChannelInfo: TDBXChannelInfo;
- public
- property ConnectProperties: TDBXProperties read FConnectProperties write FConnectProperties;
- property ChannelInfo: TDBXChannelInfo read FChannelInfo;
- end;
我們可以看到其中包含了兩個屬性ConnectProperties和ChannelInfo:
ConnectProperties 包含了客戶端連接所傳遞的參數(shù) Params 也就是一個TStrings的內(nèi)容;
ChannelInfo 里面有個很重的屬性就是它的ID 其實是TIdTCPConnection對象的ID 所以我們可以直接強制轉(zhuǎn)換成TIdTCPConnection;
然后建立個DataSnap 2009的客戶端工程,由于使用的DbExpress框架客戶端連接用的是TSQLConnection組件,只要把Driver設置成Datasnap即可。連接的服務器地址通過HostName和Port來進行設定,下面我們就實現(xiàn)個簡單的DEMO 客戶端通過用戶名和密碼連接服務端 如果密碼不爭取服務端則斷開連接。
客戶端主要函數(shù)
- procedure TMainForm.ConnectClick(Sender: TObject);
- begin
- SQLConnection.Params.Values['User_Name'] := UserName.Text;
- SQLConnection.Params.Values['PassWord'] := Password.Text;
- try
- SQLConnection.Open;
- Connect.Enabled := False;
- DisConnect.Enabled := True;
- except
- ShowMessage('連接服務器失敗!');
- end;
- end;
-
- procedure TMainForm.DisConnectClick(Sender: TObject);
- begin
- SQLConnection.Close;
- Connect.Enabled := True;
- DisConnect.Enabled := False;
- end;
服務端主要函數(shù)
- procedure TMainForm.DSServerConnect(DSConnectEventObject: TDSConnectEventObject);
- const
- SRemoteConnected = '遠程客戶端連接 %s:%d';
- SUserNameAndPassword = '用戶名: %s 密碼: %s';
- SAuthSuccess = '用戶名密碼認證成功';
- SAuthFailed = '用戶名密碼認證失敗';
- var
- Conn: TIdTCPConnection;
- begin
- Conn := TIdTCPConnection(DSConnectEventObject.ChannelInfo.Id);
- LogMessage(Memo, Format(SRemoteConnected, [Conn.Socket.Binding.PeerIP, Conn.Socket.Binding.PeerPort]));
- with DSConnectEventObject.ConnectProperties do
- begin
- LogMessage(Memo, Format(SUserNameAndPassword, [Values['User_Name'], Values['PassWord']]));
- if (Values['User_Name'] = 'Admin') and (Values['PassWord'] = '123456') then
- LogMessage(Memo, SAuthSuccess)
- else
- begin
- LogMessage(Memo, SAuthFailed);
- Conn.Disconnect;
- end;
- end;
- end;
-
- procedure TMainForm.DSServerDisconnect(DSConnectEventObject: TDSConnectEventObject);
- const
- SRemoteDisConnected = '遠程客戶端斷開連接 %s:%d';
- var
- Conn: TIdTCPConnection;
- begin
- Conn := TIdTCPConnection(DSConnectEventObject.ChannelInfo.Id);
- LogMessage(Memo, Format(SRemoteDisConnected, [Conn.Socket.Binding.PeerIP, Conn.Socket.Binding.PeerPort]));
- end;
注意:OnConnect事件中還可以使用另外一種方式拒絕客戶端連接,在代碼中拋出個異常即可 在客戶端會捕捉到一個TDBXError的異常 顯示'Remote error ' 加上異常顯示的消息?!?/span>
效果圖如下:

(方法篇)
在過去客戶端要調(diào)用遠程服務器的方法需要通過在TLB里添加接口并且在服務器對象中實現(xiàn),在DataSnap 2009中調(diào)用遠程服務器的方法是基于delphi的RTTI機制的,想要一個類允許被遠程調(diào)用需要做以下兩點:
1.把該類和DSServerClass連接在一起
注意:DSServerClass必須設置要導出的類 否則會出現(xiàn)SOnGetClassNotSet的異常信息
2.該類必須使用$MethodInfo編譯指令生成詳細的RTTI信息
所以我們使用向?qū)砑拥腟erverModule 不需要再手動添加$MethodInfo開關,同樣我們也可以不用繼承自 TDSServerModule來實現(xiàn)我們的ServerClass,只要從TPersistent繼承一個類 并且用{$MethodInfo ON}和{$MethodInfo OFF}包圍就可以輸出成員函數(shù)到客戶端。
注意:要輸出的成員函數(shù) 必須聲明為public
客戶端調(diào)用可以使用兩種方法:
1.使用SqlServerMethod組件
通過設置其ServerMethodName屬性來進行遠程調(diào)用 使用Params屬性來傳遞參數(shù)和結(jié)果值
2.使用本地代理類
選中SQLConnection組件,在右鍵菜單中單擊Generate Datasnap client classe 生成代理類單元。
下面我們通過一個簡單的DEMO來展示DataSnap 2009的遠程方法調(diào)用,我們在服務端定義了4個輸出的成員函數(shù):
- TSM = class(TDSServerModule)
- public
- function Hello(Message: String): String;
- function VariantMethod(Value: OleVariant): OleVariant;
- function StreamMethod: TStream;
- function VarOutMethod(out OutParam: OleVariant; var VarParam: OleVariant): string;
- end;
由于在DataSnap內(nèi)部是使用TDBXValue來管理參數(shù)列表的,所以使用string等delphi語言自帶的類型將會進行相應的映射。使用TDBXValue也是效率最高的,以下是可以作為參數(shù)使用的TDBXValue列表。
TDBXWideStringValue TDBXAnsiStringValue TDBXInt16Value TDBXInt32Value TDBXInt64Value TDBXSingleValue TDBXDoubleValue TDBXBcdValue TDBXTimeValue TDBXDateValue TDBXTimeStampValue TDBXBooleanValue TDBXReaderValue TDBXStreamValue
我們分別使用SqlServerMethod和代理類完成對服務端Hello方法的調(diào)用
- SqlServerMethod.ServerMethodName := 'TSM.Hello';
- SqlServerMethod.Params[0].AsString := Name.Text;
- SqlServerMethod.ExecuteMethod;
- Memo.Lines.Add('Use SqlServerMethod: ' + SqlServerMethod.Params[1].AsString);
這里參數(shù)使用了索引值進行訪問傳遞的順序是從左到右添加到Params列表 返回值是在列表的最后一個位置,同樣也可以使用 ParamByName(參數(shù)名稱).Value的形式傳遞參數(shù) 返回值的名稱默認是'ReturnParameter'。使用代理類調(diào) 用的方法和調(diào)用本地方法區(qū)別不大 因為遠程調(diào)用的具體過程已經(jīng)被代理類封裝可以看下代理類中生成的Hello方法。
- function TSMClient.Hello(Message: string): string;
- begin
- if FHelloCommand = nil then
- begin
- FHelloCommand := FDBXConnection.CreateCommand;
- FHelloCommand.CommandType := TDBXCommandTypes.DSServerMethod;
- FHelloCommand.Text := 'TSM.Hello';
- FHelloCommand.Prepare;
- end;
- FHelloCommand.Parameters[0].Value.SetWideString(Message);
- FHelloCommand.ExecuteUpdate;
- Result := FHelloCommand.Parameters[1].Value.GetWideString;
- end;
我們看到代理類使用了比SqlServerMethod更低級的DBXCommand進行了封裝 以更友好的方式給我們使用
- with TSMClient.Create(SQLConnection.DBXConnection) do
- begin
- Memo.Lines.Add('Use Proxy: ' + Hello(Name.Text));
- Free;
- end;
下面我們用TStream返回一個結(jié)構(gòu)體并且在客戶端讀出
服務端部分
- TName = packed record
- FirstName: array[0..99] of Char;
- LastName: array[0..99] of Char;
- end;
- function TSM.StreamMethod: TStream;
- var
- Name: TName;
- begin
- Name.FirstName := '愛新覺羅';
- Name.LastName := '玄燁';
- Result := TMemoryStream.Create;
- Result.Seek(0, soFromBeginning);
- Result.Write(Name, SizeOf(TName));
- Result.Seek(0, soFromBeginning);
- end;
注意:寫完數(shù)據(jù)以后需要定位到頭部 否則客戶端得到的數(shù)據(jù)長度為0
客戶端部分
- procedure TMainForm.StreamTestClick(Sender: TObject);
- var
- Name: TName;
- begin
- if SQLConnection.Connected then
- begin
- with TSMClient.Create(SQLConnection.DBXConnection) do
- begin
- StreamMethod.ReadBuffer(Name, SizeOf(TName));
- Memo.Lines.Add(Format('(StreamMethod)FirstName: %s LastName: %s',[Name.FirstName, Name.LastName]));
- Free;
- end;
- end;
- end;
最后一個函數(shù)演示了使用var和out關鍵字來返回參數(shù),以下是可以使用這兩個關鍵字的標量值類型:
boolean SmallInt Integer Int64 Single Double AnsiString String TDBXTime TDBXDate
再加上其他的參數(shù)類型
TStream TDataSet TParams TDBXReader TDBXConnection
但是在實際測試過程中發(fā)現(xiàn)在使用string類型做out和var的參數(shù)時 無法使用,跟蹤發(fā)現(xiàn)源碼中ansistring和 string的相關代碼已經(jīng)被注釋掉 估計是有BUG存在所以不支持 以后應該可以修復。
以下摘自DSReflect單元的procedure TDSMethodValues.AssignParameterValues(Parameters: TDBXParameterArray);

(生命周期篇)
DataSnap 2009的服務器對象的生命周期依賴于DSServerClass組件的設置,當DSServer啟動時從 DSServerClass組件讀取LifeCycle屬性的值。
注意:LifeCycle的值由于在啟動時就已經(jīng)讀取 啟動后再修改LifeCycle的值將沒有任何效果,LifeCycle屬性的值可以是以下三種字符串之一。
1.Session
該選項為默認設置,每個連接都會建立一個獨立的服務器對象為客戶端提供服務,服務器對象在連接關閉后釋放,因此多個客戶端訪問的是不同的服務器對象,是線程安全的。
2.Invocation
對于每次 服務端方法調(diào)用建立一個獨立的服務器對象為客戶端提供服務,服務器對象在調(diào)用結(jié)束后釋放,這個同樣也是線程安全的,但是 每次調(diào)用都創(chuàng)建和釋放服務器對象對于頻繁調(diào)用的系統(tǒng)影響很大,如果把服務端對象用對象池管理配合此種方式將是個非常不錯的解決方案。
3.Server
所有的客戶端使用同一個服務端對象,也就是該對象是單例的,需要開發(fā)人員自己來進行同步的控制,不是線程安全的。在服務端對象創(chuàng)建和釋放時將觸發(fā)DSServerClass的兩個重要的事件OnCreateInstance和 OnDestroyInstance。在這里我們可以使用自定義創(chuàng)建和釋放服務器對象 同樣我們可以用于服務端對象池,下面我們把上一次的DEMO稍微改動下來觀察下服務端對象的生命周期。
我們先將DSServer組件的AutoStart設置為False 然后拖上兩個Button分別完成Start和Stop的調(diào)用
- procedure TMainForm.StartClick(Sender: TObject);
- begin
- DSServer.Start;
- end;
- procedure TMainForm.StopClick(Sender: TObject);
- begin
- DSServer.Stop;
- end;
在OnGetClass中記錄服務啟動時使用的生命周期
- procedure TMainForm.DSServerClassGetClass(DSServerClass: TDSServerClass;
- var PersistentClass: TPersistentClass);
- begin
- DSServerClass.LifeCycle := LifeCycles.Items.Strings[LifeCycles.ItemIndex];
- LogMessage(Memo, '生命周期:' + DSServerClass.LifeCycle);
- PersistentClass := TSM;
- end;
LifeCycles是一個TRadioGroup存放了生命周期使用的三個字符串,最后在OnCreateInstance 和OnDestroyInstance事件中記錄服務器對象的創(chuàng)建和釋放。
- procedure TMainForm.DSServerClassCreateInstance(
- DSCreateInstanceEventObject: TDSCreateInstanceEventObject);
- begin
- LogMessage(Memo, '服務端對象創(chuàng)建');
- end;
- procedure TMainForm.DSServerClassDestroyInstance(
- DSDestroyInstanceEventObject: TDSDestroyInstanceEventObject);
- begin
- LogMessage(Memo, '服務端對象釋放');
-
- end;
效果圖

通過Demo我們可以明顯的看出三種生命周期的區(qū)別 注意切換生命周期需要先停止服務器再啟動,但是在我們使用Invocation的時候 會造成內(nèi)存泄露,打開服務端的ReportMemoryLeaksOnShutdown 調(diào)用了兩次方法后關閉服務端可以看到如下提示:

可以看到服務端對象并沒有釋放,這里需要我們通過在OnDestroyInstance手動釋放。
DSDestroyInstanceEventObject.ServerClassInstance.Free;
但是我們會發(fā)現(xiàn)內(nèi)存泄露依然存在TDSProviderDataModuleAdapter依然沒有釋放,這是由于 DataSnap2009中繼承自TProviderDataModule的類都使用了適配器模式來支持舊的IAppServer接口, 在服務端對象創(chuàng)建的過程TDSServerClass.CreateInstance中我們可以看到。
- if (Instance <> nil) and Instance.InheritsFrom(TProviderDataModule) then
- CreateInstanceEventObject.ServerClassInstance := TDSProviderDataModuleAdapter.Create(Instance);
因此在服務端釋放的TDSServerClass.DestroyInstance中需要釋放 TDSProviderDataModuleAdapter對象
- if DestroyInstanceEventObject.ServerClassInstance is TDSProviderDataModuleAdapter then
- begin
- Adapter := DestroyInstanceEventObject.ServerClassInstance as TDSProviderDataModuleAdapter;
- DestroyInstanceEventObject.ServerClassInstance := Adapter.FProviderDataModule;
- Adapter.FProviderDataModule := nil;
- end else
- Adapter := nil;
當使用Invocation生命周期時 傳遞的ServerClassInstance并不是TDSProviderDataModuleAdapter的對象
所以盡管我們手 動釋放了我們的服務端對象 適配器對象任然造成了內(nèi)存泄露
|