asp.net導步處理實戰之類似QQ的簡易網頁聊天
自己四個月前曾初步研究了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
四.源代碼
參考的文章

浙公網安備 33010602011771號