您的位置:首页 > 编程语言

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

2011-05-16 10:35 309 查看
解决方案

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));

}

}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: