程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 通過實例分析WCF Duplex消息交換

通過實例分析WCF Duplex消息交換

編輯:關於.NET

因為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.

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved