• 急急如律令!火速搭建一个C#即时通信系统!(附源码分享——高度可移植!)


         (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() + "
    ");
    52                 this.richTextBox_display.AppendText(chatMsg.MsgText + "
    ");
    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源码与说明文档



    版权声明:本文为博主原创文章,未经博主允许不得转载。


  • 相关阅读:
    支付清结算之基本概念和入门
    支付清结算之账户和账务处理
    支付系统设计:支付系统的账户模型(一)
    Docker架构和原理
    Docker容器的原理、特征、基本架构、与应用场景
    Docker的用途与原理
    Random函数的安全性问题与SecureRandom
    nginx配置https
    CentOS Docker 安装
    Nginx能做什么
  • 原文地址:https://www.cnblogs.com/youfenglaiyi/p/5141829.html
Copyright © 2020-2023  润新知