來源:http://blog.csdn.net/21aspnet/archive/2008/11/20/3341887.aspx
Ajax的長串連,或者有些人所說的Comet,就是指以XMLHttpRequest的方式串連伺服器,串連後伺服器並非即時寫入相應並返回。伺服器會保持串連並等待一個需要通知用戶端的事件,該事件發生後馬上將資料寫入響應,這時候用戶端就以相當“即時”的方式接收到事件通知。具體的通訊模型,請參考這篇文章:《Comet:基於 HTTP 長串連的“伺服器推”技術》,裡面已經說得非常詳細了,我就不再複述了。
我們接著開始討論如何使用.NET實現這個模型。首先我們能想到的是,我們需要一個Web Service,可以是ASP.NET Web Service,也可以是WCF Web Service,ASP.NET AJAX Library兩者都支援。在這裡,為了簡單起見,就選擇大家更熟悉的ASP.NET Web Service舉例。然後,我們寫下以下兩個函數簽名:
public void Send(Message message);
public Message Wait();
其中,Send函數用來發送一個Message對象,而Wait函數用來等待一個Message對象。然後,讓我們來討論一些細節問題。 無事件導致逾時
首先,長期保持串連時不行的。對於伺服器和用戶端來說,這不是個問題,但我們永遠都要記住中間可能存在各式各樣配置怪異的網關和代理,它們上面可能有各式各樣的逾時規則,因此Comet最好設計為定期重連。一般情況下,如果30秒沒有任何事件發生,伺服器端就應該通知用戶端確實沒有事件發生,結束掉本次請求,然後重新開始一次新的請求以便繼續等待。
那麼上述函數簽名可否用來返回一個無事件的訊息呢。這是顯然可以的,我們可以選擇返回null表示無事件,或者返回一個EmptyMessage常量,這視乎我們使用class還是struct來定義Message。(甚至,我們還可以做一個名為NoMessageMessage的Message衍生類別來做這個事情。) 定義發送目標
上述函數簽名確實能用來收發訊息,但是沒指名發給誰。可能有人會說,發送給誰可以在Message類裡面通過一個屬性來定義啊。但是Wait()方法沒有說明接受方是誰,伺服器端依然不知道哪些訊息應該讓你接收。
因此,我們引入Channel的概念,Channel使用其名稱來標識,相同名稱的就必然是同一個Channel。在發送與接受時,通過名稱指定要發送到哪個Channel,這樣問題就解決了。此時,函數簽名修改如下:
public void Send(string channelName, Message message);
public Message Wait(string channelName); 可靠的訊息佇列
想象一個可能發生的情況,伺服器端向你發送一個訊息,你沒有成功接收,但是伺服器端認為發送了就成功了,訊息從隊列刪除了,然後這個訊息就永久丟失掉了。可能有人會強調TCP多麼可靠,伺服器端發送的訊息如果在TCP的層面發生問題了,肯定會引發Socket層級的Exception,這個Exception冒泡上來,伺服器端就能截獲,從而得知發送失敗,然後先不刪除隊首訊息。可是別忘了,中間是可能存在代理的,如果代理成功把訊息收回去了,可是代理髮送到用戶端這一步失敗了,伺服器端就不一定會發生異常了。
因此,我們需要制定一種策略,來確保下行訊息總能發送到用戶端。在這裡,我們選擇了引入逐個ACK的機制,來確認訊息的接收。也就是說,伺服器端發送給用戶端的訊息帶有一個序號,在用戶端收到訊息後就將該序號發回給伺服器端,已確認它受到了該訊息。這時候,函數簽名更改如下:
public int Send(string channelName, Message message);
public Message Wait(string channelName, int sequence);
我們使用Wait()接收到的Message中,應該有一個Sequence的屬性,標記它的序號。然後,再我們執行下一次Wait()時就將該序號加1的值通過sequence參數傳遞迴去,讓伺服器知道我們期望下一條訊息的編號是這個。例如我們收到Message,其Sequence屬性為836,那麼下一次調用Wait()的時候就傳給伺服器837。伺服器端此時應該保留了編號為836的Message在對首,如果用戶端繼續請求836號訊息,證明它上次沒收到,這次仍然發送836號訊息給它;如果用戶端請求837號訊息,證明它成功收到836號訊息的,這次就發送837號訊息給它。
如果都不是,那該怎麼辦。那意味著,這是一個錯誤的請求,甚至可能是攻擊請求,因為正常情況下不應該出現這樣的請求的,伺服器端可以考慮拋個無關緊要的Exception(不要告訴攻擊者你知道他在攻擊了),甚至直接給個400 (bad request)的響應代號。
與Wait()類似的,Send()也可以加入ACK機制,只需要將傳回型別從void改為int就可以了,這個值就專門用於傳遞訊息編號,實現方式和Wait()是一樣的,不過Send()是由用戶端儲存待發送訊息的隊列。 小結
到此為止。我們的Web Service就寫好了。這就寫好了。只有簽名沒有函數體。是的,複雜的工作留給model去做,Web Service在這裡只是相當於一個view,用於將model的介面暴露出來。
在下一次的文章中,我們將開始討論如何?伺服器端的訊息傳遞機制。
在上一次的文章中,我們說到了如何設計一個ASP.NET Web Service來處理長串連請求。很多人對此就提出了問題,如何hold住請求讓它30秒不斷開了。這其實很簡單,只需要Sleep()一下就可以了:
Thread.Sleep(30 * 1000);
然而問題是,我們不是要等30秒然後看看是否有事件需要返回,而是在這30秒內隨時有事件隨時返回。因此,我們需要一套機制來在等待的過程中檢查是否有事件發生了。 Monitor模型
在.NET裡面,大家最熟悉的線程同步模型應該就是Monitor模型了。沒聽說過。就是C#的那個lock關鍵字,實際上它編譯出來就是一對Monitor.Enter()和Monitor.Exit()。
通過lock命令,我們可以針對一個對象建立一個臨界區,代碼執行到臨界區入口時必須擷取到該對象的鎖才能執行下去,並且在臨界區的出口釋放該鎖。然而這種模型不太適用於解決我們的問題,因為我們需要等待一個事件,如果使用lock來等待的話,那就是說要先在Web Service外部把對象鎖上,然後等事件觸發了就解鎖,這時候Web Service才順利進入臨界地區。
事實上,要進行這類型的阻塞,還有一個更好的選擇,那就是Mutex。 Mutex模型
Mutex,也就是mutual exclusive的縮寫,“互斥”的意思。Mutex是如何運作的。這有點像是銀行的排隊叫號系統,所有等待服務的人都坐在大廳裡等候(wait)被叫,當一個服務視窗空閑時它就會發出一個訊號(signal)來通知下一位等候服務的人。總之,所有執行wait指令的線程都在等候,而每一個signal能夠讓一個線程結束等候繼續執行。
在.NET裡面,wait和signal這兩個操作分別對應Mutex.WaitOne()和Mutex.ReleaseMutex()這兩個方法。我們可以讓Web Service的線程使用Mutex.WaitOne()進入等候狀態,而在事件發生時使用Mutex.ReleaseMutex()來通知Web Service線程。因為必須在Mutex.ReleaseMutex()發生後Mutex.WaitOne()才可能繼續執行下去,因此能夠執行下去就證明必然有事件發生了並且調用了Mutex.ReleaseMutext(),這時候就可以放心地去讀取事件訊息了。 簡單樣本
在選定使用Mutex模型後,我們來編寫一個簡單的樣本。首先,我們要在WebService衍生類別內定義一個Mutex,還有一個代表訊息的字串。
Mutex mutex = new Mutex();
string message;
然後,我們定義兩個WebMethod。為了把問題簡單化,我們選用上一篇文章中開頭所說的兩個函數簽名,也就說只能在一個Web Service內自己發自己收,沒有發送目標的概念,也沒有逾時的概念,還沒有可靠性設計。同時,我們將Message類型替換為一般字元串,以便於我們測試。
我們先編寫發送訊息的函數:
public void Send(string message) {
this.message = message;
this.mutex.ReleaseMutex();
}
在這個發送函數裡,首先我們把訊息放進了類內全域的變數中,然後讓全域的Mutex類釋放一個signal。這時候,如果有線程在等待,它可以馬上執行下去。如果此時沒有線程在等待,那麼下一個wait的線程執行到該阻塞的地方就能夠不受阻塞繼續執行下去。
現在我們來編寫接收訊息的函數:
public string Wait() {
this.mutex.WaitOne();
return this.message;
}
接收函數一開始就進入wait狀態。在得到signal後,需要做的事情就是把全域的訊息返回給用戶端。 親身體驗
最後,我們可以通過ASP.NET Web Service本身支援的Web測試介面來測試一下我們的代碼。我們開兩個瀏覽器視窗,一個進入Send()調用,一個進入Wait()調用。然後我們按照如下方法來測試: 首先執行Send("Hello"),然後執行Wait()。這時候你可以馬上看到"Hello"。 首先執行Wait(),讓它等待返回,這時候執行Send("Hello")。隨後你可以看到Wait()那段返回"Hello"了。 按如下順序執行:Send("Hello");Wait();Send("World");Wait(); 按如下順序執行:Send("Hello");Send("World");Wait();Wait(); 按如下順序執行:Wait();Wait();Send("Hello");Send("World"); 按如下順序執行:Wait();Send("Hello");Wait();Send("World");
你會發現這樣一些奇怪的結果:第3個測試返回的是"World"和"World"。第5個測試先返回"Hello"的並不一定是先執行的那個Wait()線程。後者在某些情況下不是什麼問題,特別是長串連中一般之後一個Wait()線程在等待中,所以我們可以不管。而前者,則是因為沒有訊息佇列所造成的,我們只有長度為1的訊息視窗,所以只能緩衝最後一個訊息。這個問題我們將在下一篇文章中解決。 小結
在本文中,我們看到了不同的線程同步模型的差異。Monitor模型的lock本質上是一個Semaphore,也就是一個不能連續signal的Mutex,一個signal發出去後必須被一個wait接收了才能進行下一次的signal。同時,Semaphore也限制了signal和wait必須在同一個線程內成對執行,而Mutex則沒有此限制。雖然.NET是針對Monitor模型最佳化的,但在我們的需求當中,只能通過Mutex模型來解決。
接著,我們便寫了一個小小的消協發送與接收函數,實現了我們想要的阻塞式Web Service。同時我們也看到了沒有訊息佇列造成的問題,因此確定接下來我們要做一個訊息佇列。