• 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);

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

    https://www.cnblogs.com/china1/p/3380333.html

  • 相关阅读:
    清理git提交记录并不能达到真正硬盘“瘦身”
    virtualbox虚拟机异常暂停
    百兆带宽升千兆-番外篇
    流水文之百兆带宽升千兆
    OSS设置静态网站托管+CDN加速OSS域名
    每日一坑-exsi中win虚拟机调分辨率
    nginx筛选字段+excel统计
    树莓派使用Samba共享文件
    chrome无法使用独显解决
    解决jdk16安装后无jre目录的问题
  • 原文地址:https://www.cnblogs.com/fhweixin/p/13743115.html
Copyright © 2020-2023  润新知