(2016年3月更:由於後來瞭解到GGTalk開源即時通訊系統,因此直接採用了該資源用於專案開發,在此對作者表示由衷的感謝!)
——————————————————————————————————
人在外包公司,身不由己!各種雜七雜八的專案都要做,又沒有自己的技術沉澱,每次涉足新的專案都倍感吃力,常常現學現賣,卻不免處處碰壁!當然,話說回來,也是自己的水平有限在先,一馬配一鞍,無奈也只能留在外包公司。
這不,就在上一週,領導下達一個任務:3天內搭建一個C#即時通訊系統,與原有的辦公系統整合。
我正心裡犯嘀咕;“網路程式設計自己就只知道一點皮毛啊,還是大學選修課上聽老師講的那一點東西,別說即時通訊了,以前也就只照著書上的例子寫過一個抓包工具當作業交過,徹頭徹尾的小白啊,何況都畢業幾年了,連“套接字”都快忘了!”
領導補充說:“這個即時通訊系統要儘快完成,之後還有別的的專案。”
我:“······好的”
沒辦法,就像領導常說的“有條件要上,沒有條件創造條件也要上!”,臨危受命,唯有逆流而上!
想都別想,寫即時通訊總不能從socket寫起啊,那樣寫出來的東西只能讀書的時候當作業交給老師看下,然後記一個平時成績,給領導看那就是找抽!
所以,只能“登高而招,順風而呼”,園子裡大神多,資源也多,找找看有沒有可以參考的。(這也是我一直以來的工作方法,呵呵)
終於,看到了一篇輕量級通訊引擎StriveEngine通訊demo原始碼研究學習了一下,稍加揣摩,很快就完成了領導所交付的重任!在此要鳴謝該文的作者!
言歸正傳,接下來就把自己的學習所得以及編寫過程詳盡的分享給大家!
一·C#即時通訊系統介面快照
二·網路訊息流與通訊協議
首先,網路中的資料是源源不斷的二進位制流,有如長江之水連綿不絕,那麼,即時通訊系統如何從連綿不絕的資料流中準確的識別出一個訊息呢?換言之,在悠遠綿長的網路資料流中,一個個具體的訊息應該如何被界定出來呢?
這就需要用到通訊協議。通訊協議,一個大家耳熟能詳的術語,什麼TCP啊、UDP啊、IP啊、ICMP啊,以前學《計算機網路》時,各種協議充斥寰宇。但是,從教科書上抽象的概念中,你真的瞭解什麼是通訊協議嗎?
回到開始的問題,我想恐怕可以這樣來理解:通訊協議就是要讓訊息遵循一定的格式,而這樣的格式是參與通訊的各方都知曉且遵守的,依據這樣的格式,訊息就能從資料流中被完整的識別出來。
通訊協議的格式通常分為兩類:文字訊息、二進位制訊息。
文字協議相對簡單,通常使用一個特殊的標記符作為一個訊息的結束。這樣一來,根據這個特殊的標誌符,每個訊息之間就有了明確的界限。
二進位制協議,通常是由訊息頭(Header)和訊息體(Body)構成的,訊息頭的長度固定,而且,通過解析訊息頭,可以知道訊息體的長度。如此,我們便可以從網路流中解析出一個個完整的二進位制訊息。
兩種協議各有優劣,雖然文字協議比較簡單方便,但是二進位制協議更具有普適性,諸如圖片啊、檔案啊都可以轉化為二進位制陣列,所以我在寫即時通訊時採用的是二進位制協議。
我定義的二進位制協議是:訊息頭固定為8個位元組:前四個位元組為一個int,其值表示訊息型別;後四個位元組也是一個int,其值表示訊息體長度。
先來看訊息頭的定義
1 public class MsgHead 2 { 3 private int messageType; 4 /// <summary> 5 /// 訊息型別 6 /// </summary> 7 public int MessageType 8 { 9 get { return messageType; } 10 set { messageType = value; } 11 } 12 13 private int bodyLength; 14 /// <summary> 15 /// 訊息體長度 16 /// </summary> 17 public int BodyLength 18 { 19 get { return bodyLength; } 20 set { bodyLength = value; } 21 } 22 23 public const int HeadLength = 8; 24 25 26 public MsgHead(int msgType,int bodyLen) 27 { 28 this.bodyLength = bodyLen; 29 this.messageType = msgType; 30 } 31 32 public byte[] ToStream() 33 { 34 byte[] buff = new byte[MsgHead.HeadLength]; 35 byte[] bodyLenBuff = BitConverter.GetBytes(this.bodyLength); 36 byte[] msgTypeBuff = BitConverter.GetBytes(this.messageType); 37 Buffer.BlockCopy(msgTypeBuff, 0, buff, 0, msgTypeBuff.Length); 38 Buffer.BlockCopy(bodyLenBuff, 0, buff, 4, bodyLenBuff.Length); 39 return buff; 40 } 41 }
然後我們將識別訊息的方法封裝到一個協議助手類中,即收到訊息的時候,明確如下兩個問題:1.固定前多少位是訊息頭。2.如何從訊息頭中獲取訊息體長度。
1 public class StreamContractHelper : IStreamContractHelper 2 { 3 /// <summary> 4 /// 訊息頭長度 5 /// </summary> 6 public int MessageHeaderLength 7 { 8 get { return MsgHead.HeadLength; } 9 } 10 /// <summary> 11 /// 從訊息頭中解析出訊息體長度,從而可以間接取出訊息體 12 /// </summary> 13 /// <param name="head"></param> 14 /// <returns></returns> 15 public int ParseMessageBodyLength(byte[] head) 16 { 17 return BitConverter.ToInt32(head,4); 18 } 19 } 20
三·通訊協議類
然後我們來定義滿足協議的訊息基類,其中重點是要定義ToContractStream()方法,使得訊息能夠序列化成滿足協議的二進位制流,從而通過網路進行傳輸。
1 [Serializable] 2 public class BaseMsg 3 { 4 private int msgType; 5 6 public int MsgType 7 { 8 get { return msgType; } 9 set { msgType = value; } 10 } 11 /// <summary> 12 /// 序列化為本次通訊協議所規範的二進位制訊息流 13 /// </summary> 14 /// <returns></returns> 15 public byte[] ToContractStream() 16 { 17 return MsgHelper.BuildMsg(this.msgType, SerializeHelper.SerializeObject(this)); 18 } 19 }
然後我們來看看MsgHelper類的具體實現
1 public static class MsgHelper 2 { 3 /// <summary> 4 /// 構建訊息 5 /// </summary> 6 /// <param name="msgType">訊息型別</param> 7 /// <param name="msgBody">訊息體</param> 8 /// <returns></returns> 9 public static byte[] BuildMsg(int msgType, Byte[] msgBody) 10 { 11 MsgHead msgHead = new MsgHead(msgType, msgBody.Length); 12 //將訊息頭與訊息體拼接起來 13 byte[] msg = BufferJointer.Joint(msgHead.ToStream(), msgBody); 14 return msg; 15 } 16 17 public static byte[] BuildMsg(int msgType, string str) 18 { 19 return MsgHelper.BuildMsg(msgType, Encoding.UTF8.GetBytes(str)); 20 } 21 /// <summary> 22 /// 將二進位制陣列還原成訊息物件 23 /// </summary> 24 /// <typeparam name="T">所要還原成的訊息類</typeparam> 25 /// <param name="msg">訊息資料</param> 26 /// <returns></returns> 27 public static T DeserializeMsg<T>(byte[] msg) 28 { 29 return (T)SerializeHelper.DeserializeBytes(msg, 8, msg.Length - 8); 30 } 31 }
然後我們再看一個具體的訊息類ChatMsg的定義
1 [Serializable] 2 public class ChatMsg:BaseMsg 3 { 4 private string sourceUserID; 5 /// <summary> 6 /// 傳送該訊息的使用者ID 7 /// </summary> 8 public string SourceUserID 9 { 10 get { return sourceUserID; } 11 set { sourceUserID = value; } 12 } 13 private string targetUserID; 14 /// <summary> 15 /// 該訊息所發往的使用者ID 16 /// </summary> 17 public string TargetUserID 18 { 19 get { return targetUserID; } 20 set { targetUserID = value; } 21 } 22 private DateTime timeSent; 23 /// <summary> 24 /// 該訊息的傳送時間 25 /// </summary> 26 public DateTime TimeSent 27 { 28 get { return timeSent; } 29 set { timeSent = value; } 30 } 31 private string msgText; 32 /// <summary> 33 /// 該訊息的文字內容 /// 34 /// </summary> 35 public string MsgText 36 { 37 get { return msgText; } 38 set { msgText = value; } 39 } 40 /// <summary> 41 /// 構造一個ChatMsg例項 42 /// </summary> 43 /// <param name="_sourceUserID">該訊息源使用者ID</param> 44 /// <param name="_targetUserID">該訊息目標使用者ID</param> 45 /// <param name="_MsgText">該訊息的文字內容 </param> 46 public ChatMsg(string _sourceUserID, string _targetUserID, string _MsgText) 47 { 48 base.MsgType = Core.MsgType.Chatting; 49 this.sourceUserID = _sourceUserID; 50 this.targetUserID = _targetUserID; 51 this.timeSent = DateTime.Now; 52 this.msgText = _MsgText; 53 } 54 }
四·登入的通訊過程
1.客戶端傳送登陸訊息
private void button_login_Click(object sender, EventArgs e) { this.selfID = this.textBox_ID.Text.Trim(); LoginMsg loginMsg = new LoginMsg(this.selfID); this.tcpPassiveEngine.PostMessageToServer(loginMsg.ToContractStream()); }
2.服務端回覆登陸訊息
1 if (msgType == MsgType.Logining) 2 { 3 LoginMsg loginMsg = MsgHelper.DeserializeMsg<LoginMsg>(msg); 4 this.ReplyLogining(loginMsg, userAddress); 5 //將線上使用者告知其他客戶端 6 this.TellOtherUser(MsgType.NewOnlineFriend, loginMsg.SrcUserID); 7 } 8 9 /// <summary> 10 /// 回覆登陸訊息 11 /// </summary> 12 /// <param name="loginMsg"></param> 13 /// <param name="userAddress"></param> 14 private void ReplyLogining(LoginMsg loginMsg, IPEndPoint userAddress) 15 { 16 if (this.onlineManager.Contains(loginMsg.SrcUserID))//重複登入 17 { 18 loginMsg.LogonResult = LogonResult.Repetition; 19 this.tcpServerEngine.SendMessageToClient(userAddress, loginMsg.ToContractStream()); 20 } 21 else//此demo簡化處理回覆成功,其他驗證未處理 22 { 23 this.AddUser(loginMsg.SrcUserID, userAddress); 24 this.ShowOnlineUserCount(); 25 loginMsg.LogonResult = LogonResult.Succeed; 26 this.tcpServerEngine.SendMessageToClient(userAddress, loginMsg.ToContractStream()); 27 } 28 }
3.客戶端處理登陸結果
1 private void tcpPassiveEngine_MessageReceived(IPEndPoint userAddress, byte[] msg) 2 { 3 //取出訊息型別 4 int msgType = BitConverter.ToInt32(msg, 0); 5 //驗證訊息型別 6 if (msgType == MsgType.Logining) 7 { 8 LoginMsg loginMsg = MsgHelper.DeserializeMsg<LoginMsg>(msg); 9 if (loginMsg.LogonResult == LogonResult.Succeed) 10 { 11 this.DialogResult = DialogResult.OK; 12 this.tcpPassiveEngine.MessageReceived -= new StriveEngine.CbDelegate<IPEndPoint, byte[]>(tcpPassiveEngine_MessageReceived); 13 } 14 if (loginMsg.LogonResult == LogonResult.Repetition) 15 { 16 MessageBox.Show("登入失敗,該賬號已經登入!"); 17 } 18 } 19 }
五·即時通訊客戶端通訊過程
1.客戶端A傳送聊天訊息給伺服器
1 /// <summary> 2 /// 傳送聊天訊息 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <param name="e"></param> 6 private void button_send_Click(object sender, EventArgs e) 7 { 8 string chatText = this.richTextBox_Write.Text; 9 if (string.IsNullOrEmpty(chatText)) 10 { 11 MessageBox.Show("訊息不能為空"); 12 return; 13 } 14 ChatMsg chatMsg = new ChatMsg(this.selfUserID, this.friendID, chatText); 15 this.tcpPassiveEngine.SendMessageToServer(chatMsg.ToContractStream()); 16 this.ShowChatMsg(chatMsg); 17 }
2.服務端轉發聊天訊息
1 if (msgType == MsgType.Chatting) 2 { 3 ChatMsg chatMsg = MsgHelper.DeserializeMsg<ChatMsg>(msg); 4 if (this.onlineManager.GetKeyList().Contains(chatMsg.TargetUserID)) 5 { 6 IPEndPoint targetUserAddress = this.onlineManager.Get(chatMsg.TargetUserID).Address; 7 this.tcpServerEngine.SendMessageToClient(targetUserAddress, msg); 8 } 9 }
3.客戶端B接收並顯示聊天訊息
1 void tcpPassiveEngine_MessageReceived(IPEndPoint userAddress, byte[] msg) 2 { 3 //取出訊息型別 4 int msgType = BitConverter.ToInt32(msg, 0); 5 //驗證訊息型別 6 if (msgType == MsgType.Chatting) 7 { 8 ChatMsg chatMsg = MsgHelper.DeserializeMsg<ChatMsg>(msg); 9 this.ShowChatForm(chatMsg.SourceUserID); 10 this.ChatMsgReceived(chatMsg); 11 } 12 } 13 14 /// <summary> 15 /// 顯示聊天窗 16 /// </summary> 17 /// <param name="friendUserID">聊天對方使用者ID</param> 18 private void ShowChatForm(string friendUserID) 19 { 20 if (this.InvokeRequired) 21 { 22 this.Invoke(new CbGeneric<string>(this.ShowChatForm), friendUserID); 23 } 24 else 25 { 26 ChatForm form = this.chatFormManager.GetForm(friendUserID); 27 if (form == null) 28 { 29 form = new ChatForm(this.selfID, friendUserID, this, this.tcpPassiveEngine); 30 form.Text = string.Format("與{0}對話中···", friendUserID); 31 this.chatFormManager.Add(form); 32 form.Show(); 33 } 34 form.Focus(); 35 } 36 } 37 38 39 /// <summary> 40 /// 顯示聊天訊息 41 /// </summary> 42 /// <param name="chatMsg"></param> 43 private void ShowChatMsg(ChatMsg chatMsg) 44 { 45 if (this.InvokeRequired) 46 { 47 this.Invoke(new CbGeneric<ChatMsg>(this.formMain_chatMsgReceived), chatMsg); 48 } 49 else 50 { 51 this.richTextBox_display.AppendText(chatMsg.SourceUserID + " " + chatMsg.TimeSent.ToString() + "\r\n"); 52 this.richTextBox_display.AppendText(chatMsg.MsgText + "\r\n"); 53 this.richTextBox_Write.Clear(); 54 } 55 }
五·C#即時通訊原始碼下載
原始碼說明:1.客戶端與服務端均含有配置檔案,可配置程式的IP與埠號。
2.程式碼均含有詳細註釋。
3.除錯時確保客戶端的配置檔案相關資訊無誤,先啟動服務端再啟動客戶端。
4.登入賬號與密碼均為任意。
5.點選好友頭像即可聊天。
下載:Chat.Demo
附相關係列原始碼:
版權宣告:本文為博主原創文章,未經博主允許不得轉載。