WCF 聊天室程序代码详细讲解教程

    技术2022-05-19  20

    解决方案ChatService 服务端主要的三个文件:App.config,ChatService.cs,Program.csFormChatClient 客户端主要二个文件:App.config,ChatForm.cs以下为这五个文件的全部代码及讲解,因为打算放在一篇文章里,所以本文会很长。发由本教程目的并不仅仅让初学者了解怎么开发一个聊天室。而是要通过这个例子加深对C#及WCF一些实用特性的了解。1 Service App.config

     

    <xml version="1.0" encoding="utf-8" > <configuration>   <appSettings>     <!--提供服务的通信协议、地址、端口、目录-->     <!--通信协议:net.tcp 、http 、-->     <add key="addr" value="net.tcp://localhost:22222/chatservice" />   </appSettings>   <system.serviceModel>     <services>       <!--服务名 = <命名空间>.<程序集名称>-->       <!--behaviorConfiguration 性能配置自定一个名称,<serviceBehaviors> 下的项对应此名称-->       <service name="NikeSoftChat.ChatService" behaviorConfiguration="MyBehavior">         <!--终节点-->         <!--binding 绑定类型,NetTcpBinding、WSDualHttpBinding、WSHttpBindingBase、BasicHttpBinding、NetNamedPipeBinding、NetPeerTcpBinding、MsmqBindingBase、NetPeerTcpBinding、WebHttpBinding、MailBindingBase、CustomBinding-->         <!--DuplexBinding 双工-->         <!--使用契约:<命名空间>.<接口名称>-->         <endpoint address=""                   binding="netTcpBinding"                   bindingConfiguration="DuplexBinding"                   contract="NikeSoftChat.IChat" />       </service>     </services>     <behaviors>       <serviceBehaviors>         <behavior  name="MyBehavior">           <!--会话最大数量-->           <serviceThrottling maxConcurrentSessions="10000" />         </behavior>       </serviceBehaviors>     </behaviors>     <bindings>       <netTcpBinding>         <!--双工,超时设置-->         <binding name="DuplexBinding" sendTimeout="00:00:01">           <!--可靠会话-->           <reliableSession enabled="true" />           <!--安全模式-->           <security mode="None" />         </binding>       </netTcpBinding>     </bindings>   </system.serviceModel> </configuration>  

     

    2 Service ChatService.cs

     

    // Copyright (C) 2006 by Nikola Paljetak using System; using System.Collections; using System.Collections.Generic; using System.ServiceModel; namespace NikeSoftChat {     //服务契约     // SessionMode.Required  允许Session会话     // 双工协定时的回调协定类型为IChatCallback接口)     [ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IChatCallback))]     interface IChat     {         //服务操作         // IsOneWay = false      等待服务器完成对方法处理         // IsInitiating = true  启动Session会话         // IsTerminating = false 设置服务器发送回复后不关闭会话         [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);         //服务操作         // IsOneWay = true      不等待服务器完成对方法处理         // IsInitiating = false  不启动Session会话         // IsTerminating = true  关闭会话,在服务器发送回复后         [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);     }     //定义一个客户端动作的枚举     public enum MessageType { Receive, UserEnter, UserLeave, ReceiveWhisper };     //定义一个本例的事件消息类     public class ChatEventArgs : EventArgs     {         public MessageType msgType;         public string name;         public string message;     }     // InstanceContextMode.PerSession 服务器为每个客户会话创建一个新的上下文对象     // ConcurrencyMode.Multiple      异步的多线程实例     [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple)]     public class ChatService : IChat //继承IChat接口或者说IChat的实现类     {         //定义一个静态对象用于线程部份代码块的锁定,用于lock操作         private static Object syncObj = new Object();         //创建一个IChatCallback 回调接口实例,接口成员始终是公共的,所有没有访问修饰符         IChatCallback callback = null;         //定义一个委托         public delegate void ChatEventHandler(object sender, ChatEventArgs e);         //定义一个静态的委托事件         public static event ChatEventHandler ChatEvent;         //创建一个静态Dictionary(表示键和值)集合(字典),用于记录在线成员,Dictionary<(Of <(TKey, TValue>)>) 泛型类         static Dictionary<string, ChatEventHandler> chatters = new Dictionary<string, ChatEventHandler>();         //当用客户的昵称         private string name;         //创建委托(ChatEventHandler)的一个空实例         private ChatEventHandler myEventHandler = null;         //成员进入聊天室         public string[] Join(string name)         {             bool userAdded = false;             //用MyEventHandler方法,实例化委托(ChatEventHandler)             myEventHandler = new ChatEventHandler(MyEventHandler);             //锁定,保持lock块中的代码段始终只有一个线程在调用,原因是ConcurrencyMode.Multiple 为异步的多线程实例,存在并发竞争问题             //如果不锁定,则静态成员字典chatters.ContainsKey(name) 的结果将会不确定,原因是每个线程都可以访问到它。以下凡是chatters 的操作匀加锁             //使用lock多个线程同时请示时,没有操作权的将会在线程池中等待至有操作权的线程执完成。lock 方法存在影响吞吐量的问题             lock (syncObj)             {                 //如果请求的昵称在成员字典中不存在并不空                 if (!chatters.ContainsKey(name) && name != "" && name != null)                 {                     this.name = name; //记录当前线程昵称                     chatters.Add(name, MyEventHandler);//加入到成员字典key 为当前昵称,MyEventHandler 当前的委托调用                     userAdded = true;                 }             }             if (userAdded)             {                 //获取当前操作客户端实例的通道给IChatCallback接口的实例callback,                 //此通道是一个定义为IChatCallback类型的泛类型                 //通道的类型是事先服务契约协定好的双工机制(见IChat前的ServiceContract)                 callback = OperationContext.Current.GetCallbackChannel<IChatCallback>();                 //实例化事件消息类ChatEventArgs,并对其赋值                 ChatEventArgs e = new ChatEventArgs();                 e.msgType = MessageType.UserEnter;                 e.name = name;                 //发送广播信息                 BroadcastMessage(e);                 //加入到多路广播委托的调用列表中,下面这条如果和上面一条位置互换,那么会收到自己进入聊天室的广播信息。                 ChatEvent += myEventHandler;                 //以下代码返回当前进入聊天室成员的称列表                 string[] list = new string[chatters.Count];                 lock (syncObj)                 {                     chatters.Keys.CopyTo(list, 0);//从成员字典索引0 开始复制chatters成员字典的key 值到list 字符串数组                 }                 return list;             }             else             {                 //当昵称重复或为空是,如果客户端做了为空检测,则可直接认为是名称重复,当前要在没有异常的情况下。                 return null;             }         }         //聊天室通信         public void Say(string msg)         {             ChatEventArgs e = new ChatEventArgs();             e.msgType = MessageType.Receive;             e.name = this.name;             e.message = msg;             BroadcastMessage(e);         }         //私有对话         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)             {                 //访问集合中元素的键与集合中的任何键都不匹配时所引发的异常             }         }         //成员离开聊天室         public void Leave()         {             if (this.name == null)                 return;             //删除成员字典中的当前会话的成员,及删除多路广播委托的调用列表中的当前调用             //name 和myEventHandler 的生存周期是在当前会话中一直存在的,参考Session 周期             lock (syncObj)             {                 chatters.Remove(this.name);             }             ChatEvent -= myEventHandler;             ChatEventArgs e = new ChatEventArgs();             e.msgType = MessageType.UserLeave;             e.name = this.name;             this.name = null;             BroadcastMessage(e);         }         //回调         //根据客户端动作通知对应客户端执行对应的操作         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 //异常退出,或超时,或session过期             {                 Leave();             }         }         //发送广播信息         //要点:根据上下文理解: 1 广播什么(what),2 为谁广播(who),3“谁”从哪来(where),4 如何来的(how)         private void BroadcastMessage(ChatEventArgs e)         {             //创建回调委托事件实例ChatEvent的一个副本,之所以用副本是因为ChatEvent处于多线程并状态(?此处不知理解是否正确,因为我理解后面的handler 是一个引用,自相矛盾了)             ChatEventHandler temp = ChatEvent;             if (temp != null)             {                 //GetInvocationList方法,按照调用顺序返回“多路广播委托(MulticastDelegate)”的调用列表                 foreach (ChatEventHandler handler in temp.GetInvocationList())                 {                     //异步方式调用多路广播委托的调用列表中的ChatEventHandler                     //BeginInvoke方法异步调用,即不等等执行,详细说明则是:公共语言运行库(CLR) 将对请求进行排队并立即返回到调用方。将对来自线程池的线程调用该目标方法。                     //EndAsync 为线程异步调用完成的回调方法,EndAsync 接收并操持着线程异步调用的操作状态,可通过此结果找到调用者,如此例handler,handler是一个委托实例的引用                     //        此状态为调用者(委托)的事件声明类型此例为public event ChatEventHandler ChatEvent; 中的ChatEventHandler                     //最后一个参数:包含的对象的状态信息,传递给委托;                     handler.BeginInvoke(this, e,EndAsync, null);                 }             }         }         //广播中线程调用完成的回调方法         //功能:清除异常多路广播委托的调用列表中异常对象(空对象)         private void EndAsync(IAsyncResult ar)         {             ChatEventHandler d = null;             try             {                 //封装异步委托上的异步操作结果                 System.Runtime.Remoting.Messaging.AsyncResult asres = (System.Runtime.Remoting.Messaging.AsyncResult)ar;                 //asres.AsyncDelegate 获取在异步调用asres 的委托对象,asres 来自对ar 的AsyncResult 封装,ar 来自线程异步调用的操作状态                 d = ((ChatEventHandler)asres.AsyncDelegate);                 //EndInvoke 返回由异步操作ar 生成的结果Object                 d.EndInvoke(ar);             }             catch             {                 ChatEvent -= d;             }         }     } }  

     

    3 Service Program.cs

     

    // Copyright (C) 2006 by Nikola Paljetak using System; using System.Collections.Generic; using System.Text; using System.ServiceModel; using System.Configuration; namespace NikeSoftChat {     class Program     {         static void Main(string[] args)         {             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();         }           } }  

     

    4 Client  App.config

     

    <xml version="1.0" encoding="utf-8"> <!--这里的说明可以完全参考 service 的 app.config --> <configuration>   <system.serviceModel>     <client>       <endpoint name=""                 address="net.tcp://localhost:22222/chatservice"                 binding="netTcpBinding"                 bindingConfiguration="DuplexBinding"                 contract="IChat" />     </client>     <bindings>       <netTcpBinding>         <!--这里的sendTimeout比服务端的要多出4秒,因为服务端不参入具体通信,它只是提供服务-->         <binding name="DuplexBinding" sendTimeout="00:00:05" >           <reliableSession enabled="true" />           <security mode="None" />         </binding>       </netTcpBinding>     </bindings>   </system.serviceModel> </configuration>  

     

    5 Client ChatForm.cs

     

    // Copyright (C) 2006 by Nikola Paljetak using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Runtime.InteropServices; using System.ServiceModel; namespace NikeSoftChat {     public partial class ChatForm : Form, IChatCallback     {         [DllImport("user32.dll")]         private static extern int SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam);         private const int WM_VSCROLL = 0x115;         private const int SB_BOTTOM = 7;         private int lastSelectedIndex = -1;         private ChatProxy proxy;//代理         private string myNick;  //当前我的昵称         private PleaseWaitDialog pwDlg; //状态窗口(显示等待与错误提示)         private delegate void HandleDelegate(string[] list);//委托         private delegate void HandleErrorDelegate();//委托         public ChatForm()         {             InitializeComponent();             ShowConnectMenuItem(true);         }         private void connectToolStripMenuItem_Click(object sender, EventArgs e)         {             lstChatters.Items.Clear();             NickDialog nickDlg = new NickDialog();             if (nickDlg.ShowDialog() == DialogResult.OK)             {                 myNick = nickDlg.txtNick.Text;// 得到键入的当前昵称                 nickDlg.Close();             }             txtMessage.Focus();             Application.DoEvents(); //强制处理当前队列的所有windows 消息,以名影响后面的通信,winForm程序要注意一下UI 线程的问题             //获取服务实例的上下文,为指定主机承载的服务初始化             InstanceContext site = new InstanceContext(this);             proxy = new ChatProxy(site); //初始服务实例             //BeginJoin 是svcutil工具自动生成的(如果你用工具的话,当然你也可以自己写),还有一个EndJoin 也是。             //为什么会生成这两个我们并没有在服务契约中定义的接口呢?原因是服务契约中Join 接口定义了IsOneWay = false             //IsOneWay = false 则我们在配置中绑定的是duplex 双工(双通道),指操作返回应答信息。             //duplex 双工并不会等待调用服务方法完成,而是立即返回。单工方式则为Request/Reply 本例中没有涉及             //自动生成BeginJoin 会比我们的Join 多两个参数,一个用来当BeginJoin请求在服务方完成后本地回调的方法,另一个获取作为BeginInvoke 方法调用的最后一个参数而提供的对象。             //BeginJoin 会请求服务方执行Join 方法。当Join代码执行完毕,触发回调方法OnEndJoin             IAsyncResult iar = proxy.BeginJoin(myNick, new AsyncCallback(OnEndJoin),null);             pwDlg = new PleaseWaitDialog();             pwDlg.ShowDialog();         }         private void OnEndJoin(IAsyncResult iar)         {             try             {                 //EndJoin 请求服务方返回Join 执行后的返回值                 //iar 异步调用的操作状态                 //返回聊天室当前在线成员列表                 string[] list = proxy.EndJoin(iar);                 HandleEndJoin(list);             }             catch (Exception e)             {                 HandleEndJoinError();             }         }         private void HandleEndJoinError()         {             //判断状态提示窗口是否在同一个线程内             if (pwDlg.InvokeRequired)                 //对当前线程调用目标方法,此例调用本身                 pwDlg.Invoke(new HandleErrorDelegate(HandleEndJoinError));             else             {                 pwDlg.ShowError("Error: Cannot connect to chat!");                 ExitChatSession();             }         }         //生成在线成员列表,当参数list为空时则表示当前昵称在在线成员列表中已存在         private void HandleEndJoin(string[] list)         {             //状态提示窗口是否运行在同一个线程             if (pwDlg.InvokeRequired)                 pwDlg.Invoke(new HandleDelegate(HandleEndJoin), new object[] { list });             else             {                 if (list == null)                 {                     pwDlg.ShowError("Error: Username already exist!");                     ExitChatSession();                 }                 else                 {                     pwDlg.Close();                     ShowConnectMenuItem(false);                     foreach (string name in list)                     {                         lstChatters.Items.Add(name);                     }                     AppendText("Connected at " + DateTime.Now.ToString() + " with user name " + myNick + Environment.NewLine);                 }             }         }         //发送信息         private void SayAndClear(string to, string msg, bool pvt)         {             if (msg != "")             {                 try                 {                     CommunicationState cs = proxy.State;                     //pvt 公聊还是私聊                     if (!pvt)                         proxy.Say(msg); //发送信息                     else                         proxy.Whisper(to, msg);//对指定者发送信息                     txtMessage.Text = "";                 }                 catch                 {                     AbortProxyAndUpdateUI();                     AppendText("Disconnected at " + DateTime.Now.ToString() + Environment.NewLine);                     Error("Error: Connection to chat server lost!");                 }             }         }         private void Error(string errMessage)         {             MessageBox.Show(errMessage, "Connection error", MessageBoxButtons.OK, MessageBoxIcon.Error);             ExitChatSession();         }         private void btnSay_Click(object sender, EventArgs e)         {             SayAndClear("", txtMessage.Text, false);             txtMessage.Focus();         }         private void btnWhisper_Click(object sender, EventArgs e)         {             if (txtMessage.Text == "")                 return;             object to = lstChatters.SelectedItem;             if (to != null)             {                 string receiverName = (string)to;                 AppendText("Whisper to " + receiverName + ": " + txtMessage.Text + Environment.NewLine);                 SayAndClear(receiverName, txtMessage.Text, true);                 txtMessage.Focus();             }         }         private void disconnectToolStripMenuItem_Click(object sender, EventArgs e)         {             ExitChatSession();             btnWhisper.Enabled = false;             AppendText("Disconnected at " + DateTime.Now.ToString() + Environment.NewLine);         }         #region IChatCallback 实现接口成员         //接收公聊时         public void Receive(string senderName, string message)         {             AppendText(senderName + ": " + message + Environment.NewLine);         }         //接收私聊时         public void ReceiveWhisper(string senderName, string message)         {             AppendText(senderName + " whisper: " + message + Environment.NewLine);         }         //其他人进入聊天室时         public void UserEnter(string name)         {             AppendText("User " + name + " enter at " + DateTime.Now.ToString() + Environment.NewLine);             lstChatters.Items.Add(name);         }         //其他人退出聊天室时         public void UserLeave(string name)         {             AppendText("User " + name + " leave at " + DateTime.Now.ToString() + Environment.NewLine);             lstChatters.Items.Remove(name);             AdjustWhisperButton();         }         #endregion         private void AppendText(string text)         {             txtChatText.Text += text;             //txtChatText 滚动条始终定位于最下面             SendMessage(txtChatText.Handle, WM_VSCROLL, SB_BOTTOM, new IntPtr(0));         }         //菜单项"连接"与"断开"的显/隐控制()         private void ShowConnectMenuItem(bool show)         {             connectToolStripMenuItem.Enabled = show;             disconnectToolStripMenuItem.Enabled = btnSay.Enabled = !show;         }         private void txtMessage_KeyDown(object sender, KeyEventArgs e)         {             if (e.KeyData == Keys.Enter && btnSay.Enabled)             {                 SayAndClear("", txtMessage.Text, false);             }         }         private void exitToolStripMenuItem_Click(object sender, EventArgs e)         {             ExitChatSession();             ExitApplication();         }         private void ChatForm_FormClosed(object sender, FormClosedEventArgs e)         {             ExitChatSession();             ExitApplication();         }         //退出聊天室会话         private void ExitChatSession()         {             try             {                 //离开通知                 proxy.Leave();             }             catch { }             finally             {                              AbortProxyAndUpdateUI();             }         }         //中断代理并更新UI         private void AbortProxyAndUpdateUI()         {             if (proxy != null)             {                 proxy.Abort();                 proxy.Close();                 proxy = null;             }             ShowConnectMenuItem(true);         }         private void ExitApplication()         {             Application.Exit();         }         private void txtMessage_KeyPress(object sender, KeyPressEventArgs e)         {             if (e.KeyChar == 13)             {                 e.Handled = true;                 btnSay.PerformClick();             }         }         private void lstChatters_SelectedIndexChanged(object sender, EventArgs e)         {             AdjustWhisperButton();         }         private void AdjustWhisperButton()         {             if (lstChatters.SelectedIndex == lastSelectedIndex)             {                 lstChatters.SelectedIndex = -1;                 lastSelectedIndex = -1;                 btnWhisper.Enabled = false;             }             else             {                 btnWhisper.Enabled = true;                 lastSelectedIndex = lstChatters.SelectedIndex;             }             txtMessage.Focus();         }         private void ChatForm_Resize(object sender, EventArgs e)         {             //txtChatText 滚动条始终定位于最下面             SendMessage(txtChatText.Handle, WM_VSCROLL, SB_BOTTOM, new IntPtr(0));         }     } }  


    最新回复(0)