我以前在使用飞鸽传书功能的时候,发现只要打开这个软件,局域网中的用户就会瞬间加载到我的用户列表中,同时在局域网中的用户的列表中马上也会加载我自己的用户信息。而且,飞鸽传书软件没有依靠服务器端的中转,也就是说,完全是客户端的功能。
那么这种机制到底是如何实现的呢?下面来一步一步的剖析。
首先,我上线,局域网中的用户能够加载到我的用户列表中,那么我上线的时候,肯定是局域网中的用户都到了我的上线消息,然后给我回复了一条包含他们IP地址的信息,那样,我就可以逐个来添加他们到列表中了。
其次,我上线后,他们的列表中能够添加我的用户信息,那么这个肯定是我上线的时候,侦测到了局域网中的用户,然后对每个用户发送了一个包含我的IP地址的报文。需要在这里注意的是,发送报文的时候,一定要注意广播风暴。(记得当时做测试的时候,由于广播风暴,将整整6个人的网络全部占用完毕,连网络都上不成。)
最后就是下线,当局域网有用户下线的时候,下线用户肯定是发送了一个包含自己IP地址的下线通知,然后我们的软件收到之后,将他从列表中删除。
当然,现在我们所有的想象只是猜测,现在来具体化设计一下:
上线,我们可以封装类似 0x01 ip地址 信息格式内容发送给局域网用户,用户通过拆解发送内容的标志符号 0x01来确定发送的数据类型。
聊天,我们可以封装类似 0x02 ip地址 聊天内容 信息格式的内容发送给用户,用户通过拆解0x02标志来确定这条消息是聊天内容。
下线也是类似的,也是通过拆解来完成,那么如何实现无服务器端的呢?
其实这个问题很好回答,就是开一个监听线程,让它在那儿一直轮训套接字端口的接收信息,如果接收到数据,通过拆解包头,再进行对应的处理:
///<summary>
/// 监听事件
///</summary>
privatevoid 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>
privatevoid 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交互问题,这里我采用了委托来处理:
#region ListBox线程与UI交互委托,用于添加列表数据
publicdelegatevoid AddListBoxDelegate(string info);
privatevoid addListBox(string info)
{
if (lstUsers.InvokeRequired)
{
lstUsers.Invoke(new AddListBoxDelegate(addListBox), info);
}
else
{
lstUsers.Items.Add(info);
}
}
#endregion
#region ListBox线程与UI交互委托,用于删除列表数据
publicdelegatevoid RemoveListBoxDelegate(string info);
privatevoid removeListBox(string info)
{
if (lstUsers.InvokeRequired)
{
lstUsers.Invoke(new RemoveListBoxDelegate(removeListBox), info);
}
else
{
lstUsers.Items.Remove(info);
}
}
#endregion
#region ListBox线程与UI交互委托,用于添加系统日志
publicdelegatevoid AddLogDelegate(string info);
privatevoid AddLogIntoListBox(string info)
{
if (lsbLog.InvokeRequired)
{
lsbLog.Invoke(new AddLogDelegate(AddLogIntoListBox), info);
}
else
{
lsbLog.Items.Add(info);
}
}
#endregion
#region RichTextBox线程与UI交互委托,用于添加文本内容及上色
publicdelegatevoid 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>
privatevoid 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);
}
}
elseif (2== titleOrContentFlag) //如果是消息体
{
string content = info;
CommonUntility.RichTextBoxEx.AppendText(rAllContent, content, Color.Black);
}
}
}
publicdelegatevoid SendContentFromBoxDelegate(string content);
privatevoid 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>
publicstaticvoid 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>
privatevoid 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>
privatevoid 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来通知脚本,程序运行完毕的。
下面附上全部代码:
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
{
publicpartialclass mainFrm : Form
{
public mainFrm()
{
InitializeComponent();
}
public IPAddress broadIPAddress = IPAddress.Parse("255.255.255.255"); //组播地址
publicstaticint lanPort =11011; //端口号
public IPEndPoint iep;
public UdpClient listenClient =new UdpClient(lanPort);
publicbool isRun =false; //监听是否启用的标志
publicstring localIP; //本机ip地址
publicstring localName; //本机名称
publicstring remoteIP; //远程主机ip地址
publicstring remoteName; //远程主机名称
publicstring sendInfo; //发送的信息
publicbool flag =false; //显示图片或者隐藏标志位
#region ListBox线程与UI交互委托,用于添加列表数据
publicdelegatevoid AddListBoxDelegate(string info);
privatevoid addListBox(string info)
{
if (lstUsers.InvokeRequired)
{
lstUsers.Invoke(new AddListBoxDelegate(addListBox), info);
}
else
{
lstUsers.Items.Add(info);
}
}
#endregion
#region ListBox线程与UI交互委托,用于删除列表数据
publicdelegatevoid RemoveListBoxDelegate(string info);
privatevoid removeListBox(string info)
{
if (lstUsers.InvokeRequired)
{
lstUsers.Invoke(new RemoveListBoxDelegate(removeListBox), info);
}
else
{
lstUsers.Items.Remove(info);
}
}
#endregion
#region ListBox线程与UI交互委托,用于添加系统日志
publicdelegatevoid AddLogDelegate(string info);
privatevoid AddLogIntoListBox(string info)
{
if (lsbLog.InvokeRequired)
{
lsbLog.Invoke(new AddLogDelegate(AddLogIntoListBox), info);
}
else
{
lsbLog.Items.Add(info);
}
}
#endregion
#region RichTextBox线程与UI交互委托,用于添加文本内容及上色
publicdelegatevoid 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>
privatevoid 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);
}
}
elseif (2== titleOrContentFlag) //如果是消息体
{
string content = info;
CommonUntility.RichTextBoxEx.AppendText(rAllContent, content, Color.Black);
}
}
}
publicdelegatevoid SendContentFromBoxDelegate(string content);
privatevoid SendContentFromBox(string content)
{
if (rSendContent.InvokeRequired)
{
this.Invoke(new SendContentFromBoxDelegate(SendContentFromBox), content);
}
else
{
AddTextBox(content +"\r\n", 2, 2);//将发送的消息添加到窗体中
}
}
#endregion
privatevoid mainFrm_Load(object sender, EventArgs e)
{
listenClient.EnableBroadcast =true; //允许发送和接受广播
iep =new IPEndPoint(broadIPAddress, lanPort);
lstUsers.Items.Add(" 计算机IP---主机名称");
openListeningThread(); //开启监听线程
SendInfoOnline();//发送上线广播信息
}
///<summary>
/// 开启监听线程
///</summary>
privatevoid openListeningThread()
{
isRun =true;
Thread t =new Thread(new ThreadStart(listenRemote));
t.Start();
}
///<summary>
/// 监听事件
///</summary>
privatevoid 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>
privatevoid 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>
privatevoid SendInfoOnline()
{
localIP = GetLocalIPandName.getLocalIP(); //得到本机ip
localName = GetLocalIPandName.getLocalName(); //得到本机的机器名称
sendInfo ="0x00"+"|"+ localIP +"|"+ localName;
SendInfoByIEP.SendInfoToAll(listenClient,sendInfo,iep); //发送广播上线信息
}
///<summary>
/// 向单个ip发送上线消息
///</summary>
///<param name="remoteip"></param>
privatevoid 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>
privatevoid SendInfoOffline()
{
localIP = GetLocalIPandName.getLocalIP(); //得到本机ip
localName = GetLocalIPandName.getLocalName(); //得到本机的机器名称
sendInfo ="0x03"+"|"+ localIP +"|"+ localName;
SendInfoByIEP.SendInfoToAll(listenClient,sendInfo, iep); //发送广播下线信息
}
///<summary>
/// 遍历列表,发送消息
///</summary>
privatevoid 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>
privatevoid flickerWin()
{
flickerD d =new flickerD(flickerType.quick, this);
d.flickerAction();
}
///<summary>
/// “发送按钮”点击事件
///</summary>
privatevoid 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>
privatevoid btnShark_Click(object sender, EventArgs e)
{
string sendStr ="0x02"+"|"+ localIP ; //组合待传送字符串
SendInfo(sendStr); //发送消息
flickerWin();
}
privatevoid rAllContent_TextChanged(object sender, EventArgs e) //滚动条自动滚动到最底端
{
this.rAllContent.ScrollToCaret();
}
///<summary>
/// 点击退出时,发送下线消息
///</summary>
privatevoid mainFrm_FormClosing(object sender, FormClosingEventArgs e)
{
SendInfoOffline();
Environment.Exit(0); //用户退出
}
}
}
然后展示几张图:
用户聊天:
用户下线:
希望有用,谢谢!!