本文源代码下载地址:
github下载地址:https://github.com/lishuangquan1987/WCF-Duplexing-Chat
注意:本代码的服务不依赖于IIS,而是寄托于程序本身,下载后不需要经过任何配置可以直接运行使用
最近在研究双工通讯,之前有写过用socket编写的局域网聊天软件(下载地址:Socket编写的局域网聊天软件)于是想到用WCF的双工通讯来写一个聊天软件。想实现的需求是:
1.服务端开启服务,客户端连接服务端与服务端相互聊天
2.客户端与服务端相互传输文件
3.客户端能够群发(广播)文字和传输文件
一、首先定义接口契约:(服务端和客户端的接口契约一模一样)
namespace WCFService
{
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IChatToClient))]
public interface IChatToServer
{
[OperationContract(IsOneWay=true)]
void SendMessageToServer(string msg);
[OperationContract(IsOneWay = true)]
void SendImageToServer(MyImage image);
[OperationContract(IsOneWay = true)]
void Login(string userName);
}
public interface IChatToClient
{
[OperationContract(IsOneWay = true)]
void SendMessageToClient(string msg);
[OperationContract(IsOneWay = true)]
void SendImageToClient(MyImage image);
}
}
二、服务端实现IChatToServer接口:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ServiceModel;
using WCFService;
using System.Collections.ObjectModel;
namespace WCF_双工_Server
{
/// <summary>
/// 客户端调用的方法在服务端的实现
/// </summary>
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class ChatToServer:IChatToServer
{
public ChatToServer(System.Windows.Threading.Dispatcher mainDispatcher)
{
this.mainDispatcher = mainDispatcher;
}
private System.Windows.Threading.Dispatcher mainDispatcher;//得到主线程调度,防止跨线程操作
private static ObservableCollection<User> lstUser = new ObservableCollection<User>();
public static ObservableCollection<User> LstUser
{
get { return ChatToServer.lstUser; }
set { ChatToServer.lstUser = value; }
}
/// <summary>
/// 定义委托
/// </summary>
public delegate void Dele_Login(string key,string userName);
public delegate void Dele_ReceiveMsg(string key,string msg);
public delegate void Dele_ReceiveImage(string key,MyImage image);
public delegate void Dele_ClientClosed(object sender, EventArgs e);
/// <summary>
/// 定义事件,当客户端调用时,触发此事件来更新UI
/// </summary>
public event Dele_Login LoginEvent;
public event Dele_ReceiveMsg ReceiveMsgEvent;
public event Dele_ReceiveImage ReceiveImageEvent;
public event Dele_ClientClosed ClientClosedEvent;
public void SendMessageToServer(string msg)
{
string key = OperationContext.Current.SessionId;
if (ReceiveMsgEvent != null)
ReceiveMsgEvent(key,msg);
}
public void SendImageToServer(MyImage image)
{
string key = OperationContext.Current.SessionId;
if (ReceiveImageEvent != null)
ReceiveImageEvent(key, image);
}
/// <summary>
/// 当有客户端连接时,会调用此方法注册
/// </summary>
/// <param name="userName"></param>
public void Login(string userName)
{
IChatToClient client = OperationContext.Current.GetCallbackChannel<IChatToClient>();
string key = OperationContext.Current.SessionId;
IContextChannel chanel = OperationContext.Current.Channel;
chanel.Closed += (sender, e) => { if (ClientClosedEvent != null) ClientClosedEvent(sender, e); };
//检查是否有离线用户
var users = LstUser.Where(x => x.Chanel.State != CommunicationState.Opened);
if (users != null && users.Count() > 0)
mainDispatcher.Invoke(new Action(() => users.ToList().ForEach(x => LstUser.Remove(x))));
//将用户加入进去
User user = new User() { Client = client, Key = key, Chanel = chanel, UserName = userName };
var existUser = LstUser.Where(x => x.Key == user.Key);
if (existUser != null && existUser.Count() > 0)
return;
mainDispatcher.Invoke(new Action(()=> LstUser.Add(user)));
if (this.LoginEvent != null)
{
this.LoginEvent(key, userName);
}
}
}
}
三、客户端实现IChatToClient的回调接口:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WCFService;
namespace WCF_双工_Client
{
public class ChatToClient:IChatToClient
{
/// <summary>
/// 定义委托
/// </summary>
/// <param name="msg"></param>
public delegate void Dele_ReceiveMsg(string msg);
public delegate void Dele_ReceiveImage(MyImage image);
/// <summary>
/// 定义事件
/// </summary>
public event Dele_ReceiveMsg ReceiveMsgEvent;
public event Dele_ReceiveImage ReceiveImageEvent;
#region~实现IChatToClient
public void SendMessageToClient(string msg)
{
if (ReceiveMsgEvent != null)
ReceiveMsgEvent(msg);
}
public void SendImageToClient(MyImage image)
{
if (ReceiveImageEvent != null)
ReceiveImageEvent(image);
}
#endregion
}
}
四、服务端开启服务,客户端连接服务。具体我就不晒代码了。
本代码中加入了一些界面细节和优化,让人感觉人性化些,不过在写代码的过程中遇到如下坑:
1.聊天无障碍,传输文件超过某一大小时便无效,经过查询,加入了入下代码:对于ReaderQuotas,除了要引用System.Xml外,还要using System.Runtime.Serialization;
//必须加上,否则传输大文件有问题...
bingding.ReaderQuotas.MaxDepth = int.MaxValue;
bingding.ReaderQuotas.MaxStringContentLength = int.MaxValue;
bingding.ReaderQuotas.MaxArrayLength = int.MaxValue;
bingding.ReaderQuotas.MaxBytesPerRead = int.MaxValue;
bingding.ReaderQuotas.MaxNameTableCharCount = int.MaxValue;
2.在客户端登录服务器的时候,在服务端引发的有时候是线程上得方法,线程上得方法直接去更改ObservableCollection lstUser = new ObservableCollection()是不行的,因为它跟主界面的显示绑定在一起,所以在服务器实现类中要得到主线程调度:
public ChatToServer(System.Windows.Threading.Dispatcher mainDispatcher)
{
this.mainDispatcher = mainDispatcher;
}
本代码还有不足之处是:
1.客户端无法与客户端通讯,思路已经想好,待实现。(已经实现,点击下载)
2.当客户端异常退出时,服务器无法知道,即使加入了如下判断chanel的状态也不行:待解决.
//检查是否有离线用户
var users = LstUser.Where(x => x.Chanel.State != CommunicationState.Opened);
if (users != null && users.Count() > 0)
mainDispatcher.Invoke(new Action(() => users.ToList().ForEach(x => LstUser.Remove(x))));
完成后的图片演示:
打开服务端:
服务端开启服务成功:
打开客户端输入用户名登录:
客户端在登录:
登录成功后客户端和服务端图:
客户端准备发消息:
发送消息后:
服务器对所有好友广播消息:
服务器对所有好友发送文件,客户端提示:
客户端选择接收后:
再登录一个客户端Jack,并且Jack发了一个消息:
服务器广播消息:
服务器对Jack私聊:
Jack下线:
本文源代码下载地址:
github下载地址:https://github.com/lishuangquan1987/WCF-Duplexing-Chat