相對於 WebService 來說,采用 .Net Remoting 技術的客戶端能夠訂閱服務器端事件,這個功能簡直太棒了。
如果想利用該技術作一個簡單而又典型的應用,信息廣播程序是一個不錯的選擇。以下代碼是一個簡單的廣播程序,當然,它實在太簡陋了。
服務端:
Code
class Program
{
static void Main(string[] args)
{
BinaryServerFormatterSinkProvider sfsp = new BinaryServerFormatterSinkProvider();
sfsp.TypeFilterLevel = TypeFilterLevel.Full;
Hashtable props = new Hashtable();
props["port"] = 8086;
TcpChannel channel = new TcpChannel(props, null, sfsp);
ChannelServices.RegisterChannel(channel, false);
SayHello sayHello = new SayHello();
RemotingServices.Marshal(sayHello, "SayHello");
Console.ReadKey();
sayHello.Say("Mike", "Hello, Mike");
Console.ReadKey();
sayHello.Say("John", "Hello, John");
Console.ReadKey();
}
}
客戶端:
Code
class Program
{
static void Main(string[] args)
{
BinaryServerFormatterSinkProvider sfsp = new BinaryServerFormatterSinkProvider();
sfsp.TypeFilterLevel = TypeFilterLevel.Full;
Hashtable props = new Hashtable();
props["port"] = 0;
TcpChannel channel = new TcpChannel(props, null, sfsp);
ChannelServices.RegisterChannel(channel, false);
SayHello sh = (SayHello)Activator.GetObject(typeof(SayHello), "tcp://localhost:8086/SayHello");
SayEventReappear re = new SayEventReappear();
re.ClIEntId = "John";
sh.OnSay += new SayHandler(re.Say);
re.OnSay += new SayHandler(re_OnSay);
Console.ReadKey();
}
static void re_OnSay(string text)
{
Console.WriteLine(text);
}
}
遠程對象、委托及事件重現器(需同時部署在服務端及客戶端):
Code
public class SayHello : MarshalByRefObject
{
public event SayHandler OnSay;
public void Say(string clIEntId, string text)
{
if (this.OnSay != null) this.OnSay(text);
}
}
public delegate void SayHandler(string text);
public class SayEventReappear : MarshalByRefObject
{
public event SayHandler OnSay;
public void Say(string text)
{
if (this.OnSay != null) this.OnSay(text);
}
}
OK,我的信息廣播程序就這樣完成了。
但是,我很快就發現了問題:如果我的確想讓所有訂閱我的廣播事件的客戶端都得到我要廣播的信息,這個實現應該不會有問題。但是現在我有一個消息只想通知 Mike 或 John (正如以上代碼),(注:可能這時不能再稱為“廣播”了),我的廣播程序依然將這個消息通知到了每一個客戶端。
可以想到的一個方法是,讓事件重現器(SayEventReappear)接收到信息後先判斷一下是不是發給自己的,只有發給自己的信息才激發本地事件(代碼比較容易實現,不再貼出源碼)。但是,這種處理只是在客戶端將信息忽略而己,服務器端是照常廣播了,如果你的信息非常機密,或者帶寬非常有限,這顯然不是好的解決辦法。
考慮到 .Net Remoting 客戶端訂閱事件的實現原理:事件重現器在客戶端實例化,並由服務器對按引用方式對其遠程調用(個人理解,未經確認,歡迎指正)。如果將事件重現器訂閱服務器端事件改為向服務器端“注冊”事件重現器,邏輯上應該可行,這樣就可以讓每個客戶端注冊的事件重現器攜帶自己的客戶標識,讓服務端根據標識決定是否引發特定客戶端的事件。修改後的代碼如下:
遠程對象:
Code
public class SayHello : MarshalByRefObject
{
private List reList = new List();
public void Say(string clIEntId, string text)
{
foreach (SayEventReappear re in this.reList)
{
if (re.ClientId == clIEntId) re.Say(text);
}
}
public void AddEventReappear(SayEventReappear re)
{
this.reList.Add(re);
}
}
客戶端程序:
sh.OnSay += new SayHandler(re.Say);改為 sh.AddEventReappear(re);
OK,服務端分別檢測每一個客戶端的Id,然後只引發特定客戶端的事件,真正的“定向廣播”實現了。
但是檢查一下以上代碼,會發現依然存在問題:由於事件重現器的實例存在於客戶端,服務端訪問的是它的代理類,因此每次對 ClientId 的檢查都是一次遠程調用,這無疑是一種浪費。因此將程序改為在客戶端注冊事件重現器時提交 ClIEntId 或許更合理。修改後的代碼如下:
遠程對象:
Code
public class SayHello : MarshalByRefObject
{
private Dictionary reDict = new Dictionary();
public void Say(string clIEntId, string text)
{
foreach (KeyValuePair kp in this.reDict)
{
if (kp.Key == clIEntId) kp.Value.Say(text);
}
}
public void AddEventReappear(string clIEntId, SayEventReappear re)
{
if (!this.reDict.ContainsKey(clientId)) this.reDict.Add(clIEntId, re);
}
}
事件重現器:
Code
public class SayEventReappear : MarshalByRefObject
{
public event SayHandler OnSay;
public void Say(string text)
{
if (this.OnSay != null) this.OnSay(text);
}
}
客戶端:
sh.AddEventReappear(re);改為: sh.AddEventReappear("John", re);
如此一來,不僅避免了對客戶端的頻繁調用,而且代碼也更加簡潔了。
當然,同利用“事件訂閱”方式實現時當訂閱事件的客戶端退出時應該取消訂閱一樣,該實現中的客戶端在退出時同樣應該取消在服務端的“注冊”。由於只是示例程序,以上代碼中並沒有進行取消注冊處理。
但是問題恰恰出在這兒了,如果有的客戶端沒有自覺的取消注冊或者意外退出/斷線了,服務器端該如何處理呢?當然使用事件訂閱時這個問題同樣存在,但我估計 .Net Remoting 會根據生命周期原則適時“清除”已訂閱事件而不再響應的客戶端(同樣是個人猜測,沒有證實)。如果服務器端將連接失敗的客戶端直接取消注冊,顯然是對客戶端的一種不負責任(如果客戶端因臨時資源緊張或臨時斷線無法及時響應時將被永久取消注冊),但除此之外我沒有想出合適的辦法進行處理,總不能讓已經不存在的客戶端一直處於注冊狀態吧?!當然還有一種折中的辦法是對客戶端連接失敗進行計數或計時,達到一定程度後認為客戶已經不存在而進行注銷。