天道酬勤

博观而约取,厚积而薄发!
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

几个星期之前写了一篇关于如何通过WCF进行 双向通信的文章([原创]我的WCF之旅(3):在WCF中实现双向通信(Bi-directional Communication) ),在文章中我提供了一个如果在Console Application 调用Duplex WCFService的Sample。前几天有个网友在上面留言说,在没有做任何改动得情况下,把作为Client的Console Application 换成WinformApplication,运行程序的时候总是出现Timeout的错误。我觉得这是一个很好的问题,通过这个问题,我们可以更加深入地理解WCF的消息交换的机制。

1.问题重现

首先我们来重现这个错误,在这里我只写WinForm的代码,其他的内容请参考我的文章。Client端的ProxyClass(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
    }

}

接着我们来设计我们的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被抛出:”Thisrequest operation sent tohttp://localhost:6666/myClient/4f4ebfeb-5c84-45dc-92eb-689d631b337f didnot receive a reply within the configured timeout(00:00:57.7300000). The time allotted to this operation may have been aportion of a longer timeout. This may be because the service is stillprocessing the operation or because the service was unable to send areply message. Please consider increasing the operation timeout (bycasting the channel/proxy to IContextChannel and setting theOperationTimeout property) and ensure that the service is able toconnect to the client.”。

2.原因分析

在我开始分析为什么会造成上面的情况之前,我要申明一点:由于找不到任何相关的资料,以下的结论是我从试验推导出来,我不能保证我的分析是合理的,因为有些细节我自己都还不能自圆其说,我将在后面提到。我希望有谁对此了解的人能够指出我的问题, 我将不胜感激。

我们先来看看整个调用过程的MessageExchange过程,通过前面相关的介绍,我们知道WCF可以采用三种不同的Message ExchangePattern(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组成的DuplexMEP。


从上图中我们可以很清楚地看出真个Message Exchange过程,Client调用Duplex CalculatorService,Message先从Client传递到Service,Service执行Add操作,得到运算结果之后,从当前的OperationContext获得Callback对象,发送一个Callback请求道Client(通过在Client注册的CallbackChannel:http://localhost:6666/myClient)。但是,由于Client端调用CalculatorService是在主线程中,我们知道一个UI的程序的主线程一直处于等待的状态,它是不会有机会接收来自Service端的Callback请求的。但是由于CallbackOperation是采用Request/Response方式调用的,所以它必须要收到来自Client端Reply来确定操作正常结束。这实际上形成了一个Deadlock,可以想象它用过也不能获得这个Reply,所以在一个设定的时间内(默认为1分钟),它会抛出Timeout的Exception, Error Message就像下面这个样子。

”This requestoperation sent tohttp://localhost:6666/myClient/4f4ebfeb-5c84-45dc-92eb-689d631b337f didnot receive a reply within the configured timeout(00:00:57.7300000). The time allotted to this operation may have been aportion of a longer timeout. This may be because the service is stillprocessing the operation or because the service was unable to send areply message. Please consider increasing the operation timeout (bycasting the channel/proxy to IContextChannel and setting theOperationTimeout property) and ensure that the service is able toconnect 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的ServiceContract定义就可以了。

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才导致了TimeoutException,那为什么Callback操作能过正常执行呢?而且通过我的实验证明他基本上是在抛出Exception的同时执行的。(参考第2个截图)