不過 .Net Remoting 還有一種事件派送的架構, 就是讓 Client 可向 Server 註冊事件, 當 Server 有事件發生時, 再通知 Client. (大多都以聊天室為例子)
這樣的架構對我來說, 可以應用在派送網站資料 (包括: 圖檔, RSS 資料, 等).
假設現在網站供使用者訂閱的新聞 RSS 資料是放在資料庫裡, 而網站伺服器共有三台 (NLB).
若直接讓這三台網站伺服器各自從資料庫中讀取資料, 其讀取次數將會是 [網站數] * [RSS 頻道數] . (如果沒加入網頁快取機制, 可能要再乘上 [使用者連線數] )
所以為了降低資料庫的存取次數, 過去的做法是像圖一那樣, 由一個 Remoting Client 負責處理資料庫中的資料, 處理完畢後, 再將資料一一送到各台的 Remoting Server .
圖一 傳統的 Client-Server 架構
圖一架構的優缺點如下:
- 優點: 由 Client 對資料庫做存取, 並做資料處理, 可降低前台的資料庫存取次數.
- 缺點: Client 要記錄每個 Server 的 objectUri , 若增加或減少 Server , Client 程式要再重新啟動(如果此設定放在 App.Config 中).
圖二 事件派送的 Client-Server 架構
- 建立一個類別庫的專案 (InterfaceLib), 當做 Client 與 Server 端溝通的介面. (原則上還是盡量讓 Client 端僅留存"介面", 不放跟 Server 端相關的程式碼)
在專案中加入一個程式檔: IEventDispatch.csusing System; namespace InterfaceLib { public delegate void RssEventHandler(string strMsg); public interface IEventDispatch { event RssEventHandler RssUpdate; DateTime GetServerStartUpTime(); } public abstract class IRemoteDelegatableObject : MarshalByRefObject { public void RssCallback(string strMsg) { RssCallbackInClient(strMsg); } protected abstract void RssCallbackInClient(string str); } }
- IEventDispatch: 這個介面是 Server 與 Client 共同會使用到的.
- IRemoteDelegatableObject: 這個抽象類別主要是要讓 Client 端去實作 RssCallbackInClient 這個函式.
- 建立一個實作 IEventDispatch 的專案 (RemotingLib) , 並將第一步 InterfaceLib 專案加入參考.
此專案是後續要在 Server 端引用的組件 (不另建專案, 寫在 Server 端的專案中也 OK ).
加入一個 EventDispatch.cs , 程式碼如下:using System; using InterfaceLib; namespace RemotingLib { public class EventDispatch : MarshalByRefObject, IEventDispatch { private DateTime dtStartUpTime = DateTime.Now; public EventDispatch() { Console.WriteLine("EventDispatch Startup"); } /// <summary> /// 讓此物件永遠存在 /// </summary> /// <returns></returns> public override object InitializeLifetimeService() { return null; } #region IEventDispatch 成員 public event InterfaceLib.RssEventHandler RssUpdate; /// <summary> /// 回傳此物件啟動的時間 /// </summary> /// <returns></returns> public DateTime GetServerStartUpTime() { return this.dtStartUpTime; } #endregion /// <summary> /// 事件派送 /// </summary> /// <param name="strMsg"></param> public void SendRss(string strMsg) { if (RssUpdate != null) { RssEventHandler handler = null; try { foreach (Delegate delObj in RssUpdate.GetInvocationList()) { handler = (RssEventHandler)delObj; handler(strMsg); } } catch (Exception e) { RssUpdate -= handler; Console.WriteLine("Dispatching Exception..."); } } } } }
其中, 事件派送的部分也可以直接用 RssUpdate(strMsg); .
不過, 用上述的程式碼, 可知道現在有多少個對象要派送, 也較方便偵錯. - 建立一個 RemotingServer 專案, 我用主控台應用程式, 以方便說明.
在此專案中將第一與第二步的 InterfaceLib 與 RemotingLib 兩個專案加入參考, 並將 System.Configuration 與 System.Runtime.Remoting 也加入參考.
接著, 在此專案新增一個應用程式組態檔 (App.config) , 該組態檔的內容如下:<?xml version="1.0"?> <configuration> <appSettings> <add key="objectUri" value="RemotingTest"/> </appSettings> <system.runtime.remoting> <application> <service> </service> <channels> <channel ref="tcp" port="7777"> <serverProviders> <formatter ref="binary" typeFilterLevel="Full"/> </serverProviders> </channel> </channels> </application> </system.runtime.remoting> </configuration>
請注意, 如果後續 Client 啟動有發生以下的 Exception :
"System.Security.SecurityException: 這個安全性層級不允許將型別 System.DelegateSerializationHolder 以及從它衍生的型別 (例如 System.DelegateSerializationHolder) 還原序列化. "
請確定 <formatter ref="binary" typeFilterLevel="Full"/> 這個 typeFilterLevel 屬性要定為 "Full" .
這邊不宣告啟始的 Remoting 物件, 是因為要在程式中自行建立並控制.
RemotingServer 專案預設的 Program.cs 檔修改如下:
(由於是透過 EventDispatch 這個類別去實作 IEventDispatch 所以 Client 不會知道 EventDispatch 實際的運作方式)using System; using System.Configuration; using System.Runtime.Remoting; using RemotingLib; namespace RemotingServer { class Program { static void Main(string[] args) { //讀入 Remoting 的設定檔 RemotingConfiguration.Configure(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile, false); //建立一個 Remoting 物件 EventDispatch objDispatch = new EventDispatch(); //註冊一個 Remoting 物件的宣告至 RemotingServices 中 RemotingConfiguration.RegisterWellKnownServiceType( typeof(EventDispatch) , ConfigurationManager.AppSettings["objectUri"] , WellKnownObjectMode.Singleton); //將此物件註冊至 RemotingServices 中 RemotingServices.Marshal(objDispatch , ConfigurationManager.AppSettings["objectUri"] , typeof(EventDispatch)); Console.WriteLine("Server Started."); string strMsg = string.Empty; Console.WriteLine("請輸入'exit', 以結束程式."); while ((strMsg = Console.ReadLine()) != "exit") { //將 Console 輸入的字串送到 Client 端 objDispatch.SendRss(strMsg); } } } }
- 建立一個 RemotingClient 專案(主控台應用程式), 並將 InterfaceLib 專案加入參考.
在專案中新增一個應用程式組態檔 (App.config) , 該組態檔的內容如下:<?xml version="1.0"?> <configuration> <appSettings> <add key="rssRemoting" value="tcp://192.168.3.99:7777/RemotingTest"/> </appSettings> <system.runtime.remoting> <application> <channels> <channel ref="tcp" port="0"> <serverProviders> <formatter ref="binary" typeFilterLevel="Full"/> </serverProviders> </channel> </channels> </application> </system.runtime.remoting> </configuration>
這邊要加入一個 channel 的設定, 做為 Client 端被呼叫時的設定.
port 設為 0 表示不指定 port number.
加入一個繼承 IRemoteDelegatableObject 的類別 EventCallback.cs , 程式碼如下:using System; using InterfaceLib; namespace RemotingClient { public class EventCallback : IRemoteDelegatableObject { public EventCallback() { Console.WriteLine("EventCallback Started"); } /// <summary> /// 讓此物件永遠存在 /// </summary> /// <returns></returns> public override object InitializeLifetimeService() { return null; } /// <summary> /// 處理事件 /// </summary> /// <param name="strMsg">訊息字串</param> protected override void RssCallbackInClient(string strMsg) { Console.WriteLine("Client:" + strMsg); } } }
修改專案預設的 Program.cs 如下:using System; using System.Configuration; using System.Runtime.Remoting; using InterfaceLib; using System.Timers; namespace RemotingClient { class Program { IEventDispatch dispatcher = null; EventCallback callback = null; Timer timer = null; DateTime dtClientRegisterTime; Program() { try { //讀入 Remoting 設定檔 RemotingConfiguration.Configure(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile, false); this.dispatcher = (IEventDispatch)Activator.GetObject(typeof(IEventDispatch), ConfigurationManager.AppSettings["rssRemoting"]); this.callback = new EventCallback(); //此 Timer 用來定時監測 Server 是否有重新啟動 this.timer = new Timer(5000); this.timer.Elapsed += new ElapsedEventHandler(timer_Elapsed); } catch (Exception e) { Console.WriteLine(e.ToString()); } } /// <summary> /// 對 Server 做 polling 的動作 (避免 Server 重啟, 需重新註冊事件) /// </summary> /// <param name="sender"></param> /// <param name="e"></param> void timer_Elapsed(object sender, ElapsedEventArgs e) { Console.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")); try { //比對 Server 的啟動時間與 Client 端註冊的時間 //若 Server 在事件註冊後有重新啟動 -> 重新再註冊一次事件 if (this.dtClientRegisterTime < this.dispatcher.GetServerStartUpTime()) { Connect(); Console.WriteLine("ReRegister"); } } catch (Exception ex) { Disconnect(); } } /// <summary> /// 事件註冊 /// </summary> public void Connect() { try { dispatcher.RssUpdate += new RssEventHandler(callback.RssCallback); this.dtClientRegisterTime = DateTime.Now; Console.WriteLine("Connect"); } catch (Exception e) { Console.WriteLine("Connect: " + e.Message); } finally { this.timer.Start(); } } /// <summary> /// 事件的反註冊 /// </summary> public void Disconnect() { try { if (this.dispatcher != null) { dispatcher.RssUpdate -= new RssEventHandler(callback.RssCallback); Console.WriteLine("Disconnect"); } } catch (Exception e) { Console.WriteLine("Disconnect: " + e.Message); } finally { this.timer.Stop(); } } static void Main(string[] args) { Program prog = new Program(); prog.Connect(); string strInput; Console.WriteLine("請輸入'exit', 以結束程式."); while (!(strInput = Console.ReadLine()).Equals("exit", StringComparison.OrdinalIgnoreCase)) { } prog.Disconnect(); } } }
Client 的主程式說明如下:- 在建構子中, 先從 RemotingServer 中取得一個 IEventDispatch 介面, 並在主程式中建立一個負責處理事件的 EventCallback 物件.
- 為了避免當 RemotingServer 重啟時, 當初跟 RemotingServer 註冊的事件會失效, 所以加入一個 Timer , 以 polling 的方式確認 RemotingServer 啟動的時間.
- 透過 Connect() 和 Disconnect() 來設定事件的註冊與反註冊, 若是寫 Windows 服務, 可以把 Connect() 放在 OnStart() 和 OnContinue() , Disconnect() 放在 OnStop() 和 OnPause() .
圖三 方案的結構圖
執行結果如下:
圖四 RemotingServer 的執行畫面
圖五 RemotingClient 的執行畫面
其中, 我刻意將 RemotingServer 重啟, 以確定 RemotingClient 可在重新註冊事件後繼續運作.
另外, 可以觀察一下 RemotingServer 和 RemotingClient 之間使用的 port :
- Client 端未指定 port (<channel ref="tcp" port="0">) : (192.168.3.99 是 Server ;192.168.3.104 是 Client)
圖六 Client 端未指定 port 時的 Server 端狀態
圖七 Client 端未指定 port 時的 Client 端狀態 - 當Client 端指定 port 為 9999 (<channel ref="tcp" port="9999">) 時:
圖八 Client 端指定 port 為 9999 時的 Server 端狀態
圖九 Client 端指定 port 為 9999 時的 Server 端狀態
2 則留言:
是否在Client端將被實現用於EventHandler的abstract class一定要繼承 MarshalOjbect?
Hi,
可參考 MSDN 上的說明:
http://msdn.microsoft.com/zh-tw/library/system.marshalbyrefobject(v=vs.80).aspx
"當跨越應用程式定義域界限來使用型別時,型別必須繼承自 MarshalByRefObject,並且不可以複製物件的狀態,因為物件的成員無法在建立成員的所在應用程式定義域外部使用。"
所以習慣上要在Client/Server間傳遞的類別就繼承吧.
張貼留言