程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 無服務器端的UDP群聊功能剖析

無服務器端的UDP群聊功能剖析

編輯:C#入門知識

 

我以前在使用飛鴿傳書功能的時候,發現只要打開這個軟件,局域網中的用戶就會瞬間加載到我的用戶列表中,同時在局域網中的用戶的列表中馬上也會加載我自己的用戶信息。而且,飛鴿傳書軟件沒有依靠服務器端的中轉,也就是說,完全是客戶端的功能。

那麼這種機制到底是如何實現的呢?下面來一步一步的剖析。

首先,我上線,局域網中的用戶能夠加載到我的用戶列表中,那麼我上線的時候,肯定是局域網中的用戶都到了我的上線消息,然後給我回復了一條包含他們IP地址的信息,那樣,我就可以逐個來添加他們到列表中了。

其次,我上線後,他們的列表中能夠添加我的用戶信息,那麼這個肯定是我上線的時候,偵測到了局域網中的用戶,然後對每個用戶發送了一個包含我的IP地址的報文。需要在這裡注意的是,發送報文的時候,一定要注意廣播風暴。(記得當時做測試的時候,由於廣播風暴,將整整6個人的網絡全部占用完畢,連網絡都上不成。)

最後就是下線,當局域網有用戶下線的時候,下線用戶肯定是發送了一個包含自己IP地址的下線通知,然後我們的軟件收到之後,將他從列表中刪除。

當然,現在我們所有的想象只是猜測,現在來具體化設計一下:

上線,我們可以封裝類似0x01 ip地址  信息格式內容發送給局域網用戶,用戶通過拆解發送內容的標志符號0x01來確定發送的數據類型。

聊天,我們可以封裝類似0x02 ip地址  聊天內容 信息格式的內容發送給用戶,用戶通過拆解0x02標志來確定這條消息是聊天內容。

下線也是類似的,也是通過拆解來完成,那麼如何實現無服務器端的呢?

其實這個問題很好回答,就是開一個監聽線程,讓它在那兒一直輪訓套接字端口的接收信息,如果接收到數據,通過拆解包頭,再進行對應的處理:

 /// <summary>

        /// 監聽事件

        /// </summary>

        private void listenRemote()

        {

            IPEndPoint ipEnd = new IPEndPoint(broadIPAddress,lanPort);

            try

            {

                while (isRun)

                {

                    try

                    {

                        byte[] recInfo = listenClient.Receive(ref ipEnd);  //接受內容,存儲到byte數組中

                           DealWithAcceptedInfo(recInfo); //處理接收到的數據

                    }

                    catch (Exception ex)

                    {

                        MessageBox.Show(ex.Message);

                    }

                }

                listenClient.Close();

                isRun = false;

            }

            catch (SocketException se) { }  //捕捉試圖訪問套接字時發生錯誤。

              catch (ObjectDisposedException oe) { } //捕捉Socket 已關閉

              catch (InvalidOperationException pe) { } //捕捉試圖不使用Blocking 屬性更改阻止模式。

        }

 

        /// <summary>

        /// 方法:處理接到的數據

        /// </summary>

        private void DealWithAcceptedInfo(byte[] recData)

        {

            string recStr = Encoding.Default.GetString(recData);

            string[] _recStr = recStr.Split('|');

            switch (_recStr[0])

            {

                case "0x00":  //用戶上線

                    SendInfoOnline(_recStr[1]);

                    if (lstUsers.FindString(_recStr[1] + "---" + _recStr[2]) <= 0)  //如果用戶不存在

                       {

                        addListBox(_recStr[1] + "---" + _recStr[2]);

                        AddLogIntoListBox("用戶【" + _recStr[1] + "】已經上線!");

                    }

                    break;

                case "0x01":  //用戶聊天

                    AddTextBox(_recStr[1] + "    " + DateTime.Now + "\r\n", 1, 2);  //這是接收到了別人發來的信息

                    SendContentFromBox(_recStr[3].ToString());

                    break;

                case "0x02":  //抖動屏幕

                    flickerWin();

                    break;

                case "0x03":  //用戶下線

                    if (lstUsers.FindString(_recStr[1] + "---" + _recStr[2]) > 0)  //如果用戶已經存在

                       {

                        removeListBox(_recStr[1] + "---" + _recStr[2]);   //將用戶移除隊列

                           AddLogIntoListBox("用戶【" + _recStr[1] + "】已經下線!");

                    }

                    break;

                default: break;

            }

        }

上面的代碼就是監聽的核心代碼,它使用了while (isRun) 來進行輪訓,倘若一旦接收到了數據,便會進入到DealWithAcceptedInfo(recInfo); 函數體中,這個函數主要是對不同的包頭內容進行拆解。0x00代表上線,0x01代表聊天,0x02代表抖動屏幕,0x03代表下線。

需要注意的是,在進行套接字編程的時候,避免不了的是線程和UI交互問題,這裡我采用了委托來處理:

  View Code

#region ListBox線程與UI交互委托,用於添加列表數據

        public delegate void AddListBoxDelegate(string info);

 

        private void addListBox(string info)

        {

            if (lstUsers.InvokeRequired)

            {

                lstUsers.Invoke(new AddListBoxDelegate(addListBox), info);

            }

            else

            {

                lstUsers.Items.Add(info);

            }

        }

        #endregion

 

        #region ListBox線程與UI交互委托,用於刪除列表數據

        public delegate void RemoveListBoxDelegate(string info);

 

        private void removeListBox(string info)

        {

            if (lstUsers.InvokeRequired)

            {

                lstUsers.Invoke(new RemoveListBoxDelegate(removeListBox), info);

            }

            else

            {

                lstUsers.Items.Remove(info);

            }

        }

        #endregion

 

        #region ListBox線程與UI交互委托,用於添加系統日志

        public delegate void AddLogDelegate(string info);

 

        private void AddLogIntoListBox(string info)

        {

            if (lsbLog.InvokeRequired)

            {

                lsbLog.Invoke(new AddLogDelegate(AddLogIntoListBox), info);

            }

            else

            {

                lsbLog.Items.Add(info);

            }

        }

        #endregion

 

        #region RichTextBox線程與UI交互委托,用於添加文本內容及上色

        public delegate void AddTextBoxDelegate(string info, int titleOrContentFlag, int selfOrOthersFlag);

 

        /// <summary>

        /// 添加聊天內容到聊天對話框中

        /// </summary>

        /// <param name="info">消息呈現內容</param>

        /// <param name="titleOrContentFlag">標記:此信息是信息頭還是信息內容,1代表是消息頭,2代表是消息體</param>

        /// <param name="selfOrOthersFlag">標記:此信息是自己發送的還是別人發送的,1代表是自己發送,2代表是別人發送</param>

        private void AddTextBox(string info, int titleOrContentFlag, int selfOrOthersFlag)

        {

            if (rAllContent.InvokeRequired)

            {

                rAllContent.Invoke(new AddTextBoxDelegate(AddTextBox), info, titleOrContentFlag, selfOrOthersFlag);

            }

            else

            {

                if (1 == titleOrContentFlag)  //如果是消息頭

                {

                    string title = info;

                    if (1 == selfOrOthersFlag)  //如果是自己發送

                    {

                        CommonUntility.RichTextBoxEx.AppendText(rAllContent, title, Color.Green);

                    }

                    else  //如果是別人發送

                    {

                        CommonUntility.RichTextBoxEx.AppendText(rAllContent, title, Color.Blue);

                    }

                }

                else if (2 == titleOrContentFlag)  //如果是消息體

                {

                    string content = info;

                    CommonUntility.RichTextBoxEx.AppendText(rAllContent, content, Color.Black);

                }

 

            }

        }

      

 

        public delegate void SendContentFromBoxDelegate(string content);

 

        private void SendContentFromBox(string content)

        {

            if (rSendContent.InvokeRequired)

            {

                this.Invoke(new SendContentFromBoxDelegate(SendContentFromBox), content);

            }

            else

            {

                AddTextBox(content + "\r\n", 2, 2);//將發送的消息添加到窗體中

            }

        }

        #endregion

這裡基本上是先判斷控件xx是否需要InvokeRequired,如果需要,則會通過委托來執行else代碼塊中的內容。用這種方式可以非常方便的解決線程和界面交互導致的種種問題。

還有個問題,就是發送消息,相信寫過UDP的用戶會很不陌生的,其實很簡單,函數如下:

  /// <summary>

        /// 方法:發送廣播給套接字用戶

        /// </summary>

        public static void SendInfoToAll(UdpClient listenClient,string sendInfo, IPEndPoint __iep)

        {

            byte[] sendData = Encoding.Default.GetBytes(sendInfo); //得到信息的二進制編碼

            try

            {

                listenClient.Send(sendData, sendData.Length, __iep);  //發送

            }

            catch (Exception ex) { }

        }

它是利用了一個UdpClient的實例,通過指定的套接字IPEndPoint來發送信息,需要注意的是,在進行初始化的時候,需要將發送地址加入到組播組之中,這樣才能夠正常使用廣播方式:

public IPAddress broadIPAddress = IPAddress.Parse("255.255.255.255");  //組播地址

然後就是如何實現群聊,這個就需要遍歷當前的用戶列表,然後發送消息來實現,請看代碼,有詳細的注釋:

 /// <summary>

        /// 遍歷列表,發送消息

        /// </summary>

        private void SendInfo(string data)

        {

            try

            {

                byte[] _data = Encoding.Default.GetBytes(data);

 

                foreach (string s in lstUsers.Items)   //遍歷列表

                {

                    if (s.Contains("."))  //確定包含的是ip地址

                    {

                        string _ip = s.Split('-')[0];

                        if (!_ip.Equals(localIP))  //將自身排除在外

                        {

                            IPEndPoint iepe = new IPEndPoint(IPAddress.Parse(_ip), lanPort);   //套接字申明

                                UdpClient udp = new UdpClient();

                            udp.Send(_data, _data.Length, iepe);  //發送

                        }

                    }

                }

            }

            catch (Exception ex)

            {

                MessageBox.Show(ex.Message);

            }

        }

通過使用上面的代碼,就可以循環列表,將群聊的內容發送給每個人。

最後說下下線功能,下線功能包括兩個部分,一個是下線,另外一個是結束Socket線程。第一個方式很好解決,就是通過函數發送一個0x03標志的消息即可,代碼如下:

 /// <summary>

        /// 廣播發送下線消息

        /// </summary>

        private void SendInfoOffline()

        {

            localIP = GetLocalIPandName.getLocalIP();   //得到本機ip

            localName = GetLocalIPandName.getLocalName(); //得到本機的機器名稱

            sendInfo = "0x03" + "|" + localIP + "|" + localName;

            SendInfoByIEP.SendInfoToAll(listenClient,sendInfo, iep);  //發送廣播下線信息

        }

但是如何徹底的關閉已有的Socket線程呢?

其實我以前也為這個問題困惑過,采用的是好多人說的方法:直接利用Thread.Abort(),也就是讓線程拋出異常的方式來解決。其實,這種方式很不好,但是現在有一個很好的方式,就是利用

Environment.Exit(0);   //用戶退出 

來解決這個問題,這個方式是利用系統底層的工作原來,來進行結束的,在本軟件使用過程中,線程結束狀況非常好。我以前在書寫PowerShell代碼的時候,就是利用這個來向PS腳本發送代碼結束code來通知腳本,程序運行完畢的。

下面附上全部代碼:

  View Code

using System;

using System.Drawing;

using System.Text;

using System.Windows.Forms;

using System.Net;

using System.Net.Sockets;

using System.Threading;

using MainMessgeLib;

 

namespace MyMsgApplication

{

    public partial class mainFrm : Form

    {

        public mainFrm()

        {

            InitializeComponent();

        }

 

        public IPAddress broadIPAddress = IPAddress.Parse("255.255.255.255");  //組播地址

        public static int lanPort = 11011;  //端口號

        public IPEndPoint iep;

        public UdpClient listenClient = new UdpClient(lanPort);

        public bool isRun = false;  //監聽是否啟用的標志

 

        public string localIP;   //本機ip地址

        public string localName;  //本機名稱

        public string remoteIP;   //遠程主機ip地址

        public string remoteName;  //遠程主機名稱

        public string sendInfo;   //發送的信息

 

        public bool flag = false;  //顯示圖片或者隱藏標志位

 

        #region ListBox線程與UI交互委托,用於添加列表數據

        public delegate void AddListBoxDelegate(string info);

 

        private void addListBox(string info)

        {

            if (lstUsers.InvokeRequired)

            {

                lstUsers.Invoke(new AddListBoxDelegate(addListBox), info);

            }

            else

            {

                lstUsers.Items.Add(info);

            }

        }

        #endregion

 

        #region ListBox線程與UI交互委托,用於刪除列表數據

        public delegate void RemoveListBoxDelegate(string info);

 

        private void removeListBox(string info)

        {

            if (lstUsers.InvokeRequired)

            {

                lstUsers.Invoke(new RemoveListBoxDelegate(removeListBox), info);

            }

            else

            {

                lstUsers.Items.Remove(info);

            }

        }

        #endregion

 

        #region ListBox線程與UI交互委托,用於添加系統日志

        public delegate void AddLogDelegate(string info);

 

        private void AddLogIntoListBox(string info)

        {

            if (lsbLog.InvokeRequired)

            {

                lsbLog.Invoke(new AddLogDelegate(AddLogIntoListBox), info);

            }

            else

            {

                lsbLog.Items.Add(info);

            }

        }

        #endregion

 

        #region RichTextBox線程與UI交互委托,用於添加文本內容及上色

        public delegate void AddTextBoxDelegate(string info, int titleOrContentFlag, int selfOrOthersFlag);

 

        /// <summary>

        /// 添加聊天內容到聊天對話框中

        /// </summary>

        /// <param name="info">消息呈現內容</param>

        /// <param name="titleOrContentFlag">標記:此信息是信息頭還是信息內容,1代表是消息頭,2代表是消息體</param>

        /// <param name="selfOrOthersFlag">標記:此信息是自己發送的還是別人發送的,1代表是自己發送,2代表是別人發送</param>

        private void AddTextBox(string info, int titleOrContentFlag, int selfOrOthersFlag)

        {

            if (rAllContent.InvokeRequired)

            {

                rAllContent.Invoke(new AddTextBoxDelegate(AddTextBox), info, titleOrContentFlag, selfOrOthersFlag);

            }

            else

            {

                if (1 == titleOrContentFlag)  //如果是消息頭

                {

                    string title = info;

                    if (1 == selfOrOthersFlag)  //如果是自己發送

                    {

                        CommonUntility.RichTextBoxEx.AppendText(rAllContent, title, Color.Green);

                    }

                    else  //如果是別人發送

                    {

                        CommonUntility.RichTextBoxEx.AppendText(rAllContent, title, Color.Blue);

                    }

                }

                else if (2 == titleOrContentFlag)  //如果是消息體

                {

                    string content = info;

                    CommonUntility.RichTextBoxEx.AppendText(rAllContent, content, Color.Black);

                }

 

            }

        }

      

 

        public delegate void SendContentFromBoxDelegate(string content);

 

        private void SendContentFromBox(string content)

        {

            if (rSendContent.InvokeRequired)

            {

                this.Invoke(new SendContentFromBoxDelegate(SendContentFromBox), content);

            }

            else

            {

                AddTextBox(content + "\r\n", 2, 2);//將發送的消息添加到窗體中

            }

        }

        #endregion

 

        private void mainFrm_Load(object sender, EventArgs e)

        {

            listenClient.EnableBroadcast = true;  //允許發送和接受廣播

            iep = new IPEndPoint(broadIPAddress, lanPort);

            lstUsers.Items.Add("       計算機IP---主機名稱");

 

            openListeningThread();  //開啟監聽線程

            SendInfoOnline();//發送上線廣播信息

        }

 

        /// <summary>

        /// 開啟監聽線程

        /// </summary>

        private void openListeningThread()

        {

            isRun = true;

            Thread t = new Thread(new ThreadStart(listenRemote));

            t.Start();

        }

 

        /// <summary>

        /// 監聽事件

        /// </summary>

        private void listenRemote()

        {

            IPEndPoint ipEnd = new IPEndPoint(broadIPAddress,lanPort);

            try

            {

                while (isRun)

                {

                    try

                    {

                        byte[] recInfo = listenClient.Receive(ref ipEnd);  //接受內容,存儲到byte數組中

                        DealWithAcceptedInfo(recInfo); //處理接收到的數據

                    }

                    catch (Exception ex)

                    {

                        MessageBox.Show(ex.Message);

                    }

                }

                listenClient.Close();

                isRun = false;

            }

            catch (SocketException se) { }  //捕捉試圖訪問套接字時發生錯誤。

            catch (ObjectDisposedException oe) { } //捕捉Socket 已關閉

            catch (InvalidOperationException pe) { } //捕捉試圖不使用Blocking 屬性更改阻止模式。

        }

 

        /// <summary>

        /// 方法:處理接到的數據

        /// </summary>

        private void DealWithAcceptedInfo(byte[] recData)

        {

            string recStr = Encoding.Default.GetString(recData);

            string[] _recStr = recStr.Split('|');

            switch (_recStr[0])

            {

                case "0x00":  //用戶上線

                    SendInfoOnline(_recStr[1]);

                    if (lstUsers.FindString(_recStr[1] + "---" + _recStr[2]) <= 0)  //如果用戶不存在

                    {

                        addListBox(_recStr[1] + "---" + _recStr[2]);

                        AddLogIntoListBox("用戶【" + _recStr[1] + "】已經上線!");

                    }

                    break;

                case "0x01":  //用戶聊天

                    AddTextBox(_recStr[1] + "    " + DateTime.Now + "\r\n", 1, 2);  //這是接收到了別人發來的信息

                    SendContentFromBox(_recStr[3].ToString());

                    break;

                case "0x02":  //抖動屏幕

                    flickerWin();

                    break;

                case "0x03":  //用戶下線

                    if (lstUsers.FindString(_recStr[1] + "---" + _recStr[2]) > 0)  //如果用戶已經存在

                    {

                        removeListBox(_recStr[1] + "---" + _recStr[2]);   //將用戶移除隊列

                        AddLogIntoListBox("用戶【" + _recStr[1] + "】已經下線!");

                    }

                    break;

                default: break;

            }

        }

 

        /// <summary>

        /// 廣播發送上線消息

        /// </summary>

        private void SendInfoOnline()

        {

            localIP = GetLocalIPandName.getLocalIP();   //得到本機ip

            localName = GetLocalIPandName.getLocalName(); //得到本機的機器名稱

            sendInfo = "0x00" + "|" + localIP + "|" + localName;

            SendInfoByIEP.SendInfoToAll(listenClient,sendInfo,iep);  //發送廣播上線信息

        }

 

        /// <summary>

        /// 向單個ip發送上線消息

        /// </summary>

        /// <param name="remoteip"></param>

        private void SendInfoOnline(string remoteip)

        {

            localIP = GetLocalIPandName.getLocalIP();   //得到本機ip

            localName = GetLocalIPandName.getLocalName(); //得到本機的機器名稱

            sendInfo = "0x00" + "|" + localIP + "|" + localName;

            IPEndPoint _iep = new IPEndPoint(IPAddress.Parse(remoteip), lanPort);

            SendInfoByIEP.SendInfoToAll(listenClient,sendInfo, _iep);  //發送廣播上線信息

        }

 

        /// <summary>

        /// 廣播發送下線消息

        /// </summary>

        private void SendInfoOffline()

        {

            localIP = GetLocalIPandName.getLocalIP();   //得到本機ip

            localName = GetLocalIPandName.getLocalName(); //得到本機的機器名稱

            sendInfo = "0x03" + "|" + localIP + "|" + localName;

            SendInfoByIEP.SendInfoToAll(listenClient,sendInfo, iep);  //發送廣播下線信息

        }

 

        /// <summary>

        /// 遍歷列表,發送消息

        /// </summary>

        private void SendInfo(string data)

        {

            try

            {

                byte[] _data = Encoding.Default.GetBytes(data);

 

                foreach (string s in lstUsers.Items)   //遍歷列表

                {

                    if (s.Contains("."))  //確定包含的是ip地址

                    {

                        string _ip = s.Split('-')[0];

                        if (!_ip.Equals(localIP))  //將自身排除在外

                        {

                            IPEndPoint iepe = new IPEndPoint(IPAddress.Parse(_ip), lanPort);   //套接字申明

                            UdpClient udp = new UdpClient();

                            udp.Send(_data, _data.Length, iepe);  //發送

                        }

                    }

                }

            }

            catch (Exception ex)

            {

                MessageBox.Show(ex.Message);

            }

        }

 

        /// <summary>

        /// 窗體抖動

        /// </summary>

        private void flickerWin()

        {

            flickerD d = new flickerD(flickerType.quick, this);

            d.flickerAction();

        }

 

        /// <summary>

        /// “發送按鈕”點擊事件

        /// </summary>

        private void btnSend_Click(object sender, EventArgs e)

        {

            if (rSendContent.Text == "")

            {

                MessageBox.Show("請輸入要發送的內容!");

            }

            else

            {

                string sendStr = "0x01" + "|" + localIP + "|" + localName + "|" + rSendContent.Text;  //組合待傳送字符串

                SendInfo(sendStr);  //發送消息

 

                AddTextBox(localIP + "    " + DateTime.Now + "\r\n",1,1);  //將發送的消息添加到窗體中

                AddTextBox(rSendContent.Text + "\r\n",2,1);  //將發送的消息添加到窗體中

 

                this.rSendContent.Text = string.Empty;  //清空發送內容

            }

        }

 

        /// <summary>

        /// “閃屏按鈕”點擊事件

        /// </summary>

        private void btnShark_Click(object sender, EventArgs e)

        {

            string sendStr = "0x02" + "|" + localIP ;  //組合待傳送字符串

            SendInfo(sendStr);  //發送消息

            flickerWin();

        }

 

        private void rAllContent_TextChanged(object sender, EventArgs e)   //滾動條自動滾動到最底端

        {

            this.rAllContent.ScrollToCaret();

        }

 

        /// <summary>

        /// 點擊退出時,發送下線消息

        /// </summary>

        private void mainFrm_FormClosing(object sender, FormClosingEventArgs e)

        {

            SendInfoOffline();

 

            Environment.Exit(0);   //用戶退出

        }

    }

}

然後展示幾張圖:

用戶聊天:

 

\

 

用戶下線:

 

\

 

希望有用,謝謝!!

 

作者 程序詩人

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved