65.9K
CodeProject 正在变化。 阅读更多。
Home

让 WCF 双工通信变得轻松有趣。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (7投票s)

2014 年 1 月 16 日

CPOL

9分钟阅读

viewsIcon

36268

downloadIcon

593

WCF 服务中双工通道的运行示例。

引言

WCF 已经存在了一段时间,因此,关于它的文章很多,并且它可以在许多场景中使用,其中双工场景是我们选择的主题。

促使我写这篇澄清文章的原因是,我看到很多关于同一主题的文章,其中许多都有真实的例子,但如果你去阅读它们,你仍然会发现有些东西缺失,它们都没有给你一个关于双工如何工作以及许多人发现在使其工作时遇到困难的清晰图景。

MSDN 也提供了一个清晰的工作示例,它只演示了一种场景,而不是你在工作中可能需要的任何其他情况。

本文当然不会涵盖双工的所有方面,甚至不会涵盖陷阱,我只希望它能帮助你构建你的双工解决方案,并促使你搜索有关该主题的具体问题。

双工的优点

在大多数 WCF 合同中,客户端和服务之间进行双向通信非常常见,合同中的任何操作都可以有要发送到服务的参数,它也可能有一个从服务发送回客户端的返回值,这本身就是一种双向通信,所以当你从事业务线项目时,你很少需要使用双工模式。

双工使您能够对这种双向通信施加更强大的控制,并使事情变得更有趣、更强大。

要理解双工,您可以想象两个 WCF 服务,每个服务都将对方视为客户端,在这种情况下,第一个服务将能够控制“客户端”,“客户端”将能够以相同的方式控制服务。

这在通信解决方案或主要在交互式游戏中很有用。

但是,我不鼓励如此轻易和不加区分地使用双工,这不仅因为它有点复杂,而且还因为安全问题,它比单向通道通信的风险高一倍。

构建双工所需的一切

定义服务

首先,在您考虑任何双工项目之前,最重要的是清楚地决定这两个服务将做什么,它们看起来像什么,以及哪个是主服务,哪个是辅助服务。

定义回调服务

正如我们前面所述,需要定义两个服务:主服务(可以称为服务、服务器等),而另一个服务称为回调服务(客户端、辅助服务等)。

设置行为

在主服务实现和回调服务实现上设置正确的行为。

数据绑定。

您需要仔细选择正确的绑定,并非所有绑定都适用于双工模式,但是,请放心,NetTCPBinding 和 NetNamedPipeBinding 都支持双工模式。

此外,如果您想选择 http 协议,有一个 WSDualHttpBinding 专门用于双工模式。

线程阻塞问题

当您打开双向通道或双通道并使用它进行通信时,您很可能会遇到应用程序冻结并停止交互的问题。

微软的示例强调了使用 IsOneWay;假设这将有助于解决阻塞问题,我认为这降低了双重通信的强大功能。

在 WindowsForm 中使用 DualChannel 时,Microsoft 在回调行为中引入了一个属性 UseSynchronizationContext,将其设置为 false。

一篇不错的文章讨论了许多尝试寻找此问题的解决方法,唯一有效的方法是使用单独的线程来调用服务。

Brandon Cannaday在他的文章中透露了一些值得一提的东西...

"我读过几篇文章,都说实际上只需要在回调的 OperationContract 属性上设置 IsOneWay 属性。理论上,它应该这样工作,因为如果这是真的,WCF 在发送数据时将不会执行任何锁定。不幸的是,这行不通。不过,你仍然应该将该属性设置为 true,因为它确实会做其他你真正想要的事情。
我遇到的另一个重要信息是回调实现的 ServiceBehavior 属性上的 UseSynchronizationContext 属性。这个听起来非常有希望。当设置为 false 时,回调将不会自动与 UI 线程同步。这似乎是死锁的根源,所以我认为这肯定是,但同样,没有成功。

总而言之,它归结为一个简单的修复,但需要大量的麻烦才能找到。我对此结果并不完全满意,所以如果还有其他人遇到这个问题并找到了真正的解决方案,请告诉我。"
http://tech.pro/tutorial/914/wcf-callbacks-hanging-wpf-applications

在我们的例子中,我们没有遇到这个问题,因为我们使用的是 System.Threading.Timer,这个计时器是多线程计时器,它在不同的线程中进行调用。

如果我们使用另一种不支援多线程的计时器 System.Timers.Timer,我们就会面临同样的问题。


待研究的有效问题


在服务契约中,是否需要会话模式?

是的,需要,双工确实需要会话才能工作。

在服务契约中,契约中的所有操作都需要是单向的吗?

不,正如你在示例中看到的,它们可以是双向的,尽管

你在网上看到的大多数示例都会将 oneway = true 设置为 true,以避免线程阻塞问题。

在服务契约中,我应该定义回调契约吗?

是的,这是它工作所必需的。

在 ServiceCallback 契约中,我应该添加 [ServiceContract] 属性吗?

如果你愿意,可以添加它,但通常你不需要,因为编译器会为你做这件事。

在回调服务中,并发模式是什么?

[ConcurrencyMode.Reentrant]

并非在所有情况下都是必需的;当前示例也将与 Single 和

Multiple 一起工作。

在回调服务中,我应该使用属性 UseSynchronizationContext = false 吗?

并非在所有情况下都必须如此,当您在 Windows Form 应用程序或 WPF 中托管 ServiceCallback 时,如果遇到线程阻塞问题,它会很有用。

如果它不起作用,请确保使用 ThreadPool 或任何其他技术在单独的线程中调用操作。

支持哪些绑定?

命名管道绑定支持双通道。

NetTCPBinding 也支持双通道。

对于通过 http 协议的传输,您可以使用一种专门用于该类型通信的绑定,称为 WSDualHttpBinding。

使用代码

我尽量让代码尽可能简单。

该解决方案包含 4 个项目;我们称之为 Messenger,以暗示其通信性质。

第一个项目是 MessengerContracts,在这个解决方案中,你定义了纯粹的契约,这里不应该有任何代码。(这不是必须的,你可以将实现与契约合并到一个项目中,甚至更糟,但将它们分开是 SOA 架构原则中的一个好习惯)。

在这个项目中,我定义了所有契约

IMessenger,它代表主服务,以及 IMessengerCallback,它代表辅助服务的客户端契约。

除此之外,我们还应该定义所有其他契约,例如所需的数据契约,在这种情况下,我需要一些数据在服务器和客户端之间传输,它们被标识为 IMessengerMessage。

   [ServiceContract(Name = "IMessenger", Namespace = "<a href="http://assilabdulrahim/messenger">http://assilabdulrahim/messenger", SessionMode=SessionMode.Required , CallbackContract = typeof(IMessengerCallback))]
    public interface IMessenger
    {
        [OperationContract(Action = "<a href="http://assilabdulrahim/messenger/pushMessage">http://assilabdulrahim/messenger/pushMessage", IsOneWay = false)]
        int PushMessage(MessengerMessage message);
        [OperationContract(Action = "<a href="http://assilabdulrahim/messenger/writeOnClient">http://assilabdulrahim/messenger/writeOnClient", IsOneWay = false)]
        int WriteOnClient(MessengerMessage message);
    }

 
 public interface IMessengerCallback
    {
        [OperationContract(IsOneWay = true)]
        void PullMessage(MessengerMessage message);
        [OperationContract(IsOneWay = true)]
        void WriteOnServer();
    }

 


  [DataContract]
    public class MessengerMessage
    {
        [DataMember]
        public string Text { get { return string.Format("This message is being written by {0} of instance number {1}, using thread id {2} ", ProcessName, InstanceNumber, ThreadId); } private set { } }

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

        [DataMember]
        public int ThreadId { get; set; }

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

第二个项目是实现,我们在其中实现服务契约和客户端控制台应用程序。

所以让我们从辅助服务也就是回调服务的实现开始。

它实现了IMessengerCallback

 AddressFilterMode 仅在我的情况下使用,您可能不需要它,当使用 TCP 协议时我才需要它。 

AddressFilterMode.All:“表示一个过滤器,它匹配传入消息的任何地址。

使用此值将关闭 WCF 地址过滤器检查。任何消息,无论其 WS-Adressing:To 标识符是什么,都将被接受。” 

 InstanceContextMode 和 ConcurrencyMode 供您更改和尝试,看看它们的影响。 

你注意到了我正在进行的锁定,如果你正在使用多个线程,并且你真正关心你正在使用的颜色的含义,那么锁定是很重要的。 

[ServiceBehavior(
        InstanceContextMode = InstanceContextMode.PerSession,
        ConcurrencyMode = ConcurrencyMode.Single,
        AddressFilterMode = AddressFilterMode.Any)]
    public class Messenger : IMessenger
    {
     
        private IMessengerCallback Callback
        {
            get
            {
                return OperationContext.Current.GetCallbackChannel<IMessengerCallback>();
            }
        }

        static object _synchObject = new object();

        [OperationBehavior]
        public int PushMessage(MessengerMessage message)
        {
            lock (_synchObject)
            {
                var tmp = Console.ForegroundColor;
                switch (message.InstanceNumber)
                {
                    case "1":
                        Console.ForegroundColor = ConsoleColor.Red;
                        break;
                    case "2":
                        Console.ForegroundColor = ConsoleColor.Blue;
                        break;
                    case "3":
                        Console.ForegroundColor = ConsoleColor.Yellow;
                        break;
                    default:
                        Console.ForegroundColor = ConsoleColor.White;
                        break;
                }
                Console.WriteLine(message.Text);
                Console.ForegroundColor = tmp;
            }
           
            var file = new FileInfo(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
            WriteOnClient(new MessengerMessage() { ProcessName = file.Name, InstanceNumber = "Server Singleton",ThreadId = Thread.CurrentThread.ManagedThreadId });
            return message.Text.Length;
        }


        [OperationBehavior]
        public int WriteOnClient(MessengerMessage message)
        {
            Callback.PullMessage(message);
            return message.Text.Length;
        }
    }


 

第三个项目是客户端应用程序,它代表客户端,因此它应该实现回调服务。 

在双工模式下,客户端应用程序必须实现回调契约,在我们的例子中是 IMessegnerCallback。

文档中写道

"ConcurrencyModeConcurrencyMode属性结合使用,以指定服务类是否支持单线程或多线程操作模式。单线程操作可以是可重入的或不可重入的。" 

 有关属性的更多信息,请参阅参考资料。 

    [CallbackBehavior(
      ConcurrencyMode = ConcurrencyMode.Reentrant,
      UseSynchronizationContext = false)]

    public class MessengerCallBack : IMessengerCallback
    {
        //Some peopl like to make it Disposable when they generate the proxy

        IMessenger _proxy = null;
        Timer _timer = null;
        string _instanceNumber;

        public MessengerCallBack(string clientNumber)
        {
            _proxy = DuplexChannelFactory<IMessenger>.CreateChannel(new InstanceContext(this), "DualChannel");
            _timer = new Timer(new TimerCallback((x) => { this.WriteOnServer(); }), null, 1000, 3000);
            _instanceNumber = clientNumber;
        }

        public void WriteOnServer()
        {
            var file = new FileInfo(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
            var result = _proxy.PushMessage(new MessengerMessage() { ProcessName = file.Name, InstanceNumber = _instanceNumber, ThreadId = Thread.CurrentThread.ManagedThreadId });
        }

        public void PullMessage(MessengerMessage message)
        {
            var tmp = Console.ForegroundColor;
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine( message.Text);
            Console.ForegroundColor = tmp;
        }
    }

 而且客户端应用程序,您可以运行任意数量的实例。 

    class Program
    {
        static void Main(string[] args)
        {

            MessengerCallBack client = new MessengerCallBack(args[0]);
            client.PullMessage(new MessengerMessage()
            {
                ProcessName = Thread.CurrentThread.ManagedThreadId.ToString()
                ,
                InstanceNumber = args[0]
            });

            Console.ReadLine();
        }
    } 


最后,我有一个主机应用程序,它应该托管该服务。

这应该在任何客户端实例运行之前运行。

 

    static void Main(string[] args)
        {
            string baseAddress = "net.tcp://:8000/Messenger/";
            using (var host = new ServiceHost(typeof(MessengerImpl.Messenger),new Uri(baseAddress)))
            {
                host.Open();
                Console.WriteLine("Service is hosted, has the following endpoints, Hit <ENTER> to terminate");
                host.Description.Endpoints.ToList().ForEach(x => Console.WriteLine(x.Address));
                Console.ReadLine();
            }
        } 

现在我们有两个控制台应用程序,一个代表客户端,另一个代表服务器。

为了给结果增加更多乐趣和色彩,在编译完

客户端,您为其创建 4 个快捷方式并分别传递这些数字作为参数

(1,2,3 和 4)

如下图所示

 


现在,您可以根据需要运行任意数量的客户端实例,并查看结果。
每个客户端都会使用一个计时器向服务器写入数据。

服务器将向每个客户端写入数据。

例如,运行 client1 五次,client2 十次,然后在服务器控制台上查看结果。

看到服务器如何控制客户端,以及客户端如何反过来控制服务器,这真是太有趣了。

这些客户端快捷方式和参数与双工模式无关,它们只是为了增加乐趣,作为一种添加颜色和展示通信并使其更清晰的方式。

如下图所示。

彩色控制台是服务器主机,每个客户端都在向其写入。Client1 的所有实例都以红色写入,Client2 的所有实例都以蓝色写入……等等。

请仔细注意线程 ID,不要将客户端线程和服务主机线程混淆。 

 注意

当前代码需要一个客户端参数,如通过快捷方式所解释的,如果您不传递参数,它将中断。

您可以修改代码以检查参数。 

接下来是什么: 

 请随意下载源代码并按照描述运行它,看看颜色如何发挥作用。

现在您可以尝试更改源代码并尝试使用属性,看看每个属性的影响。

有很多东西需要发现或学习,对我来说,在这种情况下重要的是线程。

您可以使用单个线程,并告诉我如何使用其他参数使其工作,例如回调行为的 Reentrant 或 SynchronizationContext,甚至操作行为的 IsOneWay。 

 

参考文献

ConcurrencyMode 枚举

AddressFilterMode 枚举 

WCF 回调导致 WPF 应用程序挂起

© . All rights reserved.