基於 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)

多是爲支持不一樣的格式吧,好像和跨域訪問也有關係。

相關文章
相關標籤/搜索