如何在异步ASP.NET Web服务调用中定义客户端超时?

3

今天我花了一些时间搜索我们面临的一个具体案例,该案例要求使用以下要求调用(外部)ASP.NET Web服务:

  • 必须异步进行调用
  • 必须实现超时机制,因为Web服务可能需要执行很长时间

在互联网和StackOverflow上出现了许多关于此主题的问题,但这些问题要么过时,要么建议使用WebRequest.TimeOut属性,而该属性仅适用于同步调用。

其中一个替代方案是使用一个System.Threading.Timer。在开始调用之前启动计时器,并在到达TimerCallback时取消它。

然而,我认为应该有更常见的方法来处理这种情况。可惜到目前为止还没有找到。有人知道如何设置异步Web服务调用的客户端超时吗?

提前致谢。


你能否提供更多关于你如何调用Web服务的细节?这是.NET 2.0还是.NET 4.0应用程序?微软为WCF的新实现提供了全新的System.ServiceModel命名空间。你使用的是哪一个? - Akash Kava
我正在从一个 .net 4 应用程序调用 Web 服务。但是该 Web 服务可能是 .net 2.0 的,超出了我的能力范围... - Herman Cordes
5个回答

8

事实上,您不能总是在异步操作中使用WebRequest.TimeOut;至少不是对于所有抽象WebRequest类的实现者。例如,在msdn文档中记录了当调用HttpWebRequest.BeginGetResponse开始异步操作时,该属性被忽略。明确指出TimeOut属性被忽略,并且如果需要,由用户负责实现超时行为。

在附带HttpWebRequest.BeginGetResponsemsdn文档的示例代码中,使用ManualResestEvent allDoneWaitOrTimerCallback结合使用,如下所示:

IAsyncResult result = (IAsyncResult) myHttpWebRequest.BeginGetResponse(
  new AsyncCallback(RespCallback), myRequestState);

// TimeoutCallback aborts the request if the timer fires.
ThreadPool.RegisterWaitForSingleObject (result.AsyncWaitHandle, 
                                        new WaitOrTimerCallback(TimeoutCallback),
                                        myHttpWebRequest, 
                                        DefaultTimeout, 
                                        true);

// The response came in the allowed time. The work processing will happen in the 
// callback function RespCallback.
allDone.WaitOne();

请参阅msdn上的完整示例
关键是您必须自己实现。

8
请检查您的 app.config 文件,其中应该有一些与服务模型相关的设置,可以配置不同的值。
当我添加新的服务引用时,在我的 app.config 文件中会出现以下内容:
<system.serviceModel>
    <bindings>
        <basicHttpBinding>
            <binding name="HeaderedServiceSoap" 
                     closeTimeout="00:01:00" 
                     openTimeout="00:01:00"
                     receiveTimeout="00:10:00" 
                     sendTimeout="00:01:00" 
                     allowCookies="false"
                     bypassProxyOnLocal="false" 
                     hostNameComparisonMode="StrongWildcard"
                     maxBufferSize="65536" 
                     maxBufferPoolSize="524288" 
                     maxReceivedMessageSize="65536"
                     messageEncoding="Text" 
                     textEncoding="utf-8" 
                     transferMode="Buffered"
                     useDefaultWebProxy="true">
                <readerQuotas maxDepth="32" 
                              maxStringContentLength="8192" 
                              maxArrayLength="16384"
                              maxBytesPerRead="4096" 
                              maxNameTableCharCount="16384" />
                <security mode="None">
                    <transport clientCredentialType="None" 
                               proxyCredentialType="None"
                               realm="" />
                    <message clientCredentialType="UserName" 
                             algorithmSuite="Default" />
                </security>
            </binding>
        </basicHttpBinding>
    </bindings>
    <client>
        <endpoint 
          address="http://localhost/MyService.asmx"
          binding="basicHttpBinding" 
          bindingConfiguration="HeaderedServiceSoap"
          contract="WSTest.HeaderedServiceSoap" 
          name="HeaderedServiceSoap" />
    </client>
</system.serviceModel>

尝试先移除再重新添加引用,确保您的应用程序目标框架为4.0,并且您正在添加服务引用(而不是Web引用)。


我目前没有配置任何内容来调用Web服务。 servicemodel configelement不仅适用于调用wcf服务吗? - Herman Cordes
ServiceModel是建立在WCF之上的,但WCF也支持Soap,你的终端点不一定要是WCF,即使它只是简单的ASMX,WCF仍然可以连接它并为您带来结果。请也查看我的编辑答案。 - Akash Kava
这是最佳解决方案。 - Davita
我认为这是最好的答案。目前正在努力理解和测试它。 - Herman Cordes
你能提供一些关于通信中超时的具体部分的参考资料吗?知道我可以安全地调整哪些部分会很好。 - Kjellski

2
我做了一个小项目来演示如何实现这个功能;这并不像我想象的那么简单,但是,什么事情又是简单的呢?
整个项目包括一个 Web 服务和一个 WPF 客户端,其中有按钮用于调用带有和不带超时的服务http://www.mediafire.com/file/3xj4o16hgzm139a/ASPWebserviceAsyncTimeouts.zip。下面是一些相关片段。我使用了 DispatcherTimer 类来实现超时,代码中描述了它的使用方法;这个对象显然很适合 WPF,并且(应该)可以解决可能遇到的所有同步问题。
注意:可能可以通过 WCF 风格的“服务引用”来完成,但是,我无法找到方法,走了很多弯路。最后,我选择了一个更旧的“Web 引用”(可以通过“添加服务引用...”菜单、选择“高级”按钮,然后选择“添加 Web 引用”来找到)。
我选择使用helper类的原因是为了演示如果你有很多调用,如何处理跟踪所有内容。如果没有这个,迅速混乱。此外,可能有可能得到一个更通用的版本,几乎所有处理都可以在代码中完成,但WCF代码占用了我大部分时间,由于服务引用代码中使用泛型的方式,这种处理方式不适用。我只是快速查看了Web服务代码,看起来更有可能实现,但不幸的是我没有足够的时间去尝试。如果您希望我进一步查看,我会尽力而为。

现在继续!;)

用于执行回调的helper: AsyncCallHelper.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

// contains base classes for webservice calls
using System.ServiceModel; 

// contains the DispatcherTimer class for callback timers
using System.Windows.Threading; 

namespace ASPSandcastleWPFClient
{
    /// <summary>
    /// DispatcherTimer usage info thanks to:
    /// 
    /// Wildermuth, Shawn, "Build More Responsive Apps With The Dispatcher", MSDN Magazine, October 2007
    /// Original URL: http://msdn.microsoft.com/en-us/magazine/cc163328.aspx
    /// Archived at http://www.webcitation.org/605qBiUEC on July 11, 2011.
    /// 
    /// this class is not set up to handle multiple outstanding calls on the same async call;
    /// if you wish to do that, there would need to be some sort of handling for multiple
    /// outstanding calls designed into the helper.
    /// </summary>
    public class AsyncCallHelper
    {
        #region Static Defaults
        private static TimeSpan myDefaultTimeout;
        /// <summary>
        /// default timeout for all instances of the helper; should different timeouts
        /// be required, a member should be created that can override this setting.
        /// 
        /// if this is set to null or a value less than zero, the timout will be set 
        /// to TimeSpan.Zero, and the helper will not provide timeout services to the 
        /// async call.
        /// </summary>
        public static TimeSpan DefaultTimeout
        {
            get
            {
                return myDefaultTimeout;
            }
            set
            {
                if ((value == null) || (value < TimeSpan.Zero))
                    myDefaultTimeout = TimeSpan.Zero;
                else
                    myDefaultTimeout = value;
            }
        }
        #endregion

        /// <summary>
        /// creates an instance of the helper to assist in timing out on an async call
        /// </summary>
        /// <param name="AsyncCall">the call which is represented by this instance. may not be null.</param>
        /// <param name="FailureAction">an action to take, if any, upon the failure of the call. may be null.</param>
        public AsyncCallHelper(Action AsyncCall, Action FailureAction)
        {
            myAsyncCall = AsyncCall;
            myFailureAction = FailureAction;

            myTimer = new DispatcherTimer();
            myTimer.Interval = DefaultTimeout;
            myTimer.Tick += new EventHandler(myTimer_Tick);
        }

        /// <summary>
        /// Make the call
        /// </summary>
        public void BeginAsyncCall()
        {
            myAsyncCall();

            if (DefaultTimeout > TimeSpan.Zero)
            {
                myTimer.Interval = DefaultTimeout;
                myTimer.Start();
            }
        }

        /// <summary>
        /// The client should call this upon receiving a response from the
        /// async call.  According to the reference given above, it seems that 
        /// the WPF will only be calling this on the same thread as the UI, 
        /// so there should be no real synchronization issues here.  
        /// 
        /// In a true multi-threading situation, it would be necessary to use
        /// some sort of thread synchronization, such as lock() statements
        /// or a Mutex in order to prevent the condition where the call completes
        /// successfully, but the timer fires prior to calling "CallComplete"
        /// thus firing the FailureAction after the success of the call.
        /// </summary>
        public void CallComplete()
        {
            if ((DefaultTimeout != TimeSpan.Zero) && myTimer.IsEnabled)
                myTimer.Stop();
        }

        private void myTimer_Tick(object sender, EventArgs e)
        {
            CallComplete();

            if (myFailureAction != null)
                myFailureAction();
        }

        /// <summary>
        /// WPF-friendly timer for use in aborting "Async" Webservice calls
        /// </summary>
        private DispatcherTimer myTimer;

        /// <summary>
        /// The call to be made
        /// </summary>
        private Action myAsyncCall;

        /// <summary>
        /// What action the helper should take upon a failure
        /// </summary>
        private Action myFailureAction;
    }

}

具有相关代码的MainWindow.xaml.cs文件:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using ASPSandcastleWPFClient.ASPSandcastleWebserviceClient;

namespace ASPSandcastleWPFClient
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private ASPSandcastleWebservice myClient = null;
        private AsyncCallHelper myHelloWorldHelper = null;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void InitializeClient()
        {
            myClient = new ASPSandcastleWebservice();
            myHelloWorldHelper = 
                new AsyncCallHelper
                    (
                        myClient.HelloWorldAsync,
                        HelloWorldTimeout
                    );
        }

        private void Window_Initialized(object sender, EventArgs e)
        {
            InitializeClient();
        }

        /// <summary>
        /// this is called prior to making a call so that we do not end up with multiple
        /// outstanding async calls
        /// </summary>
        private void DisableButtons()
        {
            btnStartAsyncCall.IsEnabled = false;
            btnStartAsyncCallWithTimeout.IsEnabled = false;
        }

        /// <summary>
        /// this is called after a result is received or the call is cancelled due to timeout
        /// so that we know it's safe to make another call.
        /// </summary>
        private void EnableButtons()
        {
            btnStartAsyncCall.IsEnabled = true;
            btnStartAsyncCallWithTimeout.IsEnabled = true;
        }

        private void btnStartAsyncCall_Click(object sender, RoutedEventArgs e)
        {
            DisableButtons();

            // disable the timeout handling
            AsyncCallHelper.DefaultTimeout = TimeSpan.Zero;

            myClient.HelloWorldCompleted += new HelloWorldCompletedEventHandler(myClient_HelloWorldCompleted);

            myHelloWorldHelper.BeginAsyncCall();
            lblResponse.Content = "waiting...";
        }

        private void btnStartAsyncCallWithTimeout_Click(object sender, RoutedEventArgs e)
        {
            DisableButtons();

            // enable the timeout handling
            AsyncCallHelper.DefaultTimeout = TimeSpan.FromSeconds(10);
            lblResponse.Content = "waiting for 10 seconds...";
            myHelloWorldHelper.BeginAsyncCall();
        }

        /// <summary>
        /// see note RE: possible multi-thread issues when not using WPF in AsyncCallHelper.cs
        /// </summary>
        private void HelloWorldTimeout()
        {
            myClient.CancelAsync(null);
            lblResponse.Content = "call timed out...";
            EnableButtons();
        }

        void myClient_HelloWorldCompleted(object sender, HelloWorldCompletedEventArgs e)
        {
            myHelloWorldHelper.CallComplete();

            if (!e.Cancelled)
                lblResponse.Content = e.Result;

            EnableButtons();
        }
    }
}

哦,感谢斯特凡诺在上面留言提到DispatchTimer...我刚刚才注意到。 :) - shelleybutterfly
你创建的示例非常好!我已经下载并查看了它,非常有用。然而,对于我的特定情况,Akash Kava的答案最符合我的需求,所以我给了他荣誉。感谢您查看它! - Herman Cordes

1

我不知道这是否习惯用语,但当我通过 WebClient.DownloadStringAsync(...) 发布异步请求时,我也使用来自 Silverlight 的计时器(DispatchTimer)。


0
这个 Web 服务返回什么?XML、JSON 还是其他格式?你是用它来做网站吗?如果是的话,为什么不尝试使用 jQuery 的 AJAX 调用呢?这样你就可以异步加载并且可以使用 .ajax() 指定超时时间。

网站的建议很好。不过,我正在使用WPF桌面应用程序。 - Herman Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接