WCF Tips之四
WCF與Web Service不一樣的是,當咱們定義了服務契約的操做時,不論是經過ChannelFactory建立服務代理對象,仍是經過SvcUtil的默認方式生成服務代理對象,客戶端在調用這些代理對象時,都沒法直接實現異步方式的調用。例如,對於以下的服務操做定義:
[OperationContract]
Stream TransferDocument(Document document);
在調用代理對象的方法時,咱們沒法找到對應於TransferDocument()操做的BeginTransferDocument()和EndTransferDocument()異步方法。
這樣的設計使得咱們沒法經過編程方式異步地調用服務的操做,除非咱們在定義服務接口時,直接加入相關操做的異步方法。然而,這又直接致使了服務的設計與方法調用方式之間的耦合。一個好的框架設計要素在於,無論客戶端的調用方式(同步或者異步),服務的設計與實現應該是一致的。對於服務的設計者而言,在設計之初,就不該該去考慮服務的調用者調用的方式。換言之,服務操做到底是否採用異步方式,應該由客戶端的調用者決定。所以,全部與異步調用相關的內容應該只與客戶端相關。WCF遵循了這一規則。
在我編寫的應用程序中,會暴露一個傳送文檔文件的服務操做。我並不知道也並不關心調用該操做的客戶端是否採用異步方式。所以,如上所述的服務操做定義是徹底正確的。
那麼,客戶端究竟應該如何執行異步調用呢?若是採用編程方式得到服務代理對象,這一問題會變得比較糟糕。由於我將服務契約的定義單獨造成了一個程序集,並在客戶端直接引用了它。然而,在這樣的服務契約程序集中,是沒有包含異步方法的定義的。所以,我須要修改在客戶端的服務定義,增長操做的異步方法。這無疑爲服務契約的重用帶來障礙。至少,咱們須要在客戶端維持一份具備異步方法的服務契約。
所幸,在客戶端決定採用異步方式調用我所設計的服務操做時,雖然須要修改客戶端的服務契約接口,但並不會影響服務端的契約定義。所以,服務端的契約定義能夠保持不變,而在客戶端則修改接口定義以下:
[ServiceContract]
public
interface
IDocumentsExplorerService
{
[OperationContract]
Stream TransferDocument(Document document);
[OperationContract(AsyncPattern
=
true
)]
IAsyncResult BeginTransferDocument(Document document,
AsyncCallback callback,
object
asyncState);
Stream EndTransferDocument(IAsyncResult result);
}
注意,在
BeginTransferDocument()方法上,必須在OperationContractAttribute中將AsyncPattern屬性值設置爲true,由於它的默認值爲false。
調用方式以下:
BasicHttpBinding binding
=
new
BasicHttpBinding();
binding.SendTimeout
=
TimeSpan.FromMinutes(
10
);
binding.TransferMode
=
TransferMode.Streamed;
binding.MaxReceivedMessageSize
=
9223372036854775807
;
EndpointAddress address
=
new
EndpointAddress
(
"
http://localhost:8008/DocumentExplorerService
"
);
ChannelFactory
<
IDocumentsExplorerService
>
factory
=
new
ChannelFactory
<
IDocumentsExplorerService
>
(binding,address);
m_service
=
factory.CreateChannel();
……
IAsyncResult result
=
m_service.BeginTransferDocument(doc,
null
,
null
);
result.AsyncWaitHandle.WaitOne();
Stream stream
=
m_service.EndTransferDocument(result);
若是採用SvcUtil生成客戶端代理文件,能夠有更好的方式實現異步,也就是使用SvcUtil的/async開關,例如:
svcutil
/
async http:
//
localhost:8008/DocumentExplorerService
惟一不足的是,它會不分青紅皁白,爲全部服務操做都生成對應的異步方法。這樣的作法未免過於武斷。
合理地利用服務的異步調用,能夠有效地提升系統性能,合理分配任務的執行。特別對於UI應用程序而言,能夠提升UI的響應速度,改善用戶體驗。在我編寫的應用程序中,下載的文件若是很大,就有必要採用異步方式。
對於異步調用的完成,雖然WCF提供了諸如阻塞、
等待
和
輪詢
等機制,但最好的方式仍是使用回調。也就是利用Begin方法參數中的AsyncCallback對象。這是一個委託對象,它的定義以下所示:
public
delegate
void
AsyncCallback(IAsyncResult ar);
利用異步方式執行服務操做,使得服務在執行過程當中不會阻塞主線程,當方法執行完成後,經過AsyncCallback回調對應的方法,能夠通知客戶端服務執行完畢。例如:
//
Invoke it Asynchronously
m_service.BeginTransferDocument(m_doc,OnTransferCompleted,
null
);
//
Do some work;
//
callback method
void
OnTransferCompleted(IAsyncResult result)
{
Stream stream
=
m_service.EndTransferDocument(result);
result.AsyncWaitHandle.Close();
lbMessage.Text
=
string
.Format(
"
The file {0} had been transfered sucessfully.
"
,
m_doc.FileName);
}
在調用
BeginTransferDocument()方法以後,主線程不會被阻塞,仍然能夠繼續執行其它工做。而當服務方法執行完畢以後,會自動調用回調方法,執行方法中的內容。
上述實現存在一個問題,就是對於lbMessage控件的操做。因爲回調方法並不是運行在主線程中,若是回調方法須要更新與異步調用結果相關的界面,例如本例中的lbMessage控件,則須要將回調的調用封送(Marshal)到當前主程序界面的同步上下文中。咱們可使用SynchronizationContext以及它的SendOrPostCallback委託,對調用進行封送:
public
ExplorerClientForm()
{
InitializeComponent();
m_synchronizationContext
=
SynchronizationContext.Current;
}
private
SynchronizationContext m_synchronizationContext;
則回調方法修改成:
//
callback method
void
OnTransferCompleted(IAsyncResult result)
{
Stream stream
=
m_service.EndTransferDocument(result);
result.AsyncWaitHandle.Close();
SendOrPostCallback callback
=
delegate
{
lbMessage.Text
=
string
.Format(
"
The file {0} had been transfered sucessfully.
"
,
m_doc.FileName);
};
m_synchronizationContext.Send(callback,
null
);
}
在調用異步方法時,因爲對Begin
TransferDocument()
和End
TransferDocument()
方法的調用可能會在不一樣的方法體中,於是我將服務代理對象定義爲private字段。若是但願將服務對象定義爲一個局部變量,能夠在調用
Begin
TransferDocument()方法時,將代理對象傳遞到方法的asyncState參數中,而後在調用
End
TransferDocument()方法以前,經過IAsyncResult得到準確的服務代理對象:
m_service.BeginTransferDocument(m_doc,OnTransferCompleted,m_service);
將m_service做爲asyncState對象傳入以後,在調用
End
TransferDocument()方法以前,就能夠根據它先得到服務代理對象:
IDocumentsExplorerService m_service
=
result.AsyncState
as
IDocumentsExplorerService;
Stream stream
=
m_service.EndTransferDocument(result);
//
rest codes