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)
可能是為支持不同的格式吧,好像和跨域訪問也有關系。
文章列表