Windows Communication Foundation入門(Part Three)
《Windows Communication Foundation之旅》系列之三
示例代碼下載: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
浙公網安備 33010602011771號