using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using static ChatServer.MessageModule;
namespace ChatServer
{
// 注意: 使用“重构”菜单上的“重命名”命令,可以同时更改代码和配置文件中的类名“ChatService”。
///
/// 设置服务器为单例,不允许创建多个ChatService对象
/// 支持多线程操作:多个客户端可以同时访问同一台服务器
///
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single
,ConcurrencyMode =ConcurrencyMode.Multiple)]
public class ChatService : IChatService
{
///
/// 保存所有在线人信息
///
List<MessageModule> messageModuleList = new List<MessageModule>();
private static object obj = new object();//用于线程互斥对象判断
///
/// 上线
///
public void Register(MessageModule loginModule)
{
lock (obj) {//线程锁:多线程访问,保证同一时刻只能有一个线程操作,保证数据的安全性
//判断是否已处于登录状态
if (!messageModuleList.Any(a=>a.SenderId==loginModule.SenderId))
{
//设置为上线模式
loginModule.MessageMode = (int)MsgMode.Online;
//保存通道:用于服务器端与客户端进行数据的交互
loginModule.CallBack = OperationContext.Current.GetCallbackChannel<ICallBack>();
messageModuleList.Add(loginModule);
//回调获取在线所有人信息
loginModule.CallBack.ToFriendList(messageModuleList);
//通知好友我上线了
BoardCast(loginModule);
}
}
}
///
/// 下线(卸载)
///
///
public void OffLine(MessageModule loginModule)
{
loginModule.MessageMode = (int)MsgMode.OffLine;//设置下线状态
//获取当前登录客户端的通道对象
ICallBack callBack= OperationContext.Current.GetCallbackChannel<ICallBack>();
lock (this) {
if (messageModuleList.Any(m=>m.CallBack== callBack)) {
MessageModule module=messageModuleList.Where(m => m.CallBack == callBack).Single<MessageModule>();
messageModuleList.Remove(module);//从在线列表中移除
BoardCast(loginModule);//通知大家
}
}
}
///
/// 广播:当某个用户登录或下线时,将通知其他好友
///
///
private void BoardCast(MessageModule loginModule) {
foreach (MessageModule module in messageModuleList) {
if (module.SenderId!=loginModule.SenderId) {//非当前登录用户
//会调用底层接口实现类的实例方法
module.CallBack.MessageNotify(loginModule);//给其他好友发送通知
}
}
}
///
/// 点对点
///
///
public void PointToPoint(MessageModule module)
{
module.MessageMode=(int) MsgMode.PointToPoint;//点对点模式
lock (obj) {
if (messageModuleList.Any(m=>m.SenderId==module.ReceiverId)) {
//获取收信人对象
MessageModule receivedModule= messageModuleList.Where(m => m.SenderId == module.ReceiverId).Single<MessageModule>();
receivedModule.CallBack.MessageNotify(module);
}
}
}
}
}
注意:这里调用回调接口中的方法(传参),实际就是客户端调用服务器端方法,然后通过通道把数据回传给客户端。
代码片段分析:
//获取调用当前操作的客户端对象的通道
OperationContext.Current.GetCallbackChannel<ICallBack>();
OperationContext.Current //获取当前线程的执行上下文对象
//泛型方法
T callBackChannel= OperationContext.Current.GetCallbackChannel(); // //获取调用当前操作的客户端对象的通道
附上微软官网链接: OperationContext执行上下文对象
[ServiceBehavior]特性:
可以在服务级别应用规则和行为,使用其属性控制行为的并发性、实例化、限流、事务、会话管理和线程等等。
这篇博客有详细介绍:ServiceBehavior特性
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single,ConcurrencyMode =ConcurrencyMode.Multiple)]
由于登录人,在其他页面可能也会使用到,作为全局数据,保存在静态类中.
添加静态类Config,用来存储项目中常用的数据
using ChatClient.ChatService;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ChatClient
{
public static class Config
{
//登录人
public static MessageModule loginModule;
}
}
LoginForm:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using DevExpress.XtraEditors;
using ChatClient.ChatService;
namespace ChatClient
{
public partial class LoginForm : DevExpress.XtraEditors.XtraForm
{
public LoginForm()
{
InitializeComponent();
}
private void btnLogin_Click(object sender, EventArgs e)
{
//调用服务器端
//ChatService.ChatServiceClient chatService = new ChatService.ChatServiceClient();
//chatService.DoWork();
Config.loginModule = new MessageModule() {
SenderId = Guid.NewGuid().ToString(),//生成唯一标识
SenderName=this.txtNickName.Text.Trim()
};
FriendListForm friendForm = new FriendListForm();
friendForm.Show();
this.Hide();
}
}
}
这边注意一下,我们会发现在引用了using ChatClient.ChatService这个namespace后可以使用服务器上的对象,其实这是由于在客户端引用了ChatServer服务。
using ChatClient.ChatService;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ChatClient
{
///
/// 处理服务器端聊天消息(回调)
///
public class ChatServiceCallBack: IChatServiceCallback//IChatServiceCallback就是服务器端的ICallBack接口
{
///
/// 定义消息通知委托事件
///
public event Action<MessageModule> MessageNotifyCallback;
//public event Func sum;
///
/// 定义获取好友列表委托事件
///
public event Action<MessageModule[]> ToFriendListCallback;
///
/// 消息通知(通用方法)
///
///
public void MessageNotify(MessageModule module)
{
if (MessageNotifyCallback!=null) {
MessageNotifyCallback(module);//调用消息通知委托事件
}
}
///
/// 获取好友列表
///
///
public void ToFriendList(MessageModule[] list)
{
if (ToFriendListCallback!=null) {
ToFriendListCallback(list);//调用获取好友列表的委托事件
}
}
}
}
服务器端回调接口为:
///
/// 回调接口:将服务器端的响应(处理)反馈给客户端
///
public interface ICallBack {//通知客户端
///
/// 消息通知
///
///
[OperationContract(IsOneWay =true)]
void MessageNotify(MessageModule module);
[OperationContract(IsOneWay =true)]
void ToFriendList(List<MessageModule> list);//通过通道传递给客户端
}
但是在客户端更新引用后,找不到这个接口。会发现在客户端引用服务后名字被自动重新生成了:
public interface IChatServiceCallback {
[System.ServiceModel.OperationContractAttribute(IsOneWay=true, Action="http://tempuri.org/IChatService/MessageNotify")]
void MessageNotify(ChatClient.ChatService.MessageModule module);
[System.ServiceModel.OperationContractAttribute(IsOneWay=true, Action="http://tempuri.org/IChatService/ToFriendList")]
void ToFriendList(ChatClient.ChatService.MessageModule[] list);
}
当用户登录之后,将跳转到好友列表页面,如果有好友在线的话,添加指定的好友到页面。
如果好友下线,则从此好友页面上移除。
双击好友则弹出聊天对话框,进行对话聊天。
先创建点对点聊天ChatForm页面:
先使用splitContainer做上下富文本框的布局划分,上下分别放一个RichTextBox控件,上面的RichTextBox控件作为双方聊天显示的内容并设为只读,下面的作为用户输入的发送信息
两个按钮:发送和关闭
页面代码:
using ChatClient.ChatService;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace ChatClient
{
public partial class ChatForm : Form
{
public ChatForm()
{
InitializeComponent();
}
MessageModule friendModule;
public ChatForm(MessageModule module)
{
InitializeComponent();
this.friendModule = module;
}
//定义发送信息的委托事件
public event Action<MessageModule> sendMessage;
//关闭聊天窗口的委托事件
public event Action<string> ChatFormClose;
///
/// 发送消息
///
///
///
private void btnSend_Click(object sender, EventArgs e)
{
this.txtMessageList.AppendText($"{Config.loginModule.SenderName}:\r{this.txtSendMessage.Text}\r");
this.txtMessageList.Select(this.txtMessageList.TextLength,0);
this.txtMessageList.ScrollToCaret();//滚动条滚动
//把信息发送到服务器
if (sendMessage!=null) {
//sendMessage(friendModule);
MessageModule messageModule = new MessageModule();
//设置发送人
messageModule.SenderId = Config.loginModule.SenderId;
messageModule.SenderName = Config.loginModule.SenderName;
messageModule.ReceiverId = friendModule.SenderId;//接收人(好友编号)
messageModule.MessageContent = this.txtSendMessage.Text;
messageModule.MessageMode = 3;//点对点聊天
sendMessage(messageModule);//调用委托事件--发送信息
this.txtSendMessage.Text = "";//清空聊天窗口
}
}
///
/// 关闭窗体
///
///
///
private void btnClose_Click(object sender, EventArgs e)
{
this.Close();
}
///
/// 窗体关闭事件
///
///
///
private void ChatForm_FormClosed(object sender, FormClosedEventArgs e)
{
if (ChatFormClose!=null) {
ChatFormClose(this.Tag.ToString());
}
}
}
}
好友列表页面功能FriendListForm的完善:
using ChatClient.ChatService;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace ChatClient
{
public partial class FriendListForm : Form
{
//定义服务器访问对象
ChatServiceClient chatServiceClient;
//用来保存好友发送来的信息
List<MessageModule> messageList = new List<MessageModule>();
//保存已打开的聊天窗口
Dictionary<string, ChatForm> dict = new Dictionary<string, ChatForm>();
public FriendListForm()
{
InitializeComponent();
}
private void FriendListForm_Load(object sender, EventArgs e)
{
this.ShowIcon = false;
this.Text += "-" + Config.loginModule.SenderName;
//实例化回调处理类
ChatServiceCallBack chatServiceCallBack = new ChatServiceCallBack();
chatServiceCallBack.MessageNotifyCallback += ChatServiceCallBack_MessageNotifyCallback;//注册消息通知委托事件
chatServiceCallBack.ToFriendListCallback += ChatServiceCallBack_ToFriendListCallback;//注册获取好友列表的委托事件
//添加服务引用生成的ChatServiceClient对象(元数据模型对象),用于调用服务端的功能
//实例化服务器端服务对象(服务端需要回传数据到客户端)
chatServiceClient =
new ChatServiceClient(new System.ServiceModel.InstanceContext(chatServiceCallBack));
//登录
chatServiceClient.Register(Config.loginModule);
}
///
/// 获取好友列表的回调方法:只有用户上线时才会调用
///
///
private void ChatServiceCallBack_ToFriendListCallback(MessageModule[] obj)
{
foreach (MessageModule module in obj)//obj:传入的是所有在线人
{
if (module.SenderId != Config.loginModule.SenderId)//非当前登录用户
{
ListViewItem item = new ListViewItem();
item.Text = module.SenderName;
item.Tag = module;
this.lvFriends.Items.Add(item);
}
}
}
///
/// 信息通知的回调方法:从服务器端回调到客户端
/// 消息模式:1.上线、 2.下线、3.点对点、4.点对面(广播、群聊)
///
///
private void ChatServiceCallBack_MessageNotifyCallback(MessageModule obj)
{
if (obj.MessageMode == 1)
{//上线:将当前登录者添加到在线的好友的页面上
ListViewItem item = new ListViewItem();
item.Text = obj.SenderName;
item.Tag = obj;
this.lvFriends.Items.Add(item);
}
else if (obj.MessageMode == 2)
{//下线:将下线的用户从其他好友的页面上移除
foreach (ListViewItem item in this.lvFriends.Items) {
MessageModule messageModule = item.Tag as MessageModule;
if (messageModule != null) {
if (messageModule.SenderId == obj.SenderId)//校验是否是当前下线用户
{
this.lvFriends.Items.Remove(item);//从好友列表中移除
break;
}
}
}
} else if (obj.MessageMode==3) {//点对点聊天
setMessage(obj);
}
}
///
/// 聊天
///
///
private void setMessage(MessageModule module) {
messageList.Add(module);//添加到消息列表
if (dict.Keys.Contains(module.SenderId)) {//判断窗口是否已经打开
ChatForm chatForm = dict[module.SenderId];
chatForm.txtMessageList.AppendText($"{module.SenderName}:\r{module.MessageContent}\r");
module.IsRead = 1;//设置为已读
}
}
///
/// 关闭窗体
///
///
///
private void FriendListForm_FormClosed(object sender, FormClosedEventArgs e)
{
//调用服务器端下线
chatServiceClient.OffLine(Config.loginModule);
Application.Exit();
}
///
/// 鼠标双击事件:点击某个好友时触发
///
///
///
private void lvFriends_MouseDoubleClick(object sender, MouseEventArgs e)
{
ListViewHitTestInfo info= this.lvFriends.HitTest(new Point(e.X,e.Y));
if (info!=null) {
MessageModule module=info.Item.Tag as MessageModule;//获取选中的好友
ChatForm cf = new ChatForm(module);
cf.Tag = module.SenderId;
cf.sendMessage += Cf_sendMessage;//注册发送信息的委托事件
cf.ChatFormClose += Cf_ChatFormClose;//注册关闭聊天窗口的委托事件
cf.Show();
dict.Add(module.SenderId,cf);//保存已打开的窗口
//点击好友打开对话框后,追加未读取的好友发送来的信息
List<MessageModule> friendModuleList= messageList.Where(m => m.SenderId == module.SenderId&&m.IsRead==0).ToList();
foreach (MessageModule friendModule in friendModuleList) {
//WinForm上添加的控件,默认都是private,这里将第一个富文本框权限改为public,可以在其他类中直接使用
cf.txtMessageList.AppendText($"{friendModule.SenderName}:\r{friendModule.MessageContent}\r");
friendModule.IsRead = 1; // 已读
}
}
}
/// ry>
///
private void Cf_sendMessage(MessageModule obj)
{
chatServiceClient.PointToPoint(obj);//发送消息到服务器
}
}
}
业务逻辑分析:用户的上线和下线,无非就是
客户端ChatClient调用服务器端ChatService的上线和下线功能,然后在服务器端通过管道回调客户端(发送通知给其他好友、获取当前登录人的其他好友列表)
回调到客户端后(调用的是ChatServiceCallBack的MessageNotify和ToFriendList方法),
在方法内再调用对应的委托事件,把参数回调到注册委托事件的对象的回调函数中,
然后在此对象中的回调函数中根据接收的参数再做处理
代码片段分析:
//把信息发送到服务器
if (sendMessage!=null) {
//sendMessage(friendModule);
MessageModule messageModule = new MessageModule();
//设置发送人
messageModule.SenderId = Config.loginModule.SenderId;
messageModule.SenderName = Config.loginModule.SenderName;
messageModule.ReceiverId = friendModule.SenderId;//接收人(好友编号)
messageModule.MessageContent = this.txtSendMessage.Text;
messageModule.MessageMode = 3;//点对点聊天
sendMessage(messageModule);//调用委托事件--发送信息
this.txtSendMessage.Text = "";//清空聊天窗口
}
//获取当前选中好友发送来的未读信息,
//messageList中的每个元素封装的是发送的信息(发送人,收信人,内容等等)
//需要去辨别发送的信息列表中对象的SenderId是否等于当前选中好友的SenderId(他作为发送者,你作为接收者)
List<MessageModule> friendModuleList= messageList.Where(m => m.SenderId == module.SenderId&&m.IsRead==0).ToList();
//实例化服务器端服务对象
chatServiceClient = new ChatServiceClient(new System.ServiceModel.InstanceContext(chatServiceCallBack));
ChatServiceClient是在客户端添加服务引用之后自动生成的客户端服务代理对象,通过它可以访问服务器并调用指定的方法,生成的代理类为:服务名称 +Client
这里没有空参的构造器是由于IChatServer服务接口的服务契约要求有回调接口,
生成的代理类的构造器需要传递一个InstanceContext对象,
附上微软官网关于此类:InstanceContext类的构造器
这里我们使用第一个构造器,但是要求传入的对象需要实现服务,我们这里传入的是回调接口ICallBack的实现子类ChatServiceCallBack对象,注意:ICallBack没有指定的服务锲约,但是我们在IChatService服务接口的服务契约中,添加了回调契约属性类型并指定其类型为ICallback,那么对应接口以及接口子类也就实现了服务
**
**
单独设置服务,则回调将为单独的服务。
我们经常在服务接口上配置回调契约,设置双工通道时的回调接口类型,这样的话在客户端通过客户端服务代理对象调用服务时,需要传入回调对象,然后在服务器端可以通过通道回传数据给客户端。
OperationContext.Current.GetCallbackChannel<ICallBack>()
获取当前客户端调用服务的通道对象,然后可以在服务端通过此通道回传数据给客户端
[ServiceContract(CallbackContract =typeof(ICallBack))]
public interface IChatService
{
................................
}
生成的代理类:
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
public partial class ChatServiceClient : System.ServiceModel.DuplexClientBase<ChatClient.ChatService.IChatService>, ChatClient.ChatService.IChatService {
public ChatServiceClient(System.ServiceModel.InstanceContext callbackInstance) :
base(callbackInstance) {
}
public ChatServiceClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName) :
base(callbackInstance, endpointConfigurationName) {
}
public ChatServiceClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress) :
base(callbackInstance, endpointConfigurationName, remoteAddress) {
}
public ChatServiceClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) :
base(callbackInstance, endpointConfigurationName, remoteAddress) {
}
public ChatServiceClient(System.ServiceModel.InstanceContext callbackInstance, System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
base(callbackInstance, binding, remoteAddress) {
}
代码逻辑的分析:
我们在ChatServiceCallBack这个回调接口的实现类中添加了两个委托事件:
public event Action<MessageModule> MessageNotifyCallback;
public event Action<MessageModule[]> ToFriendListCallback;
这里简单介绍一下委托事件的用法:
使用回调,可以把一个函数返回报告给另一个函数,.NET中的委托支持回调。
我们这里使用的是.NET内置的泛型委托:Action<>和Func<>
许多情况下,我们只需要接受一组参数并返回一个值(void)的委托。
可以使用泛型委托。
可以指向多至16参数,并返回void的方法
For Example:
//Action委托的目标
static void DisplayMessage(string msg, ConsoleColor txtColor, int printCount)
{
ConsoleColor previous = Console.ForegroundColor;
Console.ForegroundColor = txtColor;
for (int i=0;i<printCount;i++) {
Console.WriteLine(msg);
}
//重置颜色
Console.ForegroundColor = previous;
}
static void main(string[] args) {
Console.WriteLine("****** Fun with Acton and Func");
Action<string, ConsoleColor, int> actionTarget = new Action<string, ConsoleColor, int>(DisplayMessage);
//调用委托-->调用委托指向的方法
actionTarget("ACtion Message!",ConsoleColor.Yellow,5);
}
可以指向多至16个参数,并具有自定义返回值的方法,
注意: Func<>的最后一个类型参数总是方法的返回值
For Example:
//Func委托的目标
static int Add(int x,int y){
return x+y;
}
//在main方法里,调用委托
static void main(string[] args) {
Func<int,int,int> funcTarget=new Func<int,int,int>(Add);
//调用委托
int result=funcTarget(10,20);//两个输入参数,一个返回值
Console.WriteLine("result:"+result);
}
为了简化自定义方法的构建,为委托调用列表增加和删除方法,C#提供event关键字。
在编译器处理event关键字的时候,它会自动提供注册和注销方法以及任何必要的委托类型成员变量。
所以可以在定义的委托对象前添加event关键字–>委托事件
监听传入的事件:
调用者仅需使用’+="和’-='操作符,来注册和注销事件,
写完操作符,按下Tab键,注册成功会自动生成回调函数
For Example:
//实例化回调处理类
ChatServiceCallBack chatServiceCallBack = new ChatServiceCallBack();
chatServiceCallBack.MessageNotifyCallback += ChatServiceCallBack_MessageNotifyCallback;//注册消息通知委托事件
chatServiceCallBack.ToFriendListCallback += ChatServiceCallBack_ToFriendListCallback;//注册获取好友列表的委托事件
//对应的回调方法:
///
/// 获取好友列表的回调方法
///
///
private void ChatServiceCallBack_ToFriendListCallback(MessageModule[] obj)
{
...............................
}
private void ChatServiceCallBack_MessageNotifyCallback(MessageModule obj)
{
.................................
}
委托的好处在于,跨对象之间数据的回调传递
比如:Winform项目中跨关联页面数据的互相传输,可以在A页面里定义委托事件,然后在B页面注册此委托事件,在A页面里调用完此委托事件之后,会回到B页面执行对应的回调函数
注意:调用委托事件之前,务必判空
想要整个项目源码的朋友,请给我留下email地址!