不過 .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.cs12345678910111213141516171819202122using
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 , 程式碼如下:其中, 事件派送的部分也可以直接用 RssUpdate(strMsg); .123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263using
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..."
);
}
}
}
}
}
不過, 用上述的程式碼, 可知道現在有多少個對象要派送, 也較方便偵錯. - 建立一個 RemotingServer 專案, 我用主控台應用程式, 以方便說明.
在此專案中將第一與第二步的 InterfaceLib 與 RemotingLib 兩個專案加入參考, 並將 System.Configuration 與 System.Runtime.Remoting 也加入參考.
接著, 在此專案新增一個應用程式組態檔 (App.config) , 該組態檔的內容如下:請注意, 如果後續 Client 啟動有發生以下的 Exception :12345678910111213141516171819<?
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
>
"System.Security.SecurityException: 這個安全性層級不允許將型別 System.DelegateSerializationHolder 以及從它衍生的型別 (例如 System.DelegateSerializationHolder) 還原序列化. "
請確定 <formatter ref="binary" typeFilterLevel="Full"/> 這個 typeFilterLevel 屬性要定為 "Full" .
這邊不宣告啟始的 Remoting 物件, 是因為要在程式中自行建立並控制.
RemotingServer 專案預設的 Program.cs 檔修改如下:
(由於是透過 EventDispatch 這個類別去實作 IEventDispatch 所以 Client 不會知道 EventDispatch 實際的運作方式)1234567891011121314151617181920212223242526272829303132333435using
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) , 該組態檔的內容如下:這邊要加入一個 channel 的設定, 做為 Client 端被呼叫時的設定.12345678910111213141516171819<?
xml
version
=
"1.0"
?>
<
configuration
>
<
appSettings
>
<
add
key
=
"rssRemoting"
</
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
>
port 設為 0 表示不指定 port number.
加入一個繼承 IRemoteDelegatableObject 的類別 EventCallback.cs , 程式碼如下:修改專案預設的 Program.cs 如下:12345678910111213141516171819202122232425262728using
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);
}
}
}
Client 的主程式說明如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112using
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();
}
}
}
- 在建構子中, 先從 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間傳遞的類別就繼承吧.
張貼留言