急急如律令!火速搭建一個C#即時通訊系統!(附原始碼分享——高度可移植!)

C#原始碼小二郎發表於2016-01-26

     (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     }
View Code

    然後我們將識別訊息的方法封裝到一個協議助手類中,即收到訊息的時候,明確如下兩個問題: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    
View Code

          

三·通訊協議類

然後我們來定義滿足協議的訊息基類,其中重點是要定義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     }
View Code

然後我們來看看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     }
View Code

然後我們再看一個具體的訊息類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     }
View Code 

 

四·登入的通訊過程 

                                    

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());     
        }       
View Code

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         } 
View Code

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         } 
View Code                     

五·即時通訊客戶端通訊過程

                               

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         }       
View Code

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          }
View Code

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         }
View Code

 

五·C#即時通訊原始碼下載      

    原始碼說明:1.客戶端與服務端均含有配置檔案,可配置程式的IP與埠號。
2.程式碼均含有詳細註釋。
3.除錯時確保客戶端的配置檔案相關資訊無誤,先啟動服務端再啟動客戶端。
4.登入賬號與密碼均為任意。
5.點選好友頭像即可聊天。
下載:Chat.Demo
 

 


  附相關係列原始碼: 

 文字協議通訊demo原始碼

二進位制通訊demo原始碼及說明文件

打通B/S與C/S通訊demo原始碼與說明文件



版權宣告:本文為博主原創文章,未經博主允許不得轉載。


相關文章