前一篇關(guān)于如何通過(guò)WCF進(jìn)行 雙向通信的文章在文章中提供了一個(gè)如果在Console Application 調用Duplex WCF Service的Sample。前幾天有留言說(shuō),在沒(méi)有做任何改動(dòng)得情況下,把 作為Client的Console Application 換成Winform Application,運行程序的時(shí)候總是出現Timeout的錯誤。我覺(jué)得這是一個(gè)很好的問(wèn)題,通過(guò)這個(gè)問(wèn)題,我們可以更加深入地理解WCF的消息交換的機制。1.問(wèn)題重現
首先我們來(lái)重現這個(gè)錯誤,在這里我只寫(xiě)WinForm的代碼,其他的內容請參考我的文章。Client端的Proxy Class(DuplexCalculatorClient)的定義沒(méi)有任何變化。我們先來(lái)定義用于執行回調操作(Callback)的類(lèi)——CalculatorCallbackHandler.cs。代碼很簡(jiǎn)單,就是通過(guò)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
}
}
接著(zhù)我們來(lái)設計我們的UI,很簡(jiǎn)單,無(wú)需多說(shuō)。
代碼如下
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);
}
}
}
}
啟動(dòng)Host,然后隨啟動(dòng)Client,在兩個(gè)Textbox中輸入數字2和3,Click Calculate按鈕,隨后整個(gè)UI被鎖住,無(wú)法響應用戶(hù)操作。一分后,出現下面的錯誤。
我們從上面的Screen Shot中可以看到這樣一個(gè)很有意思的現象,運算結果被成功的顯示,顯示,但是有個(gè)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.原因分析
在我開(kāi)始分析為什么會(huì )造成上面的情況之前,我要申明一點(diǎn):由于找不到任何相關(guān)的資料,以下的結論是我從試驗推導出來(lái),我不能保證我的分析是合理的,因為有些細節我自己都還不能自圓其說(shuō),我將在后面提到。我希望有誰(shuí)對此了解的人能夠指出我的問(wèn)題, 我將不勝感激。
我們先來(lái)看看整個(gè)調用過(guò)程的Message Exchange過(guò)程,通過(guò)前面相關(guān)的介紹,我們知道WCF可以采用三種不同的Message Exchange Pattern(MEP)——One-way,Request/Response,Duplex。其實(shí)從本質(zhì)上講,One-way,Request/Response是兩種基本的MEP, Duplex可以看成是這兩種MEP的組合——兩個(gè)One-way,兩個(gè)Request/Response或者是一個(gè)One-way和一個(gè)Request/Response。在定義Service Contract的時(shí)候,如果我們沒(méi)有為某個(gè)Operation顯式指定為One-way (IsOneWay = true), 那么默認采用Request/Response方式。我們現在的Sample就是由兩個(gè)Request/Response MEP組成的Duplex MEP。

從上圖中我們可以很清楚地看出真個(gè)Message Exchange過(guò)程,Client調用Duplex Calculator Service,Message先從Client傳遞到Service,Service執行Add操作,得到運算結果之后,從當前的OperationContext獲得Callback對象,發(fā)送一個(gè)Callback 請求道Client(通過(guò)在Client注冊的Callback Channel:http://localhost:6666/myClient)。但是,由于Client端調用Calculator Service是在主線(xiàn)程中,我們知道一個(gè)UI的程序的主線(xiàn)程一直處于等待的狀態(tài),它是不會(huì )有機會(huì )接收來(lái)自Service端的Callback請求的。但是由于Callback Operation是采用Request/Response方式調用的,所以它必須要收到來(lái)自Client端Reply來(lái)確定操作正常結束。這實(shí)際上形成了一個(gè)Deadlock,可以想象它用過(guò)也不能獲得這個(gè)Reply,所以在一個(gè)設定的時(shí)間內(默認為1分鐘),它會(huì )拋出Timeout 的Exception, Error Message就像下面這個(gè)樣子。
”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:多線(xiàn)程異步調用
既然WinForm的主線(xiàn)程不能接受Service的Callback,那么我們就在另一個(gè)線(xiàn)程調用Calculator Service,在這個(gè)新的線(xiàn)程接受來(lái)自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);
}
}
通過(guò)實(shí)驗證明,這種方式是可行的。
方案2:采用One-way的方式調用Service 和Callback,既然是因為Exception發(fā)生在不同在規定的時(shí)間內不能正常地收到對應的Reply,那種我就 允許你不必收到Reply就好了——實(shí)際上在本例中,對于A(yíng)dd方法,我們根本就不需要有返回結果,我們完全可以使用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的角度講,這種方式實(shí)際上是采用下面一種消息交換模式(MEP):
進(jìn)一步地,由于Callback也沒(méi)有返回值,我們也可以把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成為下面一種方式:
實(shí)現證明這兩種方式也是可行的。
4 .疑問(wèn)
雖然直到現在,所有的現象都說(shuō)得過(guò)去,但是仍然有一個(gè)問(wèn)題不能得到解釋?zhuān)喝绻且驗閃inform的主線(xiàn)程不能正常地接受來(lái)自Service的Callback才導致了Timeout Exception,那為什么Callback操作能過(guò)正常執行呢?而且通過(guò)我的實(shí)驗證明他基本上是在拋出Exception的同時(shí)執行的。(參考第2個(gè)截圖)