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


    我以前在使用飞鸽传书功能的时候,发现只要打开这个软件,局域网中的用户就会瞬间加载到我的用户列表中,同时在局域网中的用户的列表中马上也会加载我自己的用户信息。而且,飞鸽传书软件没有依靠服务器端的中转,也就是说,完全是客户端的功能。

    那么这种机制到底是如何实现的呢?下面来一步一步的剖析。

    首先,我上线,局域网中的用户能够加载到我的用户列表中,那么我上线的时候,肯定是局域网中的用户都到了我的上线消息,然后给我回复了一条包含他们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交互问题,这里我采用了委托来处理:

    View Code
    #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来通知脚本,程序运行完毕的。

    下面附上全部代码:

    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
    {
    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); //用户退出
    }
    }
    }

    然后展示几张图:

    用户聊天:

    用户下线:

    希望有用,谢谢!!
     

     

  • 相关阅读:
    telnet模拟http訪问
    network: Android 网络推断(wifi、3G与其它)
    Cocos2d-x学习笔记(19)(TestCpp源代码分析-3)
    Thinkphp编辑器扩展类kindeditor用法
    逛自己的微博,回想以前的那个“我”
    微信生成二维码
    [C++]四种方式求解最大子序列求和问题
    Android 颜色渲染(二) 颜色区域划分原理与实现思路
    Android 颜色渲染(一) 颜色选择器 ColorPickerDialog剖析
    Android 图标上面添加提醒(二)使用开源UI类库 Viewbadger
  • 原文地址:https://www.cnblogs.com/scy251147/p/2223120.html
Copyright © 2020-2023  润新知