[转]COMET SERVER PUSH

COMET彗星(一)SERVER PUSH介绍

COMET介绍:

      还在为AJAX赞叹的时候,COMET竟也悄悄降临,更有甚者已经将COMET比作是AJAX的接班人。暂且不考虑服务性能和维持connection的负担,COMET的日益走红,让SERVER PUSH这样在传统C/S模式下需要借助线程服务和SOCKET通信的实现,衍生成为了借助事件和Script注册机制的WEB应用框架。

      SERVER PUSH就如同它自己的名字一样,将更多华丽的web体验“推”进了我们的视野。

      一位叫Grace Lin的工程师在自己的博客中对应用SERVER PUSH技术的在线股票系统进行了一番详细的介绍 。(地址:http://www.zkoss.org/smalltalks/stockchart/)同时,更是有很多IM应用涌现出来。本主题将从AJAX和COMET两者的结构特点进行比较,同时层层深入剖析COMET技术特点,和相应应用。

AJAX技术框架:

      大家都理解AJAX的结构,这里不再深入探究。AJAX的框架千奇百怪,但实际上就是强调了异步这个特点:数据请求后,异步的服务器处理和用户界面操作。

ajax

图1.1 传统模型与AJAX模型的对比

AJAX加入了异步运行的通讯框架。

ajax2

图1.2 AJAX异步通讯模型

      似乎一切都是那么完美,但是实际上AJAX只是实现了单用户的响应回调,与服务器的异步通讯,但是并不能反映多用户的协同相应。一个页面中同时存在的多个AJAX的异步很可能让用户修改了没有显示出来的数据,在逻辑上存在数据库事务中“脏读”或者“幻影读”的概念,还没有看到数据,发出下一个请求(当然请求是发送到了别的页面元素,但愿有页面元素中仍然是刷新了未修改的AJAX请求信息。)这些似乎都是AJAX的软肋。而COMET实际上就是解决这些问题的解决框架。

COMET技术框架:

      Comet

图2.1 AJAX与COMET框架对比

      在应用COMET框架的网页中,页面初始化后,会维持一连接,同时监听服务器端的事件信息。服务器通过事件机制来完成对浏览器(也可以是客户端)的“推”机制。

      不同客户端同时监听到服务器端的事件,并获得服务器传来的数据,而每一个客户端的请求都变成服务器的事件在网络中进行“广播”。

      当然COMET需要基于特定的服务器环境,这也是它与AJAX的很大不同。

 

      网上出现过基于客户端的服务器推技术,实际上都是使用IFRAME,APPLET,FLASH这样的元素实现的长连接,在http://www.ibm.com/developerworks/cn/web/wa-lo-comet/一文中,IBM工程师对这几者做了相应介绍,有时间的话,可以做几个demo出来与大家交流,但下一篇中,还是为大家分析一个基于comet的IM框架。

COMET彗星(二)基于SERVER PUSH的消息传输

引言:

      在上一篇随笔中,对COMET的机制和原理做了简短的介绍。网上的确有很多类似的资料可以查看,基于PHP,JSP,.NET的框架更是层出不穷。本文将简要介绍并分析一个基于.NET的SERVER PUSH框架,实现了从消息的发布,广播,到接收等一系列SERVER PUSH关键技术。

COMET技术要点:

      COMET的理念是先进而直接的,从服务器发布信息,同时让浏览器(也可以是客户端)接收并响应消息事件。就是这样简单的理念,造成了系统设计上的多重麻烦。

      首先就是如何让我们的浏览器接收到服务器发来的事件呢?

      有一种实现方式是借由无实体大小的FLASH,IFRAME或者APPLET等组件来间接打通客户端SOCKET,然后向这些元件中推入信息,并通过javascript线程,得到元件返回的信息(当然也有极个别情况下,由这些元件来直接向页面注入信息)。

      从最终结果看,我们好似得到了一个服务器的稳定连接,并刷新了页面信息。但是实际上,这样的实现方式实在是有点不够“直接”。

      FLASH(或FLEX)本身可以通过WEBSERVICE组件调用WSDL,或者直接调用java serverlet,这是FLASH PLAYER给我们的便利条件,但通过FLASH通讯的办法来实现SERVER PUSH总是让人觉得无法接受,为何不直接做一个FLASH通讯呢。

      APPLET更不用说,直接将JAR包“推”到页面运行,启动麻烦的JRE的同时还要下载庞大的JAVA程序。没有JRE的情况下,还需要相应下载。

      SILVER LIGHT的机制与FLASH类似,那是由于并没有FLASH PLAYER普及,用户同样需要下载相应播放器。

      以上三者实在谈不上是完全的web应用,我们只是想用最纯粹的javascript和html来解决问题,又何必劳师动众呢?单纯的javascript事件和线程,难道就不能满足我们的需要么?

      于是又有人想到了IFRAME,通过一个隐形的IFRAME来发送AJAX请求,通过长轮询得到消息。但这也不是真正意义上COMET,顶多是一个不成熟的AJAX应用。

      COMET的精髓就在于用服务器与javascript来维持浏览器的长连接,同时完成服务器端事件的浏览器端响应。这样的事件广播机制是跨网络的,同时也是实时的。

COMET技术难点:

      1.维持长连接

      2.维持服务器端的“心跳”

      3.浏览器端对“心跳”的“感应”

      4.维持长连接与超时的平衡

      5.线程同步

      6.通用的接口设计

     PS:这里需要注意的是,我们是在说COMET技术,而不是在谈论如何找女朋友。虽然从某个角度来看这二者似乎存在着很多类似的地方……

.NET下的COMET实现:

      我们需要COMET,我们需要源码,我们需要最单纯的实现。本着这三点需求,我为大家准备了以下这个例子,James Simpson的基于COMET技术的.NET IM。

      ClassDiagram1

图1.1 COMET基类类图

      实体类:

      1.CometClient:                      COMET的服务实体,实例化一个CometClinet的意义在于记录必要的服务信息,比如用户名称超时设置等。

      2.CometMessage:                  包含对服务信息和相信属性的实体类,是消息传输的主体。

      3.InProcCometStateProvider:继承了ICometStateProvider并实现了消息操作的方法,同时将消息保存在内存中。

      接口类:

      1.ICometStateProvider:          定义了一系列的接口来提供对CometClient实体和CometMessage的操作。

      异常类:

      1.CometException:                没有什么特殊的异常类。

      事件:

      1.CometClientEventHandler:  CometClient的事件监听类和广播类。

      线程相关:

      1.CometWaitThread:              服务器端线程池,排队等待信息请求并调用,同时提供超时等操作。

      2.CometWaitRequest:            服务器端线程,监听客户端的message请求。

      3.CometAsyncResult:           异步请求的管理类,通过继承SYSTEM.IAsyncResult来实现异步。

      主体:

      1.CometStateManager:         用来管理线程池,管理线程和用户连接,消息转发等一系列操作的工厂类。

     ps:从某种意义上来说,这个例子最好的地方就是在于它的CometStateManager实现是可以被拓展的,定制自己的管理模式,定制自己的存储操作等都可以通过这个类来进行扩展。线程管理在这个DEMO里做得相对成熟,而且事件机制可以通过定制Adapter更加丰富。

COMET彗星(三)构建自己的COMET核心

引言:

      在上一篇随笔中,对COMET使用的类和作用进行了简短的介绍,从本篇随笔开始,将从实体类开始,对COMET的核心进行构建分析。

CORE框架:

      

图1.1 COMET核心框架

CometMessage类:

     CometMessage类是COMET的通信载体,对消息的主体进行抽象,实际上这个类是最容易进行扩展的,因为从设计上看,它只是一个消息的容器。而诸如地理坐标,业务数据等,都可以通过这个类来进行直接扩充。

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Runtime.Serialization; namespace MethodWorx.AspNetComet.Core { /// <summary> /// CometMessage Class /// /// This is a CometMessage that has been sent to the client, the DataContract names have been /// shortened to remove any bytes we dont need from the message (ok, did'nt save much, but we can do it!) /// </summary> [DataContract(Name="cm")] public class CometMessage { [DataMember(Name="mid")] private long messageId; [DataMember(Name="n")] private string name; [DataMember(Name="c")] private object contents; /// <summary> /// Gets or Sets the MessageId, used to track which message the Client last received /// </summary> public long MessageId { get { return this.messageId; } set { this.messageId = value; } } /// <summary> /// Gets or Sets the Content of the Message /// </summary> public object Contents { get { return this.contents; } set { this.contents = value; } } /// <summary> /// Gets or Sets the error message if this is a failure /// </summary> public string Name { get { return this.name; } set { this.name = value; } } } }

      类的设计简单明了,这里有必要解释下使用System.Runtime.Serialization命名空间的意义。

      “System.Runtime.Serialization 命名空间包含可用于将对象序列化和反序列化的类。序列化是将对象或对象图形转换为线性字节序列,以存储或传输到另一个位置的过程。反序列化是接受存储的信息并利用它重新创建对象的过程。”

      这是MSDN给我们的解释,将对象转变为线性字节,然后方便传输与调用。当然这个例子中的数据类型并不复杂,但也包含了LONG,OBJECT,STRING这样的数据类型。其中Contents成员为object对象,这给我们留下了非常大的想像空间。(图片?复杂对象类型?自定义对象类型?……)

CometClient类:

      CometClient类是对客户端信息的抽象类,同时包含了两个关键属性ConnectionIdleSeconds和ConnectionTimeoutSeconds。由于考虑到不同客户端间传递属性,仍然使用System.Runtime.Serialization来序列化信息。

      关于JSON的应用,这个框架其实也提供了相应支持。后面的随笔中我会介绍。

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Runtime.Serialization; namespace MethodWorx.AspNetComet.Core { /// <summary> /// CometClient Class /// /// This represents a logged in client within the COMET application. This marked as a DataContract becuase /// it can be seralized to the client using JSON /// </summary> [DataContract] public class CometClient { [DataMember] private string privateToken; [DataMember] private string publicToken; [DataMember] private string displayName; [DataMember] private DateTime lastActivity; [DataMember] private int connectionIdleSeconds; [DataMember] private int connectionTimeoutSeconds; /// <summary> /// Gets or Sets the token used to identify the client to themselves /// </summary> public string PrivateToken { get { return this.privateToken; } set { this.privateToken = value; } } /// <summary> /// Gets or Sets the token used to identify the client to other clients /// </summary> public string PublicToken { get { return this.publicToken; } set { this.publicToken = value; } } /// <summary> /// Gets or Sets the display name of the client /// </summary> public string DisplayName { get { return this.displayName; } set { this.displayName = value; } } /// <summary> /// Gets or Sets the last activity of the client /// </summary> public DateTime LastActivity { get { return this.lastActivity; } set { this.lastActivity = value; } } /// <summary> /// Gets or Sets the ConnectionIdleSections property which is the number of seconds a connection will remain /// alive for without being connected to a client, after this time has expired the client will /// be removed from the state manager /// </summary> public int ConnectionIdleSeconds { get { return this.connectionIdleSeconds; } set { this.connectionIdleSeconds = value; } } /// <summary> /// Gets or Sets the ConnectionTimeOutSections property which is the number of seconds a connection will remain /// alive for whilst being connected to a client, but without receiving any messages. After a timeout has expired /// A client should restablish a connection to the server /// </summary> public int ConnectionTimeoutSeconds { get { return this.connectionTimeoutSeconds; } set { this.connectionTimeoutSeconds = value; } } } }

 

ConnectionIdleSeconds:用来设置连接线程,当connection断线后,后台Thread的存活时间。

ConnectionTimeoutSeconds:客户端的超时时间,当超过时间后,客户端重新连接服务器。

ps:有这两个属性后,基本上完成了客户端连接的控制,超时重连接,无连接时杀死后台线程。

ICometStateProvider接口:

     ICometStateProvider接口直接被CometStateMessager建立,这样的好处是实例化CometStateMessager对象后,CometStateMessager对象可以直接调用ICometStateProvider接口的实现,实际上实现了Adapter的方式,我们可以定制不同的InProcCometStateProvider类(在下面会提到)来定制自己的接口。

      ICometStateProvider接口类提供了如下几个接口。

      InitializeClient:提供ComentClient的初始化操作。

      GetMessages:得到一个Messages的消息队列。

      SendMessage:发送Message数据。

      SendMessage:可以针对Client name来进行消息发布(私人会话)。

      GetCometClient:返回一个Client。

      KillIdleCometClient:杀掉一个无效的Client对象。

      ps:当然这个接口是可以被拓展的,而且实现起来非常简单。得到Client队列信息,得到Client状态等等。甚至你可以想象一些更复杂的应用(比如加入一个流媒体消息……)。

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace MethodWorx.AspNetComet.Core { /// <summary> /// This interface can be implemented to provide a custom state provider /// for the CometStateManager class. Typical examples may be using SqlServer /// to enable the operation over a server farm /// </summary> public interface ICometStateProvider { /// <summary> /// Implementation of this method should store the cometClient instance in some sort /// of cache (eg Memory, Db etc..) /// </summary> /// <param name="cometClient"></param> void InitializeClient(CometClient cometClient); /// <summary> /// Imeplementation of this method should return all the messages that are queued /// for a specific client, it is only interested in messages that have a greater id than /// lastMessageId /// </summary> /// <param name="clientPrivateToken"></param> /// <param name="lastMessageId"></param> /// <returns></returns> CometMessage[] GetMessages(string clientPrivateToken, long lastMessageId); /// <summary> /// Implementation of this method should queue a message for the specific client /// </summary> /// <param name="clientPublicToken"></param> /// <param name="name"></param> /// <param name="contents"></param> void SendMessage(string clientPublicToken, string name, object contents); /// <summary> /// Implementation of this method should queue a message for all the clients /// </summary> /// <param name="name"></param> /// <param name="contents"></param> void SendMessage(string name, object contents); /// <summary> /// Implementation of this method should return a specific comet client /// </summary> /// <param name="clientPrivateToken"></param> /// <returns></returns> CometClient GetCometClient(string clientPrivateToken); /// <summary> /// Implementation of this method should remove a client from the cache /// </summary> /// <param name="clientPrivateToken"></param> void KillIdleCometClient(string clientPrivateToken); } }

InProcCometStateProvider类:

      InProcCometStateProvider类实现了ICometStateProvider接口,并且提供了一个很好的范例,针对这个类,我们可以想象很多很好的拓展,诸如调用AO组件,封装报警信息等等。

      InProcCometStateProvider类包含类一个私有的内部类InProcCometStateProvider,实际上可以理解为一种对消息的简单封装,设计的时候考虑到需要关联CometClient和Message,其实也可以单独作为一个外部类来设计。不过Adapter本身不应该太多关联类,这样做也是权衡了一些拓展上的需求。

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace MethodWorx.AspNetComet.Core { /// <summary> /// Class InProcCometStateProvider /// /// This class provides an implementation of ICometStateProvider that keeps the /// information in memory. This provider is not scalable as it will not run on a server /// farm but demonstrates how you should implemement the provider. /// </summary> public class InProcCometStateProvider : ICometStateProvider { /// <summary> /// Private class which holds the state of each connected client /// </summary> private class InProcCometClient { public CometClient CometClient; public Dictionary<long, CometMessage> Messages = new Dictionary<long, CometMessage>(); public long NextMessageId = 1; } /// <summary> /// Cache of clients /// </summary> private Dictionary<string, InProcCometClient> publicClients = new Dictionary<string, InProcCometClient>(); private Dictionary<string, InProcCometClient> privateClients = new Dictionary<string, InProcCometClient>(); private static object state = new object(); #region ICometStateProvider Members /// <summary> /// Store the new client in memory /// </summary> /// <param name="cometClient"></param> public void InitializeClient(CometClient cometClient) { if (cometClient == null) throw new ArgumentNullException("cometClient"); lock (state) { // ok, ensure we dont already exist if (publicClients.ContainsKey(cometClient.PublicToken) || privateClients.ContainsKey(cometClient.PrivateToken)) throw CometException.CometClientAlreadyExistsException(); InProcCometClient inProcCometClient = new InProcCometClient() { CometClient = cometClient }; // stick the client int he arrays // ready to be used publicClients.Add(cometClient.PublicToken, inProcCometClient); privateClients.Add(cometClient.PrivateToken, inProcCometClient); } // ok, they are in there ready to be used } /// <summary> /// Get the messages for a specific client /// </summary> /// <param name="clientPrivateToken"></param> /// <param name="lastMessageId"></param> /// <returns></returns> public CometMessage[] GetMessages(string clientPrivateToken, long lastMessageId) { if(string.IsNullOrEmpty(clientPrivateToken)) throw new ArgumentNullException("clientPrivateToken"); lock (state) { if (!privateClients.ContainsKey(clientPrivateToken)) throw CometException.CometClientDoesNotExistException(); // // ok, get the client InProcCometClient cometClient = privateClients[clientPrivateToken]; List<long> toDelete = new List<long>(); List<long> toReturn = new List<long>(); // wicked, we have the client, so we can get its messages from our list // we delete any before the last messageId becuase we dont want them foreach(long key in cometClient.Messages.Keys) { if(key <= lastMessageId) toDelete.Add(key); else toReturn.Add(key); } // delete the ones from the messages foreach (long key in toDelete) { cometClient.Messages.Remove(key); } // and return the ones in the toReturn array List<CometMessage> cometMessages = new List<CometMessage>(); foreach (long key in toReturn) { cometMessages.Add(cometClient.Messages[key]); } return cometMessages.ToArray(); } } /// <summary> /// Send a message to a specific client /// </summary> /// <param name="clientPublicToken"></param> /// <param name="name"></param> /// <param name="contents"></param> public void SendMessage(string clientPublicToken, string name, object contents) { if (string.IsNullOrEmpty(clientPublicToken)) throw new ArgumentNullException("clientPublicToken"); if (contents == null) throw new ArgumentNullException("contents"); lock (state) { if (!publicClients.ContainsKey(clientPublicToken)) throw CometException.CometClientDoesNotExistException(); // // ok, get the client InProcCometClient cometClient = publicClients[clientPublicToken]; // ok, stick the message in the array CometMessage message = new CometMessage(); message.Contents = contents; message.Name = name; message.MessageId = cometClient.NextMessageId; // increment cometClient.NextMessageId++; cometClient.Messages.Add(message.MessageId, message); } } /// <summary> /// Send a message to all the clients /// </summary> /// <param name="name"></param> /// <param name="contents"></param> public void SendMessage(string name, object contents) { if (contents == null) throw new ArgumentNullException("contents"); lock (state) { foreach (InProcCometClient cometClient in publicClients.Values) { // ok, stick the message in the array CometMessage message = new CometMessage(); message.Contents = contents; message.Name = name; message.MessageId = cometClient.NextMessageId; // increment cometClient.NextMessageId++; cometClient.Messages.Add(message.MessageId, message); } } } /// <summary> /// Get the client from the state provider /// </summary> /// <param name="clientPrivateToken"></param> /// <returns></returns> public CometClient GetCometClient(string clientPrivateToken) { if (!this.privateClients.ContainsKey(clientPrivateToken)) throw CometException.CometClientDoesNotExistException(); // return the client private token return this.privateClients[clientPrivateToken].CometClient; } /// <summary> /// Remove an idle client from the memory /// </summary> /// <param name="clientPrivateToken"></param> public void KillIdleCometClient(string clientPrivateToken) { if (!this.privateClients.ContainsKey(clientPrivateToken)) throw CometException.CometClientDoesNotExistException(); // get the client InProcCometClient ipCometClient = this.privateClients[clientPrivateToken]; // and remove the dictionarys this.privateClients.Remove(ipCometClient.CometClient.PrivateToken); this.publicClients.Remove(ipCometClient.CometClient.PublicToken); } #endregion } }

COMET彗星(四)COMET线程控制

引言:

      在上一篇随笔中,对操作COMET的实体类进行了简单的分析,本篇随笔将对COMET框架的线程控制进行分析,欢迎大家来拍砖。

CORE框架:

ClassDiagram1

图1.1 COMET核心框架

CometWaitRequest类:

      CometWaitRequest是一个请求信息的控制信息的抽象,包含了所有需要的控制信息,并且由CometWaitThread直接调用。简单地理解这个类,服务器事件的发布需要有一些线程进行管理,CometWaitRequest对这些事件的控制信息进行抽象,一个用户可以引发多个客户端请求,服务器端需要对这样的请求进行映射,一个用户提交给服务器的信息,需要分别由相应线程进行管制,事件的队列化,然后就是请求的队列化,消息的队列化,都需要线程的直接管理,这个类是客户端管理信息的抽象。

using System; using System.Data; using System.Configuration; using System.Linq; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Xml.Linq; namespace MethodWorx.AspNetComet.Core { /// <summary> /// Class CometWaitRequest /// /// This class contains all the information required when queue the request on a specific CometWaitThread /// </summary> public class CometWaitRequest { private CometAsyncResult result; private DateTime dateTimeAdded = DateTime.Now; private string clientPrivateToken; private long lastMessageId; private DateTime? dateDeactivated = null; /// <summary> /// Construct a new instance of a CometWaitRequest object /// </summary> /// <param name="clientPrivateToken"></param> /// <param name="lastMessageId"></param> /// <param name="context"></param> /// <param name="callback"></param> /// <param name="state"></param> public CometWaitRequest(string clientPrivateToken, long lastMessageId, HttpContext context, AsyncCallback callback, object state) { this.clientPrivateToken = clientPrivateToken; this.lastMessageId = lastMessageId; this.result = new CometAsyncResult(context, callback, state); } /// <summary> /// Gets the CometAsyncResult object associated with this CometWaitRequest /// </summary> public CometAsyncResult Result { get { return this.result; } } /// <summary> /// Gets the Date and time this request was added, so the system knows when to time it out /// </summary> public DateTime DateTimeAdded { get { return this.dateTimeAdded; } } /// <summary> /// Gets the private token of the client that is connected to this wait request /// </summary> public string ClientPrivateToken { get { return this.clientPrivateToken; } } /// <summary> /// Gets the LastMessage that is specified by the client when it connects and creates this wait request /// This is used to identify what messages we are interested in (basically any greater than LastMessageId will be /// returned to the client) /// </summary> public long LastMessageId { get { return this.lastMessageId; } } /// <summary> /// Gets a boolean flag indicating if this client is active (has it been disconnected, and is it not idle?) /// </summary> public bool Active { get { return !this.dateDeactivated.HasValue; } } /// <summary> /// Gets a DateTime indicating when the client was Deactivated, this is the period where the client can reconnect /// and become active again. If the client does not reconnect within the specified Idle time for the client, it will /// be disconnected from the server and removed from the state manager /// </summary> public DateTime? DateDeactivated { get { return this.dateDeactivated; } set { this.dateDeactivated = value; } } } }

      PS:这里需要说明的是 "DateTime?” 后加的问号最好不要去掉,它代表DateDeactivated变量可赋值为NULL,超时是可以存在也可不存在的,所以我们没有必要为DateDeactivated预先实例化。CometAsyncResult类在这个类里被实例化。

 

CometAsyncResult类:

      这个类也很有趣,MSDN上对IAsyncResult 接口有如下的解释:

      “IAsyncResult 接口由包含可异步操作的方法的类实现。它是启动异步操作的方法的返回类型,如 FileStream.BeginRead,也是结束异步操作的方法的第三个参数的类型,如 FileStream.EndRead。当异步操作完成时,IAsyncResult 对象也将传递给由 AsyncCallback 委托调用的方法。

      支持 IAsyncResult 接口的对象存储异步操作的状态信息,并提供同步对象以允许线程在操作完成时终止。”

      CometAsyncResult类继承了IAsyncResult 接口有很多的好处,首先我们可以直接借用.net框架AsyncCallback的BeginInvoke和EndInvoke启动或终结异步操作。

      “.NET Framework 允许您异步调用任何方法。为此,应定义与您要调用的方法具有相同签名的委托;公共语言运行库会自动使用适当的签名为该委托定义 BeginInvoke 和 EndInvoke 方法。

      BeginInvoke 方法可启动异步调用。它与您需要异步执行的方法具有相同的参数,另外它还有两个可选参数。第一个参数是一个 AsyncCallback 委托,该委托引用在异步调用完成时要调用的方法。第二个参数是一个用户定义的对象,该对象可向回调方法传递信息。BeginInvoke 立即返回,不等待异步调用完成。BeginInvoke 会返回 IAsyncResult,这个结果可用于监视异步调用进度。

      EndInvoke 方法检索异步调用的结果。调用 BeginInvoke 后可随时调用 EndInvoke 方法;如果异步调用尚未完成,EndInvoke 将一直阻止调用线程,直到异步调用完成后才允许调用线程执行。EndInvoke 的参数包括您需要异步执行的方法的 out 和 ref 参数(在 Visual Basic 中为 <Out> ByRef 和 ByRef)以及由 BeginInvoke 返回的 IAsyncResult。”

      使用帮助:http://msdn.microsoft.com/zh-cn/library/2e08f6yc(VS.80).aspx

      PS:如果只是从应用来说,这一段是可以跳过去的,因为拓展这套框架不需要我们修改这个类的结构。

using System; using System.Data; using System.Configuration; using System.Linq; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Xml.Linq; using System.Threading; namespace MethodWorx.AspNetComet.Core { /// <summary> /// An Implementation of IAsyncResult that enables the use of the custom thread pool /// and enables us to use the IHttpAsyncHandler implementation /// </summary> public class CometAsyncResult : IAsyncResult { private AsyncCallback callback; private object asyncState; private bool isCompleted = false; private CometMessage[] messages; private HttpContext context; /// <summary> /// Construct a new instance of CometAsyncResult /// </summary> /// <param name="context">The HTTP Context passed in from the handler</param> /// <param name="callback">The AsyncCallback passed in from the handler</param> /// <param name="asyncState">The extra data passed in from the handler</param> public CometAsyncResult(HttpContext context, AsyncCallback callback, object asyncState) { this.callback = callback; this.asyncState = asyncState; this.context = context; } #region IAsyncResult Members /// <summary> /// Gets or Sets the extra data associated with this async operation /// </summary> public object AsyncState { get { return this.asyncState; } } /// <summary> /// Not Implemented: will throw InvalidOperationException("ASP.NET Should never use this property"); } /// </summary> public WaitHandle AsyncWaitHandle { get { throw new InvalidOperationException("ASP.NET Should never use this property"); } } /// <summary> /// Gets a boolean indicating if the operation completed synchronously (always returns false) /// </summary> public bool CompletedSynchronously { get { return false; } } /// <summary> /// Gets a boolean indicating if the operation has completed /// </summary> public bool IsCompleted { get { return this.isCompleted; } } /// <summary> /// Gets the HttpContext associaetd with this async operation /// </summary> public HttpContext Context { get { return this.context; } } #endregion /// <summary> /// Gets the Messages that are to be returned upon completion of this Async Operation /// </summary> public CometMessage [] CometMessages { get { return this.messages; } set { this.messages = value; } } /// <summary> /// Signal this operation has completed /// </summary> internal void SetCompleted() { this.isCompleted = true; if (callback != null) callback(this); } } }

CometWaitThread类:

      CometWaitThread线程类负责控制CometWaitRequest,当一个用户提交了消息以后,会建立相应的CometWaitThread。

      ps:关于线程控制这部分,只是做简要的介绍,毕竟COMET框架的核心说到这里,就都是实现的机制了,而线程无非是作为各种“通道”的管理工具存在,并不需要什么拓展。当然如果做为研究,这里可以深入挖掘。比如加入一些竞争算法来优化消息控制,或者对特定消息(如紧急事件等)可以在线程控制上优先发布。

     

using System; using System.Data; using System.Configuration; using System.Linq; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Xml.Linq; using System.Collections.Generic; using System.Threading; using System.Diagnostics; namespace MethodWorx.AspNetComet.Core { /// <summary> /// Class CometWaitThread /// /// This class contains an implementation of the thread pool that controls the /// CometWaitRequest objects and returns specified messages, errors or timeout messages /// back to the client in a controlled and scalable fasion /// </summary> public class CometWaitThread { private object state = new object(); private List<CometWaitRequest> waitRequests = new List<CometWaitRequest>(); private CometStateManager stateManager; public List<CometWaitRequest> WaitRequests { get { return this.waitRequests; } } public CometWaitThread(CometStateManager stateManager) { // get the state manager this.stateManager = stateManager; Thread t = new Thread(new ThreadStart(QueueCometWaitRequest_WaitCallback)); t.IsBackground = false; t.Start(); } internal void QueueCometWaitRequest(CometWaitRequest request) { lock (this.state) { waitRequests.Add(request); } } internal void DeactivateCometWaitRequest(CometWaitRequest request) { lock (state) { //this.waitRequests.Remove(request); // we disable the request, and we hope the // client should connect immediatly else we time it out! request.DateDeactivated = DateTime.Now; } } private void QueueCometWaitRequest_Finished(object target) { CometWaitRequest request = target as CometWaitRequest; request.Result.SetCompleted(); } private void QueueCometWaitRequest_WaitCallback() { // here we are... // in a loop while (true) { //Debug.WriteLine(string.Format("QueueCometWaitRequest_WaitCallback Tick: {0} {1} ", Thread.CurrentThread.IsThreadPoolThread, Thread.CurrentThread.ManagedThreadId)); CometWaitRequest[] processRequest; lock (this.state) { processRequest = waitRequests.ToArray(); } // we have no more wait requests left, so we want exis /*if (processRequest.Length == 0) break;*/ if (processRequest.Length == 0) { // sleep for this time Thread.Sleep(100); } else { for (int i = 0; i < processRequest.Length; i++) { try { CometClient cometClient = this.stateManager.StateProvider.GetCometClient(processRequest[i].ClientPrivateToken); if (processRequest[i].Active) { Thread.Sleep(100); // timed out so remove from the queue if (DateTime.Now.Subtract(processRequest[i].DateTimeAdded).TotalSeconds >= cometClient.ConnectionTimeoutSeconds) { // dequeue the request DeactivateCometWaitRequest(processRequest[i]); // get the message CometMessage timeoutMessage = new CometMessage() { MessageId = 0, Name = "aspNetComet.timeout", Contents = null }; // // ok, we we timeout the message processRequest[i].Result.CometMessages = new CometMessage[] { timeoutMessage }; // call the message this.QueueCometWaitRequest_Finished(processRequest[i]); } else { CometMessage[] messages = this.CheckForServerPushMessages(processRequest[i]); if (messages != null && messages.Length > 0) { // we have our message processRequest[i].Result.CometMessages = messages; // and return! // dequeue the request DeactivateCometWaitRequest(processRequest[i]); // queue the response on another ASP.NET Worker thread this.QueueCometWaitRequest_Finished(processRequest[i]); } } } else { // this is an inactive this.CheckForIdleCometWaitRequest(processRequest[i], cometClient); } } catch (Exception ex) { if (processRequest[i].Active) { // ok, this one has screwed up, so // we need to dequeue the request from ASP.NET, basically disable it and return // dequeue the request DeactivateCometWaitRequest(processRequest[i]); // get the message CometMessage errorMessage = new CometMessage() { MessageId = 0, Name = "aspNetComet.error", Contents = ex.Message }; // // ok, we we timeout the message processRequest[i].Result.CometMessages = new CometMessage[] { errorMessage }; // call the message this.QueueCometWaitRequest_Finished(processRequest[i]); } else { // this is not an active request, so we dequeue it from the // thread this.DequeueCometWaitRequest(processRequest[i].ClientPrivateToken); } } } } } } private void CheckForIdleCometWaitRequest(CometWaitRequest request, CometClient cometClient) { lock (state) { if (DateTime.Now.Subtract(request.DateDeactivated.Value).TotalSeconds >= cometClient.ConnectionIdleSeconds) { // ok, this dude has timed out, so we remove it this.stateManager.KillIdleCometClient(cometClient.PrivateToken); // and deque the request this.waitRequests.Remove(request); } } } private CometMessage[] CheckForServerPushMessages(CometWaitRequest request) { // // ok, we we need to do is get the messages // that are stored in the state provider return this.stateManager.StateProvider.GetMessages(request.ClientPrivateToken, request.LastMessageId); } internal void DequeueCometWaitRequest(string privateToken) { lock (state) { for(int i =0; i < this.waitRequests.Count; i ++) { CometWaitRequest request = this.waitRequests[i]; if (request.ClientPrivateToken == privateToken) { // remove it this.waitRequests.Remove(request); break; } } } } } }

CometStateManager类:

      CometStateManager是整套COMET机制的核心,它将CometWaitThreadICometStateProviderCometClientCometMessage结合在了一起,形成了整套COMET的应用框架。是拓展COMET应用必须要改造的类。

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; using System.Threading; using System.Web; using System.Web.UI; using System.Runtime.Serialization.Json; namespace MethodWorx.AspNetComet.Core { /// <summary> /// CometStateManger Class /// /// An instance of the this class is used to control the manager the state of the COMET Application. /// This class manages an instance of a ICometStateProvider instance /// </summary> /// public class CometStateManager { private ICometStateProvider stateProvider; private int workerThreadCount; private int maximumTimeSlot; private int currentThread = 0; private CometWaitThread[] workerThreads;// private object state = new object(); /// <summary> /// Event that is called when a Client is Initialized /// </summary> public event CometClientEventHandler ClientInitialized; /// <summary> /// Event that is called when a Client is killed /// </summary> public event CometClientEventHandler IdleClientKilled; /// <summary> /// Event that is called when a Client subscribes to this channel /// </summary> public event CometClientEventHandler ClientSubscribed; /// <summary> /// Construct an instane of the CometStateManager class and pass in an /// instance of an ICometStateProvider to manage the persistence of the state /// </summary> /// <param name="stateProvider">An instance of an ICometStateProvider class that manages the persistence of the state</param> /// <param name="workerThreadCount">How many worked threads should this CometStateManager initialize</param> /// <param name="maximumTimeSlot">The maximum time in milliseconds that should be idle between each COMET client is polled within a worker thread</param> public CometStateManager(ICometStateProvider stateProvider, int workerThreadCount, int maximumTimeSlot) { if (stateProvider == null) throw new ArgumentNullException("stateProvider"); if (workerThreadCount <= 0) throw new ArgumentOutOfRangeException("workerThreadCount"); // ok, setup the member of this class this.stateProvider = stateProvider; this.workerThreadCount = workerThreadCount; this.maximumTimeSlot = maximumTimeSlot; this.workerThreads = new CometWaitThread[5]; // // ok, lets fireup the threads for(int i = 0; i < workerThreadCount; i ++) { this.workerThreads[i] = new CometWaitThread(this); } } /// <summary> /// Construct an instane of the CometStateManager class and pass in an /// instance of an ICometStateProvider to manage the persistence of the state /// /// This calls the main constructor and specifies the default values workerThreadCount = 5 and maximumTimeSlot = 100. These values /// can be tuned for your application by using the main constructor /// </summary> /// <param name="stateProvider">An instance of an ICometStateProvider class that manages the persistence of the state</param> public CometStateManager(ICometStateProvider stateProvider) : this(stateProvider, 5, 100) { } /// <summary> /// Creates a CometClient instance that is persisted into the ICometStateProvider instance. This needs to be /// called by the server prior to the client connecting from a client application. /// </summary> /// <remarks> /// This method will typically be used after a login script is executed, either from a standard ASP.NET form /// or an Ajax Method etc... /// /// The server would validate the user information, if successfull initialize a client in the COMET engine ready /// for the client to connect. /// </remarks> /// <param name="publicToken">The public token of the client, this token is used to identify the client to other clients</param> /// <param name="privateToken">The private token of the client, this token is used to identify the client to itself</param> /// <param name="displayName">The display name of the client, can be used to hold a friendly display name of the client</param> /// <param name="connectionTimeoutSeconds">The number of seconds the client will be connected to the server for, until it needs to reestablish a connection becuase no messages have been sent</param> /// <param name="connectionIdleSeconds">The number of seconds the server will wait for the client to reconnect before it treats it as an idle connection and removes it from the server</param> /// <returns>An initialized CometClient object that represents the initialized client</returns> public CometClient InitializeClient(string publicToken, string privateToken, string displayName, int connectionTimeoutSeconds, int connectionIdleSeconds) { // validate the parameters if (string.IsNullOrEmpty(publicToken)) throw new ArgumentNullException("publicToken"); if (string.IsNullOrEmpty(privateToken)) throw new ArgumentNullException("privateToken"); if (string.IsNullOrEmpty(displayName)) throw new ArgumentNullException("displayName"); if (connectionIdleSeconds <= 0) throw new ArgumentOutOfRangeException("connectionIdleSeconds must be greater than 0"); if (connectionTimeoutSeconds <= 0) throw new ArgumentOutOfRangeException("connectionTimeoutSeconds must be greater than 0"); CometClient cometClient = new CometClient(); // ok, set it up cometClient.ConnectionIdleSeconds = connectionIdleSeconds; cometClient.ConnectionTimeoutSeconds = connectionTimeoutSeconds; cometClient.DisplayName = displayName; cometClient.LastActivity = DateTime.Now; cometClient.PrivateToken = privateToken; cometClient.PublicToken = publicToken; // // send this to the state provider this.stateProvider.InitializeClient(cometClient); // ok, fire the event this.FireClientInitialized(cometClient); return cometClient; } /// <summary> /// Called from an Asynchronous HttpHandler Method to begin the Subscribe call /// </summary> /// <param name="context">HttpContext passed in from the handler</param> /// <param name="callback">AsyncCallback passed in from the handler</param> /// <param name="extraData">AsyncState passed in from the handler</param> /// <returns>An IAsyncResult used to identify and control the asynchronous operation</returns> public IAsyncResult BeginSubscribe(HttpContext context, AsyncCallback callback, object extraData) { try { long lastMessageId; string privateToken; if (!long.TryParse(context.Request["lastMessageId"] ?? "-1", out lastMessageId)) throw CometException.CometHandlerParametersAreInvalidException(); privateToken = context.Request["privateToken"]; if (string.IsNullOrEmpty(privateToken)) throw CometException.CometHandlerParametersAreInvalidException(); this.DebugWriteThreadInfo("BeginSubscribe"); lock (state) { // // get the comet client CometClient cometClient = this.GetCometClient(privateToken); // ok, fire the event this.FireClientSubscribed(cometClient); // kill the previous one if one exists // from the thread pool for (int i = 0; i < this.workerThreadCount; i++) { this.workerThreads[i].DequeueCometWaitRequest(privateToken); } // ok, this is our result, so lets queue it CometWaitRequest request = new CometWaitRequest(privateToken, lastMessageId, context, callback, extraData); // we have our request so lets queue it on a thread this.workerThreads[this.currentThread].QueueCometWaitRequest(request); // cycle the thread count this.currentThread++; if (this.currentThread >= this.workerThreadCount) this.currentThread = 0; // cycle back to 0 return request.Result; } } catch (Exception ex) { this.WriteErrorToResponse(context, ex.Message); return null; } } /// <summary> /// Called from an Asynchronous HttpHandler Method method to complete the Subscribe call /// </summary> /// <param name="result">The IAsyncResult instance that was initialized in the BeginSubscribe call</param> public void EndSubscribe(IAsyncResult result) { this.DebugWriteThreadInfo("EndSubscribe"); CometAsyncResult cometAsyncResult = result as CometAsyncResult; if (cometAsyncResult != null) { try { // get the messages CometMessage[] messages = cometAsyncResult.CometMessages; // serialize the messages // back to the client if (messages != null && messages.Length > 0) { List<Type> knownTypes = new List<Type>(); foreach (CometMessage message in messages) { if (message.Contents != null) { Type knownType = message.Contents.GetType(); if (!knownTypes.Contains(knownType)) { knownTypes.Add(knownType); } } } DataContractJsonSerializer serializer = new DataContractJsonSerializer(messages.GetType(), knownTypes); serializer.WriteObject(((CometAsyncResult)result).Context.Response.OutputStream, messages); } } catch (Exception ex) { // write the error out?? this.WriteErrorToResponse(((CometAsyncResult)result).Context, ex.Message); } } } /// <summary> /// Send a message to a specific client /// </summary> /// <param name="clientPublicToken">The public token of the client</param> /// <param name="name">The name of the message</param> /// <param name="contents">The contents of the message</param> public void SendMessage(string clientPublicToken, string name, object contents) { this.stateProvider.SendMessage(clientPublicToken, name, contents); } /// <summary> /// Send a message to all clients /// </summary> /// <param name="name">The name of the message</param> /// <param name="contents">The contents of the message</param> public void SendMessage(string name, object contents) { this.stateProvider.SendMessage(name, contents); } /// <summary> /// Gets the ICometStateProvider instance this manager consumes /// </summary> internal ICometStateProvider StateProvider { get { return this.stateProvider; } } /// <summary> /// Register the required javascript for the page /// </summary> /// <param name="page">The page we want to write the scripts to</param> public static void RegisterAspNetCometScripts(Page page) { page.ClientScript.RegisterClientScriptResource(typeof(CometStateManager), "MethodWorx.AspNetComet.Core.Scripts.AspNetComet.js"); } /// <summary> /// Kill an IdleCometClient /// </summary> /// <param name="clientPrivateToken"></param> public void KillIdleCometClient(string clientPrivateToken) { // get the comet client CometClient cometClient = this.stateProvider.GetCometClient(clientPrivateToken); // ok, tmie the clietn out this.stateProvider.KillIdleCometClient(clientPrivateToken); // and fire this.FireIdleClientKilled(cometClient); } public CometClient GetCometClient(string clientPrivateToken) { return this.stateProvider.GetCometClient(clientPrivateToken); } internal void DebugWriteThreadInfo(string message) { int workerAvailable = 0; int completionPortAvailable = 0; ThreadPool.GetAvailableThreads(out workerAvailable, out completionPortAvailable); Debug.WriteLine(string.Format("{0}: {1} {2} out of {3}/{4}", message, Thread.CurrentThread.IsThreadPoolThread, Thread.CurrentThread.ManagedThreadId, workerAvailable, completionPortAvailable)); } internal void FireClientInitialized(CometClient cometClient) { if (this.ClientInitialized != null) this.ClientInitialized(this, new CometClientEventArgs(cometClient)); } internal void FireIdleClientKilled(CometClient cometClient) { if (this.IdleClientKilled != null) this.IdleClientKilled(this, new CometClientEventArgs(cometClient)); } internal void FireClientSubscribed(CometClient cometClient) { if (this.ClientSubscribed != null) this.ClientSubscribed(this, new CometClientEventArgs(cometClient)); } private void WriteErrorToResponse(HttpContext context, string message) { // // ok, we have had an error so we have to return it CometMessage errorMessage = new CometMessage(); errorMessage.Name = "aspNetComet.error"; errorMessage.MessageId = 0; errorMessage.Contents = message; CometMessage[] messages = new CometMessage[] { errorMessage }; DataContractJsonSerializer serializer = new DataContractJsonSerializer(messages.GetType()); serializer.WriteObject(context.Response.OutputStream, messages); context.Response.End(); } } }

CometClientEventHandler.cs:

      CometClientEventHandler.cs负责事件的广播。不需要有太多修改。

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace MethodWorx.AspNetComet.Core { /// <summary> /// Delegate used in events that reference a CometClient /// </summary> /// <param name="sender"></param> /// <param name="e"></param> /// <returns></returns> public delegate void CometClientEventHandler(object sender, CometClientEventArgs args); //使用代理来完成事件操作和广播 /// <summary> /// Class CometClientEventArgs /// /// Used as an EventArgs parameter to CometClientEventHandler /// </summary> public class CometClientEventArgs : EventArgs { private CometClient cometClient; /// <summary> /// Construct a new instance of a CometClientEventArgs class /// </summary> /// <param name="cometClient"></param> public CometClientEventArgs(CometClient cometClient) { // setup the member this.cometClient = cometClient; } /// <summary> /// Gets the CometClient referenced in these EventArgs /// </summary> public CometClient CometClient { get { return this.cometClient; } } } }

posted @ 2011-04-28 15:36  豆腐干  阅读(1039)  评论(0编辑  收藏  举报