程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> 如何使用 .NET 實現 Ajax 長連接

如何使用 .NET 實現 Ajax 長連接

編輯:關於C#
 

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()調用。然後我們按照如下方法來測試:

  1. 首先執行Send("Hello"),然後執行Wait()。這時候你可以馬上看到"Hello"。
  2. 首先執行Wait(),讓它等待返回,這時候執行Send("Hello")。隨後你可以看到Wait()那段返回"Hello"了。
  3. 按如下順序執行:Send("Hello");Wait();Send("World");Wait();
  4. 按如下順序執行:Send("Hello");Send("World");Wait();Wait();
  5. 按如下順序執行:Wait();Wait();Send("Hello");Send("World");
  6. 按如下順序執行: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。同時我們也看到了沒有消息隊列造成的問題,因此確定接下來我們要做一個消息隊列。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved