我以前在使用飛鴿傳書功能的時候,發現只要打開這個軟件,局域網中的用戶就會瞬間加載到我的用戶列表中,同時在局域網中的用戶的列表中馬上也會加載我自己的用戶信息。而且,飛鴿傳書軟件沒有依靠服務器端的中轉,也就是說,完全是客戶端的功能。
那麼這種機制到底是如何實現的呢?下面來一步一步的剖析。
首先,我上線,局域網中的用戶能夠加載到我的用戶列表中,那麼我上線的時候,肯定是局域網中的用戶都到了我的上線消息,然後給我回復了一條包含他們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); //用戶退出
}
}
}
然後展示幾張圖:
用戶聊天:
用戶下線:
希望有用,謝謝!!
作者 程序詩人