因為Duplex實現了客戶端與服務端雙向通信的功能,故而我實現了一個簡單的聊天室程序,展現Duplex的特點。有朋友在閱讀了這個例子之後,提出一個問題,即“如何讓服務端向指定的客戶端發送消息?”很高興的是,這位朋友在後來的郵件中說到問題已經解決了,思路是利用Singleton對象保存客戶端的Session。雖然存在一些比較奇怪的問題,然而總算是一種思路。
我的思路與之相似,需要服務端維護一個Dictionary的集合,用以保存客戶端的信息。服務端在發送消息時,可以通過查找Dictionary對象,識別符合條件的客戶端。當我還在思考這樣的方式能否解決問題時,我在WCF官方網站上偶然發現了一個同樣利用Duplex實現聊天室的Sample。
仔細閱讀了實例代碼,我恍然發現自己在思考程序設計時,並沒有理解WCF最核心的價值,那就是“服務”。作為實現SOA體系架構的技術框架,WCF最重要的特征就在於能夠定義和提供服務。以聊天室程序為例,雖然服務端會參與消息的交互,但卻不應該參與到聊天中。也就是說,客戶端與服務端的角色任務是不相同的。通過用例圖可以看到兩者之間的區別:
圖1 正確的用例圖
圖二 錯誤的用例圖
明確了以“服務”為核心的程序結構,我們才能夠更好地利用WCF,定制自己的服務,分清楚服務的邊界,定義好消息的格式。雖然,一個聊天室程序無法體現SOA的核心精神,然而樹立面向服務的思想確實必要的。正如我們在開始面向對象程序設計時,需要樹立面向對象的思想一樣。
該聊天室程序的實現主要通過Duplex來實現,其中又利用了MulticastDelegate與異步調用。其中,服務接口的定義如下:
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IChatCallback))]
interface IChat
{
[OperationContract(IsOneWay = false, IsInitiating = true, IsTerminating = false)]
string[] Join(string name);
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)]
void Say(string msg);
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)]
void Whisper(string to, string msg);
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = true)]
void Leave();
}
回調接口的定義如下:
interface IChatCallback
{
[OperationContract(IsOneWay = true)]
void Receive(string senderName, string message);
[OperationContract(IsOneWay = true)]
void ReceiveWhisper(string senderName, string message);
[OperationContract(IsOneWay = true)]
void UserEnter(string name);
[OperationContract(IsOneWay = true)]
void UserLeave(string name);
}
服務提供了Join、Say、Whisper與Leave等接口方法,向對應的是回調接口的接口方法。在實現IChat服務接口的服務類ChatService中,定義了委托ChatEventHandler與ChatEventHandler類型的事件ChatEvent,正是通過它實現了識別了客戶的消息廣播。方法如下:private void BroadcastMessage(ChatEventArgs e)
{
ChatEventHandler temp = ChatEvent;
if (temp != null)
{
foreach (ChatEventHandler handler in temp.GetInvocationList())
{
handler.BeginInvoke(this, e, new AsyncCallback(EndAsync), null);
}
}
}
在客戶端加入聊天室程序之前,該客戶端並沒有訂閱ChatEvent事件,此時調用BroadcastMessage方法,在通過GetInvocationList方法獲取MulticastDelegate時,不存在該客戶端的委托實例。因而,其他客戶在通過聊天室進行聊天時,不會將聊天信息發送到該客戶端。體現在程序中,就是Join方法的如下代碼片斷:
myEventHandler = new ChatEventHandler(MyEventHandler);
……
callback = OperationContext.Current.GetCallbackChannel<IChatCallback>();
ChatEventArgs e = new ChatEventArgs();
e.msgType = MessageType.UserEnter;
e.name = name;
BroadcastMessage(e);
ChatEvent += myEventHandler;
……
注意看,ChatEvent += myEventHandler語句是放在BroadcastMessage方法調用之後。一旦該客戶端加入聊天室程序之後,再調用BroadcastMessage方法,該客戶端就能接收消息了。
ChatEvent事件指向的方法是MyEventHandler,該方法將執行回調接口的相關方法:
private void MyEventHandler(object sender, ChatEventArgs e)
{
try
{
switch (e.msgType)
{
case MessageType.Receive:
callback.Receive(e.name, e.message);
break;
case MessageType.ReceiveWhisper:
callback.ReceiveWhisper(e.name, e.message);
break;
case MessageType.UserEnter:
callback.UserEnter(e.name);
break;
case MessageType.UserLeave:
callback.UserLeave(e.name);
break;
}
}
catch
{
Leave();
}
}
還需要注意的是Whisper方法。由於它實現了私聊功能,因而向指定客戶發送信息時,不應該采用廣播方式。如何找到指定客戶呢?這需要一個Dictionary集合,保存客戶名和與之對應的ChatEventHandler實例。在執行Whisper方法時,就可以根據客戶名找到對應的ChatEventHandler實例進行調用:
public void Whisper(string to, string msg)
{
ChatEventArgs e = new ChatEventArgs();
e.msgType = MessageType.ReceiveWhisper;
e.name = this.name;
e.message = msg;
try
{
ChatEventHandler chatterTo;
lock (syncObj)
{
chatterTo = chatters[to];
}
chatterTo.BeginInvoke(this, e, new AsyncCallback(EndAsync), null);
}
catch (KeyNotFoundException)
{
}
}
在客戶端代碼中,服務接口的調用采用了異步調用的方式,例如客戶端加入聊天室:
proxy = new ChatProxy(site);
IAsyncResult iar = proxy.BeginJoin(myNick, new AsyncCallback(OnEndJoin), null);
運行聊天室程序時,服務端僅需要提供穩定而持續的服務。聊天的參與者均為客戶端用戶。因而服務端的運行代碼如下所示:
Uri uri = new Uri(ConfigurationManager.AppSettings["addr"]);
ServiceHost host = new ServiceHost(typeof(NikeSoftChat.ChatService), uri);
host.Open();
Console.WriteLine("Chat service listen on endpoint {0}", uri.ToString());
Console.WriteLine("Press ENTER to stop chat service...");
Console.ReadLine();
host.Abort();
host.Close();
本文Sample的作者是Nikola Paljetak。鑒於作者本人在代碼所附的許可聲明,為了幫助大家閱讀本文,在此附上Nikola Paljetak的Sample,你可以在WCF官方網站中找到它。Nikola Paljetak的許可聲明如下:
Permission is granted to anyone to use this software for any purpose, including commercial applications.
本文配套源碼