在asp.net中實現觀察者模式?難道ASP.Net中的觀察者模式有什麼特別麼?嗯,基於Http協議的Application難免有些健忘,我是這樣實現的,不知道有沒有更好的辦法?
先談談需求吧,以免陷入空談
最近一個Case, 這樣的需求:很多客戶端不斷的向Web Application提交數據,管理員進入Web的管理頁面可以即時的看到這些數據,有多個管理員可以同時浏覽,且管理員浏覽的數據從管理員開始監視那個時刻起,不能顯示以前的數據。從這個場景一看,明顯的觀察者模式,管理員開始監視時,訂閱數據,數據到達的時候向所有訂閱了數據的管理員廣播數據。
需求如下圖:
有了發布者還需要訂閱者,我們實現管理員類,來訂閱數據
public class Admin
{
/**//// <summary>
/// 用這個保存所有收到的數據
/// </summary>
public IList<string> MessageList
{ get; set; }
public Admin(Monitor monitor)
{
MessageList = new List<string>();
monitor.DataIn += new EventHandler< DataEventArgs>(ReciveMessage);
}
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
private void ReciveMessage(object sender, DataEventArgs e)
[img]/School/UploadFiles_7810/201105/20110524202211381.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211173.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
MessageList.Add(e.Message);
[img]/School/UploadFiles_7810/201105/20110524202212639.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202212316.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]
Ok,需要具備的元素我們都寫好了,但是如何讓它們工作起來?如果使Winform程序,那將毫無懸念。
分析:我們碰到的問題
第一個問題:當客戶端發送一個數據包,我們是實例化一個新的Monitor麼?如果是,哪麼每次實例化一個全新的Monitor,所有在它上面訂閱的事件將全部消失了,如果不是那這個Monitor將如何存在呢?總不能真空吧,兩個http請求之間如何保存數據呢?不過再把需求一讀,好像整個應用程序中就只需要也只能有一個這樣的Monitor呢,該是單件模式上場的時候了。
在上面的Monitor的實現中添加下面的代碼:
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]
private static Monitor _instance = null;
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]public static
Monitor Current
[img]/School/UploadFiles_7810/201105/20110524202215186.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202216100.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
get
[img]/School/UploadFiles_7810/201105/20110524202211381.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211173.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
if (_instance == null)
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
_instance = new Monitor();
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
return _instance;
[img]/School/UploadFiles_7810/201105/20110524202212639.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202212316.gif[/img]}
但是本系統存在多個客戶端,所以為了避免多線程造成問題,還是來Double Check一下吧,修改上面的代碼如下:
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]
public static Monitor Current
[img]/School/UploadFiles_7810/201105/20110524202215186.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202216100.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
get
[img]/School/UploadFiles_7810/201105/20110524202211381.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211173.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
object o = new object();
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
if (_instance == null)
[img]/School/UploadFiles_7810/201105/20110524202211381.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211173.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
lock (o)
[img]/School/UploadFiles_7810/201105/20110524202211381.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211173.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
if (_instance == null)
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
_instance = new Monitor();
[img]/School/UploadFiles_7810/201105/20110524202212639.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202212639.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
return _instance;
[img]/School/UploadFiles_7810/201105/20110524202212639.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202212316.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]
(PS:為什麼使用單件就可以跨請求保存實例了呢?因為這裡使用了一個static member保存Monitor的引用,static member在.Net的GC裡面是被作為Root的,詳細內容請參見框架程序設計那本書)
第二個問題: 當管理員頁面的AJax請求的時候,每兩個請求如何保存數據?呵呵,上面那個問題不是說了麼,用單件,但是單件是全局存在的,我們的管理員是多個,每個管理員可以決定是否訂閱數據,以及什麼時候訂閱。想起來沒?除了全局數據外我們還有Session
在管理頁面上我放置一個“開始監視”的按鈕,這個按鈕使用AJax請求服務器端的一個HttpHandler,在Handler的ProcessRequest方法裡這樣來做:
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]
Admin admin = context.Session["monitor_listener"] as Admin;
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]
if(admin == null)
[img]/School/UploadFiles_7810/201105/20110524202215186.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202216100.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
admin = new Admin(Monitor.Current);
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
context.Session["monitor_listener"] = admin;
[img]/School/UploadFiles_7810/201105/20110524202212316.gif[/img]}
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]
注意,由於這個Handler需要訪問Session,所以你需要讓這個Handler繼承IRequiresSessionState接口(為什麼使用繼承而不用實現這個術語?實際上這個接口是一個標記接口,沒有任何需要實現的成員,只是標記這個Handler可以訪問Session,我不知道為什麼MS不使用Attribute,是不是更合理些)
在管理頁面還有個一個SetInterval不斷的調用一個含有AJax的方法,去請求另外一個Handler,這個Handler將Admin收到的數據返回到web頁面,讓我們來看看這個Handler的部分實現:
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]
public void ProcessRequest(HttpContext context)
[img]/School/UploadFiles_7810/201105/20110524202215186.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202216100.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
context.Response.Buffer = true;
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
context.Response.ExpiresAbsolute = System.DateTime.Now.AddSeconds(-1);
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
context.Response.Expires = 0;
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
context.Response.CacheControl = "no-cache";
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
Admin admin = context.Session["monitor_listener"] as Admin;
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
if (admin == null || admin.MessageCollection == null ||
admin.MessageCollection.Count <= 0)
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
return;
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
string[] messages = new string[admin.MessageCollection.Count];
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
admin.MessageCollection.CopyTo(messages, 0);
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
StringBuilder sb = new StringBuilder();
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
for (int i = 0; i < messages.Length; i++)
[img]/School/UploadFiles_7810/201105/20110524202211381.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211173.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
sb.AppendFormat("<li>{0}</li>", messages);
[img]/School/UploadFiles_7810/201105/20110524202212639.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
admin.MessageCollection.Clear();
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
context.Session["monitor_listener"] = admin;
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
context.Response.Write(sb);
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
context.Response.Flush();
[img]/School/UploadFiles_7810/201105/20110524202212316.gif[/img]}
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]
OK,一個在ASP.Net環境中實現的觀察者模式基本上就算完成了,不過上面只有怎樣訂閱,那什麼時候取消訂閱了,可以在Session_End事件裡面取消訂閱
還查看了一些關於長連接的文章,發現這個不錯,准備改進一下。
完整的代碼稍後提供,希望這塊轉頭能引來一些玉
寫完這個Post後本來想把完整代碼實現傳上來,後來看到不少園友提出異議,看了大家的留言後我也一直在思索:我為什麼這樣做?當初我是怎樣想到這個解決方案的?我在幾個解決方案之間做了取捨了麼?我這樣做是不是矯枉過正了?經過這些思考有了現在的這個Post。
首先我進一步談一下需求:
這是一個Web Application,有很多客戶端向服務器端提交數據(客戶端是C++的,以http-post方式向服務器端提交二進制數據,服務器端解析這個二進制包,數據提交很頻繁),管理員可以進入監視頁面浏覽這些數據,數據要即時的,客戶端發來一條,管理員屏幕上要馬上可以看到,允許多個管理員同時監視即時數據,所有管理員看到的數據都是一樣的(目前是這樣的,也許以後對管理員要分角色,各角色管理員看到的信息將不同)。
由於數據提交非常頻繁,客戶要求不允許頻繁的數據庫操作,所以我將數據保存在一個IList的緩存裡面,當這個IList的大小超過了我在配置文件裡定義的大小的時候就將數據批量插入到數據庫。
下面我將以我當初思考的思路為主線描述:
第一個版本:
//在程序裡我寫了一個靜態類,這個靜態類保存整個程序中共享的一些數據,
相當於原來的//Application對象,但是靜態成員是編譯期類型檢查的
public static ApplicationData
{
//這個隊列用來保存客戶端傳遞過來的數據,當隊列達到一定長度的時候同步到數據庫
public static Queue<DataHead> OperateDataList = new Queue<DataHead>();
//這個List也是保存客戶端傳遞過來的數據的,但它是為監視准備數據的,
//當一個監視頁面的請求到來的時候將這個List的數據Response過去,然後Clear這個//List
public static IList<DataHead> MonitorDataList = new List<DataHead>();
}
public class ReciveDataHandler : IHttpHandler
{
//……
Public void ProcessRequest(HttpContext context)
{
//解析從客戶端傳遞過來的數據
DataHead data = GetData(context);
OperateDataList.Add(data);
If(OperateDataList.Count > BufferSize)
{
//將數據寫入到數據庫
AddToBase();
}
MonitorDataList.Add(data);
}
}
//監視頁面從這裡獲取數據
public class MonitorHandler : IHttpHandler
{
//……
Public void ProcessRequest(HttpContext context)
{
If(MonitorDataList.Count > 0)
{
//將MonitorDataList裡的數據Response出去
OutPut();
MonitorDataList.Clear();
}
}
}
說實話,我當初做出這個的時候覺得一點問題都沒有,開始的時候客戶測試也沒有發現任何問題,終於有一天客戶和我同時測試部署在同一IIS的時候,問題出現了:只有一個監視頁面有數據。看到這個後我還百思不得其解,順著程序的執行流程一步一步走下去,沒有找出任何錯誤。後來做了下日志,原來MonitorDataList是一個全局共享的,一個在監視把數據Clear了後別人就無法獲取數據了。不知道有沒有人這樣做過:有時候忘記了自己正在做一個web程序,而web程序是一個並發的,對一些共享資源的訪問有著微妙的問題,如果沒有記住這點,按照程序流程的執行步驟是找不出任何問題的。
怎麼辦?再一看這不是事件訂閱所描述的場景麼?所以就有了上一篇Post的Solution。不過那個方案受到不少人質疑,其中金色海洋提出這樣的方法:
Public class ReciveData : IHttpHandler
{
//……….
//將客戶端傳遞過來的數據存入數據庫
}
Public class MonitorHandler : IHttpHandler
{
//………
//為null的時候說明該管理員第一次監視
If(Session[“id”] == null)
{
//根據時間從服務器取出數據
//並將取出數據的最後一個id保存在session中
Session[“id”] = id;
}
//不為null則說明該管理員已經開始監視了
Else
{
//根據session裡保存的最後一個id,取出大於那個id的數據
Session[“id”] = currentId;
}
}
看似這個方案不錯,我嘗試著將我的程序修改為這樣,但是我將上面的代碼編寫完,我發現我不可以再進行下去了:上面的方案滿足不了我的需求,客戶明確要求了客戶端提交的數據要先緩存然後緩存超過配置大小(這個大小還需要可以在配置文件裡面配置,以便可以經過測試找出一個最合理的值),而這種Session記錄的方案是依靠數據庫來保存數據,這個Session[“id”]就相當於一個游標,這個游標指向的是數據庫,那好,我們將Session[“id”]指向緩存數據,但是請注意緩存隨時可能超過設置大小而被同步到數據庫並被清空。
經過一番思考後我還是回到我自己的Solution上,不過我又有了新的看法了。不是要將數據先緩存麼?看看這個緩存,實際上她也是個觀察者,至於她執行怎樣的緩存策略是她的事情,如是我又有了一個新類:
//這裡的代碼接上篇Post
using System;
using System.Collections.Generic;
using System.Text;
namespace ForyourSoft.NetTraffic.Framework
{
public sealed class DataBase
{
private IList<string> _buffer = new List<string>();
private static DataBase _instance = null;
public static void Subscribe()
{
if (_instance == null)
_instance = new DataBase(Monitor.Current);
}
public DataBase(Monitor monitor)
{
monitor.OnMessage += new EventHandler<Monitor.MessageEventArgs>(monitor_OnMessage);
}
void monitor_OnMessage(object sender, Monitor.MessageEventArgs e)
{
_buffer.Add(e.Message);
if (_buffer.Count >= Config.BufferSize)
{
//將數據添加到數據庫
}
}
}
}
PS:由於系統中我們只需要這樣唯一一個訂閱者,所以我將其實現為一個單件,在Application_Start的時候調用DataBase.Subscribe()。
現在系統是這樣的結構:
[img]/School/UploadFiles_7810/201105/20110524202218255.jpg[/img]
可以設想以後還會有更多的訂閱者。果然,昨天客戶要求在下一個版本中管理員分角色,各個角色看到的數據不同的,只有超級管理員才可以監視所有數據,OMG,呵呵,不過還好,我只需要添加幾個訂閱者就可以輕松搞定。
後記:也許是我的文章標題沒有起好,也許很多人得到模式恐懼症,提到模式總是要來考察一下你的case,不是那種Enterprise級別的用了pattern就是過火了。其實這篇文章的內容裡沒有一點模式的氣息,只是用.Net的Event實現觀察者模式的思想,我想如果合適,今天模式的投資,明天你會有收獲的。
在.Net裡面我們有事件(event),那就無需使用傳統的觀察者模式的模型了
那麼我首先實現一個Monitor類,這個類用來接收客戶端傳遞來的數據並將數據廣播出去
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]
public class DataEventArgs : EventArgs
[img]/School/UploadFiles_7810/201105/20110524202215186.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202216100.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
public string Message
[img]/School/UploadFiles_7810/201105/20110524202211381.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211173.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{get;set;}
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
public DataEventArgs(string message)
[img]/School/UploadFiles_7810/201105/20110524202211381.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211173.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
this.Message = message;
[img]/School/UploadFiles_7810/201105/20110524202212639.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202212316.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]
public class Monitor
[img]/School/UploadFiles_7810/201105/20110524202215186.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202216100.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
public event EventHandler<DataEventArgs> DataIn;
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
private void SendData(string message)
[img]/School/UploadFiles_7810/201105/20110524202211381.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211173.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
if (DataIn != null)
[img]/School/UploadFiles_7810/201105/20110524202211381.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211173.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
DataEventArgs e = new DataEventArgs(message);
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
DataIn(this, e);
[img]/School/UploadFiles_7810/201105/20110524202212639.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202212639.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202211381.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211173.gif[/img]
/**//// <summary>
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
/// 這個方法被一個HttpHandler調用,客戶端向這個Handler發送數據
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
/// 數據處理後作為字符串傳遞給該方法,該方法然後將數據廣播出去
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
/// </summary>
[img]/School/UploadFiles_7810/201105/20110524202212639.gif[/img]
/// <param name="message">處理後的數據</param>
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
public void ReciveData(string message)
[img]/School/UploadFiles_7810/201105/20110524202211381.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211173.gif[/img]
[img]/School/UploadFiles_7810/201105/20110524202211794.gif[/img]{
[img]/School/UploadFiles_7810/201105/20110524202210766.gif[/img]
SendData(message);
[img]/School/UploadFiles_7810/201105/20110524202212639.gif[/img]
}
[img]/School/UploadFiles_7810/201105/20110524202212316.gif[/img]}
[img]/School/UploadFiles_7810/201105/20110524202213332.gif[/img]