无服务器端的UDP群聊功能剖析

jelly0812 贡献于2011-12-21

作者 abc  创建于2011-10-25 00:57:00   修改者abc  修改于2011-10-25 00:58:00字数12376

文档摘要:我以前在使用飞鸽传书功能的时候,发现只要打开这个软件,局域网中的用户就会瞬间加载到我的用户列表中,同时在局域网中的用户的列表中马上也会加载我自己的用户信息。而且,飞鸽传书软件没有依靠服务器端的中转,也就是说,完全是客户端的功能。那么这种机制到底是如何实现的呢?下面来一步一步的剖析。首先,我上线,局域网中的用户能够加载到我的用户列表中,那么我上线的时候,肯定是局域网中的用户都到了我的上线消息,然后给我回复了一条包含他们IP地址的信息,那样,我就可以逐个来添加他们到列表中了。其次,我上线后,他们的列表中能够添加我的用户信息,那么这个肯定是我上线的时候,侦测到了局域网中的用户,然后对每个用户发送了一个包含我的IP地址的报文。
关键词:

无服务器端的UDP群聊功能剖析 2011-10-24 21:16 by 程序诗人, 599 visits, 收藏, 编辑 我以前在使用飞鸽传书功能的时候,发现只要打开这个软件,局域网中的用户就会瞬间加载到我的用户列表中,同时在局域网中的用户的列表中马上也会加载我自己的用户信息。而且,飞鸽传书软件没有依靠服务器端的中转,也就是说,完全是客户端的功能。 那么这种机制到底是如何实现的呢?下面来一步一步的剖析。 首先,我上线,局域网中的用户能够加载到我的用户列表中,那么我上线的时候,肯定是局域网中的用户都到了我的上线消息,然后给我回复了一条包含他们IP地址的信息,那样,我就可以逐个来添加他们到列表中了。 其次,我上线后,他们的列表中能够添加我的用户信息,那么这个肯定是我上线的时候,侦测到了局域网中的用户,然后对每个用户发送了一个包含我的IP地址的报文。需要在这里注意的是,发送报文的时候,一定要注意广播风暴。(记得当时做测试的时候,由于广播风暴,将整整6个人的网络全部占用完毕,连网络都上不成。) 最后就是下线,当局域网有用户下线的时候,下线用户肯定是发送了一个包含自己IP地址的下线通知,然后我们的软件收到之后,将他从列表中删除。 当然,现在我们所有的想象只是猜测,现在来具体化设计一下: 上线,我们可以封装类似 0x01 ip地址  信息格式内容发送给局域网用户,用户通过拆解发送内容的标志符号 0x01来确定发送的数据类型。 聊天,我们可以封装类似 0x02 ip地址  聊天内容 信息格式的内容发送给用户,用户通过拆解0x02标志来确定这条消息是聊天内容。 下线也是类似的,也是通过拆解来完成,那么如何实现无服务器端的呢? 其实这个问题很好回答,就是开一个监听线程,让它在那儿一直轮训套接字端口的接收信息,如果接收到数据,通过拆解包头,再进行对应的处理: /// /// 监听事件 /// 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 属性更改阻止模式。 } /// /// 方法:处理接到的数据 /// 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); /// /// 添加聊天内容到聊天对话框中 /// /// 消息呈现内容 /// 标记:此信息是信息头还是信息内容,1代表是消息头,2代表是消息体 /// 标记:此信息是自己发送的还是别人发送的,1代表是自己发送,2代表是别人发送 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的用户会很不陌生的,其实很简单,函数如下: /// /// 方法:发送广播给套接字用户 /// 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"); //组播地址 然后就是如何实现群聊,这个就需要遍历当前的用户列表,然后发送消息来实现,请看代码,有详细的注释: /// /// 遍历列表,发送消息 /// 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标志的消息即可,代码如下: /// /// 广播发送下线消息 /// 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); /// /// 添加聊天内容到聊天对话框中 /// /// 消息呈现内容 /// 标记:此信息是信息头还是信息内容,1代表是消息头,2代表是消息体 /// 标记:此信息是自己发送的还是别人发送的,1代表是自己发送,2代表是别人发送 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();//发送上线广播信息 } /// /// 开启监听线程 /// private void openListeningThread() { isRun = true; Thread t = new Thread(new ThreadStart(listenRemote)); t.Start(); } /// /// 监听事件 /// 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 属性更改阻止模式。 } /// /// 方法:处理接到的数据 /// 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; } } /// /// 广播发送上线消息 /// private void SendInfoOnline() { localIP = GetLocalIPandName.getLocalIP(); //得到本机ip localName = GetLocalIPandName.getLocalName(); //得到本机的机器名称 sendInfo = "0x00" + "|" + localIP + "|" + localName; SendInfoByIEP.SendInfoToAll(listenClient,sendInfo,iep); //发送广播上线信息 } /// /// 向单个ip发送上线消息 /// /// 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); //发送广播上线信息 } /// /// 广播发送下线消息 /// private void SendInfoOffline() { localIP = GetLocalIPandName.getLocalIP(); //得到本机ip localName = GetLocalIPandName.getLocalName(); //得到本机的机器名称 sendInfo = "0x03" + "|" + localIP + "|" + localName; SendInfoByIEP.SendInfoToAll(listenClient,sendInfo, iep); //发送广播下线信息 } /// /// 遍历列表,发送消息 /// 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); } } /// /// 窗体抖动 /// private void flickerWin() { flickerD d = new flickerD(flickerType.quick, this); d.flickerAction(); } /// /// “发送按钮”点击事件 /// 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; //清空发送内容 } } /// /// “闪屏按钮”点击事件 /// 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(); } /// /// 点击退出时,发送下线消息 /// private void mainFrm_FormClosing(object sender, FormClosingEventArgs e) { SendInfoOffline(); Environment.Exit(0); //用户退出 } } } 然后展示几张图: 用户聊天: 用户下线: 希望有用,谢谢!!

下载文档到电脑,查找使用更方便

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 5 金币 [ 分享文档获得金币 ] 2 人已下载

下载文档