我的WCF之旅(6):在Winform Application中調用Duplex Service出現TimeoutException原因和解決
幾個星期之前寫了一篇關於如何通過WCF進行 雙向通信的文章([原創]我的WCF之旅(3):在WCF中實現雙向通信(Bi-directional Communication) ),在文章中我提供了一個如果在Console Application 調用Duplex WCF Service的Sample。前幾天有個網友在上面留言說,在沒有做任何改動得情況下,把 作為Client的ConsoleApplication 換成Winform Application,運行程序的時候總是出現Timeout的錯誤。我覺得這是一個很好的問題,通過這個問題,我們可以更加深入地理解WCF的消息交換的機制。
1.問題重現
首先我們來重現這個錯誤,在這裡我只寫WinForm的代碼,其他的內容請參考我的文章。Client端的Proxy Class(DuplexCalculatorClient)的定義沒有任何變化。我們先來定義用於執行回調操作(Callback)的類——CalculatorCallbackHandler.cs。代碼很簡單,就是通過Message Box的方式顯示運算的結果。
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using Artech.DuplexWCFService.Contract;
using System.ServiceModel;
namespace Artech. WCFService.Client
{
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)]
public class CalculatorCallbackHandler : ICalculatorCallback
{
ICalculatorCallback Members#region ICalculatorCallback Members
public void ShowResult(double x, double y, double result)
{
MessageBox.Show(string.Format("x + y = {2} where x = {0} and {1}", x, y, result),"Result", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
#endregion
}
}
接著我們來設計我們的UI,很簡單,無需多說。
代碼如下
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace Artech. WCFService.Client
{
public partial class Form1 : Form
{
private DuplexCalculatorClient _calculator;
private double _op1;
private double _op2;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
this._calculator = new DuplexCalculatorClient(new System.ServiceModel.InstanceContext(new CalculatorCallbackHandler()));
}
private void Calculate()
{
this._calculator.Add(this._op1, this._op2);
}
private void buttonCalculate_Click(object sender, EventArgs e)
{
if (!double.TryParse(this.textBoxOp1.Text.Trim(), out this._op1))
{
MessageBox.Show("Please enter a valid number","Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
this.textBoxOp1.Focus();
}
if (!double.TryParse(this.textBoxOp2.Text.Trim(), out this._op2))
{
MessageBox.Show("Please enter a valid number","Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
this.textBoxOp1.Focus();
}
try
{
this.Calculate();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
啟動Host,然後隨啟動Client,在兩個Textbox中輸入數字2和3,Click Calculate按鈕,隨後整個UI被鎖住,無法響應用戶操作。一分後,出現下面的錯誤。
我們從上面的Screen Shot中可以看到這樣一個很有意思的現象,運算結果被成功的顯示,顯示,但是有個Exception被拋出:”This request operation sent to http://localhost:6666/myClient/4f4ebfeb-5c84-45dc-92eb-689d631b337f did not receive a reply within the configured timeout (00:00:57.7300000).The time allotted to this operation may have been a portion of a longer timeout.This may be because the service is still processing the operation or because the service was unable to send a reply message.Please consider increasing the operation timeout (by casting the channel/proxy to IContextChannel and setting the OperationTimeout property) and ensure that the service is able to connect to the client.”。
2.原因分析
在我開始分析為什麼會造成上面的情況之前,我要申明一點:由於找不到任何相關的資料,以下的結論是我從試驗推導出來,我不能保證我的分析是合理的,因為有些細節我自己都還不能自圓其說,我將在後面提到。我希望有誰對此了解的人能夠指出我的問題, 我將不勝感激。
我們先來看看整個調用過程的Message Exchange過程,通過前面相關的介紹,我們知道WCF可以采用三種不同的Message Exchange Pattern(MEP)——One-way,Request/Response,Duplex。其實從本質上講,One-way,Request/Response是兩種基本的MEP, Duplex可以看成是這兩種MEP的組合——兩個One-way,兩個Request/Response或者是一個One-way和一個Request/Response。在定義Service Contract的時候,如果我們沒有為某個Operation顯式指定為One-way (IsOneWay = true), 那麼默認采用Request/Response方式。我們現在的Sample就是由兩個Request/Response MEP組成的Duplex MEP。
從上圖中我們可以很清楚地看出真個Message Exchange過程,Client調用Duplex Calculator Service,Message先從Client傳遞到Service,Service執行Add操作,得到運算結果之後,從當前的OperationContext獲得Callback對象,發送一個Callback 請求道Client(通過在Client注冊的Callback Channel:http://localhost:6666/myClient)。但是,由於Client端調用Calculator Service是在主線程中,我們知道一個UI的程序的主線程一直處於等待的狀態,它是不會有機會接收來自Service端的Callback請求的。但是由於Callback Operation是采用Request/Response方式調用的,所以它必須要收到來自Client端Reply來確定操作正常結束。這實際上形成了一個Deadlock,可以想象它用過也不能獲得這個Reply,所以在一個設定的時間內(默認為1分鐘),它會拋出Timeout 的Exception, Error Message就像下面這個樣子。
”This request operation sent to http://localhost:6666/myClient/4f4ebfeb-5c84-45dc-92eb-689d631b337f did not receive a reply within the configured timeout (00:00:57.7300000).The time allotted to this operation may have been a portion of a longer timeout.This may be because the service is still processing the operation or because the service was unable to send a reply message.Please consider increasing the operation timeout (by casting the channel/proxy to IContextChannel and setting the OperationTimeout property) and ensure that the service is able to connect to the client.”。
3.解決方案
方案1:多線程異步調用
既然WinForm的主線程不能接受Service的Callback,那麼我們就在另一個線程調用Calculator Service,在這個新的線程接受來自Service的Callback。
於是我們改變Client的代碼:
private void buttonCalculate_Click(object sender, EventArgs e)
{
if (!double.TryParse(this.textBoxOp1.Text.Trim(), out this._op1))
{
MessageBox.Show("Please enter a valid number","Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
this.textBoxOp1.Focus();
}
if (!double.TryParse(this.textBoxOp2.Text.Trim(), out this._op2))
{
MessageBox.Show("Please enter a valid number","Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
this.textBoxOp1.Focus();
}
try
{
Thread newThread = new Thread(new ThreadStart(this.Calculate));
newThread.Start();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
通過實驗證明,這種方式是可行的。
方案2:采用One-way的方式調用Service 和Callback,既然是因為Exception發生在不同在規定的時間內不能正常地收到對應的Reply,那種我就 允許你不必收到Reply就好了——實際上在本例中,對於Add方法,我們根本就不需要有返回結果,我們完全可以使用One-way的方式調用Operation。在這種情況下,我們只需要改變DuplexCalculator和CalculatorCallback的Service Contract定義就可以了。
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
namespace Artech.DuplexWCFService.Contract
{
[ServiceContract(CallbackContract = typeof(ICalculatorCallback))]
public interface IDuplexCalculator
{
[OperationContract(IsOneWay =true)]
void Add(double x, double y);
}
}
從Message Exchange的角度講,這種方式實際上是采用下面一種消息交換模式(MEP):
進一步地,由於Callback也沒有返回值,我們也可以把Callback操作也標記為One-way.
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
namespace Artech.DuplexWCFService.Contract
{
//[ServiceContract]
public interface ICalculatorCallback
{
[OperationContract(IsOneWay = true)]
void ShowResult(double x, double y, double result);
}
}
那麼現在的Message Exchange成為下面一種方式:
實現證明這兩種方式也是可行的。
4 .疑問
雖然直到現在,所有的現象都說得過去,但是仍然有一個問題不能得到解釋:如果是因為Winform的主線程不能正常地接受來自Service的Callback才導致了Timeout Exception,那為什麼Callback操作能過正常執行呢?而且通過我的實驗證明他基本上是在拋出Exception的同時執行的。(參考第2個截圖)