RemObjects
RemObjects提示:我们相信本文是正确的,但我们不做任何保证.在此感谢Henrick 写的文章,很高兴在此发表.
介绍
RemObjects是功能强大可扩展的远程框架;但是当考虑远程对象的allocation(内存分配)/deallocation(内存释放)/serialization(序列化)问题时让人摸不到边际. 本文将讨论RO内核澄清这些问题.
Delphi开发者可以很幸运的使用RemObjects/DataAbstract创建n层服务. 使用RO,我们通过在Service Builder工具重创建服务定义库以导向的架构设计我们服务.
其中之一就是我们可以自定义结构体类型作为输入输出参数. RO使用其定义创建代码所以可以序列化他们并在网络上传输. 这能够轻松做到,RO最大程度的提升了我们的n层应用.
不幸的是还有一些细节需要我们自己管理;如. 清除我们自己分配的内存. 同Delphi. RemObjects尽力做到像本地调用一样实现远程调用,然而还是有些不同,稍候会讨论.这里有一条首要的规则: 总在客户端释放内存不能在服务端释放.
为了清晰,我们讨论的代码都是我们写过的;RO在服务端会为我们自动释放对象,所以不会引起内存泄漏.
已经有关于这个问答的FAQ 在线问答了.但是我想在这里讨论更多的细节,因为有很多细节使我们必须熟悉理解的规则,我们还有特别小心的使用in/out (或var)参数.
OK,第一件事情是说明所有从TROComplexType继承来的RO结构体. TROComplexType 可以将对象数据序列化在网上传输; 例如RO可以序列化从TROComplexType继承的对象.
Use The Source, always use The Source.
查看代码发生了什么最好的方法是调用服务方法并跟踪RO结构体或TROComplexType对象的内存申请和释放.
我已经建立了一个简单的项目project 帮助我们查看这个过程.
这里是我们要测试执行的服务端接口和方法:
{ MyStruct }
MyStruct = class(TROComplexType)
private
fStructId: Integer;
fStructData: String;
public
procedure Assign(iSource: TPersistent); override;
published
property StructId:Integer read fStructId write fStructId;
property StructData:String read fStructData write fStructData;
end;
[...]
{ ITestStructsService }
ITestStructsService = interface
['{7FD55CCE-01E8-4F4A-B64B-02380B31A8FC}']
procedure ProcessStruct(const aStruct: MyStruct);
procedure OutStruct(out aStruct: MyStruct);
function GetStruct: MyStruct;
procedure VarStruct(var aStruct: MyStruct);
end;
ITestStructsService接口结合了所有要使用的参数类型: in, out, 返回值和in/out.
为了简单我们只考虑SOAP消息, TROSOAPMessage, 当然对TROBinMessage也应用同样的规则.虽然我们只考虑结构体,但是执行规则同样适用于从TROComplexType继承来的RO数组.
通过客户端代码跟踪...
Ok,第一个要查看的方式是ProcessStruct方法. 我们再客户端直接创建结构体填充数据并作为参数调用后释放:
procedure TClientForm.ButtonProcessStructClick(Sender: TObject);
var
aStruct: MyStruct;
begin
aStruct := MyStruct.Create;
try
aStruct.StructId := 0;
aStruct.StructData := 'Client ProcessStruct';
MemoOutput.Lines.Add('invoke ProcessStruct');
OutputStruct(aStruct);
(RORemoteService as ITestStructsService).ProcessStruct(aStruct);
OutputStruct(aStruct);
finally
FreeAndNil(aStruct);
end;
end;
客户端代码与我们在Delphi中调用需要常量参数的方法一样. 客户端负责管理传输作为参数的对象生存期.
RO已经做了所有的繁琐工作;我们看看实际发生了什么. ROmemberLibrary_Intf.pas文件中我们可以看到TTestStructsService_Proxy代理类为每个接口方法序列化结构体以便在网络传输.
代理类代码中我们对调用__Message.Read 和 __Message.Write的部分感兴趣. 我们可以看到消息的序列号和反序列化. 它们负责为调用格式化消息.
我们看看TROMessage.Read 和 TROMessage.Write:
procedure TROMessage.Read(const aName: string;
aTypeInfo: PTypeInfo;
var Ptr; Attributes: TParamAttributes);
begin
Serializer.Read(aName, aTypeInfo, Ptr);
if Assigned(fOnReadMessageParameter) then
fOnReadMessageParameter(Self, aName,
aTypeInfo, pointer(Ptr),
Attributes);
end;
procedure TROMessage.Write(const aName: string;
aTypeInfo: PTypeInfo;
const Ptr; Attributes: TParamAttributes);
begin
if Assigned(fOnWriteMessageParameter) then
fOnWriteMessageParameter(Self, aName,
aTypeInfo, pointer(Ptr),
Attributes);
Serializer.Write(aName, aTypeInfo, Ptr);
end;
这里使用TROMessage的Serializer属性.方法:
function CreateSerializer : TROSerializer; virtual; abstract;
生成消息相对于的TROSerializer.TROSerializer实例化后在TROMessage构造时将其赋给TROMessage.fSerializer成员. 在TROSOAPMessage中生成TROXMLSerializer.
function TROSOAPMessage.CreateSerializer : TROSerializer;
begin
result := TROXMLSerializer.Create(pointer(fMessageNode));
end;
Ok,现在我们已经看到TROMessage 和TROSerializer直接的关系了,现在再考虑特定的ProcessStruct案例. TROSerializer.Write约定MyStruct的TypeInfo信息指定了他的tkClass并以此来调用TROSerializer.WriteObject.
WriteObject是一个复杂的方法这里不再详述,但是它使用了RTTI读取MyStruct中的值并写出来. 注意这里没有创建和释放代码,TROXMLSerializer.BeginWriteObject中也没有创建和释放代码.所以客户端简单的序列化了我们传入的常量结构体参数.我们负责分配并做相应的释放.
在服务端代码中跟踪...
服务端发生了什么? 服务端通过ROmemberLibrary_Invk.pas文件中的TTestStructsService_Invoker.Invoke_ProcessStruct方法处理客户端请求.
procedure TTestStructsService_Invoker.Invoke_ProcessStruct
(const __Instance:IInterface;
const __Message:IROMessage;
const __Transport:IROTransport;
out __oResponseOptions:TROResponseOptions);
{ procedure ProcessStruct(const aStruct: MyStruct); }
var
aStruct: ROmemberLibrary_Intf.MyStruct;
__lObjectDisposer: TROObjectDisposer;
begin
aStruct := nil;
try
__Message.Read('aStruct',
TypeInfo(ROmemberLibrary_Intf.MyStruct),
aStruct, []);
(__Instance as ITestStructsService).ProcessStruct(aStruct);
__Message.Initialize(__Transport, 'ROmemberLibrary',
'TestStructsService', 'ProcessStructResponse');
__Message.Finalize;
__oResponseOptions := [roNoResponse];
finally
__lObjectDisposer := TROObjectDisposer.Create(__Instance);
try
__lObjectDisposer.Add(aStruct);
finally
__lObjectDisposer.Free();
end;
end;
end;
我们可以看到结构体从流中读出,因此读取也使用了序列化器. 我们可以在try..finally块中发现TROObjectDisposer;这个逻辑接口释放了结构体引用的对象.猜测是正确的.这意味着什么?
这说明__Message.Read序列化器实例化MyStruct.序列化器的Read方法测定MyStruct实例是一个对象类型,所以调用了ReadObject. 随后调用 BeginReadObject. 在TROXMLSerializer.BeginReadObject中执行如下构造函数:
procedure TROXMLSerializer.BeginReadObject
[...]
anObject := TROComplexTypeClass(aClass).Create;
[...]
end;
这里实例被构造出来,所以我们需要TROObjectDisposer. RO架构实例化了这个结构体让后又会为你在服务端释放它.
所以我们看这个方法就已经知道RO实例化结构体的所有信息,使用这个实例后又为我们自动释放.序列化器是关键.当从流中读取了结构体后序列化器创建一个实例;如果要向流中写入就使用已存在的实例.这有很大的意义.
OutStruct 和 GetStruct
下一个方法我们看看 OutStruct. 我们已经认识了RO的序列化器,下面我们看看生成的代码TTestStructsService_Proxy.OutStruct,它明确了我们的职责:
procedure TTestStructsService_Proxy.OutStruct(out aStruct: MyStruct);
var
__request, __response : TMemoryStream;
begin
aStruct := nil;
__request := TMemoryStream.Create;
__response := TMemoryStream.Create;
try
__Message.Initialize(__TransportChannel,
'ROmemberLibrary', __InterfaceName, 'OutStruct');
__Message.Finalize;
__Message.WriteToStream(__request);
__TransportChannel.Dispatch(__request, __response);
__Message.ReadFromStream(__response);
__Message.Read('aStruct', TypeInfo(ROmemberLibrary_Intf.MyStruct),
aStruct, []);
finally
__request.Free;
__response.Free;
end
end;
这里序列化器要求读结构体;这意味着它将要创建一个实例.注意它不会释放这个实例,所以这个客户端的责任.
GetStruct很像OutStruct方法,但是使用aStruct的方式不同,它用于返回.如果我们查看生成的TTestStructsService_Proxy.GetStruct代码可以发现.
留心结构体做var参数!!!
最后看VarStruct方法.这是非常重要的部分,我们看生成的代码 TTestStructsService_Proxy.VarStruct:
procedure TTestStructsService_Proxy.VarStruct(var aStruct: MyStruct);
var
__request, __response : TMemoryStream;
begin
__request := TMemoryStream.Create;
__response := TMemoryStream.Create;
try
__Message.Initialize(__TransportChannel, 'ROmemberLibrary',
__InterfaceName, 'VarStruct');
__Message.Write('aStruct', TypeInfo(ROmemberLibrary_Intf.MyStruct),
aStruct, []);
__Message.Finalize;
__Message.WriteToStream(__request);
__TransportChannel.Dispatch(__request, __response);
__Message.ReadFromStream(__response);
__Message.Read('aStruct', TypeInfo(ROmemberLibrary_Intf.MyStruct),
aStruct, []);
finally
__request.Free;
__response.Free;
end
end;
代码使用uses __Message.Write 和 __Message.Read, 这以为着它使用aStruct参数传入的实例,同时也会创建一个新的实例aStruct. 客户端如何写代码就很重要了.正确的书写客户端代码保证没有内存泄露的方式如下:
procedure TClientForm.ButtonVarStructClick(Sender: TObject);
var
aInStruct, aOutStruct: MyStruct;
begin
aInStruct := MyStruct.Create;
try
aInStruct.StructId := 2;
aInStruct.StructData := 'Client VarStruct';
MemoOutput.Lines.Add('invoke VarStruct');
OutputStruct(aInStruct);
aOutStruct := aInStruct;
(RORemoteService as ITestStructsService).VarStruct(aOutStruct);
OutputStruct(aOutStruct);
finally
FreeAndNil(aOutStruct);
FreeAndNil(aInStruct);
end;
end;
我们必须释放aInStruct和aOutStruct. 为给代理传入对象我们必须实例化aInStruct. 代理将之写入流中并做调用.代理代码然后实例化一个新的MyStruct,重写var参数的引用.
这意味着客户端必要跟踪什么传入了代理,什么又从代理中传出,并将两者都释放掉.
为什么RO做这些呢?想象一下如果RO为我们释放其中的一个.问题是我们可能还要在其他的地方使用这些对象的实例. 最后,远程调用与我将在下一小节中介绍的局部调用有明显的不同.
在我看来,理想的解决方案是使用接口应用计数器来管理生存期.所以我们使用IROComplexType 替换TROComplexType,我们所有的结构体都基于接口. 问题是RO为了支持没有RTTI接口支持的delphi5.这样无法使用RTTI序列化它们;然而我们可以通过其它方法绕过RTTI.
如果我们在服务端查看调用VarStruct的代码我们可以看到如下内容:
procedure TTestStructsService_Invoker.Invoke_VarStruct
(const __Instance:IInterface;
const __Message:IROMessage;
const __Transport:IROTransport;
out __oResponseOptions:TROResponseOptions);
{ procedure VarStruct(var aStruct: MyStruct); }
var
aStruct: ROmemberLibrary_Intf.MyStruct;
__in_aStruct: ROmemberLibrary_Intf.MyStruct;
__lObjectDisposer: TROObjectDisposer;
begin
aStruct := nil;
__in_aStruct := nil;
try
__Message.Read('aStruct', TypeInfo(ROmemberLibrary_Intf.MyStruct),
aStruct, []);
__in_aStruct := aStruct;
(__Instance as ITestStructsService).VarStruct(aStruct);
__Message.Initialize(__Transport, 'ROmemberLibrary',
'TestStructsService', 'VarStructResponse');
__Message.Write('aStruct', TypeInfo(ROmemberLibrary_Intf.MyStruct),
aStruct, []);
__Message.Finalize;
finally
__lObjectDisposer := TROObjectDisposer.Create(__Instance);
try
__lObjectDisposer.Add(__in_aStruct);
__lObjectDisposer.Add(aStruct);
finally
__lObjectDisposer.Free();
end;
end;
end;
我们可以看到如何通过TROObjectDisposer释放__in_aStruct 和 the aStruct实例.这正是我们要做客户端保证没有内存泄露的做法.
抛开Delphi话题展示本地和远程调用的区别...
要真正的理解相对于本地调用的RO调用以及为什么必须释放两个结构体实例,我们需要看看一些使用Var参数的本地调用. 如果你对我们在RO的Var参数调用时为什么必须写那些代码的解释很满意,你可以越过本小节. 如果你想去对比远程和本地调用请继续阅读.
为什么你要在RO中使用in/out 和var参数呢? 这是我们向服务端传递结构体和从服务端得到更新修改的结构体的唯一方式.本地调用有更多弹性达到这个目标,我们将看到每一种选择.这对于在RO中不能传递非常量结构体参数的情况没有什么参考价值.
如果要在本地实现ITestStructsService.VarStruct的功能,我将考虑使用结构体替代类(记住RO中的结构体是从TROComplexType类继承下来的).这是因为在本地调用时结构体与RO结构体提供了同样等级的功能. 因而我们使用如下方法测试:
type
PMyRecord = ^TMyRecord;
TMyRecord = record
RecId: integer;
RecData: shortstring;
end;
[...]
procedure ChangeTheRecord(var aRec: TMyRecord);
begin
aRec.RecId := aRec.RecId + 1;
aRec.RecData := aRec.RecData + ' Changed';
end;
[...]
procedure TClientForm.ButtonLocalStackRecordClick(Sender: TObject);
var
LocalRecord: TMyRecord;
begin
LocalRecord.RecId := 1;
LocalRecord.RecData := 'Foo';
OutputRecord('before stack record', LocalRecord);
ChangeTheRecord(LocalRecord);
//value of LocalRecord is: Id = 2 and Data = 'Foo Changed'
OutputRecord('after stack record', LocalRecord);
OutputSeparator;
end;
在本例中,由于使用了分配在客户端方法(如TClientForm.ButtonLocalStackRecordClick)栈上的本地记录结构体,Delphi为我们处理所有的内存分配和释放. 这里ChangeTheRecord方法的参数必须使用var否则过程就不能更新结构体. 但是我们可以通过不同的方式使用结构体; 加入我们在堆上分配结构体:
procedure ChangeTheHeapRecord(aRec: PMyRecord);
begin
aRec.RecId := aRec.RecId + 1;
aRec.RecData := aRec.RecData + ' Changed';
end;
procedure ChangeTheHeapRecordVar(var aRec: PMyRecord);
begin
aRec.RecId := aRec.RecId + 1;
aRec.RecData := aRec.RecData + ' Changed';
end;
[...]
procedure TClientForm.ButtonLocalHeapRecordClick(Sender: TObject);
var
HeapRecord: PMyRecord;
begin
GetMem(HeapRecord, SizeOf(TMyRecord));
try
HeapRecord.RecId := 1;
HeapRecord.RecData := 'Foo';
OutputRecord('before heap record', HeapRecord^);
//all three do the same thing...so use any one of them..
//ChangeTheRecord(HeapRecord^);
ChangeTheHeapRecord(HeapRecord);
//ChangeTheHeapRecordVar(HeapRecord);
//value of HeapRecord is: Id = 2 and Data = 'Foo Changed'
OutputRecord('after heap record', HeapRecord^);
finally
FreeMem(HeapRecord);
end;
OutputSeparator;
end;
三个方法都正常执行: ChangeTheRecord, ChangeTheHeapRecord 和ChangeTheHeapRecordVar . 我们传递堆上的引用就可以改变值 (不管是否在参数前放置var标识), ChangeTheHeapRecord 和ChangeTheHeapRecordVar实现了同等功能 .或者我们可以给ChangeTheRecord传递忽略var标识的结构参数,而执行效果却和基于栈使用var时的相同.
唯一不同的地方就是我们必须要在堆上分配和释放内存. 但是注意我们只做一次分配和释放操作.
更完整的,我们看在本地传递MyStruct对象的代码是什么样的:
procedure ChangeTheStruct(aStruct: MyStruct);
begin
aStruct.StructId := aStruct.StructId + 1;
aStruct.StructData := aStruct.StructData + ' Changed';
end;
procedure ChangeTheStructVar(var aStruct: MyStruct);
begin
if (Assigned(aStruct) = False) then
begin
aStruct := MyStruct.Create;
end;
aStruct.StructId := aStruct.StructId + 1;
aStruct.StructData := aStruct.StructData + ' Changed';
end;
[...]
procedure TClientForm.ButtonLocalObjectClick(Sender: TObject);
var
aStruct: MyStruct;
begin
aStruct := MyStruct.Create;
try
aStruct.StructId := 1;
aStruct.StructData := 'Foo';
OutputStruct('invoke Local Object', aStruct);
ChangeTheStruct(aStruct);
OutputStruct('after Local Object', aStruct);
finally
FreeAndNil(aStruct);
end;
OutputSeparator;
end;
procedure TClientForm.ButtonLocalObjectVarClick(Sender: TObject);
var
aStruct: MyStruct;
begin
aStruct := nil;
try
//OR replace above two lines with:
//aStruct := MyStruct.Create;
//try
// aStruct.StructId := 1;
// aStruct.StructData := 'Foo';
OutputStruct('before Local Object var', aStruct);
ChangeTheStructVar(aStruct);
OutputStruct('after Local Object var', aStruct);
finally
FreeAndNil(aStruct);
end;
OutputSeparator;
end;
又一次清楚的看到我们只做一次分配和释放结构体的操作.因为我们在本地调用并传递的是对象的引用,方法改变参数将直接修改对象本身,因此要达到我们目的var标识不是必须的.
在对结构体做var标识的情况下我们可以传递nil或一个分配内存的实例. 更多时候在方法中使用var对象引用参数的意图是作为"Source" 方法. 这时如果将nil值传递给方法,将为调用者分配一个新实例. 同ChangeTheStructVar .但是注意及时是这种情况我们以只释放一次实例.
当然我们分配和释放对象的方法和原因完全依赖于我们调用的方法/过程. 这里特别约定了我们将在方法中直接改变调用者提供的结构/对象. 也可以将修改结果返回调用者.
这里有多种约定, ChangeTheStructVar 与"source"方法不同.RO的var参数的约定也有不同意义. 它可以使用如下代码模仿本地调用:
procedure ChangeTheStructVarLikeRO(var aStruct: MyStruct);
var
InStruct: MyStruct;
begin
InStruct := aStruct;
//this simulates the __Message.Read construction on the client
//after the server method is invoked
aStruct := MyStruct.Create;
//this simulates the __Message.Read deserialization
aStruct.Assign(InStruct);
aStruct.StructId := aStruct.StructId + 1;
aStruct.StructData := aStruct.StructData + ' Changed';
end;
[...]
procedure TClientForm.ButtonLocalObjectLikeROClick(Sender: TObject);
var
aInStruct, aOutStruct: MyStruct;
begin
aInStruct := MyStruct.Create;
try
aInStruct.StructId := 1;
aInStruct.StructData := 'Foo';
OutputStruct('before Local Object var like RO', aInStruct);
aOutStruct := aInStruct;
ChangeTheStructVarLikeRO(aOutStruct);
OutputStruct('after Local Object var like RO (aInStruct)',
aInStruct);
OutputStruct('after Local Object var like RO (aOutStruct)',
aOutStruct);
finally
FreeAndNil(aInStruct);
FreeAndNil(aOutStruct);
end;
OutputSeparator;
end;
正如你在代码中看到的, 结构比必须在TClientForm.ButtonLocalObjectLikeROClick方法中释放两次. 这时因为ChangeTheStructVarLikeRO 执行的约定很像RO执行var参数的远程调用约定.
结论
RO是令人惊奇的架构,但是使用它是必须要知道一些告诫.为了不会写出内存泄漏的代码,理解其中隐含的内容是很重要的.我希望本文可以起到引导作用,并且学到我写作时学到的东西.
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/henreash/archive/2008/04/16/2296218.aspx
DA服务器端 执行SQL命令 和查询 SQL语句
posted @ 2011-08-10 09:08 colincode 阅读(350) | 评论 (0) 编辑 |