前面吹了不少重點知識了,為了可以較為綜合地運用它們,今天,我們來做一個可以群聊的應用,就像QQ群那樣,一個服務器端,N個客戶端,服務器端運行後,每個客戶端啟動的時候會自動連接服務器生成會話,只要其中任一個客戶端向服務器發送消息,服務器都會將消息群發到所有客戶端。 我們來看看如何用WCF來取代Socket。 這個例子會用到以下知識點: 在進程中承載WCF服務。 會話的使用。 回調。 在下面說明過程中,我不會粘貼所有代碼,畢竟有點長,我只放出重要部分,隨後我會將源碼上傳到【資源】。 一、服務協定和回調協定。 [csharp] [ServiceContract(CallbackContract = typeof(ICallBack), SessionMode = SessionMode.Required)] public interface IService { [OperationContract(IsOneWay = true, IsInitiating = true, IsTerminating = false)] void Begin(); [OperationContract(IsOneWay = true)] void SendMessage(string nick, string msg, DateTime sendTime); [OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = true)] void End(); } public interface ICallBack { [OperationContract(IsOneWay = true)] void SendToClients(string nick, string msg, DateTime sendTime); } [ServiceContract(CallbackContract = typeof(ICallBack), SessionMode = SessionMode.Required)] public interface IService { [OperationContract(IsOneWay = true, IsInitiating = true, IsTerminating = false)] void Begin(); [OperationContract(IsOneWay = true)] void SendMessage(string nick, string msg, DateTime sendTime); [OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = true)] void End(); } public interface ICallBack { [OperationContract(IsOneWay = true)] void SendToClients(string nick, string msg, DateTime sendTime); }Begin方法和End方法分別是啟動會話和終止會話,這樣,每接入一個客戶端連接就會實例化一個服務類(前面文章中提過了),這樣就可以確保每個客戶端都與服務器維持一個會話。 向服務器發送的消息包括用戶的昵稱、消息內容和發送時間。而回調協定中的方法也是這幾個參數,但不同的是,服務協定是客戶端調用服務器端,而回調中是服務器端調用客戶端。 二、實現服務類。 [csharp] [ServiceBehavior(IncludeExceptionDetailInFaults = true, InstanceContextMode = InstanceContextMode.PerSession)] public class MyService : IService { public static Dictionary<string, ICallBack> ClientCallbacks = new Dictionary<string, ICallBack>(); public void Begin() { string sessionID = OperationContext.Current.SessionId; ICallBack cb = OperationContext.Current.GetCallbackChannel<ICallBack>(); MyService.ClientCallbacks[sessionID] = cb; } public void SendMessage(string nick, string msg, DateTime sendTime) { foreach (ICallBack c in MyService.ClientCallbacks.Values.ToArray()) { if (c != null) { c.SendToClients(nick, msg, sendTime); } } } public void End() { string sessionID = OperationContext.Current.SessionId; if (MyService.ClientCallbacks.ContainsKey(sessionID)) { MyService.ClientCallbacks.Remove(sessionID); } } } [ServiceBehavior(IncludeExceptionDetailInFaults = true, InstanceContextMode = InstanceContextMode.PerSession)] public class MyService : IService { public static Dictionary<string, ICallBack> ClientCallbacks = new Dictionary<string, ICallBack>(); public void Begin() { string sessionID = OperationContext.Current.SessionId; ICallBack cb = OperationContext.Current.GetCallbackChannel<ICallBack>(); MyService.ClientCallbacks[sessionID] = cb; } public void SendMessage(string nick, string msg, DateTime sendTime) { foreach (ICallBack c in MyService.ClientCallbacks.Values.ToArray()) { if (c != null) { c.SendToClients(nick, msg, sendTime); } } } public void End() { string sessionID = OperationContext.Current.SessionId; if (MyService.ClientCallbacks.ContainsKey(sessionID)) { MyService.ClientCallbacks.Remove(sessionID); } } }我本來在代碼中寫了注釋的,不過剛剛刪了,我希望大家能在沒有注釋的前提下看懂代碼。 在類中,聲明了一個靜態的Dictionary<string, ICallBack>,這是一個字典,我想大家猜到了,靜態變量是基於類的,與實例無關,我們可以把它當作全局數據,在字典集合中保存所有接入客戶端的回調,由於每個會話的ID是唯一的,因此,用SessionID作為Key是比較好操作的。 a、在Begin方法調用時,我們把傳入的客戶端的會話ID和回調存進字典中,在End方法調用時,說明會話要終結,這時候把對應的會話ID和回調從字典中刪除。 b、在SendMessage方法調用時,從字典中取出所有回調,並把接收的參數傳給回調方法,在回調調用後,這些消息就會轉發到所有連接的客戶端。 這樣就能實現群聊了。 三、建立服務器。 [csharp] static void Main(string[] args) { Console.Title = "WCF服務器端"; Uri baseAddr = new Uri("http://127.0.0.1:2713/wcfsv"); using (ServiceHost host = new ServiceHost(typeof(MyService), baseAddr)) { NetTcpBinding binding = new NetTcpBinding(); // 不需要任何安全驗證 binding.Security.Mode = SecurityMode.None; // 添加終結點 host.AddServiceEndpoint(typeof(IService), binding, "net.tcp://127.0.0.1:1736/channel"); // 元數據 ServiceMetadataBehavior mb = new ServiceMetadataBehavior(); mb.HttpGetEnabled = true; mb.HttpGetUrl = new Uri("http://127.0.0.1:87/WSDL"); host.Description.Behaviors.Add(mb); host.Opened += (source, arg) => { Console.WriteLine("服務已經啟動。"); }; // 打開服務 try { host.Open(); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadKey(); } } static void Main(string[] args) { Console.Title = "WCF服務器端"; Uri baseAddr = new Uri("http://127.0.0.1:2713/wcfsv"); using (ServiceHost host = new ServiceHost(typeof(MyService), baseAddr)) { NetTcpBinding binding = new NetTcpBinding(); // 不需要任何安全驗證 binding.Security.Mode = SecurityMode.None; // 添加終結點 host.AddServiceEndpoint(typeof(IService), binding, "net.tcp://127.0.0.1:1736/channel"); // 元數據 ServiceMetadataBehavior mb = new ServiceMetadataBehavior(); mb.HttpGetEnabled = true; mb.HttpGetUrl = new Uri("http://127.0.0.1:87/WSDL"); host.Description.Behaviors.Add(mb); host.Opened += (source, arg) => { Console.WriteLine("服務已經啟動。"); }; // 打開服務 try { host.Open(); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadKey(); } }因為TCP支持會話,而且傳輸速度快,所以終結點使用net.tcp綁定。 四、在客戶端實現回調協定。 首先在客戶端引用服務,然後實現回調協定接口。 [csharp] public class MyCallBack : WS.IServiceCallback { public void SendToClients(string nick, string msg, DateTime sendTime) { if (this.MessageReceived != null) { CallbackRecEventArgs arg = new CallbackRecEventArgs(nick, msg, sendTime); this.MessageReceived(this, arg); } } // 事件 public event EventHandler<CallbackRecEventArgs> MessageReceived; } public class MyCallBack : WS.IServiceCallback { public void SendToClients(string nick, string msg, DateTime sendTime) { if (this.MessageReceived != null) { CallbackRecEventArgs arg = new CallbackRecEventArgs(nick, msg, sendTime); this.MessageReceived(this, arg); } } // 事件 public event EventHandler<CallbackRecEventArgs> MessageReceived; } 還有一個事件參數類。 [csharp] public class CallbackRecEventArgs : EventArgs { string _Nick, _Msg; DateTime _time; public CallbackRecEventArgs(string nk, string m, DateTime t) { _Nick = nk; _Msg = m; _time = t; } public string Nick { get { return _Nick; } } public string MessageCont { get { return _Msg; } } public DateTime SendTime { get { return _time; } } } public class CallbackRecEventArgs : EventArgs { string _Nick, _Msg; DateTime _time; public CallbackRecEventArgs(string nk, string m, DateTime t) { _Nick = nk; _Msg = m; _time = t; } public string Nick { get { return _Nick; } } public string MessageCont { get { return _Msg; } } public DateTime SendTime { get { return _time; } } } 因為回調中的方法是服務器凋用的,記住了,它不是客戶端調用的。在客戶要想及時偵聽到該方法被調用,可以使用事件,當回調方法被調用,就會觸發事件,而事件的另一個發處是,它可以在其他類中定義處理方法。 比如這樣。 [csharp] cb = new MyCallBack(); cb.MessageReceived += cb_MessageReceived; cb = new MyCallBack(); cb.MessageReceived += cb_MessageReceived; [csharp] view plaincopyprint? void cb_MessageReceived(object sender, CallbackRecEventArgs e) { MessageEnt msg = new MessageEnt(); msg.NickName = e.Nick; msg.Message = e.MessageCont; msg.Time = e.SendTime; if (e.Nick == MyNickName) { msg.IsMe = true; } else { msg.IsMe = false; } this.dataSource.Add(msg); } void cb_MessageReceived(object sender, CallbackRecEventArgs e) { MessageEnt msg = new MessageEnt(); msg.NickName = e.Nick; msg.Message = e.MessageCont; msg.Time = e.SendTime; if (e.Nick == MyNickName) { msg.IsMe = true; } else { msg.IsMe = false; } this.dataSource.Add(msg); } 其他的可以參考我們隨後上傳的源碼。 我們看看運行的結果是怎麼樣的。