在WCF调用类中,如何捕获连接失败?

5
我正在尝试编写一个类来封装WCF调用(客户端是Silverlight,如果有影响)。一切都很顺利,但我不知道如何捕获连接失败的情况,就像服务器不会响应一样。似乎这部分工作发生在ChannelFactory的结果代码中,但我不确定。欢迎进行一般性的代码审查。:)
总之,在创建通道、开始异步结果委托或异步结果委托周围使用try/catch块无法捕获连接失败。我希望可以通过该catch块运行ServiceCallError事件。
public class ServiceCaller : IDisposable
{
    private IFeedService _channel;

    public ServiceCaller()
    {
        var elements = new List<BindingElement>();
        elements.Add(new BinaryMessageEncodingBindingElement());
        elements.Add(new HttpTransportBindingElement());
        var binding = new CustomBinding(elements);
        var endpointAddress = new EndpointAddress(App.GetRootUrl() + "Feed.svc");
        _channel = new ChannelFactory<IFeedService>(binding, endpointAddress).CreateChannel();
    }

    public void MakeCall(DateTime lastTime, Dictionary<string, string> context)
    {
        AsyncCallback asyncCallBack = delegate(IAsyncResult result)
        {
            var items = ((IFeedService)result.AsyncState).EndGet(result);
            if (ItemsRetrieved != null)
                ItemsRetrieved(this, new ServiceCallerEventArgs(items));
        };
        _channel.BeginGet(lastTime, context, asyncCallBack, _channel);
    }

    public event ItemsRetrievedEventHandler ItemsRetrieved;
    public event ServiceCallErrorHandler ServiceCallError;

    public delegate void ItemsRetrievedEventHandler(object sender, ServiceCallerEventArgs e);

    public delegate void ServiceCallErrorHandler(object sender, ServiceCallErrorEventArgs e);

    public void Dispose()
    {
        _channel.Close();
        _channel.Dispose();
    }
}

以下是堆栈跟踪,供好奇的人参考:

 An AsyncCallback threw an exception.
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously, Exception exception)
   at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.OnGetResponse(IAsyncResult result)
   at System.Net.Browser.BrowserHttpWebRequest.<>c__DisplayClassd.<InvokeGetResponseCallback>b__b(Object state2)
   at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(_ThreadPoolWaitCallback tpWaitCallBack)
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)

为了实现这一点,我在浏览器中启动应用程序,然后从Visual Studio中终止Web服务器进程。在测试环境中,我可以通过关闭客户端系统的网络连接来获得相同的结果。
以下是完整异常的ToString():
System.Exception: An AsyncCallback threw an exception. ---> System.Exception: An AsyncCallback threw an exception. ---> System.ServiceModel.CommunicationException: The remote server returned an error: NotFound. ---> System.Net.WebException: The remote server returned an error: NotFound. ---> System.Net.WebException: The remote server returned an error: NotFound.
   at System.Net.Browser.BrowserHttpWebRequest.InternalEndGetResponse(IAsyncResult asyncResult)
   at System.Net.Browser.BrowserHttpWebRequest.<>c__DisplayClass5.<EndGetResponse>b__4(Object sendState)
   at System.Net.Browser.AsyncHelper.<>c__DisplayClass2.<BeginOnUI>b__0(Object sendState)
   --- End of inner exception stack trace ---
   at System.Net.Browser.AsyncHelper.BeginOnUI(SendOrPostCallback beginMethod, Object state)
   at System.Net.Browser.BrowserHttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
   at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.CompleteGetResponse(IAsyncResult result)
   --- End of inner exception stack trace ---
   at System.ServiceModel.Channels.Remoting.RealProxy.Invoke(Object[] args)
   at proxy_2.EndGet(IAsyncResult )
   at CoasterBuzz.Feed.Client.ServiceCaller.<MakeCall>b__0(IAsyncResult result)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   --- End of inner exception stack trace ---
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously, Exception exception)
   at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.CallComplete(Boolean completedSynchronously, Exception exception)
   at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.FinishSend(IAsyncResult result, Boolean completedSynchronously)
   at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.SendCallback(IAsyncResult result)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   --- End of inner exception stack trace ---
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously, Exception exception)
   at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.OnGetResponse(IAsyncResult result)
   at System.Net.Browser.BrowserHttpWebRequest.<>c__DisplayClassd.<InvokeGetResponseCallback>b__b(Object state2)
   at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(_ThreadPoolWaitCallback tpWaitCallBack)
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)

顺便说一下,我能做到的唯一方法是在SL客户端中使用应用程序级别的UnhandledException事件来捕获这个问题。


还有其他内部异常吗?完整的exception.ToString()输出将确保没有其他异常。我问的原因是它说一个AsyncCallback抛出了一个异常,但是它抛出了什么异常?它只在致命异常时这样做,并且它将包括原始异常(我仍然没有在这里看到它)。 - Anderson Imes
只是为了明确,我在你发布内部异常后发表了这个评论...这看起来仍然不够。 - Anderson Imes
7个回答

8
杰夫,我并不确定你在模拟连接故障方面的具体方式。我假设“仿佛服务器无法响应”是指你正在寻找某种超时错误。你可能通过在服务器端使用Thread.Sleep()强制减慢服务调用的响应速度来模拟无响应的服务器,对吧?大致正确吗?
我曾经有一个类似的项目,所以我进行了测试,并成功地捕获了超时和其他异常。我认为你可能没有看到这些异常是因为你的自定义绑定具有非常长的默认超时时间,而你可能没有等待足够长的时间。
简要解释一下,WCF超时受绑定配置控制。从你的代码来看,你是这样创建自定义绑定的:
var elements = new List<BindingElement>();
elements.Add(new BinaryMessageEncodingBindingElement());
elements.Add(new HttpTransportBindingElement());
var binding = new CustomBinding(elements);

如果您在那里设置一个断点并检查您创建的自定义绑定,您将看到它默认具有一分钟的SendTimeout。我怀疑您要么没有等待足够长的时间,要么没有将服务的模拟超时时间设置得足够长,因此无法捕获超时异常。
以下是一些代码更改示例。首先,要在绑定上设置超时时间:
var binding = new CustomBinding(elements);
//Set the timeout to something reasonable for your service
//This will fail very quickly
binding.SendTimeout = TimeSpan.FromSeconds(1);

最后,为了捕获异常并引发正确的事件,您可以按照以下方式更新AsyncCallback委托。当调用EndGet()时会抛出异常。

AsyncCallback asyncCallBack = delegate(IAsyncResult result)
{
    IFeedService service = ((IFeedService)result.AsyncState);
    try
    {
        var items = service.EndGet(result);
        ItemsRetrieved(this, EventArgs.Empty);
    }
    catch (System.TimeoutException ex)
    {
        //Handles timeout
        ServiceCallError(this, EventArgs.Empty);
    }
    catch (System.ServiceModel.CommunicationException ex)
    {
        //Handles a number of failures:
        //  Lack of cross-domain policy on target site
        //  Exception thrown by a service
        ServiceCallError(this, EventArgs.Empty);
    }
    catch (System.Exception ex)
    {
        //Handles any other errors here
        ServiceCallError(this, EventArgs.Empty);
    }
};

就一般的代码审查而言,我建议您避免硬编码绑定配置。相反,您可以在ServiceReferences.ClientConfig文件中声明终结点配置(包括合理的超时时间),并使用终结点名称来创建新的通道工厂,例如:

new ChannelFactory<IFeedService>("feedServiceEndpoint");

这将使应用程序随着时间的推移更易于维护。

我希望这可以帮到你。

Jerry

更新:

Jeff,

请查看此代码及其中的注释以了解此处发生的情况。MakeCall()方法正在创建一个传递给BeginGet()方法的函数(委托)。该函数稍后在服务响应时执行。

请进行建议的更改,并在以“AsyncCallback asyncCallback”和“var items”开头的行处设置断点。您将看到第一次通过调试器仅声明要在服务响应时执行的代码(函数/委托),而第二次通过是实际处理该响应,从您的委托声明的内部开始。这意味着当处理服务调用的响应时,外部try / catch将不在范围内。

public void MakeCall(DateTime lastTime, Dictionary<string, string> context)
{
    try
    {
        AsyncCallback asyncCallBack = delegate(IAsyncResult result)
        {
            try
            {
                var items = ((IFeedService)result.AsyncState).EndGet(result);
                if (ItemsRetrieved != null)
                    ItemsRetrieved(this, new ServiceCallerEventArgs(items));
            }
            catch (Exception ex)
            { 
                //This will catch errors from the service call
            }
        };
        _channel.BeginGet(lastTime, context, asyncCallBack, _channel);
    }
    catch(Exception ex)
    {
        //This will not catch an error coming back from the service.  It will 
        //catch only errors in the act of calling the service asynchronously.

        //The communication with the service and response is handled on a different
        //thread, so this try/catch will be out of scope when that executes.

        //So, at this point in the execution, you have declared a delegate 
        //(actually an anonymous delegate the compiler will turn into a hidden class) 
        //which describes some code that will be executed when the service responds.

        //You can see this in action by setting a breakpoint just inside the delegate
        //at the line starting with "var items".  It will not be hit until the service
        // responds (or doesn't respond in a timeout situation).
    }
}

杰瑞


不,正如我之前提到的,我尝试在所有接触点中捕获错误,包括通道的创建,开始呼叫和结束呼叫。这种捕获错误不起作用是有道理的,因为开始和结束发生在它们自己的线程中,对吧?那些代码,在框架中看起来非常黑盒子,就是出问题的地方。 - Jeff Putz
2
堆栈跟踪表明异常是由异步回调引发的。这可能是在您声明的委托中,在EndGet()方法调用处抛出。请再次尝试在AsyncCallback的内容周围(内部而不是外部)放置try/catch块。也许您已经尝试过了,但请发布该代码以确认。如果在EndGet()调用处没有获取异常,请在以“var items”开头的行上设置断点。然后添加此监视:((System.ServiceModel.AsyncResult)(result)).exception这不应该为null。 - Jerry Bullard
之前已经尝试过了。正如我最初所说的那样,我已经在这个类的每个单独点上尝试过捕获它,但是没有一个能够捕获到异常。我唯一能够捕获到它的地方是在SL的应用程序级别的UnhandledException事件中。 - Jeff Putz
请在代码中添加我建议的try/catch,并在以下代码行上设置断点:var items = ((IFeedService)result.AsyncState).EndGet(result);在此行执行之前,请告诉我们以下值:((System.ServiceModel.AsyncResult)(result)).exception - Jerry Bullard

4

我之前遇到过类似的问题。主要问题在于IDisposable在代理/通道实例上的实现方式。我解决的方法如下所示,其中IDirector是我的服务契约:

public class ProxyWrapper : IDisposable
{
    private IDirector proxy;
    private ChannelFactory<IDirector> factory;
    int callCount = 0;

    public ProxyWrapper()
    {
        factory = new ChannelFactory<IDirector>();

        proxy = factory.CreateChannel();
    }

    public IDirector Proxy
    {
        get
        {
            if (callCount > 0)
                throw new InvalidOperationException("The proxy can only be accessed once for every ProxyWrapper instance. You must create a new ProxyWrapper instance for each service request.");
            // only allow the proxy/channel to be used for one call.

            callCount++;
            return proxy;
        }
    }

    public void Dispose()
    {
        IClientChannel channel = (IClientChannel)proxy;

        try
        {
            if (channel.State != CommunicationState.Faulted)
            {
                channel.Close();
            }
            else
            {
                channel.Abort();
            }
        }
        catch (CommunicationException)
        {
            channel.Abort();
        }
        catch (TimeoutException)
        {
            channel.Abort();
        }
        catch (Exception)
        {
            channel.Abort();
            throw;
        }
        finally
        {
            channel = null;
            proxy = null;
        }
    }
}

使用上述类的方法如下:
    public static void Login(string userName, string password)
    {
        using (ProxyWrapper wrapper = new ProxyWrapper())
        {
            currentSession = wrapper.Proxy.Login(userName, password);
        }
    }

由于ProxyWrapper类实现了IDisposable接口,如果我们在using块内使用ProxyWrapper类的实例,即使抛出异常,Dispose()方法也是被保证调用的。在Dispose()方法中的代码将处理代理/通道的所有情况和状态。然后,您可以在此方法中添加错误处理/日志记录/事件委托代码。
阅读以下博客文章获取更多信息,并获取上面代码的更通用版本:http://bloggingabout.net/blogs/erwyn/archive/2006/12/09/WCF-Service-Proxy-Helper.aspx

0

我想问一下,您是否设计了ServiceCaller类,以便可以在using块中调用它?

如果是的话,那就有问题了。WCF通道类的设计使得编写完全通用的处理所有可能错误条件的释放代码变得极其困难。如果很简单,那么通道类本身就是安全的可释放对象,您也不需要包装器类。

到目前为止,我发现唯一安全的方法是封装整个调用序列,而不是仅封装释放逻辑,如下所示:

public static void Invoke<TContract>(ChannelFactory<TContract> factory, Action<TContract> action) where TContract : class {

    var proxy = (IClientChannel) factory.CreateChannel();
    bool success = false;
    try {
        action((TContract) proxy);
        proxy.Close();
        success = true;
    } finally {
        if(!success) {
            proxy.Abort();
        }
    }
}

我知道这是一个同步调用,但原理是相同的。你不需要试图推断是否应该调用CloseAbort,而是在调用失败之前根据进展情况提前确定。

注意 - 只有实际通道需要这种特殊处理。工厂可以按照您喜欢的任何方式处理。


在桌面UI环境中进行同步调用并不是一个好主意。 - Jeff Putz
这个决定取决于你,我只是试图说明如何跟踪处理通道的确切方法。对于您的异步类,您可以添加一个包含已初始化、已打开、已关闭等状态的类级别状态字段。然后,根据状态字段的值,您的Dispose方法将调用Abort、Close或什么也不调用。 - Christian Hayter
1
我同意你关于在桌面UI环境中使用同步调用是一个坏主意的说法,但你并不一定要使用SVCUTIL提供的异步模式。你可以轻松地从后台线程(使用BackgroundWorker或类似方法)进行同步调用。这将避免绑定UI线程,并仍然能够让你得到你想要的——优雅地捕获这些异常的能力。 - Anderson Imes

0
为了捕获与服务器的通信错误,我建议将对.CreateChannel()的调用放入try...catch中,并在那里处理CommunicationException和TimeoutExceptions等问题。
另外,一个普遍的建议是:创建ChannelFactory是非常昂贵的操作,因此您可能希望将其单独创建并将对ChannelFactory对象的引用存储在某个地方。
如果您需要从ChannelFactory创建和可能重新创建通道,则可以一次又一次地使用存储/缓存的ChannelFactory实例,而无需每次都创建它。
但除此之外,我认为您在这里做得非常好!
马克

正如我所提到的,那并不起作用。当我断开服务器时,客户端会响应“一个AsyncCallback引发了异常”,而堆栈跟踪中不包括我的任何代码部分。 - Jeff Putz
如果您将代码放入异步回调方法中(我看到您正在使用匿名委托),并将其放入try...catch块中,会发生什么?通常,在异步操作中,如果服务器抛出异常,则在调用.EndXXXXX()方法时会在客户端收到此异常。 - marc_s
不行... 仍然无法捕获异常。很奇怪,是吧? - Jeff Putz

0

当超时发生时,您的ChannelFactory实例将处于“已中止”状态。您会发现异常不是在调用中发生时抛出的,而是在调用_channel.Dispose()时抛出的。

您需要以某种方式防范此异常。最简单的方法是在dispose方法的内容周围放置try/catch,但最好在尝试关闭/处理之前检查_channel实例的状态。

这就是为什么您仍然会收到异常的原因-它在这里没有被处理。

更新:

根据您的堆栈跟踪,看起来框架正在尝试执行回调委托,并在那里遇到异常。您在其他地方回应说尝试捕获委托内容并不能解决问题...内部异常呢?TheException.ToString()的完整输出是什么?

我浏览了一下 .NET Framework,确实允许返回到线程池(至少在你提供的堆栈跟踪中是这样),这将导致未处理的异常和线程死亡。如果情况变得更糟,你总可以接管线程...启动自己的线程,并在其中同步调用服务,而不是使用内置的异步模式(除非这是一种 WCF 双工通信,我认为不是)。

我仍然认为完整的异常信息(如果有更多信息)可能会有用。


没有...没有成功。就像我说的,堆栈跟踪中没有任何与我的代码相关的内容。 - Jeff Putz
它不会显示您的堆栈跟踪。尝试在 _channel.Dispose() 和 _channel.Close() 周围放置 try/catch,看看会发生什么。 - Anderson Imes
就如我所说的(没有结果),在那里添加 try/catch 并不起作用。我编辑了上面的原始内容以显示跟踪信息。 - Jeff Putz
不用在意,我看到你已经尝试过那个了... 我已更新我的答案,请求输出TheException.ToString()的完整结果(包括所有内部异常)。 - Anderson Imes
主体内容中添加了内部异常。 - Jeff Putz

0

Jeff,请在尝试在EndGet调用周围放置try/catch块时展示你的代码。很难相信那不会捕获异常,看到它将帮助我相信。

此外,作为这个实验的一部分,摆脱UnhandledException事件处理程序。我认为你在BrowserHttpRequest意识到有一个404时就捕获了这个异常。我认为没有UnhandledException事件,如果有异常,在EndGet周围放置try/catch将捕获该异常。我认为你正在捕获异常处理过程的中间状态。

我还想看到你在EndGet调用之后立即放置System.Debugger.Break或其他东西。


0

我认为问题出在您设计服务调用程序的方式上。

当创建服务调用程序时,通道会打开。这意味着通道可能会超时,而您的代码中没有任何可以从超时中恢复的内容。

我建议将通道的创建和关闭移动到“make call”方法中。

您可以在“begin get”周围以及回调内容周围使用try catch语句。


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