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 事件。

4

7 回答 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");

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

我希望这有帮助。

杰瑞

更新:

杰夫,

请查看此代码和其中的注释,以获取对这里发生的事情的更多解释。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).
    }
}

杰瑞

于 2009-08-02T08:23:58.453 回答
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使用类的实例,即使抛出异常,也保证调用该方法。该方法中的代码将处理代理/通道的所有情况和状态。然后,您可以在此方法中添加错误处理/日志记录/事件委托代码。ProxyWrapperusingDispose()Dispose()

阅读以下博客条目以获取更多信息,以及上述代码的更通用版本:http: //bloggingabout.net/blogs/erwyn/archive/2006/12/09/WCF-Service-Proxy-Helper.aspx

于 2009-08-07T09:43:42.903 回答
0

要捕获与服务器的通信错误,我建议将调用.CreateChannel()放入 try...catch 并在那里处理诸如 CommunicationException 和 TimeoutExceptions 之类的事情。

另外,一般建议的一个要点:创建 ChannelFactory 是一项非常昂贵的操作,因此您可能希望单独执行此操作并将对该 ChannelFactory 对象的引用存储在某处。

如果您需要从 ChannelFactory 创建并可能重新创建通道,则可以一遍又一遍地使用 ChannelFactory 的存储/缓存实例,而不必一直创建它。

但除此之外,我认为你在这里做得很好!

马克

于 2009-07-30T06:05:52.650 回答
0

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

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

这就是为什么您仍然会遇到异常 - 这里没有处理它。

更新:

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

我浏览了 .NET 框架,它确实被允许一路返回线程池(至少在你这里的堆栈跟踪中),这会导致未处理的异常和你的线程死亡。如果情况变得更糟,您可以随时接管线程...启动自己的线程并在内部同步调用服务,而不是使用内置的异步模式(除非这是 WCF 双工类型的通信,我不这样做'不认为是)。

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

于 2009-08-03T16:19:15.903 回答
0

Jeff,当您尝试在 EndGet 调用周围放置一个 try/catch 块时,请向我们展示您的代码。很难相信它不会捕获异常,并且看到它会帮助我相信。

此外,作为本实验的一部分,去掉 UnhandledException 事件处理程序。我认为一旦 BrowserHttpRequest 意识到存在 404,您就会捕获此异常。我认为,如果没有 UnhandledException 事件,围绕 EndGet 的 try/catch 将捕获异常(如果有)。我认为您正在获取异常处理过程的中间状态。

我还希望看到您在 EndGet 调用之后立即放置 System.Debugger.Break 或其他内容。

于 2009-08-05T02:09:21.783 回答
0

我是否认为您已经设计了您的ServiceCaller课程,以便您可以在一个using块中调用它?

如果是,那就是问题所在。WCF 通道类的设计方式使得编写完全通用的处理代码来满足所有可能的错误条件非常困难。如果它很简单,那么通道类本身可以安全地处理,并且您不需要包装类。

迄今为止我发现的唯一安全方法不是封装 dispose 逻辑,而是封装整个调用序列,因此:

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,而是根据在通话失败之前您设法获得的通话距离来提前确定。

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

于 2009-08-06T11:50:16.967 回答
0

我认为问题在于您设计服务调用者的方式。

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

我会将频道的创建和关闭移至 make call 方法。

您可以围绕开始获取和回调的内容进行尝试捕获。

于 2009-08-07T19:29:52.127 回答