原文来自于:
Comet技术原理
来自*:Comet是一种用于web的技术,能使服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有两种实现方式,长轮询和iframe流。
简单的说是一种基于现有Http协议基础上的长轮询技术,之所有会产生这种技术的主要原因是Http协议是无状态的所以客户端和服务端之间没办法建立起一套长时间的连接。比如我们要做一个聊天室,在Web环境下我们通常不能从服务端推送消息到浏览器里,而只能通过每个客户端不断的轮询服务器,以获取最新的消息,这样一来效率非常低,而且不断的向服务器发送请求对于访问量大的应用来说也会造成很大的资源占用。
于是人们就发现了这种技术,向服务器发起一个请求,然后服务器一直不响应这个请求,这样客户端和服务端之间就形成了一个长连接,直到服务端响应这个请求后结束本次连接。借用一下IBM里的图片:
通过Ajax技术可以实现长轮询的服务器推模型,客户端和服务端之间通过不断的发起长轮询即可以实现数据的交互,这个过程由于是Ajax实现的异步操作所以体验上会比较好,效率也很高。哎呀呀,说不清楚,找个网上的资料:
Comet方式通俗的说就是一种长连接机制(long lived http)。同样是由Browser端主动发起请求,但是Server端以一种似乎非常慢的响应方式给出回答。这样在这个期间内,服务器端可以使用同一个connection把要更新的数据主动发送给Browser。因此请求可能等待较长的时间,期间没有任何数据返回,但是一旦有了新的数据,它将立即被发送到客户机。Comet又有很多种实现方式,但是总的来说对Server端的负载都会有增加.虽然对于单位操作来说,每次只需要建议一次connection,但是由于connection是保持较长时间的,对于 server端的资源的占用要有所增加。
优点: 实时性好(消息延时小);性能好(能支持大量用户)
缺点: 长期占用连接,丧失了无状态高并发的特点。
应用: 股票系统、实时通讯。
参考资料:
基于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也应用了很多的设计模式。下面就这个组件的主要实现做一些介绍:
服务端
1、 首先必须实现IhttpAsyncHandler接口
CometHttpHandler:IhttpAsyncHandler,此类就是用于异步请求的处理单元,简单的说就是服务端的入口。在这里通过BeginProcessRequest方法将请求的内容hold住,同时也将callback也Hold住。当然这里有个重点要注意就是MessageBus,所有消息如何hold住就得看它的了,因为有些消息是要即刻返回给客户端的,而有些是要经过消息总线处理后再转发的,也有的是要留下来作为长连接的。具体的会在讲消息总线时再说明。
最终CometHttpHandler会在请求需要结束时调用EndProcessRequest方法,从而将消息返回给一直等待的连接,客户端会接收并处理此请求的响应包。
CometHttpHandler就是实现了一个入口和出口,通过IhttpAsyncHandler的异步处理能力从而实现了长连接状态。
2、 消息封包
对于客户端和服务端之间交互必须有一个消息封包,否则双方无法做一些约定,毕竟Http协议是松散的无状态性。在AspComet中实现了一个类:Message
这个类AspComet的开发者叫其:bauyeux message(介绍),貌似是Dojo提出的一套协议。
在这个消息封包中主要介绍几个:
Channel:消息频道,用于消息广播所在的频道 |
clientId:客户端的id |
data:数据封包(就是一个object类型,很容易用于扩展的数据包) |
version:版本号,这块对消息的向下兼容很有作用 |
advice:返回后的处理方式,叫通知也可 |
timestamp:时间戳 |
ext:貌似是扩展用的 |
封包的内容很丰富,有时候协议就是种约定,其实对于我们来说就是一个类嘛,甚至于你可以理解就是一个字符串,客户端和服务端通过某种约定可以相互解析识别就可。
3、 消息总线设计
在说到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里,这样客户端不就有接收的数据了吗?
4、 各类型消息的处理
在消息总线里提到了消息处理者,为什么会有这个东东存在呢?其实这跟整个的通信过程有关,有握手过程、连接建立过程、断开过程等等,这就要有一整套处理的方法,也就是要对每种不同的过程做一个类型分开处理。在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属性发回给客户端。
5、 客户端对象管理
在服务端要管理客户端的信息,这样才能在消息广播时向特定的客户端发送,为了保持客户端的应用无关性,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吧:
1、 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发送消息的客户端会通过订阅的方式收到自己发的消息。
2、 org.cometd.TransportRegistry类介绍
看一下官方的注释:
就是一个对象管理器吧,常用的方法就是查找、添加、删除、重置。
3、 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)
可能是为支持不同的格式吧,好像和跨域访问也有关系。