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()).Free; sleep(); 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属性,可理解为ClientId,ClientId必须是唯一的,相同的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只能做到服务器向客户端推送数据,单向的。客户端要向服务器送数据,走TDSAdminClient的NotifyCallback方法,也就是只有经过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 >)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的消息,这就让ConnectClientChannel的repeat循环也会退出(data=nil),从而关闭最开始连接。
另外, TDSClientCallbackChannelManager在界面上找不到输入认证信息的地方,比如DSAuthPassword, DSAuUser等,其实TDSClientCallbackChannelManager也好SQLConnection也好,他们都是个载体罢了,真正在后面起作用的,是TDBXProperties。监视一下DSServer的OnConnect里的DSConnectEventObject.ConnectProperties,我们就能知道TDSClientCallbackChannelManager的username, password,其实是SQLConnection的DSAuthPassword,DSAuUser。
在TDBXProperties里的键值对为
DSAuthenticationUser=
DSAuthenticationPassword=
最后,TDSClientCallbackChannelManager并没有Filters的属性,这个其实现在的客户端,就算使用SQLConnection时也不必设置Filters了,我们只要别忘记在客户端也TTransportFilterFactory.RegisterFilter一下要用的Filter即可。
datasnap的初步 序列化自己写的类
今天在网上找到了一个marshall和unmarshall的例子,将自己的定义的类,序列号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 :=; 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 =); 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
这个更细,每次ServerMethod的Call,都将创建和销毁一PersistentClass的实例。由于创建销毁比较耗资源,可以操作TDSServerClass的OnCreateInstance和OnDestroyInstance事件,在这两个事件里面做缓存池。代码如下
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;
这样定义后,就算不写DSAuthenticationManager1UserAuthorize,TDSAuthenticationManager也会自动帮你检查该角色是否有权利调用该ServerMethod。RTTI估计是学Java的Annotation才增加了TCustomAttribute。
2.客户端
客户端很简单了,设置SQLConnection的DSAuthUser和DSAuthPassword就行了。
datasnap的初步 对象的销毁
TServerMethods1Client继承自TDSAdminClient,这个类的构造函数
constructor Create(ADBXConnection: TDBXConnection); overload;
后面的AInstanceOwner参数,挺重要,理解这个,对于避免内存泄漏有很大好处。
默认情况下,我们使用Create来创建ServerMethodClient,也就AInstanceOwner为true了,也就是所有进入 TServerMethods1Client类方法的参数,都被ServerMethodClient给来释放。我觉得EMBT推荐使用 AInstanceOwner=True。
DATASNP如何释放内存,请看代码
客户端
procedureTDBXCommand.CommandExecuting; begin ifAssigned(FFreeOnCloseList)then FreeOnExecuteObjects;//这里释放 Open; CloseReader; if(FParameters <>nil)and(FParameters.Count >)then begin ifnotFPreparedthen Prepare; SetParameters; end; end;
也就是说,对每个DBXCommand,每次执行前都会清理上一回留下的垃圾。当然最后的垃圾肯定要等到DBXCommand.Close时才去清理了。
对于function(a: TA, out b: TB): TA这样的调用
AInstanceOwner为true时,a, b, 以及返回值result我们都不用去自己Free。尤其要注意入口参数a,可能进去执行后立刻被Free(需要被Marshal的类),也可能是等到下次Call时被Free(比如TStream)。
反之,则都需要自己去free。但是TDBXCallback,是个例外,就算AInstanceOwner为False,也不能自己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的初步 关于TDSTCPServerTransport的Filters
TDSTCPServerTransport的Filter属性,可以对传递的数据进行加密,压缩,再修改等,有点注入的概念。默认情况下,Datasnap自带的ZLIB, PC1,RSA三个Filter。测试了一下,RSA只对KEY加密,PC1才对内容加密,ZLIB来做压缩,ZLIB压缩实在不咋的。并且,Filter的顺序,是依次执行的。我现在打算实现,服务器的一个Log功能,记录下来进入的数据,出去的数据,要求记录下来的数据是明文。
TTransportFilter的ProcessInput,ProcessOutput光看名字比较费解,可以这么理解ProcessInput为编码,ProcessOutput可以理解为解码。
首先给DSTCPServerTransport1的Fitlers都加上默认的3个Filter。
上一个完整的代码
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实现了上面的功能,
数据进入服务器时,DataSnap的Reader读出时按顺序经过Filter进行解码,最后的Filter,也就是这里的TLogTailFilter的ProcessOutput出来的肯定应该是明文了,记录下来。
数据出服务器时, DataSnap的Writer写数据时,也按顺序经过Filter进行编码,刚开始的肯定是明文的,也就是TLogHeadFilter的ProcessInput了,记录下来。
要使用这个unit,只要在ServerContainerUnit1单一的OnCreate里面写入即可。如下
procedure TServerContainer1.DataModuleCreate(Sender: TObject); begin AddLogFilter(DSTCPServerTransport1.Filters); end;
最后,上个图,看看client和服务器之间的通讯是怎样的。
为方面看,我分开了,第一次是connect,然后二次调用了EchoString。可以看出一次servermethod,有3个来回的交流(一个prepare, 一个execute,一个command_close。prepare和command_close并不是每次必须的,这里因为我是每次都创建新的TServerMethods1Client),并且交流的数据都是JSON的整列。这里也打印出了编码解码前数据长度,以及编码解码后的数据长度,如果要测试ZLIB的压缩效果,可以参考。
或许要说DSServer的OnTrace事件也可以玩,但是 OnTrace只能记录Client进Server的数据,对出去的数据TRACE不到的,很遗憾。
最后,有一些其他的现成的开源Filter可用,尤其是压缩的,去http://code.google.com/p/dsfc/
datasnap的初步 直接返会自定义类
前面我说datasnap不支持自定义类型是错误的。其实datasnap一旦发现是自定义类型,就会自动用json给marshall了,今天的测试代码如下。
服务器端
functionTServerMethods1.GetPerson: TPerson; begin Result := TPerson.Create; Result.FirstName :='zyz'; Result.LastName :='Jacky'; Result.Age :=; 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[].Value.IsNullthen begin FUnMarshal := TDBXClientCommand(FGetPersonCommand.Parameters[].ConnectionHandler).GetJSONUnMarshaler; try Result := TPerson(FUnMarshal.UnMarshal(FGetPersonCommand.Parameters[].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使用的默认为true,UnMarshal的CreateObject产生的类,让Datasnap自己去释放了,所以p.free要注视掉。释放的时机有两个:
1。下一次调用到来
2。DBXCommand.Close
datasnap的初步 获得客户端的信息
记得datasnap 2009时,要得到客户端信息,非官方的方法,要去搞什么DSConnectEventObject.ChannelInfo.Id,弄成 TIdTCPConnection。xe2就好得多了。
仍然是在DSServer的OnConnect 事件里,
DSConnectEventObject.ChannelInfo.ClientInfo就是客户端的信息。能得到啥?
看代码
TDBXClientInfo =record IpAddress:String; ClientPort:String; Protocol:String; AppName:String; end;
也就是能取得客户端ip,端口,连接协议,不过AppName这玩意儿一直是空的。
执行到 DSServer的OnConnect的事件里,其实socket已经完全连上了,client已经调用了server的connect方法了,在这个方法里触发的OnConnect。所以DSServer的OnConnect其实并不是真的socket的OnConnect
datasnap的初步 Session的管理
Datasnap的session管理是强制的,没有选项能说不要。
管理靠一单例TDSSessionManager来管理。对于目前说到TDSTCPServerTransport,建立的的Session为TDSTCPSession,它是TDSSession的子类。
Session在开始连接后,就创建了,再连接断开后消亡。
TDSSession =class private FStartDateTime: TDateTime; /// creation timestamp FDuration:Integer; /// in miliseconds, 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可以用存储了很多东西 。用得多的是FMetaData与FMetaObjects
对于字符串,PutData放进去,GetData取出来;对于Object,PutObject放进去,GetObject取出来。
使用方法为
TDSSessionManager.GetThreadSession.PutData('userid', userId); userId := TDSSessionManager.GetThreadSession.GetData('userid');
另外,放入FMetaObjects的Object,Session的Free时,会自动帮忙Free,所以不必自己去Free的。
关于Session的超时,
这里自然就想到了TDSTCPServerTransport的KeepAliveInterval和KeepAliveTime属性,这两个属性,其实和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 >=)then begin ka.onoff :=; ka.keepalivetime := ATimeMS; ka.keepaliveinterval := AInterval; WSAIoctl(ASocket, SIO_KEEPALIVE_VALS, @ka, SizeOf(ka),nil,, @Bytes,nil,nil); endelsebegin SetSocketOption(ASocket, Id_SOL_SOCKET, Id_SO_KEEPALIVE, iif(AEnabled,,)); end; end;
里,其实就是简单设置了一下socket fd的属性,所以说TDSSessionManager毛关系都没有。
另外, KeepAliveTime默认值为300000,也就是300秒,KeepAliveInterval默认值为100,这是啥意思呢。KeepAliveTime是sockfd最后一次通讯后,等待了的时间,如果300秒内没通讯,socket栈就自己开始发送心跳探测了,如果每次都没回答,就每隔KeepAliveInterval毫秒问一次。至于问多少次认为是网络断开了,根据Windows OS来定的,windows 2000, 2003是5次,vista以后问10次。也就是说,根据TDSTCPServerTransport的默认设定,网络断了,在win7上,要300+0.1*10,也即是301秒才知道网络断了。
OS的系统设定更长,没数据通讯后2小时才开始探测,每隔1秒探测一回。
SIO_KEEPALIVE_VALS值Windows的OS独有的,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);
这样就可以了,有多少事件都可以插入监听。