(连接篇)
Delphi 的MIDAS出来了这么多年终于有改进的版本了,COM-FREE的DataSnap 2009真是清爽了很多,DataSnap 2009 除了不支持回调和Intercept组件以外 其它的该有的都有了 而且还有很多强大的特性;
第一篇就先 写点DataSnap 2009连接方面可能要用到的东西,以后再继续写写关于生命周期的管理 对象池的应用 以及远程管理 远程方法调用等方面的东西吧。
首先 建立个DataSnap 2009的服务器工程,一共用到三个组件:
DSServer 服务配置组件 用于绑定其它的组件;
DSServerClass 可以看作是一个类的工厂 用于导出需要远程调用的服务端模块;
DSTCPServerTransport 传输组件 这里使用的是indy的tcpserver;
将DSServerClass和DSTCPServerTransport 的Server设置成DSServer就可以了,客户端连接和断开连接时会触发DSServer的两个事件OnConnect和OnDisConnect,参数为 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 包含了客户端连接所传递的参数 Params 也就是一个TStrings的内容;
ChannelInfo 里面有个很重的属性就是它的ID 其实是TIdTCPConnection对象的ID 所以我们可以直接强制转换成TIdTCPConnection;
然后建立个DataSnap 2009的客户端工程,由于使用的DbExpress框架客户端连接用的是TSQLConnection组件,只要把Driver设置成Datasnap即可。连接的服务器地址通过HostName和Port来进行设定,下面我们就实现个简单的DEMO 客户端通过用户名和密码连接服务端 如果密码不争取服务端则断开连接。
客户端主要函数
- 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;
服务端主要函数
- 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 ' 加上异常显示的消息。
效果图如下:
(方法篇)
在过去客户端要调用远程服务器的方法需要通过在TLB里添加接口并且在服务器对象中实现,在DataSnap 2009中调用远程服务器的方法是基于delphi的RTTI机制的,想要一个类允许被远程调用需要做以下两点:
1.把该类和DSServerClass连接在一起
注意:DSServerClass必须设置要导出的类 否则会出现SOnGetClassNotSet的异常信息
2.该类必须使用$MethodInfo编译指令生成详细的RTTI信息
所以我们使用向导添加的ServerModule 不需要再手动添加$MethodInfo开关,同样我们也可以不用继承自 TDSServerModule来实现我们的ServerClass,只要从TPersistent继承一个类 并且用{$MethodInfo ON}和{$MethodInfo OFF}包围就可以输出成员函数到客户端。
注意:要输出的成员函数 必须声明为public
客户端调用可以使用两种方法:
1.使用SqlServerMethod组件
通过设置其ServerMethodName属性来进行远程调用 使用Params属性来传递参数和结果值
2.使用本地代理类
选中SQLConnection组件,在右键菜单中单击Generate Datasnap client classe 生成代理类单元。
下面我们通过一个简单的DEMO来展示DataSnap 2009的远程方法调用,我们在服务端定义了4个输出的成员函数:
- 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内部是使用TDBXValue来管理参数列表的,所以使用string等delphi语言自带的类型将会进行相应的映射。使用TDBXValue也是效率最高的,以下是可以作为参数使用的TDBXValue列表。
TDBXWideStringValue
TDBXAnsiStringValue
TDBXInt16Value
TDBXInt32Value
TDBXInt64Value
TDBXSingleValue
TDBXDoubleValue
TDBXBcdValue
TDBXTimeValue
TDBXDateValue
TDBXTimeStampValue
TDBXBooleanValue
TDBXReaderValue
TDBXStreamValue
我们分别使用SqlServerMethod和代理类完成对服务端Hello方法的调用
- SqlServerMethod.ServerMethodName := 'TSM.Hello';
- SqlServerMethod.Params[0].AsString := Name.Text;
- SqlServerMethod.ExecuteMethod;
- Memo.Lines.Add('Use SqlServerMethod: ' + SqlServerMethod.Params[1].AsString);
这里参数使用了索引值进行访问传递的顺序是从左到右添加到Params列表 返回值是在列表的最后一个位置,同样也可以使用 ParamByName(参数名称).Value的形式传递参数 返回值的名称默认是'ReturnParameter'。使用代理类调 用的方法和调用本地方法区别不大 因为远程调用的具体过程已经被代理类封装可以看下代理类中生成的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返回一个结构体并且在客户端读出
服务端部分
- 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;
注意:写完数据以后需要定位到头部 否则客户端得到的数据长度为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;
最后一个函数演示了使用var和out关键字来返回参数,以下是可以使用这两个关键字的标量值类型:
boolean
SmallInt
Integer
Int64
Single
Double
AnsiString
String
TDBXTime
TDBXDate
再加上其他的参数类型
TStream
TDataSet
TParams
TDBXReader
TDBXConnection
但是在实际测试过程中发现在使用string类型做out和var的参数时 无法使用,跟踪发现源码中ansistring和 string的相关代码已经被注释掉 估计是有BUG存在所以不支持 以后应该可以修复。
以下摘自DSReflect单元的procedure TDSMethodValues.AssignParameterValues(Parameters: TDBXParameterArray);
(生命周期篇)
DataSnap 2009的服务器对象的生命周期依赖于DSServerClass组件的设置,当DSServer启动时从 DSServerClass组件读取LifeCycle属性的值。
注意:LifeCycle的值由于在启动时就已经读取 启动后再修改LifeCycle的值将没有任何效果,LifeCycle属性的值可以是以下三种字符串之一。
1.Session
该选项为默认设置,每个连接都会建立一个独立的服务器对象为客户端提供服务,服务器对象在连接关闭后释放,因此多个客户端访问的是不同的服务器对象,是线程安全的。
2.Invocation
对于每次 服务端方法调用建立一个独立的服务器对象为客户端提供服务,服务器对象在调用结束后释放,这个同样也是线程安全的,但是 每次调用都创建和释放服务器对象对于频繁调用的系统影响很大,如果把服务端对象用对象池管理配合此种方式将是个非常不错的解决方案。
3.Server
所有的客户端使用同一个服务端对象,也就是该对象是单例的,需要开发人员自己来进行同步的控制,不是线程安全的。在服务端对象创建和释放时将触发DSServerClass的两个重要的事件OnCreateInstance和 OnDestroyInstance。在这里我们可以使用自定义创建和释放服务器对象 同样我们可以用于服务端对象池,下面我们把上一次的DEMO稍微改动下来观察下服务端对象的生命周期。
我们先将DSServer组件的AutoStart设置为False 然后拖上两个Button分别完成Start和Stop的调用
- 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事件中记录服务器对象的创建和释放。
- procedure TMainForm.DSServerClassCreateInstance(
- DSCreateInstanceEventObject: TDSCreateInstanceEventObject);
- begin
- LogMessage(Memo, '服务端对象创建');
- end;
- procedure TMainForm.DSServerClassDestroyInstance(
- DSDestroyInstanceEventObject: TDSDestroyInstanceEventObject);
- begin
- LogMessage(Memo, '服务端对象释放');
-
- end;
效果图
通过Demo我们可以明显的看出三种生命周期的区别 注意切换生命周期需要先停止服务器再启动,但是在我们使用Invocation的时候 会造成内存泄露,打开服务端的ReportMemoryLeaksOnShutdown 调用了两次方法后关闭服务端可以看到如下提示:
可以看到服务端对象并没有释放,这里需要我们通过在OnDestroyInstance手动释放。
DSDestroyInstanceEventObject.ServerClassInstance.Free;
但是我们会发现内存泄露依然存在TDSProviderDataModuleAdapter依然没有释放,这是由于 DataSnap2009中继承自TProviderDataModule的类都使用了适配器模式来支持旧的IAppServer接口, 在服务端对象创建的过程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的对象
所以尽管我们手 动释放了我们的服务端对象 适配器对象任然造成了内存泄露