• 通过实例分析WCF Duplex消息交换


    聊天室实例:点此下载 

        我在《Windows Communication Foundation之旅•三》中详细介绍了WCF中的Duplex消息交换模式。因为Duplex实现了客户端与服务端双向通信的功能,故而我实现了一个简单的聊天室程序,展现Duplex的特点。有朋友在阅读了这个例子之后,提出一个问题,即“如何让服务端向指定的客户端发送消息?”很高兴的是,这位朋友在后来的邮件中说到问题已经解决了,思路是利用Singleton对象保存客户端的Session。虽然存在一些比较奇怪的问题,然而总算是一种思路。

        我的思路与之相似,需要服务端维护一个Dictionary的集合,用以保存客户端的信息。服务端在发送消息时,可以通过查找Dictionary对象,识别符合条件的客户端。当我还在思考这样的方式能否解决问题时,我在WCF官方网站上偶然发现了一个同样利用Duplex实现聊天室的Sample。

        仔细阅读了实例代码,我恍然发现自己在思考程序设计时,并没有理解WCF最核心的价值,那就是“服务”。作为实现SOA体系架构的技术框架,WCF最重要的特征就在于能够定义和提供服务。以聊天室程序为例,虽然服务端会参与消息的交互,但却不应该参与到聊天中。也就是说,客户端与服务端的角色任务是不相同的。通过用例图可以看到两者之间的区别: 

    chatroom01.gif
    图1  正确的用例图            

    chatroom02.gif
    图二  错误的用例图

        明确了以“服务”为核心的程序结构,我们才能够更好地利用WCF,定制自己的服务,分清楚服务的边界,定义好消息的格式。虽然,一个聊天室程序无法体现SOA的核心精神,然而树立面向服务的思想确实必要的。正如我们在开始面向对象程序设计时,需要树立面向对象的思想一样。

        该聊天室程序的实现主要通过Duplex来实现,其中又利用了MulticastDelegate与异步调用。其中,服务接口的定义如下:
        [ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IChatCallback))]
        interface IChat
        {
            [OperationContract(IsOneWay = false, IsInitiating = true, IsTerminating = false)]
            string[] Join(string name);

            [OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)]
            void Say(string msg);

            [OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)]
            void Whisper(string to, string msg);

            [OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = true)]
            void Leave();
    }

        回调接口的定义如下:
        interface IChatCallback
        {
            [OperationContract(IsOneWay = true)]
            void Receive(string senderName, string message);

            [OperationContract(IsOneWay = true)]
            void ReceiveWhisper(string senderName, string message);

            [OperationContract(IsOneWay = true)]
            void UserEnter(string name);

            [OperationContract(IsOneWay = true)]
            void UserLeave(string name);
        }

        服务提供了Join、Say、Whisper与Leave等接口方法,向对应的是回调接口的接口方法。在实现IChat服务接口的服务类ChatService中,定义了委托ChatEventHandler与ChatEventHandler类型的事件ChatEvent,正是通过它实现了识别了客户的消息广播。方法如下:
         private void BroadcastMessage(ChatEventArgs e)
         {
             ChatEventHandler temp = ChatEvent;

             if (temp != null)
             {
                 foreach (ChatEventHandler handler in temp.GetInvocationList())
                 {
                    handler.BeginInvoke(this, e, new AsyncCallback(EndAsync), null);
                 }
             }
        }

        在客户端加入聊天室程序之前,该客户端并没有订阅ChatEvent事件,此时调用BroadcastMessage方法,在通过GetInvocationList方法获取MulticastDelegate时,不存在该客户端的委托实例。因而,其他客户在通过聊天室进行聊天时,不会将聊天信息发送到该客户端。体现在程序中,就是Join方法的如下代码片断:
        myEventHandler = new ChatEventHandler(MyEventHandler);
        ……

        callback = OperationContext.Current.GetCallbackChannel<IChatCallback>();
        ChatEventArgs e = new ChatEventArgs();
        e.msgType = MessageType.UserEnter;
        e.name = name;
        BroadcastMessage(e);
        ChatEvent += myEventHandler;
        ……

        注意看,ChatEvent += myEventHandler语句是放在BroadcastMessage方法调用之后。一旦该客户端加入聊天室程序之后,再调用BroadcastMessage方法,该客户端就能接收消息了。

        ChatEvent事件指向的方法是MyEventHandler,该方法将执行回调接口的相关方法:
        private void MyEventHandler(object sender, ChatEventArgs e)
        {
            try
            {
                switch (e.msgType)
                {
                    case MessageType.Receive:
                        callback.Receive(e.name, e.message);
                        break;
                    case MessageType.ReceiveWhisper:
                        callback.ReceiveWhisper(e.name, e.message);
                        break;
                    case MessageType.UserEnter:
                        callback.UserEnter(e.name);
                        break;
                    case MessageType.UserLeave:
                        callback.UserLeave(e.name);
                        break;
                }
            }
            catch
            {
                Leave();
            }
        }

        还需要注意的是Whisper方法。由于它实现了私聊功能,因而向指定客户发送信息时,不应该采用广播方式。如何找到指定客户呢?这需要一个Dictionary集合,保存客户名和与之对应的ChatEventHandler实例。在执行Whisper方法时,就可以根据客户名找到对应的ChatEventHandler实例进行调用:
        public void Whisper(string to, string msg)
        {
            ChatEventArgs e = new ChatEventArgs();
            e.msgType = MessageType.ReceiveWhisper;
            e.name = this.name;
            e.message = msg;
            try
            {
                ChatEventHandler chatterTo;
                lock (syncObj)
                {
                    chatterTo = chatters[to];
                }
                chatterTo.BeginInvoke(this, e, new AsyncCallback(EndAsync), null);
            }
            catch (KeyNotFoundException)
            {
            }
        }

        在客户端代码中,服务接口的调用采用了异步调用的方式,例如客户端加入聊天室:
        proxy = new ChatProxy(site);
        IAsyncResult iar = proxy.BeginJoin(myNick, new AsyncCallback(OnEndJoin), null);

        运行聊天室程序时,服务端仅需要提供稳定而持续的服务。聊天的参与者均为客户端用户。因而服务端的运行代码如下所示:
        Uri uri = new Uri(ConfigurationManager.AppSettings["addr"]);
        ServiceHost host = new ServiceHost(typeof(NikeSoftChat.ChatService), uri);
        host.Open();
        Console.WriteLine("Chat service listen on endpoint {0}", uri.ToString());
        Console.WriteLine("Press ENTER to stop chat service...");
        Console.ReadLine();
        host.Abort();
        host.Close();

        本文Sample的作者是Nikola Paljetak。鉴于作者本人在代码所附的许可声明,为了帮助大家阅读本文,在此附上Nikola Paljetak的Sample,你可以在WCF官方网站中找到它。Nikola Paljetak的许可声明如下:
        Permission is granted to anyone to use this software for any purpose, including commercial applications.

  • 相关阅读:
    Leetcode [654] 最大二叉树 &[105] 从前序与中序遍历序列构造二叉树 & [106] 从中序与后序遍历序列构造二叉树
    Leetcode [226] 翻转二叉树 & [116] 填充每个节点的下一个右侧节点指针 & [114] 二叉树展开为链表
    Leetcode 链表&二叉树刷题总结
    Leetcode 动态规划刷题总结
    Leetcode [1312] 让字符串成为回文串的最少插入次数 动态规划
    Leetcode [234] 回文链表 回文 链表
    动态规划之 KMP 算法详解(转)
    Leetcode [99] 恢复二叉搜索树 二叉树
    统计代码行数
    二叉树遍历(递归、非递归、mirror)转
  • 原文地址:https://www.cnblogs.com/wayfarer/p/667865.html
Copyright © 2020-2023  润新知