基於ASP.NET的Comet長連線技術解析

mini188發表於2015-01-29

comet技術原理

來自維基百科:Comet是一種用於web的技術,能使伺服器能實時地將更新的資訊傳送到客戶端,而無須客戶端發出請求,目前有兩種實現方式,長輪詢和iframe流。

簡單的說是一種基於現有Http協議基礎上的長輪詢技術,之所有會產生這種技術的主要原因是Http協議是無狀態的所以客戶端和服務端之間沒辦法建立起一套長時間的連線。比如我們要做一個聊天室,在Web環境下我們通常不能從服務端推送訊息到瀏覽器裡,而只能通過每個客戶端不斷的輪詢伺服器,以獲取最新的訊息,這樣一來效率非常低,而且不斷的向伺服器傳送請求對於訪問量大的應用來說也會造成很大的資源佔用。

於是人們就發現了這種技術,向伺服器發起一個請求,然後伺服器一直不響應這個請求,這樣客戶端和服務端之間就形成了一個長連線,直到服務端響應這個請求後結束本次連線。借用一下IBM裡的圖片:

通過Ajax技術可以實現長輪詢的伺服器推模型,客戶端和服務端之間通過不斷的發起長輪詢即可以實現資料的互動,這個過程由於是Ajax實現的非同步操作所以體驗上會比較好,效率也很高。哎呀呀,說不清楚,找個網上的資料:

Comet方式通俗的說就是一種長連線機制(long lived http)。同樣是由Browser端主動發起請求,但是Server端以一種似乎非常慢的響應方式給出回答。這樣在這個期間內,伺服器端可以使用同一個connection把要更新的資料主動傳送給Browser。因此請求可能等待較長的時間,期間沒有任何資料返回,但是一旦有了新的資料,它將立即被髮送到客戶機。Comet又有很多種實現方式,但是總的來說對Server端的負載都會有增加.雖然對於單位操作來說,每次只需要建議一次connection,但是由於connection是保持較長時間的,對於 server端的資源的佔用要有所增加。

優點: 實時性好(訊息延時小);效能好(能支援大量使用者)

缺點: 長期佔用連線,喪失了無狀態高併發的特點。

應用: 股票系統、實時通訊。

參考資料:

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也應用了很多的設計模式。下面就這個元件的主要實現做一些介紹:

服務端

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)

可能是為支援不同的格式吧,好像和跨域訪問也有關係。

相關文章