通过实例分析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.

posted @ 2007-03-08 11:50 张逸 阅读(6453) 评论(17) 编辑 收藏

 回复 引用 查看   
#1楼 2007-03-08 11:58 webabcd      
好文 支持 收藏
 回复 引用 查看   
#2楼 2007-03-08 12:29 怀念家驹      
如果客户端掉线了,如何同步服务器端列表呢?将掉线用户从列表中移除。
 回复 引用 查看   
#3楼 2007-03-08 13:23 Artech      
不错的例子,收藏!
 回复 引用 查看   
#4楼 2007-03-08 13:29 idior      
---
因为Duplex实现了客户端与服务端双向通信的功能
---

我一直认为我们对Duplex这个概念理解的不够到位。Duplex实现了客户端与服务端双向通信的功能,但是我在这里仅仅看到了回调功能而已。我们知道回调功能在Remoting中早就有了实现,那时并没有Duplex的概念。
那么Duplex的意义究竟在哪里?!

Artech和Wayfarer谈谈看法?

 回复 引用 查看   
#5楼[楼主] 2007-03-08 13:48 Bruce Zhang      
@idior
或许有你说的可能。但我认为不必把WCF想得很高深。虽然说Remoting也实现了回调功能,但仍然不如WCF实现的Duplex简单与方便。

所谓的双向通信,至少我看到MSDN的例子,仍然是通过回调来实现消息的双向传递。

 回复 引用 查看   
#6楼 2007-03-08 14:02 idior      
no,no,no!

Duplex绝对不是这么简单,就我所知在使用TCP作为传输信道的时候,Duplex的一个特点就是双向通信使用的是同一个信道。而不像Remoting中的回调使用了两个信道。

 回复 引用 查看   
#7楼 2007-03-09 20:56 Cooldog's .NET Space      
有个疑问,如果客户端掉线,服务器端继续使用回调方式,和客户端通讯,是否会发生异常?
 回复 引用   
#8楼 2007-03-16 14:34 dizzarz[未注册用户]
你好,我是It168的技术编辑,看到您的关于WCF的技术文章印象深刻,希望能就此内容向您约稿。我的MSN是whatishope@yahoo.com.cn,也希望和您多聊聊。
 回复 引用 查看   
#9楼 2007-04-03 19:55 stswordman      
hi bruce zhang.
问一个问题:
那个wcf聊天室程序中callback变量在每次Join的时候被赋值,但是在MyEventHandler和BroadcastMessage函数没有任何对callback赋值的语句,那么callback是怎么对应到相应的OperationContext.Current.GetCallbackChannel<IChatCallback>()呢?

我在MyEventHandler和BroadcastMessage函数都设置了断点,观察callback的变化(通过GetHashCode方法),发现callback在BroadcastMessage中没有变化,但是代码掉转到MyEventHandler时callback就发生了变化,这是为什么?

handler.BeginInvoke(this, e, new AsyncCallback(EndAsync), null);
这句话会影响到callback变量???

期待你的回答 谢谢

 回复 引用 查看   
#10楼 2007-04-04 15:35 stswordman      
终于想明白这个问题了....
 回复 引用   
#11楼 2007-10-22 12:34 xwyh24[未注册用户]
 回复 引用 查看   
#12楼 2009-02-19 15:20 Spring.Cheung      
请问,这个示例的客户端代理是怎么生成的?
 回复 引用 查看   
#13楼[楼主] 2009-02-28 11:59 张逸      
@Spring.Cheung
SvcUtil.exe

 回复 引用 查看   
#14楼 2009-03-27 12:54 sps.shareach.com      
呵呵,想不到楼主一年前就实现了这个聊天, 开始要是找到你这个文章就好了, 我们方式大致小异, 我和一样楼主在接收的时候使用了异步调用, 楼主没有说服务的InstanceContextMode和ConcurrencyMode, 我使用了单例的多线程.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
如果是percall和persession我想资源会比较大, 每次都会去实例化,释放.而且客户端是对应到Channel的,应该和Session没有直接关系的吧?
还有一点楼主没有实现掉, 呵呵, 就是对客户端掉线了再连接, 你就没有办法判断是否同一用户, 因为你是针对Session做索引的.
ChatEvent += myEventHandler;
foreach (ChatEventHandler handler in temp.GetInvocationList())
{
handler.BeginInvoke(this, e, new AsyncCallback(EndAsync), null);
}
这2个地方会有冲突, 你如果在遍历时调用了ChatEvent += myEventHandler; 也会导致程序出错.
我现在是每次未客户端创建一个动态ID(不唯一的,注册用户可以是注册的ID), 以用户Id作为索引, 即使Client channel变了,也只是收到信息时重新更新Client channel.

我对WCF还刚学, 不知说得对不对, 希望能和楼主交流. 对WCF还有很多问题感觉茫然,呵呵.
下面是我的聊天程序
http://www.cnblogs.com/yinpengxiang/archive/2009/03/23/slChat.html
http://www.shareach.com:81/chat

 回复 引用 查看   
#15楼[楼主] 2009-03-28 20:46 张逸      
@sps.shareach.com
这个聊天程序不是我写的,呵呵:)

 回复 引用 查看   
#16楼 2009-08-20 17:35 赵波涛      
引用stswordman:hi bruce zhang.<br>问一个问题:<br>那个wcf聊天室程序中callback变量在每次Join的时候被赋值,但是在MyEventHandler和BroadcastMessage函数没有任何对callback赋值的语句,那么callback是怎么对应到相应的OperationContext.Current.GetCallbackChannel<IChatCallback>()呢?<br><br>我在MyEventHandler和BroadcastMessage函数都设置了断点,观察callback的变化(通过GetHashCode方法),发现callback在BroadcastMessage中没有变化,但是代码掉转到MyEventHandler时callback就发生了变化,这是为什么?<br><br> handler.BeginInvoke(this, e, new AsyncCallback(EndAsync), null);<br>这句话会影响到callback变量???<br><br> 期待你的回答 谢谢



我也有这样的疑问,期待解答,谢谢!

 回复 引用 查看   
#17楼 2011-07-11 11:23 doit      
张老师,您好!
最近会用到关到wcf的内容,
想实现的功能是利用wcf服务向android应用广播消息,从技术上不知道利用wcf的双工通信有无实现的可能.

具体是这样子的:wcf是一种请求式的服务,但是当客户端A改变了某些信息,而会对客户端B产生影响时,需要怎么去通知客户端B.android平台可以实现双工通信的客户端的回调吗?

谢谢.