《Windows Communication Foundation之旅》系列之三

weixin_34262482發表於2007-01-12

示例程式碼下載:DuplexSample.rar

四、Service Contract程式設計模型
在Part Two中,我以“Hello World”為例講解了如何定義一個Service。其核心就是為介面或類施加ServiceContractAttribute,為方法施加OperationContractAttribute。在Service的方法中,可以接受多個引數,也可以有返回型別,只要這些資料型別能夠被序列化。這樣一種方式通常被稱為本地物件,遠端過程呼叫(local-object, Remoting-Procedure-Call)方式,它非常利於開發人員快速地進行Service的開發。

在Service Contract程式設計模型中,還有一種方式是基於Message Contract的。服務的方法最多隻能有一個引數,以及一個返回值,且它們的資料型別是通過Message Contract自定義的訊息型別。在自定義訊息中,可以為訊息定義詳細的Header和Body,使得對訊息的交換更加靈活,也更利於對訊息的控制。

一個有趣的話題是當我們定義一個Service時,如果一個private方法被施加了OperationContractAttribute,那麼對於客戶端而言,這個方法是可以被呼叫的。這似乎與private對於物件封裝的意義有矛盾。但是這樣的規定是有其現實意義的,因為對於一個服務而言,服務端和客戶端的需求往往會不一致。在服務端,該服務物件即使允許被遠端呼叫,但本地呼叫卻可能會因情況而異。如下面的服務定義:
[ServiceContract]
public class BookTicket
{
 [OperationContract]
 public bool Check(Ticket ticket)
 {
  bool flag;
  //logic to check whether the ticket is none;
  return flag;
 }
 [OperationContract]
 private bool Book(Ticket ticket)
 {
  //logic to book the ticket
 }
}
在服務類BookTicket中,方法Check和Book都是服務方法,但後者被定義成為private方法。為什麼呢?因為對於客戶而言,首先會檢查是否還有電影票,然而再預定該電影票。也就是說這兩項功能都是面向客戶的服務,會被遠端呼叫。對於Check方法,除了遠端客戶會呼叫該方法之外,還有可能被查詢電影票、預定電影票、出售電影票等業務邏輯所呼叫。而Book方法,則只針對遠端客戶,只可能被遠端呼叫。為了保證該方法的安全,將其設定為private,使得本地物件不至於呼叫它。

因此在WCF中,一個方法是否應該被設定為服務方法,以及應該設定為public還是private,都需要根據具體的業務邏輯來判斷。如果涉及到私有的服務方法較多,一種好的方法是利用設計模式的Façade模式,將這些方法組合起來。而這些方法的真實邏輯,可能會散放到各自的本地物件中,對於這些本地物件,也可以給與一定的訪問限制,如下面的程式碼所示:
internal class BusinessObjA
{
 internal void FooA(){}
}
internal class BusinessObjB
{
 internal void FooB(){}
}
internal class BusinessObjC
{
 internal void FooC(){}
}
[ServiceContract]
internal class Façade
{
 private BusinessObjA objA = new BusinessObjA();
 private BusinessObjB objB = new BusinessObjB();
 private BusinessObjC objC = new BusinessObjC();
 [OperationContract]
 private void SvcA()
 {
  objA.FooA();
 }
 [OperationContract]
 private void SvcB()
 {
  objB.FooB();
 }
 [OperationContract]
 private void SvcC()
 {
  objC.FooC();
 }
}
方法FooA,FooB,FooC作為internal方法,拒絕被程式集外的本地物件呼叫,但SvcA,SvcB和SvcC方法,卻可以被遠端物件所呼叫。我們甚至可以將BusinessObjA,BusinessObjB等類定義為Façade類的巢狀類。採用這樣的方法,有利於這些特殊的服務方法,被遠端客戶更方便的呼叫。

定義一個Service,最常見的還是顯式地將介面定義為Service。這樣的方式使得服務的定義更加靈活,這一點,我已在Part Two中有過描述。當然,採用這種方式,就不存在前面所述的私有方法成為服務方法的形式了,因為在一個介面定義中,所有方法都是public的。

另外一個話題是有關“服務介面的繼承”。一個被標記了[ServiceContract]的介面,在其繼承鏈上,允許具有多個同樣標記了[ServiceContract]的介面。對介面內定義的OperationContract方法,則是根據“聚合”的原則,如下的程式碼所示:
[ServiceContract]
public interface IOne
{
    [OperationContract(IsOneWay=true)]
    void A();
}
[ServiceContract]
public interface ITwo
{
    [OperationContract]
    void B();
}
[ServiceContract]
public interface IOneTwo : IOne, ITwo
{
    [OperationContract]
    void C();
}

在這個例子中,介面IOneTwo繼承了介面IOne和ITwo。此時服務IOneTwo暴露的服務方法應該為方法A、B和C。

然而當我們採用Duplex訊息交換模式(文章後面會詳細介紹Duplex)時,對於服務介面的回撥介面在介面繼承上有一定的限制。WCF要求服務介面IB在繼承另一個服務介面IA時,IB的回撥介面IBCallBack必須同時繼承IACallBack,否則會丟擲InvalidContractException異常。正確的定義如下所示:
[ServiceContract(CallbackContract = IACallback)]
interface IA {}
interface IACallback {}

[ServiceContract(CallbackContract = IBCallback)]
interface IB : IA {}
interface IBCallback : IACallback {}

五、訊息交換模式(Message Exchange Patterns,MEPS)
在WCF中,服務端與客戶端之間訊息的交換共有三種模式:Request/Reply,One-Way,Duplex。

1、Request/Reply
這是預設的一種訊息交換模式,客戶端呼叫服務方法發出請求(Request),服務端收到請求後,進行相應的操作,然後返回一個結果值(Reply)。

如果沒有其它特別的設定,一個方法如果標記了OperationContract,則該方法的訊息交換模式就是採用的Request/Reply方式,即使它的返回值是void。當然,我們也可以將IsOneWay設定為false,這也是預設的設定。如下的程式碼所示:
[ServiceContract]
public interface ICalculator
{
 [OperationContract]
 int Add(int a, int b);

 [OperationContract]
 int Subtract(int a, int b);
}

2、One-Way
如果訊息交換模式為One-Way,則表明客戶端與服務端之間只有請求,沒有響應。即使響應資訊被髮出,該響應資訊也會被忽略。這種方式類似於訊息的通知或者廣播。當一個服務方法被設定為One-Way時,如果該方法有返回值,會丟擲InvalidOperationException異常。

要將服務方法設定為One-Way非常簡單,只需要將OperationContractAttribute的屬性IsOneWay設定為true就可以了,如下的程式碼所示:
public class Radio
{
 [OperationContract(IsOneWay=true)]
 private void BroadCast();
}

3、Duplex
Duplex訊息交換模式具有客戶端與服務端雙向通訊的功能,同時它的實現還可以使訊息交換具有非同步回撥的作用。

要實現訊息交換的Duplex,相對比較複雜。它需要定義兩個介面,其中服務介面用於客戶端向服務端傳送訊息,而回撥介面則是從服務端返回訊息給客戶端,它是通過回撥的方式來完成的。介面定義如下:
服務介面:
[ServiceContract(Namespace = “http://Microsoft.ServiceModel.Samples“,
Session = true, CallbackContract=typeof(ICalculatorDuplexCallback))]
public interface ICalculatorDuplex
{
    [OperationContract(IsOneWay=true)]
    void Clear();

    [OperationContract(IsOneWay = true)]
    void AddTo(double n);

    [OperationContract(IsOneWay = true)]
    void SubtractFrom(double n);

    [OperationContract(IsOneWay = true)]
    void MultiplyBy(double n);

    [OperationContract(IsOneWay = true)]
    void DivideBy(double n);
}
回撥介面:
public interface ICalculatorDuplexCallback
{
    [OperationContract(IsOneWay = true)]
    void Equals(double result);

    [OperationContract(IsOneWay = true)]
    void Equation(string equation);
}
注意在介面定義中,每個服務方法的訊息轉換模式均設定為One-Way。此外,回撥介面是被本地呼叫,因此不需要定義[ServiceContract]。在服務介面中,需要設定ServiceContractAttribute的CallbackContract屬性,使其指向回撥介面的型別type。

對於實現服務的類,例項化模式(InstanceContextMode)究竟是採用PerSession方式,還是PerCall方式,應根據該服務物件是否需要儲存狀態來決定。如果是PerSession,則服務物件的生命週期是存活於一個會話期間。而PerCall方式下,服務物件是在方法被呼叫時建立,結束後即被銷燬。然而在Duplex模式下,不能使用Single方式,否則會導致異常丟擲。本例的實現如下:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class CalculatorService : ICalculatorDuplex
{
    double result;
    string equation;
    ICalculatorDuplexCallback callback = null;
    public CalculatorService()
    {
        result = 0.0D;
        equation = result.ToString();
        callback = OperationContext.Current.
        GetCallbackChannel();
    }
    public void AddTo(double n)
    {
        result += n;
        equation += ” + ” + n.ToString();
        callback.Equals(result);
    }
   // Other code not shown.
}

在類CalculatorService中,回撥介面物件callback通過OperationContext.Current.GetCallbackChannel<>()獲取。然後在服務方法例如AddTo()中,通過呼叫該回撥物件的方法,完成服務端向客戶端返回訊息的功能。

在使用Duplex時,Contract使用的Binding應該是系統提供的WSDualHttpBinding,如果使用BasicHttpBinding,會出現錯誤。因此Host程式應該如下所示:
    public static void Main(string[] args)
    {
        Uri uri = new Uri(”http://localhost:8080/servicemodelsamples“);
        using (ServiceHost host = new ServiceHost(typeof(CalculatorService), uri))
        {
            host.AddServiceEndpoint(typeof(ICalculatorDuplex),new WSDualHttpBinding(),”service.svc”);
            host.Open();
            Console.WriteLine(”Press any key to quit service.”);
            Console.ReadKey();
        }
    }
如果是使用配置檔案,也應作相應的修改,如本例:
  <system.serviceModel>
    <client>
      <endpoint name=”"
                address=”http://localhost:8080/servicemodelsamples/service.svc
                binding=”wsDualHttpBinding”
                bindingConfiguration=”DuplexBinding”
                contract=”ICalculatorDuplex” />
    </client>
    <bindings>
      <!– configure a binding that support duplex communication –>
      <wsDualHttpBinding>
        <binding name=”DuplexBinding”
                 clientBaseAddress=”http://localhost:8000/myClient/”&gt;
        </binding>
      </wsDualHttpBinding>
    </bindings>
  </system.serviceModel>

當服務端將資訊回送到客戶端後,對訊息的處理是由回撥物件來處理的,所以回撥物件的實現應該是在客戶端完成,如下所示的程式碼應該是在客戶端中:
    public class CallbackHandler : ICalculatorDuplexCallback
    {
        public void Equals(double result)
        {
            Console.WriteLine(”Equals({0})”, result);
        }
        public void Equation(string equation)
        {
            Console.WriteLine(”Equation({0})”, equation);
        }
    }

客戶端呼叫服務物件相應的為:
    class Client
    {
        static void Main()
        {
            // Construct InstanceContext to handle messages on
            // callback interface.
            InstanceContext site = new InstanceContext(new CallbackHandler());

            // Create a proxy with given client endpoint configuration.
            using (CalculatorDuplexProxy proxy =
            new CalculatorDuplexProxy(site, “default”))
            {
                double value = 100.00D;
                proxy.AddTo(value);
                value = 50.00D;
                proxy.SubtractFrom(value);
                // Other code not shown.

                // Wait for callback messages to complete before
                // closing.
                System.Threading.Thread.Sleep(500);
                // Close the proxy.
                proxy.Close();
            }
        }
    }

注意在Duplex中,會話建立的時機並不是客戶端建立Proxy例項的時候,而是當服務物件的方法被第一次呼叫時,會話方才建立,此時服務物件會在方法呼叫之前被例項化,直至會話結束,服務物件都是存在的。

以上的程式碼例子在WinFX的SDK Sample中可以找到。不過該例子並不能直接反映出Duplex功能。通過前面的介紹,我們知道Duplex具有客戶端與服務端雙向通訊的功能,同時它的實現還可以使訊息交換具有非同步回撥的作用。因此,我分別實現了兩個例項來展現Duplex在兩方面的作用。

(1)客戶端與服務端雙向通訊功能——ChatDuplexWin
例項說明:一個類似於聊天室的小程式。利用Duplex支援客戶端與服務端通訊的特點,實現了客戶端與服務端聊天的功能。

服務介面和回撥介面的定義如下:
    [ServiceContract(Namespace = “http://www.brucezhang.com/WCF/Samples/ChatDuplex“, Session = true, CallbackContract=typeof(IChatDuplexCallback))]
    public interface IChatDuplex
    {
        [OperationContract(IsOneWay=true)]
        void Request(string cltMsg);
        [OperationContract(IsOneWay = true)]
        void Start();
    }
    public interface IChatDuplexCallback
    {
        [OperationContract(IsOneWay=true)]
        void Reply(string srvMsg);
    }
很明顯,Request方法的功能為客戶端向服務端傳送訊息,Reply方法則使服務端回送訊息給客戶端。服務介面IChatDuplex中的Start()方法,用於顯示的建立一個會話,因為在這個方法中,我需要直接獲取callback物件,使得服務端不必等待客戶端先傳送訊息,而是可以利用callback物件主動先向客戶端傳送訊息,從而實現聊天功能。

實現類的程式碼如下:
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
    public class ChatDuplex:IChatDuplex
    {
        public ChatDuplex()
        {
            m_callback = OperationContext.Current.GetCallbackChannel();           
        }
        private IChatDuplexCallback m_callback = null;           
        public void Request(string cltMsg)
        {
            ChatRoomUtil.MainForm.FillListBox(string.Format(”Client:{0}”, cltMsg));           
        }
        public void Start()
        {
            ChatRoomUtil.MainForm.SetIIMDuplexCallback(m_callback);
        }
    }

因為我要求在服務端介面中,能夠將客戶端傳送來的訊息顯示在主窗體介面中。所以利用了全域性變數MainForm,用來儲存主窗體物件:
    public static class ChatRoomUtil
    {
        public static ServerForm MainForm = new ServerForm();
    }
而在服務端程式執行時,Application執行的主視窗也為該全域性變數:
Application.Run(ChatRoomUtil.MainForm);

要實現聊天功能,最大的障礙是當服務端收到客戶端訊息時,不能立即Reply訊息,而應等待服務端使用者輸入回送的訊息內容,方可以Reply。也即是說,當客戶端呼叫服務物件的Request方法時,不能直接呼叫callback物件。因此我利用Start()方法,將服務物件中獲得的callback物件傳遞到主窗體物件中。這樣,callback物件就可以留待服務端傳送訊息時呼叫了:
    public partial class ServerForm : Form
    {       
        private IChatDuplexCallback m_callback;
        private void btnSend_Click(object sender, EventArgs e)
        {
            if (txtMessage.Text != string.Empty)
            {
                lbMessage.Items.Add(string.Format(”Server:{0}”, txtMessage.Text));
                if (m_callback != null)
                {
                    m_callback.Reply(txtMessage.Text);
                }
                txtMessage.Text = string.Empty;
            }
        }       
        public void FillListBox(string message)
        {
            lbMessage.Items.Add(message);
        }
        public void SetIIMDuplexCallback(IChatDuplexCallback callback)
        {
            m_callback = callback;
        }
     //Other code not shown;
    }

對於客戶端的實現,相對簡單,需要注意的是回撥介面的實現:
    public class ChatDuplexCallbackHandler:IChatDuplexCallback
    {
        public ChatDuplexCallbackHandler(ListBox listBox)
        {
            m_listBox = listBox;
        }
        private ListBox m_listBox;       

        public void Reply(string srvMsg)
        {
            m_listBox.Items.Add(string.Format(”Server:{0}”, srvMsg));
        }
    }
由於我自定義了該物件的建構函式,所以在實利化proxy時會有稍微區別:
InstanceContext site = new InstanceContext(new ChatDuplexCallbackHandler(this.lbMessage));
proxy = new ChatDuplexProxy(site);
proxy.Start();

通過proxy物件的Start()方法,使得我們在建立proxy物件開始,就建立了會話,從而使得服務物件被例項化,從而得以執行下面的這行程式碼:
m_callback = OperationContext.Current.GetCallbackChannel(); 

也就是說,在proxy物件建立之後,服務端就已經獲得callback物件了,這樣就可以保證服務端能夠先向客戶端傳送訊息而不會因為callback為null,導致錯誤的發生。

(2)訊息交換的非同步回撥功能——AsyncDuplexWin
例項說明:本例項比較簡單,只是為了驗證當回撥物件被呼叫時,客戶端是否可以被非同步執行。呼叫服務物件時,服務端會進行一個累加運算。在運算未完成之前,客戶端會執行顯示累加數字的任務,當服務端運算結束後,只要客戶端程式的執行緒處於Sleep狀態,該回撥物件就會被呼叫,然後根據使用者選擇是否再繼續執行剩下的任務。本例中服務端為控制檯應用程式,客戶端則為Windows應用程式。

例子中的介面定義非常簡單,不再贅述,而實現類的程式碼如下:
    public class SumDuplex:ISumDuplex
    {
        public SumDuplex()
        {
            callback = OperationContext.Current.GetCallbackChannel();
        }
        private ISumDuplexCallback callback = null;

        #region ISumDuplex Members
        public void Sum(int seed)
        {
            int result = 0;
            for (int i = 1; i < = seed; i++)
            {
                Thread.Sleep(10);
                Console.WriteLine("now at {0}",i);
                result += i;               
            }
            callback.Equals(result);
        }
        #endregion
    }
很顯然,當客戶端呼叫該服務物件時,會在服務端的控制檯上列印出迭代值。

由於客戶端需要在callback呼叫時,停止對當前任務的執行,所以需要用到多執行緒機制:
    public delegate void DoWorkDelegate();
    public partial class ClientForm : Form
    {
        public ClientForm()
        {
            InitializeComponent();
            InstanceContext site = new InstanceContext(new SumDuplexCallbackHandler(this.lbMessage));
            proxy = new SumDuplexProxy(site);           
        }
        private SumDuplexProxy proxy;
        private Thread thread = null;
        private DoWorkDelegate del = null;       
        private int counter = 0;

        private void btnStart_Click(object sender, EventArgs e)
        {
            proxy.Sum(100);
            thread = new Thread(new ThreadStart(delegate()
            {
                while (true)
                {
                    if (ClientUtil.IsCompleted)
                    {
                        if (MessageBox.Show("Game over,Exit?", "Notify", MessageBoxButtons.YesNo,
                            MessageBoxIcon.Question) == DialogResult.Yes)
                        {
                            break;
                        }
                    }
      if (counter > 10000)
                    {
                        break;
                    }
                    if (del != null)
                    {
                        del();
                    }
                    Thread.Sleep(50);
                }
            }
            ));
            del += new DoWorkDelegate(DoWork);
            thread.Start();
        }                  

        private void DoWork()
        {
            if (lbMessage.InvokeRequired)
            {
                this.Invoke(new DoWorkDelegate(DoWork));
            }
            else
            {
                lbMessage.Items.Add(counter);               
                lbMessage.Refresh();
                counter++;
            }
        }
        private void ClientForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (thread != null)
            {
                thread.Abort();
            }
        }
    }

因為需要在多執行緒中對ListBox控制元件的items進行修改,由於該控制元件不是執行緒安全的,所以應使用該控制元件的InvokeRequired屬性。此外,線上程啟動時的匿名方法中,利用while(true)控制當前執行緒的執行,並利用全域性變數ClientUtil.IsCompleted判斷回撥物件是否被呼叫,如果被呼叫了,則會彈出對話方塊,選擇是否退出當前任務。這裡所謂的當前任務實際上就是呼叫DoWork方法,向ListBox控制元件的items中不斷新增累加的counter值。注意客戶端的回撥物件實現如下:
    class SumDuplexCallbackHandler:ISumDuplexCallback
    {
        public SumDuplexCallbackHandler(ListBox listBox)
        {
            m_listBox = listBox;
        }
        private ListBox m_listBox;      
        #region ISumDuplexCallback Members
        public void Equals(int result)
        {
     ClientUtil.IsCompleted = true;
            m_listBox.Items.Add(string.Format(”The result is:{0}”, result));
            m_listBox.Refresh();           
        }     
        #endregion
    }
當客戶端點選Start按鈕,呼叫服務物件的Sum方法後,在服務端會顯示迭代值,而客戶端也開始執行自己的任務,向ListBox控制元件中新增累加數。一旦服務端運算完畢,就將運算結果通過回撥物件傳送到客戶端,全域性變數ClientUtil.IsCompleted將被設定為true。如果新增累加值的執行緒處於sleep狀態,系統就會將結果值新增到ListBox控制元件中,同時會彈出對話方塊,決定是否繼續剩下的任務。

注:本文示例的程式碼和例項均在Feb 2006 CTP版本下執行。

< 未完待續>

參考:
1、David Chappell,Introducing Windows Communication Foundation
2、Microsoft Corporation,WinFX SDK

相關文章