自己四個月前曾初步研究了Asp.net導步處理模型並寫了一遍學習總結:asp.net非同步處理機制研究 ,由於一直沒有應用的機會,不久就拋之腦後了。前天一朋友說需要實現一個類似QQ聊天的網頁聊天工具,我立馬就想到了它。經過幾個小時的奮戰,終於做出一個簡易的聊天Demo,如下:
左右兩圖代表單獨開啟的兩個瀏覽器介面,當右面的使用者選中一個線上使用者,在輸入架構填入資訊並發送時,左側的使用者就能立馬收到資訊。
一.概要
1.前台
前台代碼裡最重要的函數當數wait如下:
function wait() { $.post("SendHandler.ashx?ran=" + new Date().getTime(), { "senderId": $("#hdCurrentUserId").val() }, function (result) { if (result.Content) { $("#divContent").append("<div>" + result.Sender.Name + " 說:" + result.Content + "</div>") } if (result.List) { refreshOnlineMember(result.List, $("#hdCurrentUserId").val()); } wait(); })}
一目瞭然,這是一個遞迴的函數,向伺服器發送請求後就一直處於串連狀態,當伺服器回應後,跟據返回資訊更新介面資訊,最後再次調用自己。
2.後台
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData){ MessageAsyncResult result = new MessageAsyncResult(context, cb); if (string.IsNullOrEmpty(context.Request.Params["receiverId"])) { MessageCenter.Active(new Guid(context.Request.Params["senderId"]), result); } else { MessageCenter.SendMessage(new Guid(context.Request.Params["senderId"]), new Guid(context.Request.Params["receiverId"]), context.Request.Params["content"], result); } return result;}public void EndProcessRequest(IAsyncResult result){ MessageAsyncResult messageResult = result as MessageAsyncResult; messageResult.HttpContext.Response.ContentType = "application/json"; messageResult.HttpContext.Response.Write(JsonConvert.SerializeObject(new { Sender = messageResult.Sender, Content = messageResult.Content, List = MessageCenter.GetOnlineMember() }));}
後台代碼嚴格按照非同步模型進行編寫,實現了IHttpAsyncHandler介面,在Begin函數裡調用相關處理函數,在End函數裡向用戶端返回資訊。
3.回調對象
public class MessageAsyncResult : IAsyncResult{ //+靜態部分 //靜態內部類區 //靜態欄位區 //靜態屬性區 //靜態建構函式區,按參數由少到多排列 //靜態方法區 //+動態部分 //動態內部類區 //動態欄位區 private bool _isComplete = false; private AsyncCallback _asyncCallback; //動態屬性區 public Member Sender { get; private set; } public HttpContext HttpContext { get; set; } public string Content { get; set; } //動態建構函式區,按參數由少到多排列 public MessageAsyncResult(HttpContext context, AsyncCallback asyncCallback) { HttpContext = context; _asyncCallback = asyncCallback; } //動態方法區 //-本類方法區 public void SendMessage(Member sender, string content) { Sender = sender; Content = content; _isComplete = true; if (_asyncCallback != null) { _asyncCallback(this); } } //-重寫基類方法區,從直接超類開始,層層追溯至Object //-重寫介面方法區,按其出現的先後順序,依次實現 #region IAsyncResult public object AsyncState { get { return null; } } public System.Threading.WaitHandle AsyncWaitHandle { get { return null; } } public bool CompletedSynchronously { get { return false; } } public bool IsCompleted { get { return _isComplete; } } #endregion //+析構部分 //析構方法區}
此函數編寫毫無新意,起了兩個作用:1作為中介者傳遞對象,如HttpContext, Member等,2調用asyncCallback委託觸發結束事件。
4.處理常式
private static Dictionary<Guid, MessageAsyncResult> _members = new Dictionary<Guid, MessageAsyncResult>();public static void Active(Guid id, MessageAsyncResult result){ DataSource.Members.Find(m => m.Id == id).LastActiveTime = DateTime.Now; DataSource.Members.Find(m => m.Id == id).IsLogin = true; _members[id] = result;}public static void SendMessage(Guid senderId, Guid receiverId, string message, MessageAsyncResult result){ DataSource.Members.Find(m => m.Id == senderId).LastActiveTime = DateTime.Now; DataSource.Members.Find(m => m.Id == senderId).IsLogin = true; if (_members.Keys.Contains(receiverId)) { _members[senderId].SendMessage(DataSource.Members.Find(m => m.Id == senderId), string.Empty); result.SendMessage(DataSource.Members.Find(m => m.Id == senderId), string.Empty); _members[receiverId].SendMessage(DataSource.Members.Find(m => m.Id == senderId), message); }}
這裡有個_member對象,儲存了使用者與回調對象的索引值對,另外還有兩個方法,啟用登入狀態,發送資訊。
二.思路與要點
要明白上面代碼的含義與下面論述的意思,要求讀者瞭解Asp.net的非同步處理模型。
1.用戶端限制
首先A使用者登入,這裡服務端就有個HttpApplication對象為其服務,並產生一個IAsyncResult對象,裡麵包括了一個AsyncCallback委派物件。我們簡稱HA1,RA1,CA1對象;B使用者登入,又有一系列的對象為其服務,簡稱HB1,RB1,CB1對象。這時,B對象向A對象發送訊息,當B對象點擊發送按鈕後,一個新的請求發送到服務端。注意,這是一個新的請求,所以會有一個新的HttpApplication對象為其服務,我稱其為HB2對象,產生一個新的IAsyncResult對象,我稱其為RB2對象,但是AsyncCallback委派物件卻還是原來那一個。
然後通過緩衝中的索引值對找到之前A使用者對應的HA1,RA1,CA1對象,在RA1裡找到CA1對象,執行之。資訊發回用戶端,處理之後又會有一個新的請求自動發送過來,這時又會建立起新的為A使用者服務的對象HA2,RA2,CA2。
通過上面的描述,可以探索服務器什麼時候回應,是由什麼時候有人給自己發資訊決定的。
目前,B使用者共有兩組對象為其服務:HB1,RB1,CB1與HB2,RB2,CB1。現在就有兩個選擇:保持之,主動返回給用戶端然後由用戶端重建立立。我的第一個困惑來原由此。如果選擇第一種方案,可以想像如果B一直向外發資訊而從不接收資訊,服務端就會有越來越多的HB*,RB*對象被其佔用而不被釋放。我編寫代碼時一開始考慮的也是第一種方案,就會發現一個奇怪的問題,B使用者IE下連續發送9次或FF下連續發送5次資訊後,服務端就再也不會接收B發來的資訊了。之前一直找不到原因,後來一想,這會不會是用戶端單域並發串連數的限制,因為如果服務端對象一直被佔用而不返回,在用戶端看來請求就沒有完成。由於頁面載入時會自動發起一次請求,那麼算起來IE下就是10次而FF下就是6次了,這正好符合各自並發串連數的預設值。如:
可以看到,左圖中當說過“5”之後,瀏覽器已經有6個未返回的串連了,達到了單域最大串連數的設定值。這時如果再點發送,瀏覽器與伺服器都將沒有響應。
2.改進後的通訊模型
這時就要選擇上面所說的第二種方案。我現在再把思路梳理一遍。A使用者建立串連,產生HA1,RA1,CA1對象;B使用者建立串連,產生HB1,RB1,CB1對象;B使用者向A使用者發送資訊,產生HB2,RB2,CB1對象;這時要做的事件有兩件,第一件RA1對象被調用,CA1對象被執行,回調HA1對像的End方法向用戶端發送資訊,本次請求處理完成。A用戶端接收資訊作處理後,重新發起請求,產生HA2,RA2,CA2對象。第二件,將建立立的HB2,RB2,CB1對象執行掉,具體就是RB2對象被調用,CB1對象被執行,回調HB2對象的End方法向用戶端發送資訊,結束本次請求,將之前的HB1,RB1,CB1對象執行掉,具體就是RB1對象被調用,CB1對象被執行,回調HB1對象的End方法向用戶端發送資訊,B用戶端接收資訊作處理後,重新發起請求,產生HB3,RB3,CB2對象。如
上面示範了A,B先後登入,然後B連續發兩次資訊給A,一共有三次互動過程。彎曲的箭頭表達了請求與響應的對應關係,黑色箭頭表達了使用者之間的觸發關係。這樣,所有使用者與伺服器在大部分時間內就只保持了一個串連。
上面的講解也解答了在《基於ASP.NET的comet簡單實現》一文中為什麼要單獨執行這句代碼的原因。
asyncResult.Send(null);
來看一下在用戶端的具體編程方式。
$(function () { $.post("LoginHandler.ashx", null, function (result) { if (result.User) { $("#spanCurrentUser").text(result.User.Name); $("#hdCurrentUserId").val(result.User.Id); refreshOnlineMember(result.List, result.User.Id); wait(); } })})function sendMessage() { $.post("SendHandler.ashx?ran=" + new Date().getTime(), { "senderId": senderId, "receiverId": receiverId, "content": content });}function wait() { $.post("SendHandler.ashx?ran=" + new Date().getTime(), { "senderId": $("#hdCurrentUserId").val() }, function (result) { if (result.Content) { $("#divContent").append("<div>" + result.Sender.Name + " 說:" + result.Content + "</div>") } if (result.List) { refreshOnlineMember(result.List, $("#hdCurrentUserId").val()); } wait(); })}
可以看到,登入成功後,會執行wait方法。這個方法最大的特點就是回調完成後自我調用重新發起新請求。頁面載入完成後A,B使用者會自動登入,成功後各自都已進入wait方法的等待回調。
B使用者向A使用者發資訊,也就是在沒有等到wait回調時發起新的請求,這時A使用者的串連數是1個,B是2個,可以看到調用的是sendMessage方法。這個方法的最大特點是沒有回調,也就是說當服務端返回後不會自動發起新請求。這樣就明白了。A使用者的唯一的串連由wait發起,B使用者觸發服務端返回A請求後,會自動發起新請求。B使用者有兩個串連,一個由wait方法發起,服務端返回後會重建立立請求,一個是sendMessage方法發起,服務端返回後就結束了。這樣A,B使用者在大部分的時間內與服務端的串連數都是1個。
3.遺留問題
(1).並發問題
如果B向A發了資訊,服務端在已結束A請求與新的A請求之間收到新的發給A的資訊,這時資訊就會丟失,或者A所對應的回調對像已不存在,服務端發生異常。更穩妥的方式是讓資料庫完成大部份功能,而僅讓這種非同步編程模型完成訊息的即時性,每次請求時都訪問資料庫有無最新資訊。
(2).連線逾時
顯然,任何瀏覽器的串連都不是無限時等待的。這時應該在服務端應記錄最後活躍時間並定時檢查所有正在串連的請求,如果發現超出一定的時間,如1分鐘,則主動將此返回給用戶端並讓其重建立立請求。
(3).下線
這個跟連線逾時的原理差不多,當請求返回給用戶端而用戶端並未建立新的請求時,就可認為其已下線。
4.功能增強
這個基本就是向QQ看齊了,如群發,隱藏等。在上面的基礎上擴充起來不算太難
三.花絮
1.IIS7.5下擴充請求類型
《基於ASP.NET的comet簡單實現》的範例在VS下可正常運行,但在IIS裡卻行不通,報404錯誤。原因是作者把處理請求的路徑副檔名設為.asyn,而實際上不存在這個檔案,web.conbfig裡的httpHandlers配置節將其映射到自訂類的配置也未起到作用。在iis7.5的傳統模式下,需要在IIS裡的Handler Mappings模組裡單獨進行配置,讓IIS將此副檔名的請求讓asp.net來處理,而不是簡單的返回一個404錯誤。具體來講就是增加一個指令碼映射,處理副檔名為.asyn,處理常式為aspnet_isapi.dll即可。
2.IIS7.5下掛載asp.net 4.0網站
IIS7.5預設情況下不一定能夠處理4.0網站。在Handler Mappings模組裡,如果沒有4.0的處理常式,那麼掛載4.0網站後,請求它時會返回500伺服器內部錯誤。如果先安裝的VS2010後開啟的IIS可能就有此問題。解決的方法是手工註冊IIS的4.0環境,在命令列裡運行下面的命令即可
%WINDIR%\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe -i
四.原始碼
傳送門
參考的文章
asp.net非同步處理機制研究
基於ASP.NET的comet簡單實現
在Win7旗艦版內建IIS7.5中調試.Net 4.0網站出現無厘頭500錯誤的解決辦法
IIS7 asp.net URL重寫配置