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的介面暴露出來。