前一篇关于如何通过WCF进行 双向通信的文章在文章中提供了一个如果在Console Application 调用Duplex WCF Service的Sample。前几天有留言说,在没有做任何改动得情况下,把 作为Client的Console Application 换成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被锁住,无法响应用户操作。一分后,出现下面的错误。
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。
”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;
从Message Exchange的角度讲,这种方式实际上是采用下面一种消息交换模式(MEP):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); } } 进一步地,由于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成为下面一种方式: 虽然直到现在,所有的现象都说得过去,但是仍然有一个问题不能得到解释:如果是因为Winform的主线程不能正常地接受来自Service的Callback才导致了Timeout Exception,那为什么Callback操作能过正常执行呢?而且通过我的实验证明他基本上是在抛出Exception的同时执行的。(参考第2个截图) |
|
来自: waywin > 《Technology》