这次是一个在线聊天的插件,插件参考了MSDN中Duplex WCF服务的实现和网上一些聊天程序,基本可以实现用户登录和聊天,如果用户不存在就保存聊天数据到数据库,等用户下次登陆的时候读入。

这个是聊天时候的图例:


1: 定义聊天时候传输聊天的数据实体,这个所有WCF都是一样的,没什么特殊的,代码如下:

[DataContract]
public class ChatMessage
{
    [DataMember]
    public string From;
    [DataMember]
    public string To;
    [DataMember]
    public int Type;//1:Text;2:Url;3:File;4:Image;5:Audio;6:Video;0:None;-1:System;-2:Heart;
    [DataMember]
    public string Data;
    [DataMember]
    public byte[] Content;

}

这样用户每次聊天就发送的使这个数据了。这是服务器端的定义。

2:定义回调时候的接口:

[ServiceContract]

public interface IWSChatClient
{
    [OperationContract(IsOneWay = true)]
    void Receive(Message returnMessage);

}

 3:定义Duplex WCF服务的实现,

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceContract(Namespace = "WindCloud", SessionMode = SessionMode.Required, CallbackContract = typeof(IWSChatClient))]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple)]
public class WSChatService
{
    static Dictionary<string, IWSChatClient> chatClients = new Dictionary<string, IWSChatClient>();
    static List<string> m_NameList = new List<string>();
    private static Object syncObj = new Object();
    static int guestIndex = 1;

    string name = "";

}

这个WCF服务并没有从接口继承过来,而是直接地实现,这个和MSDN上边稍微有点差异,之所以这样做,因为将来可以使用Forms验证模式来验证用户,这是一个主要的原因,将来说到权限的时候,我会具体的说到这一点。

4: 接受心跳信息:

    /// <summary>
    /// 获取心跳信息更新连接
    /// </summary>
    /// <param name="receivedMessage"></param>
    [OperationContract(IsOneWay = true)]
    public void Heart(Message receivedMessage)
    {
        ChatMessage chatMessage = receivedMessage.GetBody<ChatMessage>();
        IWSChatClient chatclient = OperationContext.Current.GetCallbackChannel<IWSChatClient>();
        if (chatClients.ContainsKey(chatMessage.From))
        {
            chatClients[chatMessage.From] = chatclient;
            //
            chatMessage.From = "SystemServer";
            chatMessage.To = chatMessage.From;
            chatMessage.Type = -2;
            Message returnMessage = Message.CreateMessage(MessageVersion.Soap11, "WindCloud/WSChatService/Receive", chatMessage);
            chatclient.Receive(returnMessage);
        }
    }

客户端会定时发送心跳信息到服务器端,如果客户端有段时间没有发送心跳信息,这就是说客户端掉线或者其他原因了。

5:用户进入聊天室:

//进入聊天室
    [OperationContract(IsOneWay = true)]
    public void Join(Message receivedMessage)
    {
        bool userAdded = false;
        Message returnMessage;

        ChatMessage chatMessage = receivedMessage.GetBody<ChatMessage>();

        IWSChatClient chatclient = OperationContext.Current.GetCallbackChannel<IWSChatClient>();

        ChatMessage sendChatMessage = new ChatMessage();
        sendChatMessage.From = "Server:";
        sendChatMessage.Data = "Service:请等待...";
        sendChatMessage.Type = -1;

        //
        lock (syncObj)
        {          
            if (!chatClients.ContainsKey(chatMessage.From) && chatMessage.From != "" && chatMessage.From != null)
            {
                chatMessage.Data = "Service:开始添加用户...";
                returnMessage = Message.CreateMessage(MessageVersion.Soap11, "WindCloud/WSChatService/Receive", chatMessage);

                chatclient.Receive(returnMessage);

                this.name = chatMessage.From;
                m_NameList.Add(chatMessage.From);
                chatClients.Add(chatMessage.From, chatclient);
                userAdded = true;
            }
            else
            {
                ChatMessage cm = new ChatMessage();
                cm.From = "Server:";
                cm.Data = "Service:User:" + chatMessage + "加入失败,用户已存在。";
                returnMessage = Message.CreateMessage(MessageVersion.Soap11, "WindCloud/WSChatService/Receive", cm);
                //
                chatclient.Receive(returnMessage);

                return;
            }
            //发送留言
            ChatBLL bll = new ChatBLL();
            List<ChatMessage> cms = bll.GetChats(chatMessage.From);
            foreach (ChatMessage cm in cms)
            {
                SendMessageToClient(cm);
            }
            //删除留言
            bll.Delete(chatMessage.From);
        }

        if (userAdded)
        {
            string text2 = "Service:User:" + chatMessage + " is already Joined";
            ChatMessage cm = new ChatMessage();
            cm.From = "Server:";
            cm.Data = text2;
            SendMessageToClient(cm);
        }
    }

 用户加入聊天室之后,加入到用户队列中,同时从用户聊天的数据库读入其他用户给当前用户的留言并删除留言。

6:用户说话,这是平常一直要使用的方法,接受用户的聊天信息:

   

  /// <summary>
    /// 聊天
    /// </summary>
    /// <param name="receivedMessage"></param>
    [OperationContract(IsOneWay = true)]
    public void Say(Message receivedMessage)
    {
        IWSChatClient chatclient = OperationContext.Current.GetCallbackChannel<IWSChatClient>();
        ChatMessage text = receivedMessage.GetBody<ChatMessage>();

        if (!m_NameList.Contains(text.To))
        {
            //保存数据到数据库
            ChatBLL bll = new ChatBLL();
            bll.Add(text);
            return;
        }       
        if (!chatClients.ContainsValue(chatclient))
        {
            return;
        }
        SendMessageToClient(text);
    }

 

代码很少 ,如果用户没有登陆,就把聊天信息保存到数据库,否则,直接发送到用户的客户端去。

 7:用户退出聊天室:

   }
    /// <summary>
    /// 退出聊天室
    /// </summary>
    /// <param name="receivedMessage"></param>
    [OperationContract(IsOneWay = true)]
    public void Exit(Message receivedMessage)
    {
        IWSChatClient client = OperationContext.Current.GetCallbackChannel<IWSChatClient>();
        ChatMessage cm = receivedMessage.GetBody<ChatMessage>();

        if (chatUsers.Contains(cm.From))
        {
            chatUsers.Remove(cm.From);
        }
        if (chatClients.ContainsKey(cm.From))
        {
            chatClients.Remove(cm.From);
        }

    }

代码就是直接删除了队列中的用户。

公共函数:发送数据到客户端:

  private void SendMessageToClient(ChatMessage chatMessage)
    {
        if (chatMessage.To != null && chatClients.ContainsKey(chatMessage.To))
        {
            IWSChatClient chatclient = chatClients[chatMessage.To];
            Message returnMessage = Message.CreateMessage(MessageVersion.Soap11, "WindCloud/WSChatService/Receive", chatMessage);
            try
            {
                chatclient.Receive(returnMessage);
            }
            catch
            {
                if (chatUsers.Contains(chatMessage.To))
                {
                    chatUsers.Remove(chatMessage.To);
                }
                if (chatClients.ContainsKey(chatMessage.To))
                {
                    chatClients.Remove(chatMessage.To);
                }
            }
        }

    }

如果发送的时候发生异常,就从队列删除。

8:ServerHost的代码,这个从MSDN上抄过来的:

public class ChatHostFactory : ServiceHostFactoryBase
{
    public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses)
    {
        return new ChatServiceHost(baseAddresses);
    }
}

class ChatServiceHost : ServiceHost
{
    public ChatServiceHost(params System.Uri[] addresses)
    {
        base.InitializeDescription(typeof(WSChatService), new UriSchemeKeyedCollection(addresses));
    }

    protected override void InitializeRuntime()
    {
        // Define the binding and set time-outs.
        PollingDuplexBindingElement pdbe = new PollingDuplexBindingElement()
        {
            ServerPollTimeout = TimeSpan.FromSeconds(10),
            InactivityTimeout = TimeSpan.FromMinutes(1)
        };

        // Add an endpoint for the given service contract.
        this.AddServiceEndpoint(
            typeof(WSChatService),
            new CustomBinding(
                pdbe,
                new TextMessageEncodingBindingElement(
                    MessageVersion.Soap11,
                    System.Text.Encoding.UTF8),
                new HttpTransportBindingElement()),
                "");

        base.InitializeRuntime();
    }
}

9:svc文件的代码:

<%@ServiceHost language=c# Debug="true" Factory="ChatHostFactory" %>

这个也是从MSDN上抄写过来的。

读写数据库的代码其实可以完全注释掉,这个只是为了有个保存功能。整个代码还有不完善的地方,但是基本可以实现Duplex WCF的从服务器向客户端推数据的功能,保证数据实时,这也是长久也来我想了解的服务器和客户端实时通信的问题的一个解决办法。