示例程式碼下載: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/”>
</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