使用 .NET 实现 Ajax 长连接
作者:http://www.cnblogs.com/cathsfz/
?
Ajax的長連接,或者有些人所說的Comet,就是指以XMLHttpRequest的方式連接服務(wù)器,連接后服務(wù)器并非即時(shí)寫入相應(yīng)并返回。服務(wù)器會(huì)保持連接并等待一個(gè)需要通知客戶端的事件,該事件發(fā)生后馬上將數(shù)據(jù)寫入響應(yīng),這時(shí)候客戶端就以相當(dāng)“實(shí)時(shí)”的方式接收到事件通知。具體的通信模型,請參考這篇文章:《Comet:基于 HTTP 長連接的“服務(wù)器推”技術(shù)》,里面已經(jīng)說得非常詳細(xì)了,我就不再復(fù)述了。
我們接著開始討論如何使用.NET實(shí)現(xiàn)這個(gè)模型。首先我們能想到的是,我們需要一個(gè)Web Service,可以是ASP.NET Web Service,也可以是WCF Web Service,ASP.NET AJAX Library兩者都支持。在這里,為了簡單起見,就選擇大家更熟悉的ASP.NET Web Service舉例。然后,我們寫下以下兩個(gè)函數(shù)簽名:
public void Send(Message message);
public Message Wait();
其中,Send函數(shù)用來發(fā)送一個(gè)Message對(duì)象,而Wait函數(shù)用來等待一個(gè)Message對(duì)象。然后,讓我們來討論一些細(xì)節(jié)問題。
無事件導(dǎo)致超時(shí)
首先,長期保持連接時(shí)不行的。對(duì)于服務(wù)器和客戶端來說,這不是個(gè)問題,但我們永遠(yuǎn)都要記住中間可能存在各式各樣配置怪異的網(wǎng)關(guān)和代理,它們上面可能有各式各樣的超時(shí)規(guī)則,因此Comet最好設(shè)計(jì)為定期重連。一般情況下,如果30秒沒有任何事件發(fā)生,服務(wù)器端就應(yīng)該通知客戶端確實(shí)沒有事件發(fā)生,結(jié)束掉本次請求,然后重新開始一次新的請求以便繼續(xù)等待。
那么上述函數(shù)簽名可否用來返回一個(gè)無事件的消息呢?這是顯然可以的,我們可以選擇返回null表示無事件,或者返回一個(gè)EmptyMessage常量,這視乎我們使用class還是struct來定義Message。(甚至,我們還可以做一個(gè)名為NoMessageMessage的Message派生類來做這個(gè)事情。)
定義發(fā)送目標(biāo)
上述函數(shù)簽名確實(shí)能用來收發(fā)消息,但是沒指名發(fā)給誰。可能有人會(huì)說,發(fā)送給誰可以在Message類里面通過一個(gè)屬性來定義啊。但是Wait()方法沒有說明接受方是誰,服務(wù)器端依然不知道哪些消息應(yīng)該讓你接收。
因此,我們引入Channel的概念,Channel使用其名稱來標(biāo)識(shí),相同名稱的就必然是同一個(gè)Channel。在發(fā)送與接受時(shí),通過名稱指定要發(fā)送到哪個(gè)Channel,這樣問題就解決了。此時(shí),函數(shù)簽名修改如下:
public void Send(string channelName, Message message);
public Message Wait(string channelName);
可靠的消息隊(duì)列
想象一個(gè)可能發(fā)生的情況,服務(wù)器端向你發(fā)送一個(gè)消息,你沒有成功接收,但是服務(wù)器端認(rèn)為發(fā)送了就成功了,消息從隊(duì)列刪除了,然后這個(gè)消息就永久丟失掉了。可能有人會(huì)強(qiáng)調(diào)TCP多么可靠,服務(wù)器端發(fā)送的消息如果在TCP的層面發(fā)生問題了,肯定會(huì)引發(fā)Socket級(jí)別的Exception,這個(gè)Exception冒泡上來,服務(wù)器端就能截獲,從而得知發(fā)送失敗,然后先不刪除隊(duì)首消息。可是別忘了,中間是可能存在代理的,如果代理成功把消息收回去了,可是代理發(fā)送到客戶端這一步失敗了,服務(wù)器端就不一定會(huì)發(fā)生異常了。
因此,我們需要制定一種策略,來確保下行消息總能發(fā)送到客戶端。在這里,我們選擇了引入逐個(gè)ACK的機(jī)制,來確認(rèn)消息的接收。也就是說,服務(wù)器端發(fā)送給客戶端的消息帶有一個(gè)序號(hào),在客戶端收到消息后就將該序號(hào)發(fā)回給服務(wù)器端,已確認(rèn)它受到了該消息。這時(shí)候,函數(shù)簽名更改如下:
public int Send(string channelName, Message message);
public Message Wait(string channelName, int sequence);
我們使用Wait()接收到的Message中,應(yīng)該有一個(gè)Sequence的屬性,標(biāo)記它的序號(hào)。然后,再我們執(zhí)行下一次Wait()時(shí)就將該序號(hào)加1的值通過sequence參數(shù)傳遞回去,讓服務(wù)器知道我們期望下一條消息的編號(hào)是這個(gè)。例如我們收到Message,其Sequence屬性為836,那么下一次調(diào)用Wait()的時(shí)候就傳給服務(wù)器837。服務(wù)器端此時(shí)應(yīng)該保留了編號(hào)為836的Message在對(duì)首,如果客戶端繼續(xù)請求836號(hào)消息,證明它上次沒收到,這次仍然發(fā)送836號(hào)消息給它;如果客戶端請求837號(hào)消息,證明它成功收到836號(hào)消息的,這次就發(fā)送837號(hào)消息給它。
如果都不是,那該怎么辦?那意味著,這是一個(gè)錯(cuò)誤的請求,甚至可能是攻擊請求,因?yàn)檎G闆r下不應(yīng)該出現(xiàn)這樣的請求的,服務(wù)器端可以考慮拋個(gè)無關(guān)緊要的Exception(不要告訴攻擊者你知道他在攻擊了),甚至直接給個(gè)400 (bad request)的響應(yīng)代號(hào)。
與Wait()類似的,Send()也可以加入ACK機(jī)制,只需要將返回類型從void改為int就可以了,這個(gè)值就專門用于傳遞消息編號(hào),實(shí)現(xiàn)方式和Wait()是一樣的,不過Send()是由客戶端保存待發(fā)送消息的隊(duì)列。
小結(jié)
到此為止。我們的Web Service就寫好了。這就寫好了?只有簽名沒有函數(shù)體?是的,復(fù)雜的工作留給model去做,Web Service在這里只是相當(dāng)于一個(gè)view,用于將model的接口暴露出來。
在下一次的文章中,我們將開始討論如何實(shí)現(xiàn)服務(wù)器端的消息傳遞機(jī)制。
?
在上一次的文章中,我們說到了如何設(shè)計(jì)一個(gè)ASP.NET Web Service來處理長連接請求。很多人對(duì)此就提出了問題,如何hold住請求讓它30秒不斷開了?這其實(shí)很簡單,只需要Sleep()一下就可以了:
Thread.Sleep(30 * 1000);
然而問題是,我們不是要等30秒然后看看是否有事件需要返回,而是在這30秒內(nèi)隨時(shí)有事件隨時(shí)返回。因此,我們需要一套機(jī)制來在等待的過程中檢查是否有事件發(fā)生了。
Monitor模型
在.NET里面,大家最熟悉的線程同步模型應(yīng)該就是Monitor模型了。沒聽說過?就是C#的那個(gè)lock關(guān)鍵字,實(shí)際上它編譯出來就是一對(duì)Monitor.Enter()和Monitor.Exit()。
通過lock命令,我們可以針對(duì)一個(gè)對(duì)象創(chuàng)建一個(gè)臨界區(qū),代碼執(zhí)行到臨界區(qū)入口時(shí)必須獲取到該對(duì)象的鎖才能執(zhí)行下去,并且在臨界區(qū)的出口釋放該鎖。然而這種模型不太適用于解決我們的問題,因?yàn)槲覀冃枰却粋€(gè)事件,如果使用lock來等待的話,那就是說要先在Web Service外部把對(duì)象鎖上,然后等事件觸發(fā)了就解鎖,這時(shí)候Web Service才順利進(jìn)入臨界區(qū)域。
事實(shí)上,要進(jìn)行這類型的阻塞,還有一個(gè)更好的選擇,那就是Mutex。
Mutex模型
Mutex,也就是mutual exclusive的縮寫,“互斥”的意思。Mutex是如何運(yùn)作的?這有點(diǎn)像是銀行的排隊(duì)叫號(hào)系統(tǒng),所有等待服務(wù)的人都坐在大廳里等候(wait)被叫,當(dāng)一個(gè)服務(wù)窗口空閑時(shí)它就會(huì)發(fā)出一個(gè)信號(hào)(signal)來通知下一位等候服務(wù)的人。總之,所有執(zhí)行wait指令的線程都在等候,而每一個(gè)signal能夠讓一個(gè)線程結(jié)束等候繼續(xù)執(zhí)行。
在.NET里面,wait和signal這兩個(gè)操作分別對(duì)應(yīng)Mutex.WaitOne()和Mutex.ReleaseMutex()這兩個(gè)方法。我們可以讓W(xué)eb Service的線程使用Mutex.WaitOne()進(jìn)入等候狀態(tài),而在事件發(fā)生時(shí)使用Mutex.ReleaseMutex()來通知Web Service線程。因?yàn)楸仨氃贛utex.ReleaseMutex()發(fā)生后Mutex.WaitOne()才可能繼續(xù)執(zhí)行下去,因此能夠執(zhí)行下去就證明必然有事件發(fā)生了并且調(diào)用了Mutex.ReleaseMutext(),這時(shí)候就可以放心地去讀取事件消息了。
簡單示例
在選定使用Mutex模型后,我們來編寫一個(gè)簡單的示例。首先,我們要在WebService派生類內(nèi)定義一個(gè)Mutex,還有一個(gè)代表消息的字符串。
Mutex mutex = new Mutex();
string message;
然后,我們定義兩個(gè)WebMethod。為了把問題簡單化,我們選用上一篇文章中開頭所說的兩個(gè)函數(shù)簽名,也就說只能在一個(gè)Web Service內(nèi)自己發(fā)自己收,沒有發(fā)送目標(biāo)的概念,也沒有超時(shí)的概念,還沒有可靠性設(shè)計(jì)。同時(shí),我們將Message類型替換為普通字符串,以便于我們測試。
我們先編寫發(fā)送消息的函數(shù):
public void Send(string message) {
? this.message = message;
? this.mutex.ReleaseMutex();
}
在這個(gè)發(fā)送函數(shù)里,首先我們把消息放進(jìn)了類內(nèi)全局的變量中,然后讓全局的Mutex類釋放一個(gè)signal。這時(shí)候,如果有線程在等待,它可以馬上執(zhí)行下去。如果此時(shí)沒有線程在等待,那么下一個(gè)wait的線程執(zhí)行到該阻塞的地方就能夠不受阻塞繼續(xù)執(zhí)行下去。
現(xiàn)在我們來編寫接收消息的函數(shù):
public string Wait() {
? this.mutex.WaitOne();
? return this.message;
}
接收函數(shù)一開始就進(jìn)入wait狀態(tài)。在得到signal后,需要做的事情就是把全局的消息返回給客戶端。
親身體驗(yàn)
最后,我們可以通過ASP.NET Web Service本身支持的Web測試界面來測試一下我們的代碼。我們開兩個(gè)瀏覽器窗口,一個(gè)進(jìn)入Send()調(diào)用,一個(gè)進(jìn)入Wait()調(diào)用。然后我們按照如下方法來測試:
- 首先執(zhí)行Send("Hello"),然后執(zhí)行Wait()。這時(shí)候你可以馬上看到"Hello"。
- 首先執(zhí)行Wait(),讓它等待返回,這時(shí)候執(zhí)行Send("Hello")。隨后你可以看到Wait()那段返回"Hello"了。
- 按如下順序執(zhí)行:Send("Hello");Wait();Send("World");Wait();
- 按如下順序執(zhí)行:Send("Hello");Send("World");Wait();Wait();
- 按如下順序執(zhí)行:Wait();Wait();Send("Hello");Send("World");
- 按如下順序執(zhí)行:Wait();Send("Hello");Wait();Send("World");
你會(huì)發(fā)現(xiàn)這樣一些奇怪的結(jié)果:第3個(gè)測試返回的是"World"和"World"。第5個(gè)測試先返回"Hello"的并不一定是先執(zhí)行的那個(gè)Wait()線程。后者在某些情況下不是什么問題,特別是長連接中一般之后一個(gè)Wait()線程在等待中,所以我們可以不管。而前者,則是因?yàn)闆]有消息隊(duì)列所造成的,我們只有長度為1的消息窗口,所以只能緩存最后一個(gè)消息。這個(gè)問題我們將在下一篇文章中解決。
小結(jié)
在本文中,我們看到了不同的線程同步模型的差異。Monitor模型的lock本質(zhì)上是一個(gè)Semaphore,也就是一個(gè)不能連續(xù)signal的Mutex,一個(gè)signal發(fā)出去后必須被一個(gè)wait接收了才能進(jìn)行下一次的signal。同時(shí),Semaphore也限制了signal和wait必須在同一個(gè)線程內(nèi)成對(duì)執(zhí)行,而Mutex則沒有此限制。雖然.NET是針對(duì)Monitor模型優(yōu)化的,但在我們的需求當(dāng)中,只能通過Mutex模型來解決。
接著,我們便寫了一個(gè)小小的消協(xié)發(fā)送與接收函數(shù),實(shí)現(xiàn)了我們想要的阻塞式Web Service。同時(shí)我們也看到了沒有消息隊(duì)列造成的問題,因此確定接下來我們要做一個(gè)消息隊(duì)列。
總結(jié)
以上是生活随笔為你收集整理的使用 .NET 实现 Ajax 长连接的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 冒顿最后一关的伊芙利特怎么打
- 下一篇: 我家孩子学习习惯特别不好,物理成绩也特别