基于 Asp.Net的 Comet 技术解析

Comet技术原理

来自维基百科:Comet是一种用于web的技术,能使服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有两种实现方式,长轮询和iframe流。javascript

简单的说是一种基于现有Http协议基础上的长轮询技术,之全部会产生这种技术的主要缘由是Http协议是无状态的因此客户端和服务端之间没办法创建起一套长时间的链接。好比咱们要作一个聊天室,在Web环境下咱们一般不能从服务端推送消息到浏览器里,而只能经过每一个客户端不断的轮询服务器,以获取最新的消息,这样一来效率很是低,并且不断的向服务器发送请求对于访问量大的应用来讲也会形成很大的资源占用。html

因而人们就发现了这种技术,向服务器发起一个请求,而后服务器一直不响应这个请求,这样客户端和服务端之间就造成了一个长链接,直到服务端响应这个请求后结束本次链接。借用一下IBM里的图片:java

 

经过Ajax技术能够实现长轮询的服务器推模型,客户端和服务端之间经过不断的发起长轮询便可以实现数据的交互,这个过程因为是Ajax实现的异步操做因此体验上会比较好,效率也很高。哎呀呀,说不清楚,找个网上的资料:git

Comet方式通俗的说就是一种长链接机制(long lived http)。一样是由Browser端主动发起请求,可是Server端以一种彷佛很是慢的响应方式给出回答。这样在这个期间内,服务器端可使用同一个connection把要更新的数据主动发送给Browser。所以请求可能等待较长的时间,期间没有任何数据返回,可是一旦有了新的数据,它将当即被发送到客户机。Comet又有不少种实现方式,可是总的来讲对Server端的负载都会有增长.虽然对于单位操做来讲,每次只须要建议一次connection,可是因为connection是保持较长时间的,对于 server端的资源的占用要有所增长。github

优势: 实时性好(消息延时小);性能好(能支持大量用户)web

缺点: 长期占用链接,丧失了无状态高并发的特色。数据库

应用: 股票系统、实时通信。json

参考资料:设计模式

Comet:基于 HTTP 长链接的“服务器推”技术跨域

基于Asp.Net的实现Comet的技术基础

Asp.Net自己就是为web而生的技术,因此先天是知足滴。基于Ajax技术与Asp.net的异步请求处理能够为Comet提供更增强大的能力。在此隆重推出:IHttpAsyncHandler接口。

  • IHttpAsyncHandler接口简介

IhttpAsyncHandler是继承于IhttpHandler,可是不一样的是IHttpAsyncHandler具备天生的异步能力。他比IHttpHandler多2个方法:

IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData);
void EndProcessRequest(IAsyncResult result);

BeginProcessRequest 方法返回的是IAsyncResult接口,一般在BeginProcessRequest中处理一些比较繁重费时的任务,好比IO操做,读取Web服务等。一旦异步操做完成以后,则能够经过EndProcessRequest方法得到异步的结果。

IHttpAsyncHandler的好处在于,在它处理异步方法的时候,处理请求的线程能够暂时获得释放,而有空闲去处理其余请求,等异步方法运行完毕以后,在由线程去处理接下来的请求。

  • Asp.Net实现Comet

有了技术基础那么来看看如何实现这项技术:

在客户端咱们须要实现发送请求,这方面能够经过Ajax技术来实现,能够经过javascript比较简单方便的实现异步请求操做。

在服务端监听专门的请求类型,经过实现IhttpAsyncHandler处理请求,BeginProcessRequest方法中有个AsyncCallback类型的参数cb,这是个回调函数,在asp.net中若是不调用这个回调函数cb则不会响应请求,即不会向客户端返回内容,这就实现长链接。直到服务端有数据须要返回给客户端,服务端再调用cb函数以触发执行EndProcessRequest方法,此时客户端才会接收到响应包。

在客户端接收完数据后能够继续向服务端发起请求,重复这个过程就能够模拟出一个长链接的状态。

AspComet组件介绍

在asp.net里有个开源的组件AspComet比较好的实现了Comet,此组件的开源站点:https://github.com/nmosafi/aspcomet

在AspComet中的核心主要是经过Ajax发起请求,在服务端基于IhttpAsyncHandler来处理请求,经过一个消息总线处理了一整套的Web推技术。组件分为服务端和客户端两部分,都具有良好的扩展性,服务端有比较灵活的委托处理,也能够经过本身继承实现改写本身须要的业务处理,很是方便的二次开发。而客户端也提供了良好的封装性,支持多种主流js脚本库,如Jquery,dojo等,在官方的demo中就提供了这两种脚本库的实现。

在阅读了Aspcomet的源代码后仍是比较感叹的,虽然看起来很费劲,但也着实感受到了这套代码对二次开发提供了很好的支持。基本都是面向对象来实现了整个组件,即便是JS也应用了不少的设计模式。下面就这个组件的主要实现作一些介绍:

服务端


 

一、 首先必须实现IhttpAsyncHandler接口

CometHttpHandler:IhttpAsyncHandler,此类就是用于异步请求的处理单元,简单的说就是服务端的入口。在这里经过BeginProcessRequest方法将请求的内容hold住,同时也将callback也Hold住。固然这里有个重点要注意就是MessageBus,全部消息如何hold住就得看它的了,由于有些消息是要即刻返回给客户端的,而有些是要通过消息总线处理后再转发的,也有的是要留下来做为长链接的。具体的会在讲消息总线时再说明。

最终CometHttpHandler会在请求须要结束时调用EndProcessRequest方法,从而将消息返回给一直等待的链接,客户端会接收并处理此请求的响应包。

CometHttpHandler就是实现了一个入口和出口,经过IhttpAsyncHandler的异步处理能力从而实现了长链接状态。

二、 消息封包

对于客户端和服务端之间交互必须有一个消息封包,不然双方没法作一些约定,毕竟Http协议是松散的无状态性。在AspComet中实现了一个类:Message

这个类AspComet的开发者叫其:bauyeux message(介绍),貌似是Dojo提出的一套协议。

在这个消息封包中主要介绍几个:

Channel:消息频道,用于消息广播所在的频道

clientId:客户端的id

data:数据封包(就是一个object类型,很容易用于扩展的数据包)

version:版本号,这块对消息的向下兼容颇有做用

advice:返回后的处理方式,叫通知也可

timestamp:时间戳

ext:貌似是扩展用的

封包的内容很丰富,有时候协议就是种约定,其实对于咱们来讲就是一个类嘛,甚至于你能够理解就是一个字符串,客户端和服务端经过某种约定能够相互解析识别就可。

三、 消息总线设计

在说到IhttpAnyscHandler时就提到了消息总线,在AspComet中抽象为一个接口:IMessageBus。

public interface IMessageBus

{

void HandleMessages(Message[] messages, ICometAsyncResult cometAsyncResult); }

 

就一个方法,这也就是AspComet用于处理消息的核心方法了,方法的意思就是处理消息,在这个方法里主要是将接收的消息分配给不一样的消息处理者进行处理,好比:发起握手协议时要将消息给MetaHandshakeHandler来处理,这就是一个消息中转中心。

参数messages是消息封包,由于多是多个消息因此用了数组。

参数cometAsyncResult是对异步请求回调函数的一个二次封装,主要目的是将callback给接住,不让其响应,这样就能够控制何时返回响应包了。ICometAsyncResult 接口就两个方法

 

SendAwaitingMessages是用于将发送等待的消息,主要是用于将要发送的消息写入到发送管道中

CompleteRequestWithMessages是用于完成请求的过程,主要是调一下callback以告诉IhttpAsyncHandler请求能够返回啦

经过这两个方法的配合就能够实现将消息向客户端发送消息啦。

这里提一点:其实向客户端发送数据的方法很简单,Http分请求包和响应包,客户端发给服务端的叫请求(Request),服务端发给客户端的叫响应(Response),这下应该明白了吧。SendAwaitingMessages就是把数据写入到Response里,这样客户端不就有接收的数据了吗?

四、 各种型消息的处理

在消息总线里提到了消息处理者,为何会有这个东东存在呢?其实这跟整个的通讯过程有关,有握手过程、链接创建过程、断开过程等等,这就要有一整套处理的方法,也就是要对每种不一样的过程作一个类型分开处理。在AspComet中有一个接口:ImessageHandler,它定义了一个消息处理的统一方法:

 

经过继承这个接口实现特定的消息处理类就能够完成一些特定的业务了,下面列举一下各类消息处理类:

MetaHandshakeHandler

握手协议处理

MetaConnectHandler

链接协议处理

MetaDisconnectHandler

断开链接处理

MetaSubscribeHandler

订阅处理

MetaUnsubscribeHandler

中止订阅处理

ForwardingHandler

消息转发处理

ExceptionHandler

异常消息处理

SwallowHandler

吞掉消息处理,不给客户端返回

从字面意思应该就能够理解大致了,发什么消息作什么处理,就这个意思。

说到消息的分类处理有个东西必须说明,在MessageBus中如何区分消息类型并找到对应的处理者呢?这就是和ImessagesProcessor的功劳了。

 

在这个接口中Process方法就是用于处理每条消息的转发,这个设计也很好,咱们甚至能够实现一个本身的MessagesProcessor彻底按本身的要求进行消息转发和处理。在此我仍是看一下官方的默认实现吧,在AspComet组件中有个默认的实现MessagesProcessor,代码以下:

 

在代码中能够看到,MessageProcessor是经过一个HandlerFactory来获取实际的ImessageHandler实例,进而处理消息的,这个过程也不复杂,官方提供的实现就是MessageHandlerFactory类:

 

在这里处理的方法是根据channel的不一样调用相应的handler。

回到ImessageHandler,就得说明一下AspComet对单独消息处理时释放出来的委托设计,在Handler执行Handlemessage方法时会调用相应的委托,外部程序能够订阅委托实现进行一些处理。好比我在握手过程当中验证客户端合法性,但这个客户端的合法性须要外部应用程序才能检验,怎么办呢?就能够经过MetaHandshakeHandler 中HandleMessage方法释放出来的两个委托进行处理,代码以下:

 

在这段代码里有两个EventHub.Publish(…)的调用,这就是两个委托调用,咱们要实现客户端合法性验证就要在第一个委托时作处理,好比上面代码中有两行这样的代码:

这就是调用一个委托,参数是handshakingEvent。外部订阅此委托的程序会处理相应的逻辑,若是不符合要求则将其Cancel属性设置为true,就说明本次消息发送过程要取消掉,而且能够写入相应的缘由。下面是一个实现的例子:

 

CheckHandshake方法就是订阅了委托的方法,其中的参数就是从EventHub.Publish(handshakingEvent);中传过来的。在CheckHandshake里能够取得相应的Client对象并作一些检查等,若是不符合要求能够将ev.Cancel设置为true,并将缘由写入CancellationReason属性发回给客户端。

五、 客户端对象管理

在服务端要管理客户端的信息,这样才能在消息广播时向特定的客户端发送,为了保持客户端的应用无关性,AspComet定义了Iclient接口:

Iclient说明

 

这里定义了对Client的一些基础定义,继承此接口实现一个客户端类就好了。

这里所说有客户端并不是指的实际的浏览器端,而是服务器用于区分长链接的客户端标识的,以及管理每一个客户端相应信息的对象。

IclientRepository说明

有一个问题特别值的注意,就像聊天室,能够创建不一样的房间,进入到具体房间的人只会收到跟这个房间相关的消息。要实现这一点,消息就要经过某种规则区分。在AspComet里就经过 channel来作这个事情。在Message封包中就有channel的定义,有了这个字段,消息转发时就能够向订阅了channel的全部客户端发送消息了。因此在服务端还要定义一个列表以用于管理链接的客户端,每一个客户端会记录本身的订阅channel,而后由此列表提供一些方法给其余程序访问,AspComet设计了IclientRepository来作此事,看一下代码:

 

在服务端会维护一个客户端的仓库,用于管理链接的客户端状况,想要知道哪些客户端订阅了某个channel经过WhereSubscribedTo方法就能够查询出来了,而后向这个列表里发送消息就能够向特定channel广播了。AspComet默认实现了一个内存仓库类:

 

就是一个集合,将全部的客户端放在这个集合中。

若是想要持久化数据,就能够经过继承 IclientRepository实现一个数据库或者文件方式存放的仓库。

客户端


 

在AspComet组件里并无明确提供一套基于js的客户端API,只是在其Demo里放了一个基于JS的一套API。主要是下面几个文件:

 

Dobj的我没列出来,其中最为重要的就是cometd.js,这个基本是核心API了,主要的功能都在这里面实现。下面就着重说明一下这个cometd.js吧:

一、 org.cometd.Cometd类介绍

这个类是最为主要的,包括了全部的功能,代码和功能都特别多,不一一列举,大致的讲分为这几部分:

  • 初始化方法

在使用org.cometd.cometd类时须要初始化一些变量和参数,configure方法是用于外部配置的核心方法。将Ajax请求的url传入就是经过调用configure来实现的。还有一些参数如最大链接数_maxConnections等等:

 

这里面不少的参数均可以经过传入进行设置初始化。固然若是不配置也会有默认的值。因此目前看来必定要设置的就是url咯。

  • 公共方法

公共方法也在这个类里面提供了,固然主要是与组件相关的一些处理才会内置:

 

  • 管道对象

在AspComet里提供的js代码中设计了一个transport的对象,将其定义为与服务端通信的管道,为此还抽象了一个抽象基类org.cometd.Transport,这样就能够为其定制不一样的管道来实现请求的发送和处理服务器的响应,好处就是transport能够在本身开发一套,好比咱们团队只会用jQuery,那么就能够基于jQuery创建一套transport,运行时注册进来就能够了。

并且管道这种设计方法也为整个的传输层的功能进行了抽象,这很符合面向对象的思想,把同类的业务放在一个对象上,即方便复用,也有利于业务封装。

这个设计很精秒啊。

  • 事件管理

由于将整个的请求和响应过程封装在了org.cometd.Cometd类中,并且是基于异步请求的,那么对于调用的程序来讲要获取到对应的结果就必须能够回调或者某种监听的方式。AspComet就经过发布事件来实现对响应的订阅,在org.cometd.Cometd类中与事件相关的字段、方法有如下几个:

事件监听列表

   

       在代码内部维护一个数组,将外部订阅的事件放在此数组里。

事件通知

      一旦有了须要通知的事件那么就会调用一个方法_notify,此方法会逐一的调用_listseners里的订阅方法,将符合要求的callback调用一下。这个过程就其实实现了事件的原理啦。

事件订阅

      那么外部程序调用时如何订阅事件呢?就是addListener方法,此方法会传入三个参数,看下注释:

   

参数说明一下:

Channel:订阅的频道

Scope:貌似是个回调函数,能够省略,不知具体用处

Callback:明显是个回调函数,就是用于事件响应的方法咯

    事件订阅移除

      有了订阅,固然就能够移除事件订阅了; _removeListener,很少做解释了。

  • 消息发送/接收管理

最为重要的仍是消息的整个管理机制,在org.cometd.Cometd类中对这部分的实现仍是比较复杂的。一方面要实现对各种消息的发送和处理,另外一方面要不断的创建长链接以响应推送。

但实际使用起来并不麻烦比较简单,只要实例化org.cometd.Cometd类,而后调用其handshake方法与服务器实现握手,成功后调用publish方法就能够发送消息了。

但在内部就没这么简单了,handshake是发送给了什么给服务器呢?为何publish方法能够广播消息?分别作一下讲解吧:

那么先说一下handshake

因为服务端会对客户端链接做验证,因此要求客户端在与服务端进行正常的消息通信前要作一次握手,以保证客户端和服务端是互信的,这个过程叫handshake。执行的步骤以下:

1) 首先必定要实例化一个org.cometd.Cometd对象,为对象实例设置请求url

2) 调用handshake方法开始握手

3) 握手后根据返回的状态执行回调函数处理响应包。对于握手成功的响应处理调用_handleResponse,失败时调用_handleFailure

4) 若是是握手成功了那么会调用_receive(message);在_receive方法中会调用_connectResponse(message);发起长链接

5) 若是失败了就会作善后处理

完成了握手后那么就会有一个长链接创建了,创建长链接是个比较有意思的方法,调用过程以下:_connectResponse-> _delayedConnect->_delayedSend。

先看一下_delayedConnect方法的代码:

 

主要是通用一下_delayedSend,而里面会传入一个_connect()方法,这里很重要,_connect()方法就是向服务端发起链接请求,服务端接收到此方法发送的消息后会创建一个长链接。

_delayedSend的代码以下:

 

注意_setTimeout方法,为此次方法设置了过时时间。我想这个作法主要是不想让长链接长时间的连在服务器上,会超过一段时间后调用一次。在实际的运行状态下了发现会每隔10秒调用一次_connect()方法,从新发起长链接。

这样的好处我想应该是减小长链接在服务器上呆的时间吧。这10秒中若是服务器有响应则能够当即接受,若是没有那么就10秒断一次重联,应该是能够减小服务器链接的压力。

长链接过程就是这么简单,不断的_connect。

Publish方法

还有一个方法是publish方法,就是消息广播。这个方法调用过程是将封包好的消息经过_queueSend(message)发送到服务端去。代码:

能够看到这个方法中消息封包仅定义了channel和data,因此服务端接受后仅会向相应的channel广播一下,以后就不会作处理,并非一次长链接。

经过publish发送消息的客户端会经过订阅的方式收到本身发的消息。

二、 org.cometd.TransportRegistry类介绍

看一下官方的注释:

 

就是一个对象管理器吧,经常使用的方法就是查找、添加、删除、重置。

三、 org.cometd.Transport类

 

这个类的职责主要是抽象出通道的经常使用功能,差很少算基类吧。这个类中主要是完成对消息封包在后台形式的长链接发送。

介绍一下这里面几个主要的方法:

function _transportSend(envelope, request)

这个是发送消息的主方法,参数

Envelope:消息封包

Request:请求

this.send = function(envelope, longpoll)

发送消息

Longpoll:true表示发起长链接,不然不是

function _queueSend(envelope)

直接发送消息,不是长链接

function _longpollSend(envelope)

以长链接的方式发送消息

this.transportSend

管道真实的发送消息方法

这是一个虚方法,供派生类重写,因此真正的发送是在派生类里实现的。 

 

在官方的代码中从org.cometd.Transport派生了两个类:org.cometd.LongPollingTransport和org.cometd.CallbackPollingTransport。这两个类我感受差很少,而两个类都重写了transportSend方法,并且都是分别调用了两个类中新定义的虚方法:

org.cometd.LongPollingTransport中定义的叫this.xhrSend = function(packet)

org.cometd.CallbackPollingTransport中定义的叫this.jsonpSend = function(packet)

多是为支持不一样的格式吧,好像和跨域访问也有关系。

相关文章
相关标签/搜索