datasnap的初步

datasnap的初步-回调函数

服务器端

TServerMethods1 =class(TComponent)



  private



    { Private declarations }



  public



    { Public declarations }



    functionTest(funcCallBack: TDBXCallback):boolean;



  end;



  



functionTServerMethods1.Test(funcCallBack: TDBXCallback):boolean;



begin



  funcCallBack.Execute(TJSONNumber.Create(20)).Free;



  sleep(1000);



  Result :=True;



end;

客户端,这个必须继承自TDBXCallback

TFuncCallback =class(TDBXCallback)



    functionExecute(constArg: TJSONValue): TJSONValue;override;



  end;



functionTFuncCallback .Execute(constArg: TJSONValue): TJSONValue;



var



  i:Integer;



begin



  i := TJSONNumber(Arg).AsInt;//可以的到服务器回调来的参数



  Result := TJSONNull.Create;



end;



  



procedureTForm2.Button1Click(Sender: TObject);



begin



  ClientModule1.ServerMethods1Client.Test(funcCallBack);



end;



  



initialization



  funcCallBack:= TFuncCallBack.Create;



finalization



  //FreeAndNil(funcCallBack);

 

到此,实现了最基本的回叫。

D2010起提供了DSClientCallbackChannelManager这个控件,这是为了实现一次注册,多次回叫,使用方法很简单

1。客户端使用 DSClientCallbackChannelManager注册回叫函数

function RegisterCallback(const CallbackId: String; const Callback: TDBXCallback): boolean; overload; 

 

DSClientCallbackChannelManager控件带有一个ChannelName的属性,用于CallbackId分组用。ManagerId属性,可理解为ClientIdClientId必须是唯一的,相同的ClientId,会被认为是相同地点来的连接。

不明白为啥 DSClientCallbackChannelManager要自己设置连接属性,而不是走TSQLConnection

2。服务器端是TDSServer来做事,它有

两个函数

 

function BroadcastMessage(const ChannelName: String; const Msg: TJSONValue; const ArgType: Integer = TDBXCallback.ArgJson): boolean; overload; 









function BroadcastMessage(const ChannelName: String; const CallbackId: String; const Msg: TJSONValue; const ArgType: Integer = TDBXCallback.ArgJson): boolean; overload;

第二个是回调ChannelName里面指定的CallBackId

服务器上用GetAllChannelCallbackId能返回在某个ChannelName里面所有的CallbackId

到此,我们就能使用DSClientCallbackChannelManager来制作一个简单的聊天软件,并能实现私聊的功能。但是如何处理,聊天用户掉线的问题,就比较麻烦了。

RO比较,这设计有些像RO里的TROEventReceiver,但远没RO灵活, TROEventReceiver直接就能订阅(RegisterEventHandlers)上一堆服务器的事件,DataSnap却要定义一堆的TDBXCallback

datasnap的初步 TDSClientCallbackChannelManager的工作方式

理解一下TDSClientCallbackChannelManager的工作方式吧

客户端调用RegisterCallback,其实就是开始了一个线程TDSChannelThread,该线程起一个dbxconnection,连接到服务器上,执行DSAdmin.ConnectClientChannel,服务器上的这个DSAdmin.ConnectClientChannel很神奇,所有的数据传输都在这里了,这个连接不会关闭,以做到服务器往客户端push数据。TDSClientCallbackChannelManager只能做到服务器向客户端推送数据,单向的。客户端要向服务器送数据,走TDSAdminClientNotifyCallback方法,也就是只有经过SQLConnection

DSAdmin(在Datasnap.DSCommonServer单元)是TDBXServerComponent(Datasnap.DSPlatform单元)的子类,ConnectClientChannel函数直接call ConsumeAllClientChannel

代码摘抄

functionTDBXServerComponent.ConsumeAllClientChannel(constChannelName,



  ChannelId, CallbackId, SecurityToken:String; ChannelNames: TStringList;



  ChannelCallback: TDBXCallback; Timeout:Cardinal):Boolean;



... 



begin



  



        // wait for exit message



        repeat  //这里开始



          Data :=nil;



          IsBroadcast :=false;



          ArgType := TDBXCallback.ArgJson;



  



          QueueMessage :=nil;



  



          TMonitor.Enter(CallbackTunnel.Queue);



          try



            {Wait for a queue item to be added if the queue is empty, otherwise



             don't wait and just pop the next queue item}



            ifCallbackTunnel.Queue.QueueSize =0then



              IsAquired := TMonitor.Wait(CallbackTunnel.Queue, Timeout)



            else



              IsAquired :=true;



  



            ifIsAquiredand(CallbackTunnel.Queue.QueueSize >0)then



            begin



              {Get the next queued item from the tunnel}



              QueueMessage := CallbackTunnel.Queue.PopItem;



              Data := QueueMessage.Msg;



              IsBroadcast := QueueMessage.IsBroadcast;



              ArgType := QueueMessage.ArgType;



            end;



          finally



            TMonitor.Exit(CallbackTunnel.Queue);



          end;



  



          ifIsAquiredand(Data <>nil)then



            ifIsBroadcastthen



            begin



              try



                Msg := TJSONObject.Create(TJSONPair.Create('broadcast',



                                                          TJSONArray.Create(Data).Add(ArgType)));



                if(QueueMessage.ChannelName <> EmptyStr)and



                   (QueueMessage.ChannelName <> CallbackTunnel.ServerChannelName)then



                  Msg.AddPair(TJSONPair.Create('channel', QueueMessage.ChannelName));



  



                try



                  ChannelCallback.Execute(Msg).Free;



                except



                  try



                    // Remove the callback tunnel from the list, it will be freed at the end of this method



                    InternalRemoveCallbackTunnel(DSServer, CallbackTunnel);



                  except



                  end;



                  raise;



                end;



              finally



                QueueMessage.InstanceOwner :=false;



                FreeAndNil(QueueMessage);



              end;



            end



            elseifAssigned(QueueMessage)then



            begin



              TMonitor.Enter(QueueMessage);



              try



                Msg := TJSONObject.Create( TJSONPair.Create('invoke',



                       TJSONArray.Create( TJSONString.Create(QueueMessage.CallbackId),



                                          Data).Add(ArgType)));



                try



                  QueueMessage.Response :=  ChannelCallback.Execute(Msg);



                except



                  onE : Exceptiondo



                  begin



                    QueueMessage.IsError :=True;



                    QueueMessage.Response := TJSONObject.Create(TJSONPair.Create('error', E.Message));



                    ifChannelCallback.ConnectionLostthen



                    begin



                      WasConnectionLost :=True;



                      TMonitor.Pulse(QueueMessage);



                      try



                        // Remove the callback tunnel from the list, it will be freed at the end of this method



                        InternalRemoveCallbackTunnel(DSServer, CallbackTunnel);



                      except



                      end;



                      Break;



                    end;



                  end;



                end;



                TMonitor.Pulse(QueueMessage);



              finally



                TMonitor.Exit(QueueMessage);



              end;



            end



        until(notIsAquired)or(Data =nil);//这里结束



...



end.

客户端调用UnregisterCallback,调用的线程(一般就是主线程),直接起一个dbxconnection,让服务器执行DSAdmin.UnregisterClientCallback,执行后立刻dbxconnection.close, 服务器执行UnregisterClientCallback只是剔除消息筛选;

如果客户端没有订阅别的Callback,就再次起一个dbxconnection,让服务器执行DSAdmin.CloseClientChannel,服务器的CloseClientChannel里,会往CallbackTunnel广播一个nil的消息,这就让ConnectClientChannelrepeat循环也会退出(data=nil),从而关闭最开始连接。

另外, TDSClientCallbackChannelManager在界面上找不到输入认证信息的地方,比如DSAuthPassword, DSAuUser等,其实TDSClientCallbackChannelManager也好SQLConnection也好,他们都是个载体罢了,真正在后面起作用的,是TDBXProperties。监视一下DSServerOnConnect里的DSConnectEventObject.ConnectProperties,我们就能知道TDSClientCallbackChannelManagerusername, password,其实是SQLConnectionDSAuthPasswordDSAuUser

TDBXProperties里的键值对为

DSAuthenticationUser=

DSAuthenticationPassword=

最后,TDSClientCallbackChannelManager并没有Filters的属性,这个其实现在的客户端,就算使用SQLConnection时也不必设置Filters了,我们只要别忘记在客户端也TTransportFilterFactory.RegisterFilter一下要用的Filter即可。

datasnap的初步 序列化自己写的类

今天在网上找到了一个marshallunmarshall的例子,将自己的定义的类,序列号json对象

uses DBXJSONReflect, DBXJSON



TPerson = class



FirstName: String;



LastName: String;



Age: Integer; 



end;

procedureTForm1.Button1Click(Sender: TObject);



var



  Mar: TJSONMarshal;//序列化对象



  UnMar: TJSONUnMarshal;// 反序列化对象



  person: TPerson;//我们自定义的对象



  SerializedPerson: TJSONObject;//Json对象



begin



  Mar := TJSONMarshal.Create(TJSONConverter.Create);



  try



    person := TPerson.Create;



    try



      person.FirstName :='Nan';



      person.LastName :='Dong';



      person.Age :=29;



      SerializedPerson := Mar.Marshal(person)asTJSONObject;



    finally



      FreeAndNil(person);



    end;



  finally



    Mar.Free;



  end;



  // show一下person的json对象的信息



  ShowMessage(SerializedPerson.ToString);



end;



  



反序列化



//UnMarshalling



  UnMar := TJSONUnMarshal.Create;



  try



    person := UnMar.UnMarshal(SerializedPerson)asTPerson;



    try



      // 我们用断言检查一下,unmarshal后的信息完全正确。



      Assert(person.FirstName ='Nan');



      Assert(person.LastName ='Dong');



      Assert(person.Age =29);



    finally



      person.Free;



    end;



  finally



    UnMar.Free;



  end;

datasnap的初步 生命期LifeCycle

TDSServerClass有一个属性LifeCycle,这个属性有三个值,很好理解

1.Session,这是默认值。

就是一个连接,一个Session,一个Session的意思就是连接上来后,服务器端就创建一个DSServerClassGetClass里返回的PersistentClass一个实例,并一直保持到连接断开,所有这期间的ServerMethod调用,都是这个实例的调用。所以这是线程安全的。

2.Server

顾名思义,就是全局就一个PersistentClass的实例,所有的连接Call上来的ServerMethod都是这唯一实例的调用,单例模式,当然,这也就不是线程安全的,需要自己来实现线程安全。

3.Invocation

这个更细,每次ServerMethodCall,都将创建和销毁一PersistentClass的实例。由于创建销毁比较耗资源,可以操作TDSServerClassOnCreateInstanceOnDestroyInstance事件,在这两个事件里面做缓存池。代码如下

procedureTServerContainer1.DSServerClass1CreateInstance(



  DSCreateInstanceEventObject: TDSCreateInstanceEventObject);



begin



  DSCreateInstanceEventObject.ServerClassInstance := 缓存池取一个实例



end;



  



procedureTServerContainer1.DSServerClass1DestroyInstance(



  DSDestroyInstanceEventObject: TDSDestroyInstanceEventObject);



begin



  将DSCreateInstanceEventObject.ServerClassInstance的实例还给缓存池



end;

缓存池的实现很简单了,就不写了。

datasnap的初步 TDSAuthenticationManager的用法

xe开始有了TDSAuthenticationManager,这个主要用来做用户认证,用法也很简单

服务器端

1.TDSAuthenticationManager有两个主要的事件

在这个事件里面,看看检测连上来的用户名,密码是否合法,valid如果置为false,这就为非法连接了,DSServer会立刻抛出异常后close连接。

另外,UserRoles的设计,我觉得比RO高明。

procedureTServerContainer1.DSAuthenticationManager1UserAuthenticate(



  Sender: TObject;constProtocol, Context, User, Password:string;



  varvalid:Boolean; UserRoles: TStrings);



begin



  valid := User ='zyz';



  



  ifUser ='admin'then



    UserRoles.Add('admins');



end;

在这个事件里面,判断已经连接上来的用户,对ServerMethod的调用是否合法,注视里也写了,默认是如何检测是否合法的。

procedureTServerContainer1.DSAuthenticationManager1UserAuthorize(



  Sender: TObject; EventObject: TDSAuthorizeEventObject;



  varvalid:Boolean);



begin



  { TODO : Authorize a user to execute a method.



    Use values from EventObject such as UserName, UserRoles, AuthorizedRoles and DeniedRoles.



    Use DSAuthenticationManager1.Roles to define Authorized and Denied roles



    for particular server methods. }



  //valid := True;



end;

上面我说UserRoles的设计比较高明,主要还是因为这个UserRole的设计用到了java的那种注释类的技术,比如服务器上这么定义一个方法

[TRoleAuth('admins')]



functionEchoString(Value:string):string;

这样定义后,就算不写DSAuthenticationManager1UserAuthorizeTDSAuthenticationManager也会自动帮你检查该角色是否有权利调用该ServerMethodRTTI估计是学JavaAnnotation才增加了TCustomAttribute

 2.客户端

客户端很简单了,设置SQLConnectionDSAuthUserDSAuthPassword就行了。

datasnap的初步 对象的销毁

TServerMethods1Client继承自TDSAdminClient,这个类的构造函数

constructor Create(ADBXConnection: TDBXConnection); overload; 

后面的AInstanceOwner参数,挺重要,理解这个,对于避免内存泄漏有很大好处。

 默认情况下,我们使用Create来创建ServerMethodClient,也就AInstanceOwnertrue了,也就是所有进入 TServerMethods1Client类方法的参数,都被ServerMethodClient给来释放。我觉得EMBT推荐使用 AInstanceOwner=True

 DATASNP如何释放内存,请看代码

客户端 

procedureTDBXCommand.CommandExecuting;



begin



  ifAssigned(FFreeOnCloseList)then  



    FreeOnExecuteObjects;//这里释放



  Open;



  CloseReader;



  if(FParameters <>nil)and(FParameters.Count >0)then



  begin



    ifnotFPreparedthen



      Prepare;



    SetParameters;



  end;



end;

也就是说,对每个DBXCommand,每次执行前都会清理上一回留下的垃圾。当然最后的垃圾肯定要等到DBXCommand.Close时才去清理了。

对于function(a: TA, out b: TB): TA这样的调用 

 AInstanceOwnertrue时,a, b, 以及返回值result我们都不用去自己Free。尤其要注意入口参数a,可能进去执行后立刻被Free(需要被Marshal的类),也可能是等到下次Call时被Free(比如TStream)

 反之,则都需要自己去free。但是TDBXCallback,是个例外,就算AInstanceOwnerFalse,也不能自己Free

服务器端

procedureTDSMethodValues.AssignParameterValues(



  Parameters: TDBXParameterArray);



begin



  ClearReferenceParameters;//这里清理



  ifLength(FMethodValues) <> Length(Parameters)then



  begin



    SetLength(FMethodValues, Length(Parameters));



    SetLength(FAllocatedObjects, Length(Parameters));



  end;



 

也是一样的,每回清理前一回留下的垃圾,最后的也是客户端调用DBXCommand.Close时服务器收到"command_close"时被清理,当然服务器自己关闭DBXCommand时也会清理的。

从这个规则,也能看出,客户端,如果要多线程访问服务器,要么访问服务器时聚集到一起,用关键区或者信号量控制同时只有一个线程能上服务器,要么起多个连接。以避免A线程正读的欢呢,B线程就去Call同样的ServerMethod了,把返回结果给Free了。

最后读读EMBT的帖子吧

datasnap的初步 内存泄漏的原因

终于找到了datasnap内存泄漏的原因了,只要你写了下面的代码,肯定出现内存泄漏,无论是session还是invocation。我表示很悲痛。

procedureTServerContainer1.DSServerClass1CreateInstance(



  DSCreateInstanceEventObject: TDSCreateInstanceEventObject);



begin



//



end;



  



procedureTServerContainer1.DSServerClass1DestroyInstance(



  DSDestroyInstanceEventObject: TDSDestroyInstanceEventObject);



begin



//



end;

Help里面写道

 DSServer.TDSServerClass.OnCreateInstance

Happens upon creation of server class instances. 

Use this event to override the default creation of server class instances. This allows for custom initialization and custom object pooling if the LifeCycle property is set to TDSLifeCycle.Invocation. 

 是说只有在Invocation才使用这两个事件。可session模式下就算写了,也不应该内存泄漏吧。再说了,invocation模式下,这个函数啥也不干,还是是泄漏了。

datasnap的初步 关于TDSTCPServerTransportFilters 

TDSTCPServerTransportFilter属性,可以对传递的数据进行加密,压缩,再修改等,有点注入的概念。默认情况下,Datasnap自带的ZLIB, PC1RSA三个Filter。测试了一下,RSA只对KEY加密,PC1才对内容加密,ZLIB来做压缩,ZLIB压缩实在不咋的。并且,Filter的顺序,是依次执行的。我现在打算实现,服务器的一个Log功能,记录下来进入的数据,出去的数据,要求记录下来的数据是明文。

 TTransportFilterProcessInputProcessOutput光看名字比较费解,可以这么理解ProcessInput为编码,ProcessOutput可以理解为解码。

首先给DSTCPServerTransport1Fitlers都加上默认的3Filter

上一个完整的代码

unituLogFilter;



  



interface



  



uses



  SysUtils, DBXPlatform, DBXTransport;



  



type



  TLogHeadFilter =class(TTransportFilter)



  public



    constructorCreate;override;



    destructorDestroy;override;



    functionProcessInput(constData: TBytes): TBytes;override;



    functionProcessOutput(constData: TBytes): TBytes;override;//do nothing



    functionId: UnicodeString;override;



  end;



  



  TLogTailFilter =class(TTransportFilter)



  public



    constructorCreate;override;



    destructorDestroy;override;



    functionProcessInput(constData: TBytes): TBytes;override;//do nothing



    functionProcessOutput(constData: TBytes): TBytes;override;



    functionId: UnicodeString;override;



  end;



  



procedureAddLogFilter(Filters: TTransportFilterCollection);



  



implementation



  



uses



  CodeSiteLogging;



  



const



  LogFilterName_Tail ='LogTail';



  LogFilterName_Head ='LogHead';



  



procedureAddLogFilter(Filters: TTransportFilterCollection);



var



  fs: TDBXStringArray;



  i:Integer;



begin



  fs := Filters.FilterIdList;



  Filters.Clear;



  Filters.AddFilter(LogFilterName_Head);



  fori := Low(fs)toHigh(fs)do



  begin



    Filters.AddFilter(fs[i]);



  end;



  Filters.AddFilter(LogFilterName_Tail);



end;



  



constructorTLogTailFilter.Create;



begin



  inheritedCreate;



  //CodeSite.Send(csmBlue, 'TLogTailFilter.Create');



end;



  



destructorTLogTailFilter.Destroy;



begin



  //CodeSite.Send(csmBlue, 'TLogTailFilter.Destroy');



  inheritedDestroy;



end;



  



functionTLogTailFilter.ProcessInput(constData: TBytes): TBytes;



begin



  Result := Data;



  CodeSite.Send(csmOrange,'To Client: '+ IntToStr(Length(Data)));



end;



  



functionTLogTailFilter.ProcessOutput(constData: TBytes): TBytes;



begin



  Result := Data;



  CodeSite.Send(csmOrange,'From Client: '+ IntToStr(Length(Data)),



    TEncoding.ASCII.GetString(Data));



end;



  



functionTLogTailFilter.Id: UnicodeString;



begin



  Result := LogFilterName_Tail;



end;



  



{ TLogInputFilter }



  



constructorTLogHeadFilter.Create;



begin



  inherited;



  //CodeSite.Send(csmBlue, 'TLogHeadFilter.Create');



end;



  



destructorTLogHeadFilter.Destroy;



begin



  //CodeSite.Send(csmBlue, 'TLogHeadFilter.Destroy');



  inherited;



end;



  



functionTLogHeadFilter.Id: UnicodeString;



begin



  Result := LogFilterName_Head;



end;



  



functionTLogHeadFilter.ProcessInput(constData: TBytes): TBytes;



begin



  Result := Data;



  CodeSite.Send(csmYellow,'To Client: '+ IntToStr(Length(Data)),



    TEncoding.ASCII.GetString(Data));



end;



  



functionTLogHeadFilter.ProcessOutput(constData: TBytes): TBytes;



begin



  Result := Data;



  CodeSite.Send(csmYellow,'From Client: '+ IntToStr(Length(Data)));



end;



  



initialization



  



TTransportFilterFactory.RegisterFilter(LogFilterName_Tail, TLogTailFilter);



TTransportFilterFactory.RegisterFilter(LogFilterName_Head, TLogHeadFilter);



  



finalization



  



TTransportFilterFactory.UnregisterFilter(LogFilterName_Tail);



TTransportFilterFactory.UnregisterFilter(LogFilterName_Head);



  



end.

这个unit实现了上面的功能,

数据进入服务器时,DataSnapReader读出时按顺序经过Filter进行解码,最后的Filter,也就是这里的TLogTailFilterProcessOutput出来的肯定应该是明文了,记录下来。

数据出服务器时, DataSnapWriter写数据时,也按顺序经过Filter进行编码,刚开始的肯定是明文的,也就是TLogHeadFilterProcessInput了,记录下来。

要使用这个unit,只要在ServerContainerUnit1单一的OnCreate里面写入即可。如下

procedure TServerContainer1.DataModuleCreate(Sender: TObject);



begin



  AddLogFilter(DSTCPServerTransport1.Filters); 



end; 

最后,上个图,看看client和服务器之间的通讯是怎样的。

为方面看,我分开了,第一次是connect,然后二次调用了EchoString。可以看出一次servermethod,有3个来回的交流(一个prepare, 一个execute,一个command_closepreparecommand_close并不是每次必须的,这里因为我是每次都创建新的TServerMethods1Client),并且交流的数据都是JSON的整列。这里也打印出了编码解码前数据长度,以及编码解码后的数据长度,如果要测试ZLIB的压缩效果,可以参考。

或许要说DSServerOnTrace事件也可以玩,但是 OnTrace只能记录ClientServer的数据,对出去的数据TRACE不到的,很遗憾。

最后,有一些其他的现成的开源Filter可用,尤其是压缩的,去http://code.google.com/p/dsfc/

datasnap的初步 直接返会自定义类

前面我说datasnap不支持自定义类型是错误的。其实datasnap一旦发现是自定义类型,就会自动用jsonmarshall了,今天的测试代码如下。

服务器端

functionTServerMethods1.GetPerson: TPerson;



begin



  Result := TPerson.Create;



  Result.FirstName :='zyz';



  Result.LastName :='Jacky';



  Result.Age :=21;



end;

客户端,让SQLConnection自动产生代理代码,可以的到

functionTServerMethods1Client.GetPerson: TPerson;



begin



  ifFGetPersonCommand =nilthen



  begin



    FGetPersonCommand := FDBXConnection.CreateCommand;



    FGetPersonCommand.CommandType := TDBXCommandTypes.DSServerMethod;



    FGetPersonCommand.Text :='TServerMethods1.GetPerson';



    FGetPersonCommand.Prepare;



  end;



  FGetPersonCommand.ExecuteUpdate;



  ifnotFGetPersonCommand.Parameters[0].Value.IsNullthen



  begin



    FUnMarshal := TDBXClientCommand(FGetPersonCommand.Parameters[0].ConnectionHandler).GetJSONUnMarshaler;



    try



      Result := TPerson(FUnMarshal.UnMarshal(FGetPersonCommand.Parameters[0].Value.GetJSONValue(True)));



      ifFInstanceOwnerthen



        FGetPersonCommand.FreeOnExecute(Result);



    finally



      FreeAndNil(FUnMarshal)



    end



  end



  else



    Result :=nil;



end;

也就是说,客户端也会自动给unmarshal的。

调用代码

procedureTForm1.btn3Click(Sender: TObject);



var



  p: TPerson;



begin



  p := FServerMethod.GetPerson;



  withpdo



  ShowMessage(Format('FirstName=%s, LastName=%s, Age=%d',



      [FirstName, LastName, Age]));



  //p.Free;



end;

由于我的FInstanceOwner使用的默认为trueUnMarshalCreateObject产生的类,让Datasnap自己去释放了,所以p.free要注视掉。释放的时机有两个:

1。下一次调用到来

2DBXCommand.Close

datasnap的初步 获得客户端的信息

记得datasnap 2009时,要得到客户端信息,非官方的方法,要去搞什么DSConnectEventObject.ChannelInfo.Id,弄成 TIdTCPConnectionxe2就好得多了。

仍然是在DSServerOnConnect 事件里,

DSConnectEventObject.ChannelInfo.ClientInfo就是客户端的信息。能得到啥? 

看代码 

TDBXClientInfo =record



    IpAddress:String;



    ClientPort:String;



    Protocol:String;



    AppName:String;



  end;

也就是能取得客户端ip,端口,连接协议,不过AppName这玩意儿一直是空的。

执行到 DSServerOnConnect的事件里,其实socket已经完全连上了,client已经调用了serverconnect方法了,在这个方法里触发的OnConnect。所以DSServerOnConnect其实并不是真的socketOnConnect 

datasnap的初步 Session的管理 

Datasnapsession管理是强制的,没有选项能说不要。

管理靠一单例TDSSessionManager来管理。对于目前说到TDSTCPServerTransport,建立的的SessionTDSTCPSession,它是TDSSession的子类。

Session在开始连接后,就创建了,再连接断开后消亡。

TDSSession =class



 private



   FStartDateTime: TDateTime;  /// creation timestamp



   FDuration:Integer;         /// in miliseconds, 0 for infinite (default)



   FStatus: TDSSessionStatus;  /// default idle



   FLastActivity:Cardinal;    /// timestamp of the last activity on this session



   FUserName:String;          /// user name that was authenticated with this session



   FSessionName:String;       /// session name, may be internally generated, exposed to 3rd party



   FMetaData: TDictionary<STRING,STRING>;/// map of metadata stored in the session



   FMetaObjects: TDictionary<STRING,TOBJECT>;/// map of objects stored in the session



   FUserRoles: TStrings;       /// user roles defined through authentication



   FCache: TDSSessionCache;



   FLastResultStream: TObject; /// Allow any object which owns a stream, or the stream itself, to be stored here



   FCreator: TObject;          /// Creator of the session object reference

可以看出,Session可以用存储了很多东西 。用得多的是FMetaDataFMetaObjects

对于字符串,PutData放进去,GetData取出来;对于ObjectPutObject放进去,GetObject取出来。

使用方法为

TDSSessionManager.GetThreadSession.PutData('userid', userId);



userId := TDSSessionManager.GetThreadSession.GetData('userid');

另外,放入FMetaObjectsObjectSessionFree时,会自动帮忙Free,所以不必自己去Free的。

关于Session的超时, 

这里自然就想到了TDSTCPServerTransportKeepAliveIntervalKeepAliveTime属性,这两个属性,其实和Session管理没关系。

跟踪代码,这两个属性的反应在IdStackWindows.pas

procedureTIdStackWindows.SetKeepAliveValues(ASocket: TIdStackSocketHandle;



  constAEnabled:Boolean;constATimeMS, AInterval:Integer);



var



  ka: tcp_keepalive;



  Bytes: DWORD;



begin



  // SIO_KEEPALIVE_VALS is supported on Win2K+ only



  ifAEnabledand(Win32MajorVersion >=5)then



  begin



    ka.onoff :=1;



    ka.keepalivetime := ATimeMS;



    ka.keepaliveinterval := AInterval;



    WSAIoctl(ASocket, SIO_KEEPALIVE_VALS, @ka, SizeOf(ka),nil,0, @Bytes,nil,nil);



  endelsebegin



    SetSocketOption(ASocket, Id_SOL_SOCKET, Id_SO_KEEPALIVE, iif(AEnabled,1,0));



  end;



end;

里,其实就是简单设置了一下socket fd的属性,所以说TDSSessionManager毛关系都没有。

另外, KeepAliveTime默认值为300000,也就是300秒,KeepAliveInterval默认值为100,这是啥意思呢。KeepAliveTimesockfd最后一次通讯后,等待了的时间,如果300秒内没通讯,socket栈就自己开始发送心跳探测了,如果每次都没回答,就每隔KeepAliveInterval毫秒问一次。至于问多少次认为是网络断开了,根据Windows OS来定的,windows 2000, 20035次,vista以后问10次。也就是说,根据TDSTCPServerTransport的默认设定,网络断了,在win7上,要300+0.1*10,也即是301秒才知道网络断了。

OS的系统设定更长,没数据通讯后2小时才开始探测,每隔1秒探测一回。

SIO_KEEPALIVE_VALSWindowsOS独有的,Unix还是用SO_KEEPALIVE

跑题远了,回到正题。如何监控Session呢,TDSSessionManager提供了方法给你插入监听事件。

上代码

var



  event: TDSSessionEvent;



  



initialization



  event :=procedure(Sender: TObject;



            constEventType: TDSSessionEventType;



            constSession: TDSSession)



  begin



    caseEventTypeof



      SessionCreate:



        begin



          LogInfo('SessionCreate');



          LogInfo(Format('SessionName=%s', [Session.SessionName]));



        end;



      SessionClose:



        begin



          LogInfo('SessionClose');



          LogInfo(Format('SessionName=%s', [Session.SessionName]));



        end;



    end;



  end;



  TDSSessionManager.Instance.AddSessionEvent(event);



finalization



  TDSSessionManager.Instance.RemoveSessionEvent(event);

这样就可以了,有多少事件都可以插入监听。

你可能感兴趣的:(Data)