作者:hzqghost.cnblogs.com
本例使用了 Nikola Paljetak 的聊天示例,原始下载 http://www.ms.phy.hr/wcfchat/
解决方案
ChatService 服务端主要的三个文件:App.config,ChatService.cs,Program.cs
FormChatClient 客户端主要二个文件: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)); } } }