Где перехватить неудачное соединение в вызывающем классе WCF?

StackOverflow https://stackoverflow.com/questions/1204534

  •  05-07-2019
  •  | 
  •  

Вопрос

Я пытаюсь написать класс, который инкапсулирует вызовы WCF (клиент - Silverlight, если это имеет значение).Все это работает без сбоев, но я не уверен, как зафиксировать сбой соединения, как будто сервер не отвечает.Казалось бы, какая-то часть работы происходит где-то в результирующем коде из ChannelFactory, но я не уверен.Общий обзор кода также приветствуется.:)

Итог, связанный с созданием канала, или делегат begin или async result в 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.В тестовой среде я получаю то же самое, отключая сетевое соединение для клиентской системы.

Вот ПОЛНОЕ исключение 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)

Кстати, единственный способ, которым я вообще могу это уловить, - это использовать событие UnhandledException на уровне приложения в клиенте SL.

Это было полезно?

Решение

Джефф, из твоего вопроса я не совсем уверен, как ты имитируешь сбой соединения.Я предполагаю, что "как будто сервер не отвечает" означает, что вы ищете какую-то ошибку тайм-аута.Вероятно, вы имитируете сервер, не отвечающий на запросы, вызывая медленный ответ от вашего вызова службы с помощью Thread.Sleep() на стороне сервера.Верно?Достаточно близко?

У меня был похожий проект, поэтому я протестировал это и смог успешно перехватить тайм-аут и другие исключения.Я думаю, возможно, вы не видели этих исключений, потому что ваша пользовательская привязка имела очень длительный тайм-аут по умолчанию, и вы, возможно, недостаточно долго ждали.

Поясню, что тайм-ауты WCF контролируются конфигурацией привязки.Глядя на ваш код, вы создаете свою собственную пользовательскую привязку следующим образом:

var elements = new List<BindingElement>();
elements.Add(new BinaryMessageEncodingBindingElement());
elements.Add(new HttpTransportBindingElement());
var binding = new CustomBinding(elements);

Если вы установите там точку останова и проверите созданную вами пользовательскую привязку, вы увидите, что у нее есть одна минута Время отправки истекло по умолчанию.Я подозреваю, что вы либо недостаточно долго ждали, либо недостаточно долго устанавливали имитируемый тайм-аут вашей службы, поэтому вам не удалось перехватить исключение тайм-аута.

Вот некоторые изменения в коде для демонстрации.Во-первых, чтобы установить тайм-аут для привязки:

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".Вы увидите, что первый проход через отладчик просто объявляет код (функцию / делегат) для выполнения, когда служба отвечает, а второй проход - это фактическая обработка этого ответа, начиная с внутренней части вашего объявления делегата.Это означает, что внешняя попытка / перехват не будет находиться в области видимости при обработке ответа на вызов службы.

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).
    }
}

Джерри

Другие советы

Я сталкивался с подобной проблемой раньше. Основная проблема заключается в том, как 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 , если мы используем экземпляр класса ProxyWrapper внутри using блок, метод Dispose () гарантированно вызывается, даже если выдается исключение. Код в методе Dispose () будет обрабатывать все случаи и состояния прокси / канала. Затем вы можете добавить свой код обработки ошибок / ведения журнала / делегата события в этом методе.

Прочитайте следующую запись в блоге для получения дополнительной информации и более общей версии кода выше: http://bloggingabout.net/blogs/erwyn/archive/2006/12/09/WCF-Service-Proxy-Helper.aspx

Чтобы перехватить ошибки связи с сервером, я бы предложил вызвать .CreateChannel() попробуйте ... перехватить и обработать там такие вещи, как CommunicationException и TimeoutExceptions.

Кроме того, один из самых важных общих советов:создание ChannelFactory - довольно дорогостоящая операция, поэтому вы можете захотеть сделать это отдельно и где-нибудь сохранить эту ссылку на объект ChannelFactory.

Если вам нужно создать и, возможно, воссоздать канал из ChannelFactory, вы можете затем использовать этот сохраненный / кэшированный экземпляр ChannelFactory снова и снова, не создавая его постоянно.

Но в остальном, я бы подумал, что у вас здесь все в порядке!

Марк

Ваш экземпляр ChannelFactory будет находиться в состоянии "Прервано", когда наступит этот тайм-аут.Вы обнаружите, что исключение генерируется не тогда, когда исключение происходит при вызове, а когда вы вызываете _channel .Dispose() .

Вам нужно будет как-то защититься от этого исключения.Самым простым способом было бы поместить try / catch вокруг содержимого вашего метода dispose, но было бы лучше, если бы вы проверили состояние экземпляра _channel, прежде чем пытаться закрыть / dispose.

Вот почему вы все еще получаете исключение - оно здесь не обрабатывается.

Обновить:

Основываясь на вашей трассировке стека, похоже, что фреймворк пытается выполнить ваш делегат обратного вызова и получает там исключение.Вы ответили в другом месте, что попытка / перехват содержимого вашего делегата не решает проблему...а как насчет внутренних исключений?Каков полный результат исключения.toString()?

Я прошелся по .NET Framework, и ему действительно разрешено пройти весь путь до threadpool (по крайней мере, в stacktrace, который у вас есть здесь), что привело бы к необработанному исключению и остановке вашего потока.Если дело дойдет до худшего, вы всегда можете взять на себя управление потоками...запустите свой собственный поток и внутри вызовите службу синхронно, вместо того чтобы использовать встроенный асинхронный шаблон (если только это не дуплексный тип связи WCF, чего я не думаю, что это так).

Я все еще думаю, что полная информация об исключениях (если есть что-то еще) может быть полезна.

Джефф, покажи нам свой код, когда попробуешь поместить блок try / catch вокруг вызова EndGet. Трудно поверить, что не поймать исключение, и вид его поможет мне поверить.

Кроме того, в рамках этого эксперимента избавьтесь от обработчика событий UnhandledException. Я думаю, что вы ловите это исключение, как только BrowserHttpRequest обнаружит, что есть 404. Я думаю, что без события UnhandledException, попытка / перехват вокруг EndGet будет перехватывать исключение, если оно есть. Я думаю, что вы выбираете промежуточное состояние процесса обработки исключений.

Я также хотел бы, чтобы вы поместили System.Debugger.Break или что-то еще сразу после вызова EndGet.

Правильно ли я считаю, что вы разработали свой класс 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();
        }
    }
}

Я знаю, что это синхронный вызов, но принцип тот же. Вместо того, чтобы пытаться определить, следует ли вам вызывать Close или Abort , вы заранее определяете его, основываясь на том, как далеко вы успели пройти вызов, прежде чем он упал.

Примечание. Только конкретный канал нуждается в этой специальной обработке. Вы можете распоряжаться фабрикой так, как вам нравится AFAIK.

Я думаю, что проблема в том, как вы спроектировали службу вызова.

Канал открывается при создании вызывающей стороны. Это означает, что время ожидания канала истекло, и в вашем коде нет ничего, что могло бы восстановиться после указанного времени.

Я бы переместил создание и закрытие канала в метод вызова вызова.

Вы можете попытаться поймать начало и начало содержимого обратного вызова.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top