Delphi MVC模式[转]
译文:skyblue(转载请注明作者)
在一篇已经发表的文章中做了微小改动;可以从这篇文章中看到关于Model View Presenter(模型-视图-推荐者)的概念更胜于Model View Controller(模型-视图-控制器)。“但是我还不知道什么是模型-视图-控制器!”,你可能会说。好,在本文得最后篇章中我希望你得问题或者其他更多得问题能够得到解答。
在最近得文章中,我已经讨论过oo设计模式,包括观察者模式,并且我猜想MVC可能被当成一种超级观察者;但是我想最好把它描述成一种开发框架。
为了不使你有太多迷惑,我将忽略MVC中控制器的概念,并用MVP中的推介者作为替代来描述;他们完成大多数相同的工作,不同的是推介者在GUI脚本中的操作有点诡异。
让我们开始描述MVP的构成元素。
模型(Model)
模型在一个系统中表示一个具体对象的数据和行为,例如,一个列表或者一个树。模型将扮演驱动视图的角色。
视图(View)
视图是一种显示下层模型的可视方式。例如,TListView 或者 TTreeView。视图将扮演模型的观察者角色。
选择(Selection)
模型维护一个可供选择的对象,这个对象可以反映当前视图中突出的条目。任何指令发给模型处理都依赖这个选择对象。
指令(Command)
一个指令是模型上预知的每个行为。处理者和交互都要求模型处理指令。指令可以不被处理或者重新被处理,从而为结构提供基础。指令通常作为观察者,在选择模式中执行相同的操作于每个条目上。
处理者(Handler)
处理者用作获取简单的鼠标和菜单事件并对事件做出不需要在一个GUI组件上写入其逻辑反映的一种方式。一个处理者保存一个相关指令的引用。
交互(Interactor)
一个交互被用作处理复杂的事件,例如拖放,和处理者一样,从任何一个GUI元素上分离逻辑反映。一个交互通常从处理者派生
推荐者(Presenter)
推介者被依赖于:
管理来自于视图的输入表示
根据GUI选择对象刷新模型选择对象
激活和撤销处理者和交互
管理和执行指令
组件(Componet)
在一个窗体里每个可视控件通常以一种特殊格式呈现数据;然而这里有不止需要一个在下层数据上的视图的地方。MVP允许在一个单独组件里面同时有数据封装(model),可视外观(View)和输入管理(Presenter)。这些组件能够聚合形成大的组件和应用程序。
回到现实世界
讨论理论固然很好,但是在这个例子里面,我将创建一个简单但是完整的组件示范一下这些组成部分怎样建立和协同工作的。
我将描述的这个组件将基于字符串列表;在每个人大喊“难道这不是多余的?”之前,我必须强调一点,这是学习实践的捷径而且我们需要从简单学起。
在我开始示范MVP之前,我需要为观察者模式定义一些接口:
ISubject = interface;
IObserver = interface
['{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}']
procedure Update(Subject: ISubject);
end;
ISubject = interface
['{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}']
procedure Attach(Observer: IObserver);
procedure Detach(Observer: IObserver);
procedure Notify;
end;
你可能注意到观察者包含一个update方法;因为在delphi里面许多可视组件都包含一个update方法不需要ISubject参数,我们需要做一些特别的组件用作视图。
模型
下一步,我们需要定义基于这个mvp组件的模型的行为:
IListModel = interface
['{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}']
procedure BeginUpdate;
procedure EndUpdate;
function GetCount: Integer;
function GetItem(Idx: Integer): string;
procedure Add(Item: string);
procedure Clear;
procedure Insert(Item, Before: string);
procedure Move(Item, Before: string);
procedure Remove(Item: string);
property Count: Integer
read GetCount;
property Item[Idx: Integer]: string
read GetItem;
end;
因为每个在这个模型中改变的属性应该告知它的观察者对象它已经改变了,BeginUpdate 和 EndUpdate用来让我们告诉事件发生,不是每个细小的变化都会被告知。随着我们讨论这个实现类这将变得明朗。
GetCount和GetItem都是属性的简单的存取方法。
增加和移除方法是我们定义模型的行为。
由于接口不包含实现,现在我们需要创建一个能执行定义的方法的类:
TListModel = class(TInterfacedObject, IListModel, ISubject)
这个类的申明有两点需要解释一下:
我们必须从TInterfacedObject继承胜于从TObject,因为这将自动让我们定义_AddRef, _Release and QueryInterface;这都是实现任何接口所必须的。
用接口允许我们建立一个实现类包括更多的行为。这不是多重继承,但是它给我们用于支持更多的特征或者行为提供了可能。
private
fItems: TStringList;
fObservers: IInterfaceList;
fUpdateCount: Integer;
私有开始部分申明了三个成员:
fItems是一个一般对象指针用于保存我们管理的字符串列表
fObeservers是一个接口指针用于保存每个分离的观察者列表;因为我们用这个接口指针在用完之后我们将不需要释放它。
fUpdateCount用于BeginUpdate和EndUpdate方法,我们将很快讨论得。
随着我们继续讨论这个类得方法,我想强调这些方法已经申明在私有部分是因为一个很好得原因。一个接口紧紧有'public',一旦实现对象已经创建到IModel接口指针中,TModel类型将不再需要引用它。因此每个接口得所有方法可以而且必须写在私有部分。
// IListModel
procedure BeginUpdate;
procedure EndUpdate;
function GetCount: Integer;
function GetItem(Idx: Integer): string;
procedure Add(Item: string);
procedure Clear;
procedure Insert(Item, Before: string);
procedure Move(Item, Before: string);
procedure Remove(Item: string);
// ISubject
procedure Attach(Observer: Observer);
procedure Detach(Observer: IObserver);
procedure Notify;
public
constructor Create; virtual;
destructor Destroy; override;
end;
现在让我们看看这些方法得实现部分:
constructor TListModel.Create;
begin
inherited Create;
fItems := TStringList.Create;
fObservers := TInterfaceList.Create;
end;
destructor TListModel.Destroy;
begin
fItems.Free;
inherited Destroy;
end;
注意到我们把fObservers定义成一个TInterfaceList,由于我想保存一个接口指针得列表而且fObservers不需要释放因为它得引用计数接口指针再用完之后将全部垃圾回收。
function TListModel.GetCount: Integer;
begin
Result := fItems.Count;
end;
function TListModel.GetItem(Idx: Integer): string;
begin
Result := fItems[Idx];
end;
GetCount和GetItem可以自动说明,但是现在我将实现方法影响到列表模型的状态:
procedure TListModel.Add(Item: string);
begin
BeginUpdate;
fItems.Add(Item);
EndUpdate;
end;
procedure TListModel.Clear;
begin
BeginUpdate;
fItems.Clear;
EndUpdate;
end;
procedure TListModel.Insert(Item, Before: string);
begin
BeginUpdate;
fItems.Insert(fItems.IndexOf(Before), Item);
EndUpdate;
end;
procedure TListModel.Move(Item, Before: string);
var
IndexOfBefore: Integer;
begin
BeginUpdate;
IndexOfBefore := fItems.IndexOf(Before);
if IndexOfBefore < 0 then
IndexOfBefore := 0;
fItems.Delete(fItems.IndexOf(Item));
fItems.Insert(IndexOfBefore, Item);
EndUpdate;
end;
procedure TListModel.Remove(Item: string);
begin
BeginUpdate;
fItems.Delete(fItems.IndexOf(Item));
EndUpdate;
end;
这些方法得代码相当直接,但是我想你应该注意这些方法最后都调用了BeginUpdate和EndUpdate方法。这是这些方法得代码:
procedure TListModel.BeginUpdate;
begin
Inc(fUpdateCount);
end;
procedure TListModel.EndUpdate;
begin
Dec(fUpdateCount);
if fUpdateCount = 0 then
Notify;
end;
运用这些方法之后得想法不言而喻如果我们看到同时刷新很多属性的时候。如果不用这种结构,再每个属性改变之后,模型必须通知每个观察者它已经改变;如果相当多的属性很快发生了变化,这将触发成倍连续性得可视刷新,导致视图闪烁。然而,通过再每个属性改变之前调用BeginUpdate,我们增加 fUpdateCount,它意味着当第一个属性发生改变,BeginUpdate和EndUpdate都将调用,但是测试fUpdateCount=0 失败。当我们再所有改变都完成之后调用EndUpdate,fUpdateCount现在应该被减小到0并调用Nodify。
最后,我们要实现这个模型得ISubject;但是注意当加入一个观察者后,我们需要调用Nodify确保新加入得观察者已经更新。
procedure TListModel.Attach(Observer: IObserver);
begin
fObservers.Add(Observer);
Notify;
end;
procedure TListModel.Attach(Observer: IObserver);
begin
fObservers.Remove(Observer);
end;
procedure TListModel.Notify;
var
i: Integer;
begin
for i := 0 to Pred(fObservers.Count) do
(fObservers[i] as IObserver).Update(self);
end;
视图
或者这里应该是“一个视图”?用MVP框架得一个好处就是改变和增加视图不影响模型。这是一个很长简单得视图用来表示我们得列表:
TListBoxView = class(TListBox, IObserver)
private
procedure IObserver.Update = ObserverUpdate;
procedure ObserverUpdate(Subject: ISubject);
end;
和 TListModel一样,任何接口得方法可以也必须申明再类得私有部分。因为你将注意到,一般类得方法名定义得和它的接口一样;但是这里有个冲突再 TControl的Update方法和IObserver的Update方法中,解决办法就是在Update方法前加入IObserver指明。 ObserverUpdate方法就是为了解决Update的命名冲突。
procedure TListBoxView.ObserverUpdate (Subject: ISubject);
var
Obj: IListModel;
i: Integer;
begin
Subject.QueryInterface(IListModel, Obj);
if Obj <> nil then
begin
Items.BeginUpdate;
Items.Clear;
for i := 0 to Pred(Obj.Count) do
Items.Add(Obj.Item[i]);
Items.EndUpdate;
end;
end;
所有这些都是为观察者模式的观察者建立一个可视控件所必须的,也是为了实现IObserver.Update方法;这样就使得可视控件具有主观意识。
Subject参数通过ISubject指针传入这个方法,但是我们需要用TListModel来处理;第一行我们利用QueryInterface判断是否传入的Subject支
持IListModel接口。如果支持,Obj变量将设置成有效指针;如果不支持Obj将设置为nil。如果我们有一个有效Obj指针,我们现在就可以像对
待其他任何一个IListModel的引用使用Obj。
首先我们调用基于ListBox的Items字符串列表的BeginUpdate方法(避免闪烁);然后我们简单的清除字符串列表并读入ListMode中的每个Item。
当然最好紧紧更新那些已经变化了的Items,但是我已经说过这个实例将尽可能要简单!
另一视图
当我们创建我们第一个列表视图,我想我将告诉你在ListModel上创建一个选择性视图会多么简单:
TComboBoxView = class(TComboBox, IObserver)
private
procedure IObserver.Update = ObserverUpdate;
procedure ObserverUpdate(Subject: ISubject);
end;
你将看到这个控件的申明和其他可视控件的申明多么类似;这个时候我想让用户在这个List Model上选择一个Item。当我们实现推介者的时候你将知道我用这个选择条目来做什么。
procedure TComboBoxView.ObserverUpdate (Subject: ISubject);
var
Obj: IListModel;
i: Integer;
begin
Subject.QueryInterface(IListModel, Obj);
if Obj <> nil then
begin
Items.BeginUpdate;
Items.Clear;
for i := 0 to Pred(Obj.Count) do
Items.Add(Obj.Item[i]);
ItemIndex := 0;
Items.EndUpdate;
end;
end;
唯一不同之处就是Combo box视图设置它的ItemIndex属性以便这个选择了的Item在编辑框中可见。现在我将其值设置为0;这紧紧是个临时值——以后将看到。
推介者
推介者的概念主要是处理所有视图和模型之间的交互。为了根见清楚的明白推介者的作用,我将最先描述另一个参与在MVP框架中组件;这将会
导致这篇文章过长,所以我将在下期更加全面的讨论。
如果我们紧紧描述MVC框架,可能所有的事件操作者都写入控制器会比在GUI窗体上面好;这将让我们和所有交互在列表MVC组件的上下文中的列表视图处理所有的逻辑关系。当我们创建好这些MVC组件时,我们在窗体上面可以简单的放置一个视图组件,创建一个控制器并用这个可视组件为控制器的视图属性赋值。
这样做我们将拦截控制器的所有这些事件并把这个视图加入进去作为Subject模型的一个观察者。
IController = interface
['{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}']
function GetModel: IModel;
function GetView: IView;
procedure SetModel(Value: IModel);
procedure SetView(Value: IView);
property Model: IModel
read GetModel
write SetModel;
property View: IView
read GetView
write SetView;
end;
我将完整的IController的申明;当然实现类也要包括任何来自视图的事件操作者并将通过那些操作者调用模型,但是随着我们看到推介者和它相关联的操作者和交互者,还有接下来篇章中的选择模式和命令模式,我这里将不作更深的详细阐述。
结束语
在这篇文章中我们开始学习关于MVP的概念;高级oo设计模式。
MVP解决一个包含了模型,视图和推介者组成的组件的建立,并使之和选择模式,命令模式,操作者模式和交互者模式相联系。
我们建立了实现IListModel接口和ISubject接口的TListModel类。让我们可以不用通过多重继承混和不同的方法行为。
在建立TListModel中,我们注意到所有实现方法都应该放在类的私有部分中因为不能通过接口被访问他们。
视图的建立讨论了怎样获得一个标准可视控件并使它具有主观意识;我们也看到了怎样解决Observer的Update方法和TControl的Update方法的
命名冲突。
最后我们简单的接触到了推介者的概念,简单描述了控制器在MVC框架中怎样扮演它的角色。
该段落就此结束,希望你将赞同MVP在建立一个好的设计和可以轻松维护的程序中确实是最有价值的扮演者。
part 2
在第一篇文章中,我们学到了关于MVP框架的概念,高级oo设计模式。
我们创建了一个列表模型类用于实现一个列表模型接口和一个Subject接口。在设计的两个视图中,讨论了怎样获得一个标准可视控件并使它具
有主观意识;我们也看到了怎样解决Observer的Update方法和TControl的Update方法的命名冲突。
我们仅仅是接触了一点推介者的概念,简单描述了控制器在MVC框架中怎样扮演它的角色。在我们学习一些MVP架构中地其他部分之后,我们以后将更加全面地讨论推介者。
The Latest Model
我们在上章节用到的列表模型只是简单的介绍了MVP的基本概念。现在我们需要修改IListModel的版本以便在模型的所有Item中我们都可以用接口指针。
我们可以利用接口这种方式写出起连结作用的代码,尽管实现接口有许多种方式,所以我们需要用字符串做一个接口的模型,并用一个类实现它。
IString = interface
['{xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx}']
function GetAsString: string;
procedure SetAsString(const Value: string);
property AsString: string
read GetAsString
write SetAsString;
end;
TString = class(TInterfacedObject, IString, IVisited)
private
fString: string;
// IString
function GetAsString: string;
procedure SetAsString(Value: string);
// IVisited
procedure Accept(Visitor: IVisitor);
end;
现在我们需要改变列表模型接口使其可以和普通的IInterface类型兼容:
IListModel = interface
['{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}']
function GetCount: Integer;
function GetItem(Idx: Integer): IInterface;
procedure Add(const Item: IInterface);
procedure Clear;
procedure Insert(const Item, Before: IInterface);
procedure Move(const Item, Before: IInterface);
procedure Remove(const Item: IInterface);
property Count: Integer
read GetCount;
property Item[Idx: Integer]: IInterface
read GetItem;
end;
现在有了一个一般的列表模型,在这个接口里面我们可以接受任何Item类型,只要它实现IInterface。你将注意到BeginUpdate和EndUpdate方法没有出现;后面会提到:
TListModel = class(TInterfacedObject, IListModel, ISubject)
private
fItems: IInterfaceList;
fSubject: ISubject;
protected
property Items: IInterfaceList
read fItems;
// ISubject
property Subject: ISubject
read fSubject
implements ISubject;
// IListModel
procedure Add(const Item: IInterface);
procedure Clear;
function GetCount: Integer;
function GetItem(Idx: Integer): IInterface;
procedure Insert(const Item: IInterface; Before: IInterface);
procedure Move(const Item: IInterface; Before: IInterface);
procedure Remove(const Item: IInterface);
end;
这里我们申明了一个类,实现了IListModel接口和ISubject接口。这个类的主要部分和前一个基本相同,除了我们用IInterface和TInterfaceList代替了String和TStringList。在实现上这两个版本的区别是不言而喻的:
procedure TListModel.Add(const Item: IInterface);
begin
fSubject.BeginUpdate;
if fItems = nil then
fItems := TInterfaceList.Create;
fItems.Add(Item);
fSubject.EndUpdate;
end;
procedure TListModel.Clear;
begin
fSubject.BeginUpdate;
fItems.Clear;
fItems := nil;
fSubject.EndUpdate;
end;
function TListModel.GetCount: Integer;
begin
if fItems <> nil then
Result := fItems.Count
else
Result := 0;
end;
function TListModel.GetItem(Idx: Integer): IInterface;
begin
Result := fItems[Idx];
end;
procedure TListModel.Insert(const Item, Before: IInterface);
var
InsertIdx: Integer;
begin
if fItems = nil then
fItems := TInterfaceList.Create;
if fItems.IndexOf(Item) < 0 then
begin
fSubject.BeginUpdate;
InsertIdx := fItems.IndexOf(Before);
if InsertIdx < 0 then
InsertIdx := 0;
fItems.Insert(InsertIdx, Item);
fSubject.EndUpdate;
end;
end;
procedure TListModel.Move(const Item, Before: IInterface);
var
IdxItem: Integer;
IdxBefore: Integer;
MoveItem: IInterface;
begin
if fItems <> nil then
begin
fSubject.BeginUpdate;
IdxItem := fItems.IndexOf(Item);
if IdxItem >= 0 then
begin
MoveItem := fItems[IdxItem];
fItems.Delete(IdxItem);
IdxBefore := fItems.IndexOf(Before);
if IdxBefore >= 0 then
fItems.Insert(IdxBefore, MoveItem);
fSubject.EndUpdate;
end;
end;
end;
procedure TListModel.Remove(const Item: IInterface);
begin
if fItems <> nil then
begin
fSubject.BeginUpdate;
fItems.Remove(Item);
fSubject.EndUpdate;
end;
end;
控制你的Subject
你可能注意到调用BeginUpdate实际上是作用着fSubject成员。利用接口的一个好处就是你可以用一个非常有力叫做聚合的技巧用另一个类委托一个接口的实现;一个类只能被写入一次然后就可以一次一次重复使用。
在MVP种,模型通常是有一个或更多的加入其中作为观察者的视图的Subjects。我们前面已经讨论过,这意味着模型发生改变时所有这些视图都将被通告并更新他们自己。
现在我们可以为观察者模式申明一个增强的接口:
IObserver = interface
['{7504BB57-65D8-4D5D-86F1-EC8FFED8ED5E}']
procedure Update(const Subject: IInterface);
end;
ISubject = interface
['{7A5BDDA0-C40C-40E5-BAB0-BF27E538C72A}']
procedure Attach(const Observer: IObserver);
procedure Detach(const Observer: IObserver);
procedure Notify;
procedure BeginUpdate;
procedure EndUpdate;
end;
现在你看到BeginUpdate和EndUpdate在上篇文章的IListModel中现在放到了ISubject接口中;这是因为他们被用来作为Subject更新机制的一个必须部分。
注意IObserver.Update现在用IInterface作为参数比ISubject要好;这样使其更加具有通用型。
虽然IObserver要在每个需要更新的类中重新实现,ISubject可以被一个类完全通用的实现,如果它用作一个实现ISubject和其他接口类的子对象,例如TListModel。
TSubject = class(TInterfacedObject, ISubject)
private
fController: Pointer;
fObservers: IInterfaceList;
fUpdateCount: Integer;
function GetController: IInterface;
procedure Attach(const Observer: IObserver);
procedure Detach(const Observer: IObserver);
procedure Notify;
procedure BeginUpdate;
procedure EndUpdate;
public
constructor Create(const Controller: IInterface);
end;
在TSubject类中有三个明显的地方:
1、fController和GetController用来使Notify方法可以传递一个包含这个Subject委托的类的实例,比传递它本身好,因为这意味观察者对象将仅仅拥有ISubject接口的属性,比需要实际的模型更好。
constructor TSubject.Create(const Controller: IInterface);
begin
inherited Create;
fController := Pointer(Controller);
end;
如果我们注意TSubject的构造我们将看到我们强制储存一个IInterface的引用;这样避免在控制器对象上增加引用计数并让Subject在适当的时候释放。
function TSubject.GetController: IInterface;
begin
Result := IInterface(fController);
end;
GetController方法用于转换指针变量为IInterface类型并不增加引用计数。
2、fObservers简单的获取一个加入的Observers列表,是加入,分离和通知的重点。
procedure TSubject.Attach(const Observer: IObserver);
begin
if fObservers = nil then
fObservers := TInterfaceList.Create;
if fObservers.IndexOf(Observer) < 0 then
fObservers.Add(Observer);
end;
procedure TSubject.Detach(const Observer: IObserver);
begin
if fObservers <> nil then
begin
if fObservers.IndexOf(Observer) >= 0 then
fObservers.Remove(Observer);
if fObservers.Count = 0 then
fObservers := nil;
end;
end;
procedure TSubject.Notify;
var
i: Integer;
begin
if fObservers <> nil then
for i := 0 to Pred(fObservers.Count) do
(fObservers[i] as IObserver).Update(GetController);
end;
服务端:添加ADOConnection,ADOQuery1,ADOQuery2。设置ADOQuery1为主表,ADOQuery2为从表。(通过 ADOQuery2.SQL.Text:= 'select * from 从表 where FKID=:主表PKID'设置)。添加DataSetProvider1和DataSetProvider2分别连接到ADOQuery1和 ADOQuery2。 客户端:添加 ClientDataSet1,ClientDataSet2。ClientDataSet1连接到DataSetProvider1。双击 ClientDataSet1,右键Add All Field。可以看到一个TDataSetField字段。设置ClientDataSet2的DataSetField属性为 ClientDataSet1中的TDataSetField列。添加DataCourse1、DataSource2分别连接到 ClientDataSet1和ClientDataSet2控件。添加DBGrid1和DBGrid2分别连接到DataCourse1、 DataSource2。设置ClientDataSet1.Active:= True;这时发现ClientDataSet2也自动打开。 自增列处理:假如主从表的主外键都是自增列,这时可以在服务端处理主从表关联。在DataSetProvider1中的BeforeUpdateRecord事件中添加如下代码: procedure TForm1.DataSetProvider1BeforeUpdateRecord(Sender: TObject; |
DataSetProviders 的作用是把数据导出到外部世界,而ClientDataSets的功能是接收数据(并把请求和更新发送回DataSetProvider)。许多 DataSnap客户端都可以连接到单个DataSnap服务器,向服务器请求数据,而服务器往往就是数据吞吐量瓶颈发生的地方。
让我们开始吧
让我们就从这个问题的解决开始,确认线路上发送数据的真实量是很有必要的。因此,我们不会去研究(请求和接收数据的)ClientDataSets,而是要研究一下DataSetProvider组件——它会回应数据请求从而发送数据。幸运的是,DataSetProvider组件有一些有用的虚拟方法,它们能够被用来强制替代(别的方法)以及包含我们的“追踪”代码,这样你就能够了解数据吞吐的真实情况。
一旦了解了情况,你就可以开始着手降低吞吐量了。当然,你不能因为能够优化就开始进行优化;总得有个原因。但是一旦有几百个客户端连接到你的DataSnap服务器,所有客户端都会请求成兆的数据,你就要因为这个原因而立即开始优化了。
DataSnap就是原来的MIDAS
DataSnap 技术是MIDAS的新名字,它在Delphi企业版里(或者C++Builder企业版里)被用来创建多层应用程序。当我谈到DataSnap的效率时,我主要关心的是数据吞吐量的瓶颈,包括限制从服务器层发送到客户层的数据量从而防止瓶颈的方法。在你能够限制吞吐量之前,你首先要测量它。
TB42DataSetProvider
在本文的后半部分里,我会假设你有一个DataSnap服务器和客户端可用。如果你需要的话,可以在Delphi6DemosMidasMstrDtl获得一个DataSnap主从复合结构(master-detail)的示例项目。为了测量数据量,你把TdataSetProvider组件用一个叫做 TB42DataSetProvider的专用版本替换掉。新的组件有一个BytesTransferred属性,它会包含自从其被创建以来被传送的字节数量。
它会强制替代InternalGetRecords方法,以增加FbytesTransferred里的值,并报告所传送的字节数量,还会报告新的数据包已经被发送,包括记录的数目以及数据包的大小。这两个方法(见Listing A)会被ClientDataSet或者XMLBroker组件调用,用以响应DataSnap客户端的数据请求。
要注意,eBob42PRO单元里的代码会使用InternalGetRecords和CreateDataPacket方法里的简单writeln声明。这就意味着你必须把{$APPTYPE CONSOLE}这一行添加到你DataSnap服务器的主项目里,这样才能为调式的输出打开一个控制台窗口。如果你忘了做这个,那也不用担心 DataSnap服务器会向你报I/O错误了,因为IsConsole的检查会确保writeln声明只在控制台应用程序里被确实地调用。
要使用这个组件很简单:把TB42DataSetProvider组件添加到组件调色盘里的一个Delphi工具包里,并使用TB42DataSetProvider替换掉你远程数据模块上的普通TdataSetProvider。
ClientDataSet的PacketRecords
使用ClientDataSet连接到DataSetProvider(它指向整个表格)的潜在危险之一是:当你打开ClientDataSet的时候,它会缺省地从DataSetProvider请求所有的数据。这就意味着(服务器端)表格的全部内容都要通过线路送到ClientDataSet。对于 Delphi自己的示例数据库表格当然是没有问题的,举个例子的话,因为消费者和订单的表格所包含的记录会少于100条。但是,在现实生活中,公司的消费者会多于100个(而订单的数量会更多),获取所有客户(的信息)会花上一段时间,在你每次打开ClientDataSet的时候把表格的内容从 DataSnap服务器发送到DataSnap客户端也要花上一段时间。
解决这个问题的一种方法是使用ClientDataSet组件的PacketRecords属性。这个属性会对数据的初始请求进行修改,让其包含被请求数据包的大小(以记录的形式)。所以如果有上千条记录存在的话,你只用取得文件X,而文件X是PacketRecords属性的值。在缺省条件下,这个属性被设置为-1,即“把所有的记录都给我”。
1. DATASNAP 历史
作为MIDAS起始于Delphi3,Delphi4是MIDAS II,Delphi5中是MIDASIII,而后基于COM远程数据模块方式使
用TCP/IP,HTTP,(D)COM构建出强大的通讯能力.从Delphi6开始改名为DataSnap,直到D2007这个框架一直在使
用.D2009重新架构了DataSnap—移除COM依赖,使用TCP/IP以更轻量级的方式生成远程服务对象和客户端连接
能力.同时提供了与Delphi Prism2009开发的.NET程序通讯的功能.
Delphi2010中构建于D2009架构之上,并对此架构做了进一步的扩展,包括使用两个向导来创建新的
部署目标(VCL窗体,Window服务,控制台及面向Web的ISAPI,CGI或Web App Debugger).HTTP(S)传输协
议,HTTP验证,客户端回调函数,REST和JSON的支持,及使用过滤器来支持压缩和解压缩.
1.1 DATASNAP范例数据位置
本白页中我建议您使用Demo和范例来学习.虽然Delphi支持很多数据库系统,使用DBX4,ADO dbGo,或
其他数据存取技术,为了演示方便我这里使用DBX4来操作BlackfishSQL的employee.jds数据库.见
[ C:/Documents and Settings/All Users/Documents/RAD
Studio/7.0/Demos/database/databases/BlackfishSQL/employee.jds].在截图中可以看到我使用的
是Windows Vista或Win7操作系统,使用Windows Server 2008 Web编辑器来部署DataSnap ISAPI服务.
2. DATASNAP目标:如何获取数据
DataSnap2010支持三种不同的Windows方式:VCL窗体,Windows服务和控制台应用程序.本节中我们
将讨论他们的好处,不同和每种方式最适合在什么情况下使用.
下面会创建一个DataSnap服务端和客户端,我们将讲解
TDSServer,TDSServerClass,TDSTCPServerTransport,TDSHTTPService,TDSHTTPWebDispatcher和
TDSHTTPServiceAuthenticationManager组件,以及自定义的服务方法和TDSServerModule类.
将讨论不同的传输协议(TCP,HTTP)的好处及传输效率.并讨论DataSnap服务对象的不同生命期选项
(Server,Session,Invocation),及他们的效率和使用的建议.最后,讨论部署.
2.1. DATASNAP 服务端范例
在Object Repository中有两个不同的DataSnap服务向导:一个是生成基于Windows的Datasnap服务
项目,一个是生成基于WebBroker的DataSnap服务项目(需要部署到IIS或Apache).我们将会演示.
启动了Delphi2010,点击File.New.Other,你会在Object Repository中看到DataSnap服务向导中
显示的三个图标:DataSnap Server,DataSnap WebBroker Server,和Server Module.
双击第一个(后面的两个在下面的小结中讲解),弹出如下对话框:
界面中第一部分是控制项目类型的.默认可以生成可视化的带有主窗体的VCL窗体应用程序.第二个
选项是创建控制台应用程序,生成一个控制台窗口—可以用来输出请求应答信息(用Writeln语句输出
服务应用程序正在做什么).这两种方式都是为了做范例或最初部署,很少用于最终部署.由于
DataSnap架构不再基于COM,客户端将不能使服务端启动.因此为了响应客户端的请求,DataSnap服务
端应该一直在运行.如果你希望应用7X24小时全天候运行,DataSnap服务端必须同时也在运行中.对应
VCL窗体或控制台应用程序,需要一个账户登录到Windows中后才能启动DataSnap服务,背离了这种要
求.第三者选择在这时最适合:一个Windows服务应用程序,安装后配置成为自动启动,当计算机启动后
将自动运行(不需要账户登录).服务应用程序不会弹出界面,很难调试Bug.然而,为了整合这三种的优
势,我将用几分钟创建一个项目组,包括VCL窗体应用程序的DataSnap服务,控制台DataSnap服务,及
Windows服务Datasnap服务,都共享同一个自定义的服务方法,这样就可以开发一个Datasnap服务应用
程序,在需要的时候编译出三个不同类型的部署方式.
第二部分是选择使用的Datasnap服务的通讯协议.和DanaSnap2009相比,我们可以看到多了一个
HTTP通讯,及HTTP验证.为了更加灵活,这里建议选择全部选项,我们可以同时使用TCP/IP,HTTP,及使
用HTTP引入的HTTP验证.
第三部分已经为我们配置好了,如果我们要提供一个服务方法类,我们可以选择它的基
类:TPersistent,TDataModule或TDSServerModule.推荐使用最后的一个选项,可使用RTTI来启动执行
函数 (也可能你觉得使用TDataModule更合适—不操作数据库,或不使用其他非可视控件,这时使用
TPersitent也够用了).
现在是从DSServer.pas中贴出来的一小段代码,来说明TDSServerModule和
TProviderDataModule(也是继承于TDataModule)之间的关系.
TDSServerModuleBase = class(TProviderDataModule)
public
procedure BeforeDestruction; override;
destructor Destroy; override;
end;
{$MethodInfo ON}
TDSServerModule = class(TDSServerModuleBase)
end;
{$MethodInfo OFF}
当无法确定时就使用TDSServerModule选项作为基类.
2.1.1. 创建多目标项目组-- VCL 窗体项目
如上面所说,这里创建多目标的Datasnap服务项目组.首先创建一个VCL窗体应用程序作为Datasnap
服务,选择所有的通讯协议.
默认创建了一个叫做Project1.dproj的项目,并带有三个单元文
件,ServerContainerUnit1.pas,ServerMethodUnit1.pas和Unit1.pas.首先File.Save Project As保
存项目,并输入有实际意义的文件名称.将Unit1.pas保存为MainForm.pas,ServerMethodsUnit1.pas
保存为ServerMethodsUnitDemo.pas文件,保存Project1.dproj为DataSnapServer.dproj.
稍后我们将向项目组添加控制台应用程序和Window服务应用程序.首先我们来检查一下项目,并编
译工程.如果你编译DataSnapServer项目,将会出现一个错误信息(由于我们将
ServerMethodsUnit1.pas改名所致).错误原因是由于ServerContainerUnitDemo.pas单元中的
Implementation部分引用了ServerMethodsUnit1.pas单元.为了修复这个冲突,修改引用单元的文件
名称,从新编译.这是发现在第37行出现错误,使用了ServerMethodsUnit1中的TServerMethods1类型.
修改ServerMethodsUnit1为ServerMethodsUnitDemo.这时可以正确的编译项目了.
ServerContainerUnitDemo的引用部分应该向下面代码所示:
implementation
uses
Windows, ServerMethodsUnitDemo;
{$R *.dfm}
procedure TServerContainer1.DSServerClass1GetClass(
DSServerClass: TDSServerClass; var PersistentClass: TPersistentClass);
begin
PersistentClass := ServerMethodsUnitDemo.TServerMethods1;
end;
end.
2.1.1.1. SERVERCONTAINERUNITDEMO
打开ServerContainerUnitDemo单元,将会看到不少于五个组件:一个TDSServer,一个
TDSServerClass,一个TDSTCPServerTransport(用于TCP/IP通讯),一个TDSHTTPService(用于HTTP
通讯),一个TDSHTTPServiceAuthenticationManager组件(用于HTTP验证).
前面两个一直会存在,其他的三个则是根据选择的通讯协议生成的.
2.1.1.1.1. TDSSERVER
TDSServer组件只有四个属性,AutoStart,HideDSAdmin,Name和Tag.AutoStart属性默认设置为
True,意味着在窗体创建后自动启动DataSnap服务.如果将AutoStart设置为False,需要手动调用
Start方法启动服务,并调用Stop方法停止服务.可以调用Started方法验证DataSnap服务是否已经
启动.
HideAdmin属性默认设置为False.如果设置为True,连接到DataSnap服务的客户端将无法调用
Datasnap服务中的TDSAdmin类的内置方法.TDSAdmin不是一个真正的类,我们可以调用的TDSAdmin
方法定义在DSNames单元:
TDSAdminMethods = class
public
const CreateServerClasses = 'DSAdmin.CreateServerClasses';
const CreateServerMethods = 'DSAdmin.CreateServerMethods';
const FindClasses = 'DSAdmin.FindClasses';
const FindMethods = 'DSAdmin.FindMethods';
const FindPackages = 'DSAdmin.FindPackages';
const GetPlatformName = 'DSAdmin.GetPlatformName';
const GetServerClasses = 'DSAdmin.GetServerClasses';
const GetServerMethods = 'DSAdmin.GetServerMethods';
const GetServerMethodParameters = 'DSAdmin.GetServerMethodParameters';
const DropServerClasses = 'DSAdmin.DropServerClasses';
const DropServerMethods = 'DSAdmin.DropServerMethods';
const GetDatabaseConnectionProperties = 'DSAdmin.GetDatabaseConnectionProperties';
end;
TDSServer组件有五个事件:OnConnect,OnDisconnect,OnError,OnPrepare和OnTrace.我们可以
实现这五个事件来响应不同的情况,例如向日志文件中写入日志.
OnConnect,OnDisconnect,OnError和OnPrepare事件有一个继承于TDSEventObject的参数,包含
了DxContext,传输,服务和DbxConnection组件的属性,在OnConnect和OnDisconnect事件中
TDSConnectEventObject类型还包含了ConnectionProperties和ChannelInfo属性.
TDSConnectEventObject也包括了由错误引起的异常, TDSConnectEventObject还包括了我们要使
用的MethodAlias和ServerClass属性.
OnTrace事件有一个TDBXTraceInfo类型的参数.注意由于这个OnTrace事件处理程序也会包含一
些代码错误,如TDBXTraceInfo和CBRType是编译器未知的.为了解决这个问题,我们需要引用
DBXCommon单元(为识别TDBXTraceInfo类型)和DBComonTypes单元(为识别CBRType类型).
在OnConnect事件处理中,我们可以通过ChannelInfo来查看连接信息,例如(使用自定义的函数
LogInfo向日志文件中写入信息):
procedure TServerContainer1.DSServer1Connect(
DSConnectEventObject: TDSConnectEventObject);
begin
LogInfo('Connect ' + DSConnectEventObject.ChannelInfo.Info);
end;
在OnTrace事件处理程序中我们可以使用TraceInfo.Message中的信息记录服务端正在做什么.
function TServerContainer1.DSServer1Trace(TraceInfo: TDBXTraceInfo): CBRType;
begin
LogInfo('Trace ' + TraceInfo.CustomCategory);
LogInfo(' ' + TraceInfo.Message);
Result := cbrUSEDEF; // take default action
end;
注意,在客户端也可以使用连接到TSQLConnection组件的TSQLMonitor组件来跟踪DataSnap服务
端和客户端之间的通讯(在创建这个DataSnap服务的客户端时讲解).
一个跟踪日志输出如下所示:
17:05:55.492 Trace
17:05:55.496 read 136 bytes:{"method":"reader_close","params":[1,0]}
{"method":"prepare","params":[-1,false,"DataSnap.ServerMethod",
"TServerMethods1.AS_GetRecords"]}
17:05:55.499 Prepare
如你所见,TraceInfo.Message中包括了传输信息的字节数和被调用的方法名称等信息.
2.1.1.1.2. TDSSERVERCLASS
TDSServerClass组件将服务端特定的类发布给远程客户端(使用动态方法调用).
TDSServerClass组件有一个Server属性指向TDSServer组件.其他除了Name和Tag外的重要属
性是LifeCycle.默认是Session,但是也可设置为Server或Invocation.从长到短,Server,Session
和Invocation的意思是一个类的实例在服务端的生命周期为整个服务,一个DataSnap会话或一次
方法调用.Session表示每个连接将获取其自己的服务类实例.如果将其改为Invocation,将会得到
一个无状态的服务类—可用于部署CGI Web服务应用程序(其也是无状态的,每个请求都进行加载
卸载).将LifeCycle改为Server,则所有的连接请求使用一个服务类实例.这可以用于计算请求数
量,但是必须自己保证线程安全.
TDSServerClass有四个事件:OnCreateInstance,OnDestroyInstance(当实例创建和注销时触
发).OnGetClass和OnPrepare.OnPrepare事件可用于准备服务方法.使用D2009或使用D2010手动向
容器中添加TDSServerClass时,OnGetClass事件必须由我们自己实现,以便于指定一个可远程调用
的类.在D2010的向导中,已经自动为我们实现了OnGetClass事件,如下:
procedure TServerContainer1.DSServerClass1GetClass(
DSServerClass: TDSServerClass; var PersistentClass: TPersistentClass);
begin
PersistentClass := ServerMethodsUnitDemo.TServerMethods1;
end;
注意:当我们重命名了自动生成的代码单元ServerMethodsUnit1为
ServerContainerUnitDemo.pas后必须修改这里.
2.1.1.1.3. TDSTCPSERVERTRANSPORT
TDSTCPServerTransport组件负责在DataSnap服务端和客户端进行通讯,使用TCP/IP协议.
TDSTCPServerTransport组件有五个重要的属性:BufferKBSize,Filters(D2010新特
性),MaxThreads,PoolSize,Port和Server.
BufferKBSize属性指定通讯缓冲区大小,默认设置为32KB.Filters属性可以包含一个传输过滤
器集合,将在第四节讲解.MaxThreads属性定义最大线程数(默认为0不限制).PoolSize可用于连接
池(如果修改了这里,也需要相应的修改DataSnap客户端).
Server属性指向TDSServer组件.TDSTCPServerTransport组件没有事件.
2.1.1.1.4. TDSHTTPSERVICE
TDSHTTPService组件负责使用HTTP协议组织DataSnap服务端和客户端通讯.
TDSHTTPService组件有十个属性(除了Name和
Tag):Active,AuthenticationManager,DSHostName,DSPort,Filters,HttpPort,Name,RESTContext,Server,和只读的ServerSoftware属性.
Active属性指定DSHTTPService开始侦听请求.可以在设计时设置,但是这会影响DataSnap服务
在运行时启动(由于DSHTTPService组件侦听了同一个端口—在设计时启动侦听,在运行时就不能
再启动一个侦听了).最好的方式在在TServerContainer的OnCreate事件中激活TDSHTTPService:
procedure TServerContainer1.DataModuleCreate(Sender: TObject);
begin
DSHTTPService1.Active := True;
end;
AuthenticationManager属性用于定义处理HTTP验证的管理组件,这里指向了
TDSHTTPServiceAuthenticationManager组件.这个组件在下节详述.
DSHostName和DSPort属性用于定义DataSnap服务端连接,但只有在没有指定Server属性时生效.
通常都是使用Server属性.
Filters属性可以包含一系列传输过滤器,在第四节详述.
HttpPort属性定义DSHTTPService组件侦听的特定端口以响应连接.注意这个属性默认是80端口,
通常在发布时必须做修改(IIS等Web服务已占用了80端口).
RESTContext属性指定REST上下文URL,这样就可以以REST服务的方式调用DataSnap服务.默
认,RESTContext属性设置为rest,我们可以用http://localhost/datasnap/rest/...来调用服务.
在第六节详述REST,JSON和客户端回调函数.
最后,Server属性指向同一个容器中的TDSServer组件.如果没有指定Server属性,也可以使用
DSHostName和DSPort属性连接到使用TCP的DataSnap服务.当设置了Server属性,DSHostName和
DSport属性失效.
TDSHTTPService组件有五个事件:四个是REST相关的,一个是跟踪事件,REST相关事件将在第六
节详述.
OnTrace事件可用于跟踪对DSHTTPService组件的调用,例如:
procedure TServerContainer1.DSHTTPService1Trace(Sender: TObject;
AContext: TDSHTTPContext; ARequest: TDSHTTPRequest;
AResponse: TDSHTTPResponse);
begin
LogInfo('HTTP Trace ' + AContext.ToString);
LogInfo(' ' + ARequest.Document);
LogInfo(' ' + AResponse.ResponseText);
end;
注意HTTP跟踪信息只有当客户端使用HTTP连接到服务端是才会触发(默认使用TCP/IP协议).
跟踪输出如下所示:
17:05:55.398 HTTP Trace TDSHTTPContextIndy
17:05:55.400 /datasnap/tunnel
17:05:55.403 OK
从中可见,AContext设置为TDSHTTPContextIndy,ARequest设置为/DataSnap/tunnel,AResponse
设置为OK.
2.1.1.1.5. TDSHTTPSERVICEAUTHENTICATIONMANAGER
当选中HTTP通讯协议的验证复选框后,TDSHTTPServiceAuthenticationManager组件将自动出现
在服务容器中.也可手动添加到服务容器中,当然TDSHTTPService组件的AuthenticationManager
属性必须指向TDSHTTPServiceAuthenticationManager组件.
TDSHTTPServiceAuthenticationManager组件有一个事件:OnHTTPAuthenticate事件,可以验证
Datasnap客户端到服务端连接的HTTP信息.
procedure TServerContainer1.DSHTTPServiceAuthenticationManager1HTTPAuthenticate(
Sender: TObject; const Protocol, Context, User, Password: string;
var valid: Boolean);
begin
if (User = 'Bob') and (Password = 'Swart') then
valid := True
else
valid := False
end;
当然,你可以使用数据库技术来扩展验证方式.客户端最好使用HTTPS方式将用户名和密码等
HTTP验证信息发送到服务端,所以我希望易博龙可以在现有HTTP和TCP/IP基础上在添加一个HTTPS
协议.
HTTPS可以确保连接安全和数据包加密, 数据包被窃取也不会泄露用户和密码信息.可与你所在
域的ISP或Web管理员协商是否有可能使用HTTPS—--强烈推荐(如我使用的
是https://www.bobswart.hl/).
DataSnap服务应用程序另一个优势是可将HTTP验证信息(所用协议和上下文信息)记录下来.以
便于查找谁登陆过,谁试图登录(如做欺骗登录操作试图获取DataSnap服务的权限).
2.1.1.2. SERVERMETHODSUNITDEMO
注意我们已经检查了ServerContainerUnitDemo.pas单元,现在看一下DataSnap服务的另外重要
的单元:ServerMethodsUnitDemo.pas单元.在新建DataSnap服务对话框中我们指定了
TDSServerModule类作为基类,因此TServerMethods1类型继承于TDSServerModule(其继承于
TDSServerModuleBase,又继承于TProvideDataModule,添加一个析构函数和BeforeDestruction过
程. TProvideDataModule继承于正常的TDataModule,增加了服务提供者的能力 更多信息见后
面).
由于TServerMethods1继承于TDataModule,设计时可以看到一个数据模块的可视区域.我们可以
向其中添加一下不可视组件,如数据存取控件等.在第三节将操作数据库,现在保持设计区为空,仅
向TServerMethods1类添加方法.
如果不想向项目组添加其他类型的项目—DataSnap控制台应用程序及DataSnap Windows服务应
用程序,可跳到2.1.4节查看实现服务方法.
2.2. DATASNAP 客户端
第一个DataSnap服务端范例启动并侦听请求,现在创建一个客户端.本节中,我们将讲解如何在
客户端连接服务端,如何导入方法生产服务类.
确信DataSnap服务已启动,我们创建一个DataSnap客户端应用程序项目.为了在设计时方便切换
DataSnap服务项目和DataSnap客户端项目,将客户端项目添加到同一个项目组.任何类型的项目都
可作为DataSnap客户端项目,这里我们选择VCL窗口应用程序,保存为DataSnapClient.dpr,主窗体
为ClientForm.pas.控件栏中DataSnap服务目录中含有六个DataSnap(服务)组件,DataSnap客户端
目录中不含我们现在需要的组件.
DataSnap客户端目录中的组件都是一些老的DataSnap组件,可以使用,但不推荐.但可使用新
的TDSProviderConnect组件,可以在新的DataSnap客户端上连接老的DataSnap服务(3.2中详述).
除了DataSnap客户端目录,我们还要看看dbEpress目录,可以找到一个新的组件叫做
TSQLServerMethod(注意:在下一个截图中这个新组件很容易发现,其用TSql前缀替代了TSQL前
缀).
TSqlServerMethod组件可用于调用DataSnap服务的远程方法,但首先需要连接到DataSnap服务.
可以使用TSQLConnect组件建立连接—--不在使用原来的TXXXConnection组件.为了灵活
性,TSQLConnection组件的Drive属性下拉框中有个新选项:DataSnap.在ClientForm上放置一个
TSQLConnection组件,设置其Driver属性为DataSnap.Driver属性将变成可展开的对象,展开后如
下图:
注意:默认CommunicationProtocol属性为空,TSQLConnection将使用TCP/IP作为通信协议(端口
211).BufferKBSize为32(KB),端口设置为211(与服务端设置相同).实际应用中一般将端口号设置
为其他端口(同时修改服务端客户端),因为211端口是DataSnap默认端口不安全.
HostName,UserName和Password用于连接到DataSnap服务.在本地测试时,将HostName设置为
localhost,通常可以设置为服务器名称,DNS或IP地址.
不要忘记将TSQLConnection的LoginPropt属性设置为False,否则将会在连接的时候弹出登录窗
口.
一旦TSQLConnection的Driver属性设置好,就可以设置Connected属性为True去连接DataSnap服
务端.注意这时DataSnap服务必须运行才能连接成功.
2.2.1. DATASNAP客户端类
在确定连接正常后,右击TSQLConnection组件,选择Generate DataSnap Class选项,将生成一个
新单元,默认叫做Unit1,含有一个叫做TServerMethods1Client的类(在DataSnap服务方法类名称
后加了一个Client).保存单元为ServerMethodsClient.pas.
这个TServerMethods1Client类如下所示
type
TServerMethods1Client = class
private
FDBXConnection: TDBXConnection;
FInstanceOwner: Boolean;
FEchoStringCommand: TDBXCommand;
FServerTimeCommand: TDBXCommand;
public
constructor Create(ADBXConnection: TDBXConnection); overload;
constructor Create(ADBXConnection: TDBXConnection;
AInstanceOwner: Boolean); overload;
destructor Destroy; override;
function EchoString(Value: string): string;
function ServerTime: TDateTime;
end;
如你所见,TServerMethods1Client类有两个构造方法,一个析构方法和两个我们在服务端定义
的服务方法.
为使用这些方法,将ServerMethodsClient单元引用到ClientForm,在客户端窗体上放置一个按
钮,在按钮的OnClick事件中写如下代码:
procedure TForm2.Button1Click(Sender: TObject);
var
Server: TServerMethods1Client;
begin
Server := TServerMethods1Client.Create(SQLConnection1.DBXConnection);
try
ShowMessage(DateTimeToStr(Server.ServerTime))
finally
Server.Free
end;
end;
这个代码将创建一个TServerMethods1Client类实例,然后调用ServerTime服务类,最后释放这
个DataSnap服务的代理对象.
点击按钮将弹出对话框显示服务端的时间.
同样方法测试EchoString.
2.2.1.1. HTTP COMMUNICATION PROTOCOL
注意我已经提到TSQLConnection组件以TCP/IP作为默认通讯协议.这样我们就看不到任何HTTP
跟踪信息(在2.1.1.1.4小结中定义).然而,很容易修改通讯协议,仅需在TSQLConnection的Driver
属性中的CommunicationProtocal子属性中输入HTTP即可.注意这是我们还要修改Port属性,因为
211被TCP/IP占用,同时要修改服务端的TDSHTTPService组件的端口号.
修改后运行DataSnap客户端,会出现如下错误:
解决方法是在DataSnap客户端向ClientForm中添加DSHTTPLayer单元引用.
2.2.1.2. HTTP 验证
使用HTTP通讯协议的好处之一是可以使用其包含的HTTP验证.由DataSnap服务端的
TDSHTTPServiceAuthenticationManager组件所支持(详见2.1.1.1.5).
如果实现了OnHTTPAuthenticate事件处理,将会核对HTTP验证.我们必须保证输入正确的信息才
能确保TDSHTTPServiceAuthenticationManager验证通过.否则将会得到一个HTTP/1.1 401未验证
错误.
HTTP检验从客户端传递到服务端的用户名和密码,及其他TDSHTTPServiceAuthentication特定
信息,我们需要在DataSnap客户端的TSQLConnection控件中填写DSAuthUser和DSAuthPassword属
性.
注意我们也需要指定HostName的值,除非是在同一台电脑上测试.
2.3. DATASNAP服务部署
范例的服务端和客户端在同一台电脑上运行良好,但是实际环境中,DataSnap服务将运行在服务
器上,一或多台客户端通过网络连接服务端.服务端程序通常部署在没有安装Delphi的电脑上.这
种情况下就需要考虑不用运行时包来编译DataSnap,而仅生成一个大的可执行文件.由于我们还没
有使用任何数据操作组件,也不需要任何的其他数据库驱动或DLL文件.
2.3.1. DATASNAP 客户端部署
假设客户端与服务端运行在不同的电脑上,我们必须保证客户端可以连接到服务端.为了保证这
点,客户端的TSQLConnection组件不能仅仅指定Driver属性中的CommunicationProtocol和端口,
还需要指定HostName属性.设置IP地址或DNS.例如连接我的DataSnap服务的HostName属性
为http://www.bobswart.nl/.(注意不需要指定http://前缀,因为在CommunicationProtocol属性中指定了
通讯协议).
3. DATASNAP和数据库
除了使用Delphi2010 DataSnap框架创建简单的服务方法,我们还可以在服务端添加数据库操作,
实现多层数据库应用—DataSnap服务连接到数据库,但DataSnap客户端是瘦客户端,不含任何数据
库驱动.
我们同样可以快速创建数据库操作的DataSnap范例,只使用SQLConnection组件和已创建的客户
端类.也可使用另外两个DataSnap组件TsqlServerMethod和TDSProviderConnection.
首先,我们要确保服务端公布了一个数据集,打开ServerMethodsUnitDemo并在数据模块中添加
一个TSQLConnection组件.连接TSQLConnection组件到数据库和表(本例我连接到BlackFishSQL的
Employees表).将TSQLConnection组件添加到数据模块的设计区域.设置Driver属性为
BlachFishSql.然后设置Database属性为employee.jds文件的路径: C:/Documents and Settings/All
Users/Documents/RAD Studio/7.0/Demos/database/databases/BlackfishSQL on Windows
XP, or C:/Users/Public/Documents/ RAD Studio/7.0/Demos/database/databases/BlackfishSQL.
将TSQLConnection的LoginPrompt属性设置为False.设置Connected属性为True验证是否能正确连
接.
下一步,添加一个TSQLDataSet组件,将其SQLConnection设置为TSQLConnection组件.
设置其CommandType为ctQuery,双击CommandText属性输入SQL语句.
SELECT EMP_NO, FIRST_NAME, LAST_NAME, HIRE_DATE, JOB_COUNTRY FROM EMPLOYEE
确保在设计时设置了TSQLConnection组件的LoginPrompt和Connected属性为False,及
TSQLDataSet的Active属性为False.
现在在ServerMethodsUnitDemo单元的TServerMethods1类中添加一个public方法返回一个
TSQLDataSet组件.
type
TServerMethods1 = class(TDSServerModule)
SQLConnection1: TSQLConnection;
SQLDataSet1: TSQLDataSet;
private
{ Private declarations }
public
{ Public declarations }
function EchoString(Value: string): string;
function ServerTime: TDateTime;
function GetEmployees: TDataSet;
end;
如你所见,TServerMethods1类中定义了一个叫做GetEmployees的方法,实现如下:
function TServerMethods1.GetEmployees: TDataSet;
begin
SQLDataSet1.Open; // make sure data can be retrieved
Result := SQLDataSet1
end;
重新编译服务并运行.注意如你关闭了服务但无法重新编译,可能是DataSnap服务还在运行.
3.1. TSQLSERVERMETHOD
回到DataSnap客户端,TSQLConnection组件应该已经断开到DataSnap服务端的连接(如果仍然连
接,说明你没有重新编译服务端).重新将Connected设置为True,从服务端更新信息,并重新从服务
端生成客户端类(使用右键菜单).
为了重写原来的ServerMethodsClient单元,推荐从项目中移除原来的ServerMethodsClient后
在选择’Generate DataSnap Client Classes’方法.保存了新的单元文件后不需要修改客户端代
码.生成了新文件后,可以从中看到GetEmplorees方法.
type
TServerMethods1Client = class
private
FDBXConnection: TDBXConnection;
FInstanceOwner: Boolean;
FEchoStringCommand: TDBXCommand;
FServerTimeCommand: TDBXCommand;
FGetEmployeesCommand: TDBXCommand;
public
constructor Create(ADBXConnection: TDBXConnection); overload;
constructor Create(ADBXConnection: TDBXConnection;
AInstanceOwner: Boolean); overload;
destructor Destroy; override;
function EchoString(Value: string): string;
function ServerTime: TDateTime;
function GetEmployees: TDataSet;
end;
为使用GetEmployees方法获取TDataSet,我们可以使用TsqlServerMethod组件.将
TsqlServerMethod组件添加到客户端窗体,设置其SQLConnection属性为SQLConnection1,然后打
开ServerMethodName下拉框显示可用的方法:一些DSAdmin方法(可通过设置TDSServer的
HideDSAdmin属性为True禁止),接下来是三个DSMetaData方法,七个TServerMethods.As_XXX方法
(由原来的IAppServer提供),和最后我们自己的TServerMethods1的方法:EchoString,ServerTime,
和GetEmployees方法.
本例我们选择TServerMethods1.GetEmployees作为ServerMethodName属性的值.根据
ServerMethodName属性值,SqlServerMethod控件获取查询结果记录集.
我们使用TDataSetProvider,TClientDataSet和TDataSource来使数据显示在TDBGrid中.
在客户端添加一个TDataSetProvider控件,将其DataSet设置为SqlServerMethod控件.下一步添
加一个TClientDataSet控件,将其ProviderName设置为DataSetProvider控件.设置其
RemoteServer属性为空—--这是用于原有DataSnap的方法,在新的DataSnap架构中不再使用.
最后,放一个TDataSource控件,设置其DataSet为TClientDataSet控件,然后就可以放TGBGrid和
TDBNavigator控件了.设置其DataSource为TDataScource组件,将可以在客户端窗体中看到数据.
为了在设计时验证连接,我们设置ClientDataSet的Active属性为True.将触发SqlServerMethod
的Active为True(开始检索数据,然后自动将Active设置为False),同时会将
TSQLConnection.Connected设置为True连接到服务端.
这种方式连接一直保持,TCliendtDataSet和TSQLConnection的被激活,数据显示在TDBGrid中.
这种方式很容易以只读方式查看数据.注意这里说只读,因为TSQLServerMethod不允许
TDataSetProvide-TClientDataSet组合将客户端的数据变更传回服务端.
TSQLServerMethod是轻量级方式连接获取只读数据.这样,如果你需要获取DataSnap服务器上的
数据但不需要做修改,当前这种发布一个返回TSQLDataSet结果集方法的架构就很好.
然而,有时我们需要更新数据,这时就必须使用不同的方法获取数据.
3.2. TDSPROVIDERCONNECTION
如果我们想提交更新,我们就需要TDSProviderConnection组件与服务端的TDataSetProvider关
联,因此我们不但可以读取数据也可以修改数据.
首先,我们需要修改服务端数据模块,现有的只是返回一个TDataSet,我们必须添加一个实际的
TDataSetProvider,确保将其从DataSnap服务端发布到客户端.所以,回到ServerMethodsUnitDemo
单元放一个TDataSetProvider组件,设置其DataSet属性为TSQLDataSet.应该将TDataSetProvider
重命名,如dspEmplyees;
现在,重新编译DataSnap服务,运行.然后修改客户端.
3.2.1. TDSPROVIDERCONNECTION 客户端
为检索到发布的TDataSetProvider组件需要修改一下DataSnap客户端.将TSQLServerMethod和
TDataSetProvider组件从客户端窗体删除,添加一个TDSProviderConnection组件.设置其
SQLConnection属性为TSQLConnection组件,连接到DataSnap服务.我们也需要设置一个
ServerClassname属性值(很不幸不能选择只能输入).现在只能手动输入TDSServerModule的名字,
这里是TServerMethods1.
在前一个例子中,TClientDataSet只设置了一个ProviderName属性.然而,使用
TDSProviderConnection组件,我们必须首先设置其RemoteServer属性为TDSproviderConnection
组件,然后设置ProviderName属性(这个属性还为原来设置的DataSetProvider1,现在设置为
dsEmployeer----在服务端发布的TDataSetProvider组件的名称).
在ProvideName属性的下拉框中显示dspEmployees选项(在服务端的ServerDataModule单元中发
布的名称).
现在我们可以设置TClientDataSet.Active为True,在设计时查看数据.
设置TClientDataSet.Active为True,同时将TSQLConnection.Connected置为True.注意在设计
时最好不要将这两个属性设置为True.首先,如果你在IDE中打开DataSnap客户端项目,将会试图连
接服务端,如果服务端没开启将失败.其次,如果在运行时启动应用程序,并且连接不可用,应用程
序将会抛出异常.从而应用程序不能使用本地数据,导致远程连接无效将不能使用应用程序.
最好的方式是使用菜单选项或按钮明确的设置TSQLConnection组件连接,TClientDataSet组件
进行获取数据.并提交包括username/password的信息,将在后面说明.现在确保
TClientDataSet.Active属性设置为False,同时TSQLConnection.Connected属性为False.在客户
端窗体中放一个按钮,在OnClick事件中明确的打开TClientDataSet.
procedure TForm2.Button2Click(Sender: TObject);
begin
ClientDataSet1.Open;
end;
现在添加代码提交数据变更,保存回服务器.
3.2.2 数据库更新
有两种方法将数据修改保存会服务端:自动和手动.都是调用一下方法,但是会自动调用或手动
调用,各有优缺点.
对于自动方式,我们可以使用TClientDataSet的数据修改时触发的OnAfterInsert,OnAfterPost
和OnAfterDelete事件.在事件处理程序中,实现很简单,调用TClientDataSet的ApplyUpdates方法,
发送变更,将Delta包发送到服务端保存回数据库.
procedure TForm2.ClientDataSet1AfterPost(DataSet: TDataSet);
begin
ClientDataSet1.ApplyUpdates(0);
end;
如果发生了更新错误,将会触发TClientDataSet的OnReconcileError事件,更多信息见3.2.3
手动方式发生更新也是使用TClientDataSet的ApplyUpdates方法.但是这时方法不在
OnAfterInsert,OnAfterPost和OnAfterDelete事件中执行.而是我们添加一个按钮让用户显示的提交更
新.
procedure TForm2.btnUpdateClick(Sender: TObject);
begin
ClientDataSet1.ApplyUpdates(0);
end;
自动提交的好处当然用户不会忘记将变更保存回服务端.然而,缺点是无法提供Undo能力.一旦
提交数据就更新回了服务器.另外,如果使用手动提交,所有变更保存在客户端---TClientDataSet
组件的内存中.这样就允许用户Undo部分变更:使特定记录或全部记录放弃更新.点击更新按钮显
示调用ApplyUpdate方法.可能会导致用户忘记提交修改数据.我们应该在窗口关闭时添加代码检
查TClientDataSet中是否还有未提交数据(检查TClientDataSet.ChangeCount属性).
3.2.3. RECONCILE ERRORS
TClientDataSet.ApplyUpdates方法有一个参数:应用更新时允许发生的最大错误数量.如果有
两个客户端连接到了DataSnap服务端,获取Employees数据并同时修改了第一行数据.依据目前为
止我们的实现,两个客户端都会使用TClientDataSet的ApplyUpdates将数据变更到DataSnap服务
端.如果都将ApplyUpdates的参数MaxErrors设置为0,则第二个客户端的提交将会停止.第二个客
户端应该使用一个大于0的参数指定允许的错误/冲突数.然而,即使第二个客户端将MaxErrors设
置为-1(不管有多少错误发生都继续提交后面的更新记录),都不会提交被第一个用户更新过的记
录.换句话说,你需要执行一系列冲突处理来解决这些更新已经被更新的记录或列的冲突问题.
幸运的是,Delphi提供了一个很强大的对话框来处理这个问题.当在DataSnap客户端需要做一些
冲突处理时,都可以使用这个对话框(或自己实现,但最终都是处理冲突问题).
使用Delphi提供的功能,File.New.Other,在Delphi文件子目录中选择Reconcile Error对话框
图标.
选中这个图标点击OK,保存为RecError.pas,加入到DataSnapClient项目.这个单元包括了定义
和实现更新错误对话框.来解决数据库更新错误.
ReconcileErrorForm窗体实例将按需要动态创建.那么如何使用这个特殊的ReconcileErrorForm窗体呢?
好,其实很简单.对于每个没有成功更新的记录,都会触发TClientDataSet的OnReconcileError事件.定义如
下:
procedure TForm2.ClientDataSet1ReconcileError(DataSet: TClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
这个事件处理程序与四个参数,第一个是抛出错误的TClientDataSet,第二个参数是引发错误冲
突的原因,第三个参数是更新类型UpdateKind(insert,delete,modify),第四个参数是你要如何处
理冲突.可以返回如下枚举类型值:
- raSkip:不更新这条记录,但在变更日志中保留未提交的变更,下次提交在试.
-raAbort:取消记录冲突处理..
-raMerge:将更新记录与远程数据库记录合并,仅在客户端变更修改过的远程字段
-raCorrect:使用正确的值替换更新记录,这需要用户介入.
-raCancel:对本记录的修改全部放弃.回到初始值状态.
-raRefresh:对本记录修改全部放弃,但重新加载当前数据库的记录值.
关于ReconcileErrorForm不需要考虑全部执行选项.只需要做两件事件.一,在DataSnap客户端主窗体中
引用错误处理对话框单元.二,在OnReconcileError事件中写一行代码调用ReconcileErrorForm单元中的
HandleReconcileError全局函数. HandleReconcileError函数也有四个同样的参数,只需要按顺序传递即可.
如下所示:
procedure TFrmClient.ClientDataSet1ReconcileError(DataSet: TClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
begin
Action := HandleReconcileError(DataSet, UpdateKind, E)
end;
3.2.4. 示范冲突错误
现在最大的问题是:实际工作中如何使用的冲突处理?为了测试,需要两个或更多DataSnap客户
端同时运行.为使用当前的客户端和服务端进行测试,需要执行如下步骤:
-启动服务端应用程序
-启动第一个客户端应用程序,点击链接按钮,获取数据
-启动第二个客户端应用程序,点击链接按钮,获取数据
-使用第一个客户端应用程序,修改第一行数据的FirstName列
-使用第二个客户端应用程序,修改第一行数据的FirstName列
-在第一个客户端应用程序中点击更新按钮
-在第二个客户端应用程序中点击更新按钮,这时将会发生一个或多个错误.因为第一个应用程
序已经修改了同一行的同一个列.引起冲突. OnReconcileError被触发.
-进入更新错误对话框,现在可以处理冲突(忽略(Abort),取消(Abort),合并(Merge),更正(Correct),取消
(Cancel),更新(Refresh)).测试一下Skip和Cancel的不同,及Correct,Refresh和Merge的不同.
Skip移动到下一行记录,忽略更新请求.但其更新将保留在更新日志中.Cancel也忽略更新请求
同时清除本记录所有以前的更新记录.
Refresh清除本记录所有的变更记录,并将数据库中的值作为当前记录的值.Merge试图将数据库
记录和更新记录合并.将变更提交到数据库.更新和合并的记录都不会再进行处理,记录已经与数
据库同步.
Correct是一个强大的选项,在事件处理中给你一个指定更新记录值的机会.需要写代码或弹出
对话框指定新值.
3.3. DATASNAP 数据库部署
部署一个使用数据库的DataSnap服务需要比部署一个简单DataSnap服务的步骤要多些.客户端,
没什么变化,还是一个瘦客户端,如将MidasLib加入到了项目引用就仅需部署一个单一的可执行文
件.
服务端,必须部署数据库驱动.及所选数据库依赖的驱动和文件.使用DBX4,确保发布
TSQLConnection组件和dbxconnections.ini及dbxgrivers.ini文件(可在C:/Documents and
Settings/All Users/ Documents/RAD
Studio/dbExpress/7.0 directory on Windows XP or in the C:/Users/Public/Documents/RAD
Studio/dbExpress/7.0中找到). dbxdrivers.ini文件指定所用驱动, DriverPackageLoader及
MetaDataPackageLoader(通常指向同一个包).对于BlackFishSQL,使用的DBXClientDriver140.bpl文件,需要
部署到服务端.更多关于部署BlashFishSQL信息见RAD Studio/7.0 目录下的deploy_en.htm文件.
3.4. 重用已有的远程数据模块
如果你有一个远程数据模块类,也可以将其组合到新的DataSnap项目中来.但是必须要牺牲一些特性,尤其
是引入了COM.
首先,如果有一个你要迁移的DataSnap服务应用程序,而不仅仅是一个远程数据模块,你需要使用命令行
/unregister注销DataSnap服务.不做这步将无法注销远程数据模块.在远程数据模块单元,移除initialization区
域.如果还希望这个单元在D2007及一下版本重用,可以使用编译开关:
{$IF CompilerVersion >= 20}
initialization
TComponentFactory.Create(ComServer, TRemoteDataModule2010,
Class_RemoteDataModule2010, ciMultiInstance, tmApartment);
{$IFEND}
end.
从项目中移除UpdateRegistry函数或用编译开关修饰:
{$IF CompilerVersion >= 20}
class procedure UpdateRegistry(Register: Boolean;
const ClassID, ProgID: string); override;
{$IFEND}
最重要的变更—将项目转换为无COM依赖的DataSnap服务.-及移除类型库(.ridl文件)和类型库导入单元.这
无法通过编译器开关修改,因此需要为D2007及一下版本和D2009及以上版本分别生成一个项目文件.在
TRemoteDataModule类中放一个TDSServerClass组件.最后,将所有的自定义方法都转到
TRemoteDataModule单元的public节中.
4. DATASNAP 过滤器[FILTER]用法
本节将说明过滤器工作原理,及如何使用已存在的过滤器(如压缩)或创建新的DataSnap过滤器.DataSnap
过滤器是一个特殊的DLL,拦截通讯流,在整个过滤器链中操作通讯流.所以本例中我们可以使用压缩和解压缩
过滤器,或记录压缩等.
必须在客户端和服务端指定过滤器.在服务端,必须指定TDSTCPServerTransport组件的过滤器属性列表.
在客户端,必须在客户端项目中引用过滤器单元文件.对于客户端这就足够了,因为每个DataSnap过滤器都会
自动注册.
当处理OnConnect事件时,可以检查用于连接的已注册的过滤器,例如使用自定义的日志函数输入日志信息,
procedure TServerContainer1.DSServer1Connect(
DSConnectEventObject: TDSConnectEventObject);
var
i: Integer;
begin
LogInfo('Connect ' + DSConnectEventObject.ChannelInfo.Info);
for i:=0 to DSConnectEventObject.Transport.Filters.Count-1 do
LogInfo(' Filter: ' +
DSConnectEventObject.Transport.Filters.GetFilter(i).Id);
end;
4.1. ZLIBCOMPRESSION FILTER
作为范例,我们使用已随D2010提供的DataSnap过滤器.可用于在客户端和服务端压缩数据流.这里说的
ZlibCompression过滤器可以在DbxCompressionFilter单元找到.
TDSTCPServer和TDSHTTPService组件都有一个TTransportFiltersCollection类型的Filters属性.点击Filters
属性后面的按钮,编辑过滤器列表.在这个对话框中,我们可以加一个新的TTransportFilterItem,然后在Object
Inspector中设置FilterID和一些属性.在下拉框中可以找到Delphi2010提供的ZLibCompression过滤器.
注意除了设置服务端TDSTCPServerTransport组件Filters属性外,也需要在客户端指定一个相应的过滤器
(压缩请求解压缩应答).这时,我们进需要将DbxCompressionFilter单元引用到ClientForm.其将自动注册一个
TTransportCompressionFilter并与服务端通讯.
如果没有在客户端添加DbxCompressionFilter单元引用,运行客户端后将会抛出异常信息:
4.2. LOG FILTER
Delphi 2010 DataSnap 允许自定义传输过滤器.我们可以从TTransportFilter 类型继承自己的类.在这个新
类中,可以重写基类中的方法,实现这些方法.例如我们创建一个TLogFilter类:
unit LogFilter;
interface
uses
SysUtils, DBXPlatform, DBXTransport;
type
TLogFilter = class(TTransportFilter) ;
private
protected
function GetParameters: TDBXStringArray; override;
function GetUserParameters: TDBXStringArray; override;
public
function GetParameterValue(const ParamName: UnicodeString): UnicodeString; override;
function SetParameterValue(const ParamName: UnicodeString;
const ParamValue: UnicodeString): Boolean; override;
constructor Create; override;
destructor Destroy; override;
function ProcessInput(const Data: TBytes): TBytes; override;
function ProcessOutput(const Data: TBytes): TBytes; override;
function Id: UnicodeString; override;
end;
const
LogFilterName = 'Log'
这个类的很多方法实现都是空的:由于仅仅用于记录ProcessInput和ProcessOutput方法传输的数据,很多方
法都不用实现.非空方法如下:
function TLogFilter.SetParameterValue(const ParamName, ParamValue: UnicodeString): Boolean;
begin
Result := True;
end;
constructor TLogFilter.Create;
begin
inherited Create;
end;
destructor TLogFilter.Destroy;
begin
inherited Destroy;
end;
function TLogFilter.ProcessInput(const Data: TBytes): TBytes;
begin
Result := Data; // log incoming data
end;
function TLogFilter.ProcessOutput(const Data: TBytes): TBytes;
begin
Result := Data; // log outgoing data
end;
function TLogFilter.Id: UnicodeString;
begin
Result := LogFilterName;
end;
最后,重要的实现部分是在initialization和finalization中注册DataSnap传输过滤器.确保客户端可以找到这个传
输过滤器,并在请求时自动使用.
initialization
TTransportFilterFactory.RegisterFilter(LogFilterName, TLogFilter);
finalization
TTransportFilterFactory.UnregisterFilter(LogFilterName);
end.
为了在DataSnap服务端使用这个传输过滤器,我们需要将其加入到TDSTCPServer或DSHTTPService组件
的Filters属性中,非常简单.在设计时,已知存在一个ZLibCompression过滤器,但无法感知新的过滤器(除非将其
添加到设计时包中并安装).幸运的是我们也可以在运行时添加过滤器,在ServerContainerUnitDemo单元中引用
过滤器单元,然后手动向Filters属性中添加过滤器.如.
procedure TServerContainer1.DataModuleCreate(Sender: TObject);
begin
DSTCPServerTransport1.Filters.AddFilter(LogFilterName);
DSHTTPService1.Filters.AddFilter(LogFilterName);
DSHTTPService1.Active := True;
end;
这将确保服务端使用LogFilter,客户端只要在单元中引用LogFilter单元就会自动使用LogFilter.否则将抛出
错误信息:
注意DataSnap客户端和服务端将获取各自的logfile实例.虽然使用的同一个过滤器,但是却不需用
ParamStr(0)来区分日志信息.
4.3. 加密过滤器
即使是一个如4.2中的一个简单的过滤器,也需要自己去扩展,很复杂.DataSnap提供的过滤器不太完整.事实
上,有大量的三方过滤器可以使用,有Daniele Teti开发的DataSnap Filters Compendium可以
在http://www.danieleteti.it/?p=168中获取,其中不少于9个用于DataSnap2010的附加过滤器,分成三个
组.Hash组支持MD5,MD4,SHA1和SHA512,Cipher组支持Blowfish,Rijndael, 3TDES 和 3DES, Compress
组支持LZO.并且是全源码版.
5. 如何构建DATASNAP WEB项目
除了生成Windows项目,还提供了生成ISAPI,CGI或Web App Debugger目标项目的向导.首先我们讨论
一下每种项目类型的优缺点,并展示如何在一个项目组中同时创建出这三个项目,并让他们共享公共的单元
文件.这样我们就可以为同一个DataSnap项目产生三个不同的部署目标.
虽然到目前为止我们构建的DataSnap服务应用程序运行良好.但是在有些情况下就无法部署服务程序.例
如如果你不能或不允许在防火墙中打开请求的端口让客户端连接服务器.幸运的是,这种情况下我们可以使
用Web服务来部署,而80端口号一直都是打开的.如果我们使用IIS来作为Web服务器,我们就可以使用新的
DataSnap WebBroker应用程序向导来创建一个可部署在IIS上的应用.
DataSnap WebBroker应用程序向导提供了三个选项,第一个选项实际上不是真正的WebBroker应用,但
仅仅Web App Debugger可执行文件,这是用于调试目的的. Web App Debugger可执行文件很强大,允许我
们使用Web App Debugger(Delphi IDE Tooles菜单中)作为调试Web App Debugger应用程序的宿主程序.
调试CGI或ISAPI/NSAPI Web应用非常不方便,因此在开发过程中最好选择Web App Debugger模式.
而ISAPI/NSAPI Dynamic Link Library 和 CGI Stand-alone executable 类型项目可用于真正部署的
DataSnap服务项目上.
注意,选择CGI Stand-alone executable不是一个好主意,因为这个可执行文件将在每次请求中加载卸载.
在加上要连接到数据库执行一些任务,你必须要考虑到应用程序的执行效率.使用ISAPI的DLL形式,只需要
加载一次,保存在内存中,后续请求(可能来自其他用户)不需要再次加载.ISAPI DLL的主要缺点是升级困难
(如果你用FTP连接到Web服务器).但可以联系Web服务供应商.
ISAPI DLL的另一个缺点是调试不方便—必须用IIS作为宿主应用,不能总是按计划的那样运行.但可以用
Web App Debugger executable来解决这个问题—同时创建两个项目,他们使用公共的DataSnap方法和代
码.第一个范例就是这种形式,加入一些实用的功能构建一个框架.
5.3. 服务方法,部署,客户端
增加功能时只需要修改被两个项目共享的ServerMethodUnit1.pas单元.默认有一个范例函数,就是上面的
范例,我们包含多个方法(组件说明和源码见2.1.4.).服务端方法实现后,我们就可以将ISAPI DLL部署到IIS.
详细信息可见http://blogs.embarcadero.com/jimtierney/2009/08/20/31502.
本例中如你没有Web服务可用于部署,可以使用我已部署的DataSnap ISAPI.注意我没有发布
TDataSetProvider,也没有实现返回数据的GetEmployees方法,但是ServerTime和EchoString方法都运行
良好,可以用来测试DataSnap客户端了.
在客户端连接ISAPI DataSnap服务端前,可以使用Data Explorer检查一下是否可以连接到ISAPI
DataSnap服务. Data Explorer上有一个新的叫做DATASNAP的目录,展开后,第一个连接叫做
DataSnapConnection,右键修改连接.在这个对话框中我们可以选择协议,主机(如你没有自己的Web服务可
以使用http://www.bobswart.nl/),端口号,以及ISAPI DataSnap服务应用程序在Web服务器上的URL路径,这里是
cgi-bin/DSISAPIServer.dll.点击测试连接.
点击OK关闭窗口,在Data Explorer,展开DATASNAPCONNECTION节点查看表,视图,过程,函数和
同义词.下图中,过程包括DSAdmin,DSMetaData,TServerMethods1.AS_XXX及我们自定义的三个函数
EchoString,ServerTime 和 GetEmployees.
不需要写DataSnap客户端,现在就可以测试这些方法.例如EchoString方法(发送什么返回什么).右键点击
TServerMethods1.GetEmployees方法,选择View Parameters,弹出一个新窗口,输入参数(例如42).在这个新
窗口中右击,选择”执行远程服务方法”.运行结果将显示在ReturnValue中.
这样我们就可以调用远程DataSnap服务方法.为了在客户端连接到远程服务端,我们只需要修改
TSQLConnection组件的属性.原来我们连接都Windows版本的DataSnap服务,现在我们需要修改设置连接到
Web版本.
注意,如果你使用的是我发布的DSISAPIServer.dll,我已经禁用了TDataSetProvider,而且GetEmployees方法
不返回任何数据.但你可以使用ServerTime 和 EchoString方法.
6.如何使用REST和JSON
DataSnap2010支持REST和JSON.DataSnap2010特性REST支持DataSnap HTTP请求.例如,如果
DataSnap服务的URI是http://www.bobswart.nl/cgi-bin/DSISAPIServer.dll.我们可以在此URL后
加 /datasnap/rest,后跟服务类名称,方法名称和参数.语法如下:
http://server/datasnap/rest/%3Cclass%3E/%3Cmethod%3E/%3Cparameters>
对于我的服务器上的TServerMethod1模块中的ServerTime方法,URL如下:
http://www.bobswart.nl/cgi-bin/DSISAPIServer.dll/datasnap/rest/TServerMethods1/ServerTime
在浏览器中输入这个REST支持的URL,如下图:
在浏览器中返回结果是JSON结构:
{"result":["2009-10-16 16:01:33.145"]}
更多信息见Marco Cantù的Delphi2010和REST客户端白页.
6.1. 回调
除了用REST支持调用DataSnap服务方法外,JSON还用于实现回调方法.DataSnap2010支持客户端回调
函数,使其执行在服务方法上下文中.这样就可以实现客户端调用服务端方法时,服务端就可以调用由客户端传
递好参数的回调函数.
例如,我们修改EchoString方法,向其中添加回调支持.修改后的EchoString方法如下:
function EchoString(Value: string; callback: TDBXcallback): string;
TDBXcallback类定义在DBXJSON单元.在我们实现EchoString方法前,先搞清楚如何在客户端定义回调函
数(毕竟,这是一个可以让服务端调用的客户端方法).
在客户端,我们必须定义一个新类,继承在TDBXCallback,重写其Execute方法.
type
TCallbackClient = class(TDBXCallback)
public
function Execute(const Arg: TJSONValue): TJSONValue; override;
end;
在Execute方法中,有一个TJSONValue类型的参数,可以复制(Clone)这个参数然后设置其具体内
容.Execute方法也返回一个TJSONValue类型的值,这里我们只返回同样的值:
function TCallbackClient.Execute(const Arg: TJSONValue): TJSONValue;
var
Data: TJSONValue;
begin
Data := TJSONValue(Arg.Clone);
ShowMessage('Callback: ' + TJSONObject(Data).Get(0).JSonValue.value);
Result := Data
end;
例如, 在方法实际返回前(如方法正在执行),回调函数将显示EchoString方法传递参数的值.服务端新的
EchoString方法实现需要将String值赋给一个TJSONObject对象,并将其传递给回调函数.如下:
function TServerMethods2.EchoString(Value: string; callback: TDBXcallback): string;
var
msg: TJSONObject;
pair: TJSONPair;
begin
Result := Value;
msg := TJSONObject.Create;
pair := TJSONPair.Create('ECHO', Value);
pair.Owned := True;
msg.AddPair(pair);
callback.Execute(msg);
end;
注意这个回调函数将在客户端执行—然后在服务端Echostring方法执行完毕前返回
最后,在客户端调用EchoString方法也需要修改,因为我们现在要提供一个回调类TCallbackClient的实例,如
下所示:
var
MyCallback: TCallbackClient;
begin
MyCallback := TCallbackClient.Create;
try
Server.EchoString(Edit1.text, MyCallback);
finally
MyCallback.Free;
end;
end;
这个范例阐释了如何在DataSnap2010中使用客户端回调函数.
7. 使用DATASNAP 和 .NET
Delphi Prism 2010可用来构建使用我们先前生成Wind32服务的DataSnap .NET客户端.为了构建Delphi
Prism 2010 DataSnap客户端,先确保DataSnap服务正常运行.
启动Delphi Prism 2010,点击View. Server Explorer启动Delphi Prism Server Explorer.首先建立一个连
接,验证我们将以使用的DataSnap服务.
Server Explorer的根节点叫做Data Connections.右击Data Connections选择添加连接.对话框如下,在
Data Sources列表中选择DataSnap(注意如果数据源已经预选好了我们需要点击变更一下)
不要选中 Always use this selectiong.除非你一直构建DataSnap数据连接.
点击Continue按钮进入下一步.指定连接的DataSnap服务详细信息.在协议下拉框中选择TCP/IP或HTTP.
接下来,指定服务器(运行DataSnap服务的主机名称,如在本机测试可指定localhost),然后指定端口号.默认
HTTP为80端口,TCP/IP为211端口.但从本白页中可知这两个端口都应该修改,并确保和你在
ServerContainerUnitDemo单元中设置的端口号对应.下一个属性包含路径,这在你要连接到基于Web
Broker的DataSnap服务上很重要.设置为http://后面的部分.
最后,不要忘记验证用户和密码,本例DataSnap服务使用HTTP验证.
点击测试按钮,验证连接.如果弹出连接成功信息表示连接可用.
点击OK按钮,在连接树中显示了一个新的DataSnap连接.本例中是localhost节点.展开这个节点,显示表,
视图,存储过程子节点.表和视图节点为空,但存储过程节点包括所有在DataSnap服务端定义的服务方法.包
括我们自定义的EchoString,GetEmployees和ServerTime.
我们现在可以在Server Explorer中测试服务方法.例如,右击EchoString方法,选择查看参数.弹出新窗口,
输入参数.这里输入42.右击窗口选择执行.将执行服务端的EchoString方法.如下图.
更好的是可以使用GetEmployees方法演示如何从Employees表中获取数据.这个存储过程没有参数,但还
有选择 View Parambers命令,返回一个空参数列表.右击窗体选择 执行.这是返回一个记录集.如下图:
7.1. WINFORMS 客户端
虽然已经可以运行服务端方法了,但更有用的方法是在.NET应用程序中调用这些方法.最后一个范
例,File.New Project启动Delphi Prism新项目向导.选择项目类型.
在Windows 项目类型中选择Windows Application,修改WindowsApplication1为DataSnapClient.
点击OK按钮,创建一个带有Main.pas单元的新项目.
在Server Explorer,选择新建的DataSnap服务连接,属性框中找到ConnectionString,如下:
communicationprotocol=http;hostname=localhost;port=8080;dsauthenticationuser=Bob;ds
authenticationpassword=Swart
右击数据连接节点,选择生成客户端代理(Generate Client Proxy)选项.生成新文件ClientProxy1.pas,其中
定义了TServerMethods1Client类及其中的方法(EchoString, ServerTime, 和GetEmployees)..如下:
TServerMethods1Client = class
public
constructor (ADBXConnection: TAdoDbxConnection);
constructor (ADBXConnection: TAdoDbxConnection; AInstanceOwner: Boolean);
function EchoString(Value: string): string;
function ServerTime: DateTime;
function GetEmployees: System.Data.IDataReader;
除了代理类还在项目的引用节点中添加了Borland.Data.AdoDbxClient 和Borland.Data.DbxClientDriver
引用.
从TServerMethods1Client类代码片段中可见,类有两个构造函数:有使用了一个ADBXConnection参数,
第二个构造函数还有一个AInstanceOwner的Boolean类型参数.这意味着我们必须使用参数调用构造函数.
为了支持这个功能,必须修改项目属性设置.在解决方案管理器中右击DataSnapClient,选择属性.如下图,点
击Compatibility标签,选中”Allow Create constructor calls”,将允许我们调用.Create构造方法,传递参数,而不
仅仅是使用new关键字.
现在回到主窗体,添加一个按钮.在Click事件中创建一个DataSnap服务连接并调用方法.
method MainForm.button1_Click(sender: System.Object; e: System.EventArgs);
var
Client: ClientProxy1.TServerMethods1Client;
Connection: Borland.Data.TAdoDbxDatasnapConnection;
begin
Connection := new Borland.Data.TAdoDbxDatasnapConnection();
Connection.ConnectionString :=
'communicationprotocol=http;hostname=localhost;port=8080;dsauthenticationuser=Bob;dsauth
enticationpassword=Swart';
Connection.Open;
try
Client := ClientProxy1.TServerMethods1Client.Create(Connection);
MessageBox.Show(
Client.EchoString('Delphi Prism 2010'));
finally
Connection.Close;
end;
end;
运行结果如下图所示:
同样方式,我们调用GetEmployees方法获取结果集并显示到DataGridView.这里有个小问题,
DataGridView方法返回的是IDataReader(等价于TSQLDataSet结果集),而不是DataSet和DataTable.我们
必须写几行代码将GetEmployees返回的结果集保存到DataSet的DataTabl中(等价于Win32中的
TClientDataSet).
method MainForm.button1_Click(sender: System.Object; e: System.EventArgs);
var
Client: ClientProxy1.TServerMethods1Client;
Connection: Borland.Data.TAdoDbxDatasnapConnection;
Employees: System.Data.IDataReader;
ds: System.Data.DataSet;
dt: System.Data.DataTable;
begin
Connection := new Borland.Data.TAdoDbxDatasnapConnection();
Connection.ConnectionString :=
'communicationprotocol=http;hostname=localhost;port=8080;dsauthenticationuser=Bob;dsauthenticationpassword=Swart';
Connection.Open;
try
Client := ClientProxy1.TServerMethods1Client.Create(Connection);
Employees := Client.GetEmployees;
ds := new DataSet();
dt := new DataTable("DataSnap");
ds.Tables.Add(dt);
ds.Load(Employees, LoadOPtion.PreserveChanges, ds.Tables[0]);
dataGridView1.DataSource := ds.Tables[0];
MessageBox.Show(
Client.EchoString('Delphi Prism 2010'));
finally
Connection.Close;
end;
end;