首页 > 解决方案 > 如何在不崩溃应用程序的情况下通过 WCF 双工服务发送消息?

问题描述

我有一个 WCF 服务,用于在Admin PC多个 Client PC之间进行通信。

在这里,WCF 服务将实时托管,而管理员 PC 和客户端 PC 使用 WPF 中构建的应用程序。WCF 作为双工服务处理事件和向其他用户广播事件。

就像管理员将消息发送到服务一样,它将被广播给所有客户端,而当客户端向服务发送消息时,它也会广播给所有其他用户。

消息发送服务时,它会使应用程序崩溃。在这里,我把我的代码放在下面,所以请检查它并建议我解决这个问题。

WCF 服务代码

IBroadcastorService1界面:

[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = 
typeof(IBroadcastorCallBack1))]
public interface IBroadcastorService1
{
    [OperationContract(IsOneWay = true)]
    void RegisterClient(string clientName);

    [OperationContract(IsOneWay = true)]
    void NotifyServer(EventDataType eventData);
}
public interface IBroadcastorCallBack1
{
    [OperationContract(IsOneWay = true)]
    void BroadcastToClient(EventDataType eventData);
}
[DataContract]
public class EventDataType
{
    [DataMember]
    public string ClientName { get; set; }

    [DataMember]
    public string EventMessage { get; set; }
}

BroadcastorService.svc.cs包含以下代码:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, 
                 ConcurrencyMode = ConcurrencyMode.Multiple)]
public class BroadcastorService : IBroadcastorService1
{
    private static Dictionary<string, IBroadcastorCallBack1> clients = new Dictionary<string, IBroadcastorCallBack1>();
    private static object locker = new object();

    public void RegisterClient(string clientName)
    {
        if (clientName != null && clientName != "")
        {
            try
            {
                IBroadcastorCallBack1 callback = OperationContext.Current.GetCallbackChannel<IBroadcastorCallBack1>();
                lock (locker)
                {
                    //remove the old client
                    if (clients.Keys.Contains(clientName))
                        clients.Remove(clientName);
                    clients.Add(clientName, callback);
                }
            }
            catch (Exception ex)
            {
            }
        }
    }

    public void NotifyServer(EventDataType eventData)
    {
        lock (locker)
        {
            var inactiveClients = new List<string>();
            foreach (var client in clients)
            {
                if (client.Key != eventData.ClientName)
                {
                    try
                    {
                        client.Value.BroadcastToClient(eventData);
                    }
                    catch (Exception ex)
                    {
                        inactiveClients.Add(client.Key);
                    }
                }
            }

            if (inactiveClients.Count > 0)
            {
                foreach (var client in inactiveClients)
                {
                    clients.Remove(client);
                }
            }
        }
    }
}

}

而 web.config 文件就像:

<services>
  <service behaviorConfiguration="Service" name="WcfMultipleCallBacks.BroadcastorService">
    <endpoint address="" binding="wsDualHttpBinding" contract="WcfMultipleCallBacks.IBroadcastorService1" />
    <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
  </service>
</services>

WPF 管理应用程序代码。我为处理与 WCF 服务参考相关的事件创建了一个类:BroadcastorCallback.cs 类:

public class BroadcastorCallback : IBroadcastorService1Callback
{
        private System.Threading.SynchronizationContext synContext = AsyncOperationManager.SynchronizationContext;
        private EventHandler _broadcastorCallBackHandler;


        //SetHandler, is used to set the callback handler for the client.
        public void SetHandler(EventHandler handler)
        {
            this._broadcastorCallBackHandler = handler;
        }

//BroadcastToClient, is used to allow the service to call the client. 
        //When other clients send an event notification to the service, the service will connect to this client 
        //through the callback channel, then call this method to notify this client the event.
        public void BroadcastToClient(EventDataType eventData)
        {
            synContext.Post(new System.Threading.SendOrPostCallback(OnBroadcast), eventData);
        }

        //OnBroadcast, is the connection between the client callback handler, which is set in the first method, 
        //and the actual service call, which will be invoked by the service through the second method. 
        //When the service calls the second method, BroadcastToClient, to notify a event, the call will be delegated to 
        //this method, OnBroadcast, and then the same call will be delegated to the client callback handler.
        public void OnBroadcast(object eventData)
        {
            this._broadcastorCallBackHandler.Invoke(eventData, null);
        }

   }
}

虽然 MainWindow.cs 包含如下代码:

private ServiceReference1.BroadcastorService1Client _client;
public MainWindow()
        {
            InitializeComponent();
            RegisterClient();
        }
private delegate void HandleBroadcastCallback(object sender, EventArgs e);
        public void HandleBroadcast(object sender, EventArgs e)
        {
            try
            {
                var eventData = (ServiceReference1.EventDataType)sender;
                if (this.txtEventMessages.Text != "")
                    this.txtEventMessages.Text += "\r\n";
                this.txtEventMessages.Text += string.Format("{0} (from {1})",
                    eventData.EventMessage, eventData.ClientName);
            }
            catch (Exception ex)
            {
            }
        }

private void RegisterClient()
        {
            if ((this._client != null))
            {
                this._client.Abort();
                this._client = null;
            }

            BroadcastorCallback cb = new BroadcastorCallback();
            cb.SetHandler(this.HandleBroadcast);

            System.ServiceModel.InstanceContext context = new System.ServiceModel.InstanceContext(cb);
            this._client = new ServiceReference1.BroadcastorService1Client(context);

            //this._client.RegisterClient(this.txtClientName.Text);
            this._client.RegisterClient("Harry Potter");
        }

private void btnSendEvent_Click(object sender, RoutedEventArgs e)
        {
this._client.NotifyServer(
                       new ServiceReference1.EventDataType()
                       {
                           ClientName = "Harry Potter",
                           //EventMessage = this.txtEventMessage.Text
                           EventMessage = count.ToString()
                       });
          }
        }

当消息发送不是太快时,此代码运行完美。但是,当消息通信太快时,它会使 wpf 应用程序崩溃。

出于测试目的,当我在 SendEvent 上应用 While 循环时,它就像崩溃的 WPF 应用程序一样。

private bool isRun = false;
private void btnSendEvent_Click(object sender, RoutedEventArgs e)
        {
isRun = true;
while(isRun = true)
{
this._client.NotifyServer(
                       new ServiceReference1.EventDataType()
                       {
                           ClientName = "Harry Potter",
                           //EventMessage = this.txtEventMessage.Text
                           EventMessage = count.ToString()
                       });
}
          }
        }

我检查设置调试器,但不知道哪个部分导致我的应用程序崩溃以及为什么它在快速通信中发生。

标签: c#wpfwcf

解决方案



这是我对问题的评论的摘要。


我认为当您在负载下调用服务时,您的代码中会出现一些问题。

此 WCF声明将您的服务标记为单例多线程感知服务。ServiceBehavior

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, // singleton
                 ConcurrencyMode = ConcurrencyMode.Multiple)] // thread-safe(?)
public class BroadcastorService : IBroadcastorService1 { ... }

InstanceContextMode.Single是直截了当的,因为 WCF 为您设置了这一点,您实际上不需要做任何事情,但是 ConcurrencyMode.Multiple声明可以接受多个线程的多个并发调用。您声明您接受所有责任并且您不追究 WCF 的责任。WCF 相信您不会在自己的脚下开枪。

通常最好让 WCF 确定来自客户端的调用如何以及何时进入您的服务代码。默认情况下,WCF一次序列化所有调用服务上的方法的调用使您的服务线程安全,而无需开发人员对可怕lock()的 s 做任何事情。与InstanceContextMode它一起使用可以提高服务主机的可扩展性。

还要考虑到每个 WCF 服务方法所做的第一件事是lock()对整个硬编码的单例执行 a,您不会从ConcurrencyMode.Multiple. 您也可以使用ConcurrencyMode.Single,删除所有lock()s 并让 WCF 为您完成所有方法调用的序列化。比手动使您的服务线程安全更安全另外,如果您想删除服务的单例性质并使用 say InstanceContextMode.PerCallor InstanceContextMode.PerSession,则可以说是单行更改。

当您的应用程序处于负载状态时,您的服务被标记为:

  • “我可以处理任意数量的并发线程调用”:P 和
  • “让所有线程针对同一个服务对象执行” :P

...可能导致非常危险的服务。通过进行上述建议的更改,您可以有效地限制来自所有客户端的并发调用数,从而使服务更加稳定

崩溃、异常处理和调试技巧

您提到您的应用程序正在崩溃,但您没有说明错误是什么。查看您的代码,我可以看到很多:

try
{
    // something here
}
catch (Exception ex)
{
}

通常,您希望避免做这种事情,因为您告诉 .NET 您想以静默方式捕获所有异常。作为开发人员,您甚至没有被告知您的代码中可能存在令人讨厌的错误。捕获所有异常是相当顽皮的,因为您真的只想捕获您期望的异常,并且可能为代码中的其他所有内容设置一个未处理的异常处理程序,该处理程序只是在稍微优雅地关闭应用程序之前向用户显示一条致命消息

若要改进调试,请确保在 Visual Studio 调试器中运行应用。

Debug菜单中,选择Debug.Windows.Exception Settings

在出现的工具窗口中,勾选Common Language Runtime Exceptions框。这告诉 VS 你想知道所有的CLR 异常。(稍后你可以进去,选择你想要的)

现在,无论何时抛出异常,调试器都会停止并将光标放在有问题的行上。 注意:此时这就是所谓的第一次机会异常,因为调试器会立即停止在线。让我们想象一个TimeoutException被抛出。正如您可能会说的那样,这不一定是错误catch (TimeoutException)。它还没有进展到第一个catch()(如果有的话)块,所以不要惊慌。如果您按下F5(或Debug.Continue菜单),则调试器将恢复停止在您的catch(TimeoutException). 现在如果您没有勾选调试设置中的框,调试器将直接进入您的catch(TimeoutException)没有发出第一次机会通知。现在的问题是,如果不查看对象中的调用堆栈,您不知道错误发生在哪里Exception

代理客户端

虽然可能不是一个直接的问题,但我也注意到您的客户正在创建 WCF 代理并将其存储在应用程序MainWindow类的字段中。代理的问题是它们在一段时间后会被破坏,WCF 也不例外。通常它们代表一个网络连接。网络来来去去。如果空闲,连接可以简单地超时并被服务器关闭。客户端在调用它之前不会知道它,此时为时已晚。你会得到一个并且代理将被标记为有故障的,这意味着它不能再次使用。你需要再做一个。xxxException

Dispose()出于这个原因,通常最好在进行第一次调用之前创建一个代理,然后在当前批次的调用完成后摆脱它(你应该这样做)。那,或者构建到您的应用程序中,它可以处理 WCF 错误并在需要时重新创建代理。

现在,根据您使用的 WCF 绑定,超时会有所不同,可能是 1、5 或 10 分钟。

同样,这只是一个仅供参考,我认为它不会在这里发生,但你永远不会知道。

不要让 UI 线程调用 WCF 服务

操作:

当从管理员端开始向服务发送信号时,我无法在管理屏幕上做任何事情。即使我无法最小化,最大化或关闭该屏幕

您的管理客户端正在冻结,因为您正在从您的btnSendEvent_Click处理程序调用 WCF 服务。在该方法返回之前,UI 不会做任何事情。这是所有 UI 的本质。没有UI 是多线程的。您的点击处理程序正在执行昂贵且及时的网络调用这一事实只会使您的 UI 更加明显地无响应。也许您需要在BackgroundWorker组件提供的工作线程中调用它(新手更容易)或异步通过async/await(更好)。

操作:

非常感谢您的支持。现在我已经BackgroundWorker在管理端应用程序中使用并按照您的说明在 WCF 服务上应用更改。现在它正在顺利发送信号。没有崩溃和冻结管理端应用程序。

我很高兴听到我的建议帮助解决了这个问题。

告诉我更多

我强烈推荐这本优秀的 WCF 圣经,无论是书本还是 Kindle 形式:

在此处输入图像描述


推荐阅读