UDP网络程序开发

作者:追风剑情 发布于:2021-8-22 18:31 分类:C#

UDP(User Datagram Protocol,用户数据报协议)是简单的、面向数据报的无连接协议,它提供了快速但不一定可靠的传输服务。和 TCP 一样,UDP 也是构建于IP之上的传输层协议。UDP工作与发手机短信相似,在通信前不需要连接,只要输入对方方号码即可,无须考虑对方手机处于什么状态。

一、UDP 程序开发的主要技术

UDP 协议是 Internet 协议族中支持无连接的传输协议。UDP 协议提供了一种方法来发送经过封装的 IP 数据报,与 TCP 不同, UDP 无须建立连接就可以发送这些 IP 数据报。
1、UDP 与 TCP 的区别与优势

UDP 与 TCP 除了前者是无连接的,而后者是面向连接的之外,还具有以下不同和优势。

1. UDP 的可靠性不如 TCP

    TCP协议包含专门的传递保证机制,当收到发送方信息时,会向发送方发送确认消息,而发送方在接收到了这个确认消息后才会继续发送其他信息,否则会重传已发信息。UDP与TCP不同,UDP并没有这样的保证机制,所以就算发送方的数据在途中丢失,UDP协议本身也不会做出任何检测。因此,人们通常把UDP称为不可靠的传输协议。

2. UDP 不能保证有序传输

    对于数据的传输,UDP不能保证数据发送和接收顺序,所以有时在网络拥挤的情况下可能会出现乱序的问题。

3. UDP 拥有比 TCP 更快的传输速度

    由于UDP的传输不需要建立连接,也不需要确认,所以它的传输速度会比TCP快很多。这样,对于一些对可靠性要求不高,却强调传输速度的应用而言(如网络音视频播放、视频点播等),使用UDP不失为很好的选择。

4. UDP 有消息边界

    UDP把上层应用程序交下来的报文添加首部后直接交给IP层,既不拆分,也不合并,这样就保留了这些报文的边界。所以在使用UDP时,无须考虑边界问题(即,无须考虑粘包问题),因此UDP在使用上比TCP简单。

5. UDP 可以一对多传输

    由于UDP传输数据不需要建立连接,也不需要维持连接状态,所以一台服务器可以向多个客户机同时传递相同的消息。利用UDP的广播和组播功能可以同时向网上的所有客户发送消息,这一点也比TCP方便。

2、使用 UDP 类进行网络传输

.NET 库中的 UdpClient 类对基础Socket进行了封装,发送和接收数据时不必考虑底层套接字在收发时必须要处理的细节问题,在一定程度上降低了 UDP 编程的难度,提高了编程效率。

1. UdpClient 类

    TCP 有 TcpListener 和 TcpClient 两个类,而 UDP 只有 UdpClient 一个类,位于 System.Net.Sockets 命名空间中。这是因为 UDP 是无连接的协议,所以只需要一种封装后的 Socket。

    UdpClient 拥有 6 种重载的构造函数,对于 IPv4 来说,常用的重载形式有 4 种。

    1) public UdpClient()

    此构造函数创建一个新的 UdpClient 对象,并自动分配合适的本地 IPv4 地址和端口号,但该构造函数不执行套接字绑定。如果使用这种构造函数,在发送数据报之前,必须先调用 Connect 方法,而且只能将数据报发送到 Connect 方法建立的远程主机。用法如下:

UdpClient udpClient = new UdpClient();
udpClient.Connect("www.cqut.edu.cn", 51666); //指定默认远程主机和端口号
Byte[] sendBytes = Encoding.Unicode.GetBytes("你好!");
udpClient.Send(sendBytes, sendBytes.Length); //发送给默认主机

    2) public UdpClient(int port)

    如果创建 UdpClient 对象只是为了发送数据报,则可以使用此构造函数,参数 port 为本地端口号。用法如下:
    UdpClient sendUdpClient = new UdpClient(0);
    端口号置为0,表示让系统自动为其分配一个合适的端口号,这样就不会发生端口号冲突的情况,因此这种形式是创建 UdpClient 对象最方便、最简单的方式。

    3) public UdpClient(IPEndPoint localEp)

    如果创建 UdpClient 对象是用来接收远程主机发送到本地主机某个端口的数据报,则使用此构造函数比较合适。用法如下:

IPAddress localIp = Dns.GetHostAddress(Dns.GetHostName())[0];
IPEndPoint localIPEndPoint = new IPEndPoint(localIp, 51666);
UdpClient udpClient = new UdpClient(localIPEndPoint);

其中,localIPEndPoint 是一个 IPEndPoint(网络端点) 类型的对象实例,封装了本地的一个确定的端口号。这样一来,只要远程主机知道本地主机的IP地址,就可以直接向本机的指定端口发送数据报。

    4) public UdpClient(string hostname, int port)

    此构造函数创建一个新的 UdpClient 实例,并自动分配合适的本地IP地址和端口号,用于收发数据。函数中的参数分别为远程主机域名和端口号。用法如下:
UdpClient udpClient = new UdpClient("www.cqut.edu.cn", 51666);
这种构造函数适用于向默认主机发送数据,或者只接收默认远程主机发来的数据,而其他主机发送来的数据报自动丢失的场合。

UdpClient 类的常用方法和属性
方法和属性 说明
Send 方法 发送数据报
Receive 方法 接收数据报
BeginSend 方法 开始从连接的Socket中异步发送数据报
BeginReceive 方法 开始从连接的Socket中异步接收数据报
Close 方法 关闭UDP连接,并释放相关资源
EndSend 方法 结束挂起的异步发送数据报
EndReceive 方法 结束挂起的异步接收数据报
JoinMulticastGroup 方法 添加多地址发送,用于连接一个多组播
DropMulticastGroup 方法 除去多地址,断开与多组播连接
Active 属性 获取或者设定一个值,指示是否已建立默认远程主机
Client 属性 获取或设置基础套接字
EnableBroadcast 属性 是否接收或发送广播包

3、UDP 下的同步与异步通信

在同步通信方式下,实现通信主要是运用 UdpClient 对象的 Send 方法和 Receive 方法。

1. 同步通信

同步发送数据时,可以调用 UdpClient 对象的 Send 方法。该方法有3种不同的重载形式:

1) public int Send(byte[] data, int length, IPEndPoint iep)

该方法将UDP数据报发送到位于指定远程端点的主机。它的3个参数分别为发送的数据、希望发送的字节数、远程 IPEndPoint 对象。返回值为成功发送的字节数。用法如下:

private UdpClient sendUdpClient;
IPAdress remoteIp = Dns.GetHostAddresses(Dns.GetHostName())[0];
IPEndPoint remoteIPEndPoint = new IPEndPoint(remoteIp, 51666);
byte[] sendBytes = Encoding.Unicode.GetBytes("你好!");
sendUdpClient.Send(sendBytes, sendBytes.Length, remoteIPEndPoint);

2) public int Send(byte[] data, int length, string hostname, int port)

该方法将UDP数据报发送到指定远程主机上的指定端口。它的4个参数分别为发送的数据、希望发送的字节数、远程主机名称和远程主机端口号。返回值为成功发送的字节数。用法如下:

UdpClient udpClient = new UdpClient();
byte[] sendBytes = Encoding.Unicode.GetBytes("hello!");
udpClient.Send(sendBytes, sendBytes.Length, "www.cqut.edu.cn", 51666);

3) public int Send(byte[] data, int length)

该方法假定已经通过 Connect 方法指定了远程主机,因此只需要用 Send 方法指定发送的数据和希望发送的字节数即可。返回值为成功发送的字节数。用法如下:

UdpClient udpClient = new UdpClient("www.cqut.edu.cn", 51666);
byte[] sendBytes = Encoding.Unicode.GetBytes("hello!");
udpClient.Send(sendBytes, sendBytes.Length);

同步接收数据可以用 UDP 的 Receive 方法来接收远程主机发过来的数据报。例如:
public byte[] Receive(ref IPEndPoint remoteEP)
其中唯一的参数 IPEndPoint 表示发送方的 IP 地址和端口号,该参数具体值由发送方填写。

2. 异步通信

如果任务执行的时间比较长,则采用异步通信的方式比较好。

1)异步发送数据

UdpClient 类的每个同步方法都有与之对应的异步 BeginSend 和 EndSend 方法。所以,异步通信的 BeginSend 方法也有 3 种不同的重载形式:

(1) public int BeginSend(byte[] data, int length, IPEndPoint iep, AsyncCallback ac, Object obj)
(2) public int BeginSend(byte[] data, int length, string hostname, int port, AsyncCallback ac, Object obj)
(3)public int BeginSend(byte[] data, int length, AsyncCallback ac, Object obj)

对比同步通信可以看出,对于每个BeginSend方法,除了同步Send方法具有相同的参数外,每个方法又增加了两个参数:一个是AsyncCallback类型的委托,用于指定异步操作完成时调用的方法;另一个是Object类型的对象,用于将状态信息传递给回调方法。当不使用的时候,这两个新增的参数都可以置为nulll。

下面以最常用的 BeginSend 为例,说明如何异步发送数据。

static void SendMessage(string server, string message)
{
   UdpClient udpClient = new UdpClient(server, 51666);
   byte[] sendByte = Encoding.Unicode.GetBytes("hello!");
   //异步方式发送数据
   IAsyncResult result = udpClient.BeginSend(sendByte, sendByte.Length, null, null);
   //在发送没有结束之前可以做一些其他的操作,这里以Thread.Sleep(100)代替
   while (!result.IsCompleted)
   {
      Thread.Sleep(100);
   }
   int sendbytes = udpClient.EndSend(result); //EndSend方法进行资源回收
}

需要注意的是,在调用了BeginSend方法后,必须调用UdpClient对象的EndSend方法,本例中,返回了实际发送的字节数,并进行资源回收。

2) 异步接收数据

异步接收数据将用到与同步接收数据时所用的Receive方法相对应的BeginReceive方法。形式如下:
public IAsyncResult BeginReceive(AsyncCallback requestCallback, Object state);
下面将以此为例说明如何运行此方法。

private void ReceiveData()
{
   UdpClient receiveClient = new UdpClient(5656);//指定本机5656端口号用于接收
   receiveClient.BeginReceive(new AsyncCallback(ReceiveUdpClientCallback), receiveClient);
}
//回调方法
void ReceiveUdpClient(IAsyncResult ar)
{
   UdpClient  u = (UdpClient) ar.AsyncState;
   IPEndPoint remote = null;
   Byte[] receiveBytes = u.EndReceive(ar, ref remote);
   String  str = Encoding.UTF8.GetString(receiveBytes, 0, receiveBytes.Length);
}

二、UDP 的广播与组播程序开发

前面提到,UDP 有一个可以进行一对多数据传输的优势,这个优势主要用于本章节要介绍的广播和组播。

1、广播与组播的基本概念

      TCP 协议虽然具有可靠性高、有序到达等优势,但它只支持一对一的传输,所以当需要进行大量的数据传送时,TCP 所表现出的性能就不如 UDP 了。UDP 不仅能够用于发送大量的数据,而且还能同时进行一对多的通信。

      所谓广播,就是同时向子网中的所有主机发送信息。为了能让所有的主机都收到信息,发送的广播消息必须包含一个特殊的 IP 地址,这个 IP 地址的主机号的二进制表现形式全为1。例如,子网掩码为 255.255.255.0,子网号为 192.168.0.0 的广播地址为 192.168.0.255。广播消息地址分为两种类型:本地广播和全球广播。所谓本地广播,是向子网中的所有主机发送信息,而其他网络是不会收到这个信息的。而全球广播则使用 255.255.255. 255 这个全球的广播地址作为 IP 地址向网络上的所有设备发送数据,由于路由器会自动过滤全球广播,所以 255.255.255.255 也就没有了实际的意义。

      当然,广播技术也会遇到一些问题,例如,不是子网中的每个主机都希望收到你的广播数据.或者你想发送数据的几个对象位于不同的几个子网中,这时广播就有点力不从心了。可以用组播技术来解决这个问题。所谓组播,就是可以将消息从一台计算机发送到子网或者全网中选择的对象主机集合上,即发送到那些加入指定组播组的计算机上。组播组是开放的,每台计算机都可以通过程序随时加入到组播组中,也可以随时离开。

2、组播组的加入与退出

组播组是分享一个组播地址的一组设备,又称为多路广播组。和 IP 广播类似,IP 组播使用特殊的 IP 地址范围来表示不同的组播组。组播地址范围是 224.0.0.0~239.255.255.255 的 D类 IP 地址。任何发送到组播地址的消息都会被发送到组内的所有成员设备上。大多数的组播是临时的,仅在有成员的时候才存在,用户创建一个新的组播组时,只需从地址范围内选出一个地址,然后为这个地址构造一个对象,就可以开始发送消息了。

使用组播时要注意 TTL(Time to live,生存周期)值的设置。TTL 是允许路由器转发的最大次数,默认情况下,TTL的值为1,如果使用默认值,则只能在子网内发送。

UdpClient 对象提供了一个 Ttl 属性,可以利用它修改 TTL 的值。用法如下:
UdpClient udpClient = new UdpClient();
udpClien.Tt1= 8;
该语句把 TTL 的值设置为 8,这样一来发送的组播消息最多可以经过 8 次的路由器转发,保障了组播消息能够发送到其他子网上的加入到该组播组的主机上。

1). 加入多路广播组

UdpClient 类提供了 JoinMulticastGroup 方法用于将 UdpClient 加入到使用指定IPAddress 的多路广播组中。调用 JoinMulticaseGroup 方法后,Socket 会自动向路由器发送数据包,请求成为多路广播组成员。一旦成为组播组成员,就可以接收到该组播组的数据报。

JoinMulticastGroup 有两种常用的重载形式。

(1) JoinMulticastGroup(IPAdress multicastAddr)。用法如下:
//要加入广播组的UdpClient仅能指定端口,不能指定IP
UdpClient udpClient = new UdpClient(8001);
udpClient.JoinMulticastGroup(IPAddress.Parse("224.100.0.1"));

多路广播地址的范围是 224.0.0.1~239.255.255.254,如果指定的地址在此范围之外,或者所请求的路由器不支持多路广播组,则会抛出异常。当使用Send方法向多路广播地址发消息时,所有加入此广播组的成员都会收到此消息。

(2) JoinMulticastGroup(IPAddress multicastAddr, int timeToLive)。该方法还涉及TTL的运用。用法如下:
UdpClient udpClient = new UdpClient(8001);
udpClient.JoinMulticastGroup(IPAddress.Parse("224.100.0.1"), 8);
其中, 8 为TTL的值。

2). 退出多路广播组

广播组的退出用到了UdpClient对象的 DropMulticastGroup 方法。当 UdpClient 对象从组中收回后,将不能再接收到该组的数据报。用法如下:
udpClient.DropMulticastGroup(IPAddress.Parse("224.100.0.1"));

三、基于广播和组播的网络会议程序开发

通过 Internet 实现群发功能的形式有两种:一种是利用广播向子网中的所有用户发送信息,例如各种通知、公告、集体活动日程安排等; 另一种是利用组播向 Internet 上不同的子网发送信息,例如集团向其所属的公司或用户子网发布信息公告等。

示例:网络会议


using System;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace NetMeeting
{
    public partial class Form1 : Form
    {
        private enum ListBoxOperation
        {
            AddItem, RemoveItem
        }

        //定义一个委托
        private delegate void SetListBoxItemCallback(ListBox listbox, string text, ListBoxOperation operation);
        SetListBoxItemCallback listBoxCallback;
        //使用的IP地址
        private IPAddress broderCastIp = IPAddress.Parse("233.1.1.1");
        //使用的接收端口号
        private int port = 8300;
        //广播端口号
        private int broadcastPort = 8003;
        private UdpClient udpClient;

        private void Form1_Load(object sender, EventArgs e)
        {
            listBoxMessage.HorizontalScrollbar = true;
            buttonlogin.Enabled = true;
            buttonlogout.Enabled = false;
            Thread mybroacast = new Thread(BroadcastNews);
            //自己登录时,广播告知其他人自己登录了
            mybroacast.IsBackground = true;
            mybroacast.Start();
            Thread listenBroacast = new Thread(ReceiveBroadcast);
            //有人登录时,自己可以收到广播消息
            listenBroacast.IsBackground = true;
            listenBroacast.Start();
        }

        //广播,告知其他用户自己登录了
        private void BroadcastNews()
        {
            UdpClient mybroacastUDP = new UdpClient();
            try
            {
                string name = Dns.GetHostName();
                //广播时用8003端口,区别于组播
                IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Broadcast, broadcastPort);
                byte[] bytes = Encoding.UTF8.GetBytes(name + "登录了!");
                mybroacastUDP.Send(bytes, bytes.Length, ipEndPoint);
            }
            catch (Exception err)
            {
                MessageBox.Show(err.ToString());
            }
        }

        //接收广播消息
        private void ReceiveBroadcast()
        {
            //指定本机的8003端口接收广播消息
            UdpClient uc = new UdpClient(broadcastPort);
            IPEndPoint remotes = null;
            while (true)
            {
                try
                {
                    byte[] databytes = uc.Receive(ref remotes);
                    string str = Encoding.UTF8.GetString(databytes, 0, databytes.Length);
                    //把收到的广播消息放到会议信息框中
                    listBoxMessage.Items.Add(str);
                }
                catch
                {
                    break;
                }
            }
        }

        public Form1()
        {
            InitializeComponent();
            //将SetListBoxItem方法委托给listBoxCallback委托对象
            listBoxCallback = new SetListBoxItemCallback(SetListBoxItem);
        }

        //向ListBox中设置数据
        private void SetListBoxItem(ListBox listBox, string text, ListBoxOperation operation)
        {
            if (listBox.InvokeRequired == true)
            {
                this.Invoke(listBoxCallback, listBox, text, operation);
            }
            else
            {
                if (operation == ListBoxOperation.AddItem)
                {
                    if (listBox == listBoxAddress)
                    {
                        //如果对应的listBox中不存在text会议人员记录,则添加
                        if (listBox.Items.Contains(text) == false)
                            listBox.Items.Add(text);
                    }
                    else
                    {
                        listBox.Items.Add(text);
                    }
                    //完成一次操作之后,行数减1
                    listBox.SelectedIndex = listBox.Items.Count - 1;
                    listBox.ClearSelected();
                }
                else if (operation == ListBoxOperation.RemoveItem)
                {
                    //退出会议,删除相应信息
                    listBox.Items.Remove(text);
                }
            }
        }

        //发消息
        private void SendMessage(IPAddress ip, string sendString)
        {
            UdpClient myUdpClient = new UdpClient();
            //必须使用组播地址范围内的地址
            IPEndPoint iep = new IPEndPoint(ip, port);
            //将发送内容转换为字节数组
            byte[] bytes = Encoding.UTF8.GetBytes(sendString);
            try
            {
                //向子网发送信息
                myUdpClient.Send(bytes, bytes.Length, iep);
            }
            catch(Exception ex)
            {
                MessageBox.Show("发送失败: "+ex.ToString());
            }
            finally
            {
                myUdpClient.Close();
            }
        }

        //接收组播消息
        private void ReceiveMessage()
        {
            udpClient = new UdpClient(port);
            //加入多路广播组
            udpClient.JoinMulticastGroup(broderCastIp);
            //设置可以经过40次路由器转发
            udpClient.Ttl = 40;
            IPEndPoint remote = null;
            //无限循环,一直处于“监听”状态
            while(true)
            {
                try
                {
                    //关闭updClient时此句产生异常
                    byte[] bytes = udpClient.Receive(ref remote);
                    string str = Encoding.UTF8.GetString(bytes, 0, bytes.Length);
                    string[] splitString = str.Split(',');
                    int s = splitString[0].Length;
                    //splitString[0]的值为接收到的字符串str中"逗号"之前的字符串
                    switch(splitString[0])
                    {
                        case "Login"://进入会议
                            //在"会议信息框"中提示有人加入了会议讨论
                            SetListBoxItem(listBoxMessage, string.Format("[{0}]进入了会议", remote.Address), ListBoxOperation.AddItem);
                            //在"在线会议成员"框中加入这个新加入的成员
                            SetListBoxItem(listBoxAddress, remote.Address.ToString(), ListBoxOperation.AddItem);
                            string userListString = "List," + remote.Address.ToString();
                            for (int i=0; i<listBoxAddress.Items.Count; i++)
                            {
                                //让字符串userListString得到现在在线的所有会议人员的记录
                                userListString += "," + listBoxAddress.Items[i].ToString();
                            }
                            SendMessage(remote.Address, userListString);
                            break;
                        case "List":
                            //新登录的人员可以将在线的所有会议人员
                            //记录添加到自己的“在线会议成员”框中
                            for (int i=1; i<splitString.Length; i++)
                            {
                                SetListBoxItem(listBoxAddress, splitString[i], ListBoxOperation.AddItem);
                            }
                            break;
                        case "Message"://发送内容
                            SetListBoxItem(listBoxMessage, string.Format("[{0}]说:{1}", remote.Address, str.Substring(8)), ListBoxOperation.AddItem);
                            break;
                        case "Logout"://退出会议室
                            SetListBoxItem(listBoxMessage, string.Format("[{0}]退出了会议.", remote.Address), ListBoxOperation.AddItem);
                            SetListBoxItem(listBoxAddress, remote.Address.ToString(), ListBoxOperation.RemoveItem);
                            break;
                    }
                }
                catch
                {
                    //退出循环,结束线程
                    break;
                }
            }
        }

        //单击“发送”按钮触发事件
        private void buttonSend_Click(object sender, EventArgs e)
        {
            //判断如果“发言”框不为空,则可以组播发送信息
            if (textBoxMessage.Text.Trim().Length > 0)
            {
                SendMessage(broderCastIp, "Message," + textBoxMessage.Text);
                textBoxMessage.Text = "";
            }
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (buttonlogout.Enabled == true)
            {
                MessageBox.Show("请先离开会议室,然后再退出!", "", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                e.Cancel = true;
            }
        }

        //单击“进入会议”按钮触发事件
        private void buttonlogin_Click(object sender, EventArgs e)
        {
            Cursor.Current = Cursors.WaitCursor;
            Thread myThread = new Thread(ReceiveMessage);
            myThread.Start();
            //当前线程睡眠一秒,将CPU权利让给myThread线程,让接收线程准备完毕
            Thread.Sleep(1000);
            SendMessage(broderCastIp, "Login");
            buttonlogin.Enabled = false;
            buttonlogout.Enabled = true;
            Cursor.Current = Cursors.Default;
        }

        //单击“退出会议”按钮触发事件
        private void buttonlogout_Click(object sender, EventArgs e)
        {
            Cursor.Current = Cursors.WaitCursor;
            SendMessage(broderCastIp, "Logout");
            udpClient.DropMulticastGroup(this.broderCastIp);
            //等待接收线程处理完毕
            Thread.Sleep(1000);
            //结束接收线程
            udpClient.Close();
            buttonlogin.Enabled = true;
            buttonlogout.Enabled = false;
            Cursor.Current = Cursors.Default;
            listBoxAddress.Items.Clear();//清空数据
        }
    }
}


111111.png
[百度网盘] 下载示例工程 提取码:0zlu

下面这个工具类可以直接在Unity中使用

using System;
using System.Collections.Generic;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using UnityDebug = UnityEngine.Debug;
/// <summary>
/// UDP通信网络
/// </summary>
public sealed class UdpNetwork
{
    //单例
    private static UdpNetwork _instance;
    //发送端
    private static UdpClient sendUdpClient;
    //接收端
    private static UdpClient receiveUdpClient;
    //接收端口
    public static int receivePort = 18080;
    //消息队列
    private static Queue<byte[]> messageQueue = new Queue<byte[]>();
    //错误消息
    private static string errorMessage = null;
    //使用的组播IP地址
    private static IPAddress _castGroupIP = IPAddress.Parse("233.1.1.1");
    public static string CastGroupIP
    {
        get { return _castGroupIP.ToString(); }
        set { _castGroupIP = IPAddress.Parse(value); }
    }
    //本地IP
    private static string _localIP;
    //协议解算器
    private static ProtocolResolver resolver = new ProtocolResolver();
    //回调事件
    public static Action<uint, uint, byte[], string> receiveEvent;
    public static Action<string> errorEvent;

    //单例锁
    private static Object s_lock = new Object();
    public static UdpNetwork Instance
    {
        get
        {
            if (_instance != null)
                return _instance;

            lock (s_lock)
            {
                if (_instance == null)
                {
                    var network = new UdpNetwork();
                    //确保构造器执行完毕后,再把实例引用发布给_instance
                    Volatile.Write(ref _instance, network);
                }
            }
            return _instance;
        }
    }

    // 启动UDP通信
    public static void Startup()
    {
        _localIP = GetLocalIP();
        Thread receiveThread = new Thread(ReceiveMessage);
        receiveThread.Start();
        //当前线程睡眠一秒,将CPU权利让给receiveThread线程,让接收线程准备完毕
        Thread.Sleep(1000);
    }

    // 发消息
    public static void SendMessage(uint type)
    {
        SendMessage(type, 0);
    }

    public static void SendMessage(uint type, uint id)
    {
        //向加入组播IP的所有成员发消息
        SendMessage(type, id, _castGroupIP);
    }

    public static void SendMessage(uint type, uint id, string ip)
    {
        SendMessage(type, id, IPAddress.Parse(ip));
    }

    public static void SendMessage(uint type, uint id, IPAddress ip)
    {
        SendMessage(type, id, null, ip);
    }

    public static void SendString(uint type, uint id, string sendString)
    {
        //byte[] data = Encoding.UTF8.GetBytes(sendString);
        //向加入组播IP的所有成员发消息
        SendString(type, id, sendString, _castGroupIP);
    }

    public static void SendString(uint type, uint id, string sendString, string ip)
    {
        SendString(type, id, sendString, IPAddress.Parse(ip));
    }

    public static void SendString(uint type, uint id, string sendString, IPAddress ip)
    {
        byte[] data = Encoding.UTF8.GetBytes(sendString);
        SendMessage(type, id, data, ip);
    }

    public static void SendMessage(uint type, uint id, byte[] data)
    {
        //向加入组播IP的所有成员发消息
        SendMessage(type, id, data, _castGroupIP);
    }

    public static void SendMessage(uint type, uint id, object obj, string ip)
    {
        byte[] data = Serialize(obj);
        SendMessage(type, id, data, ip);
    }

    public static void SendMessage(uint type, uint id, byte[] data, string ip)
    {
        if (string.IsNullOrEmpty(ip))
        {
            UnityDebug.LogError("ip invalid");
            return;
        }
        SendMessage(type, id, data, IPAddress.Parse(ip));
    }

    public static void SendMessage(uint type, uint id, byte[] data, IPAddress ip)
    {
        if (sendUdpClient == null)
            sendUdpClient = new UdpClient();
        //如果ip为组播IP,则所有加入组播IP的成员都会收到消息
        IPEndPoint iep = new IPEndPoint(ip, receivePort);
        try
        {
            byte[] bytes = resolver.Encode(type, id, data, _localIP);
            //向子网发送信息
            sendUdpClient.Send(bytes, bytes.Length, iep);
        }
        catch (Exception ex)
        {
            UnityDebug.LogErrorFormat("发送失败: {0}", ex.ToString());
        }
        finally
        {
            //sendUdpClient.Close();
        }
    }

    // 接收组播消息
    private static void ReceiveMessage()
    {
        try
        {
            //只有端口没有IP的UdpClient才能加入广播组
            receiveUdpClient = new UdpClient(receivePort);
            //加入多路广播组
            receiveUdpClient.JoinMulticastGroup(_castGroupIP);
        }
        catch (SocketException e)
        {
            errorMessage = e.ToString();
            UnityDebug.LogError(e.ToString());
            return;
        }

        //设置可以经过的路由器转发次数
        receiveUdpClient.Ttl = 5;
        IPEndPoint remote = null;
        UnityDebug.Log("UDP network is ready!");
        //无限循环,一直处于“监听”状态
        while (true)
        {
            try
            {
                //关闭UpdClient时此句产生异常
                byte[] bytes = receiveUdpClient.Receive(ref remote);
                //string str = Encoding.UTF8.GetString(bytes, 0, bytes.Length);
                //UnityDebug.LogFormat("[收到数据] {0}", str);
                EnqueueMessage(bytes);
            }
            catch
            {
                UnityDebug.Log("UDP stop receive.");
                //退出循环,结束线程
                break;
            }
        }
    }

    // 关闭UDP通信
    public static void Close()
    {
        if (receiveUdpClient != null)
        {
            receiveUdpClient.Close();
            receiveUdpClient.Dispose();
        }
        receiveUdpClient = null;

        if (sendUdpClient != null)
        {
            sendUdpClient.Close();
            sendUdpClient.Dispose();
        }
        sendUdpClient = null;
    }

    // 消息入队
    private static void EnqueueMessage(byte[] message)
    {
        lock (messageQueue)
        {
            messageQueue.Enqueue(message);
        }
    }

    // 消息出队
    private static byte[] DequeueMessage()
    {
        lock (messageQueue)
        {
            if (messageQueue.Count == 0)
                return null;
            byte[] message = messageQueue.Dequeue();
            return message;
        }
    }

    // 需要外部每帧调用此方法,检测消息队列中是否有消息
    public static void Tick()
    {
        if (!string.IsNullOrEmpty(errorMessage))
        {
            errorEvent?.Invoke(errorMessage);
            errorMessage = null;
        }

        byte[] rawBytes = DequeueMessage();
        if (rawBytes == null)
            return;
        resolver.Dencode(rawBytes);
        receiveEvent?.Invoke(resolver.type, resolver.id, resolver.data, resolver.remoteIP);
    }

    // 获取本机IP
    public static string GetLocalIP()
    {
        string AddressIP = string.Empty;
        foreach (IPAddress _IPAddress in Dns.GetHostEntry(Dns.GetHostName()).AddressList)
        {
            if (_IPAddress.AddressFamily.ToString() == "InterNetwork")
            {
                AddressIP = _IPAddress.ToString();
            }
        }
        return AddressIP;
    }

    public static string ToString(byte[] bytes)
    {
        string str = Encoding.UTF8.GetString(bytes, 0, bytes.Length);
        return str;
    }

    // 将对象序列化为字节数组
    public static byte[] Serialize(object obj)
    {
        MemoryStream stream = new MemoryStream();
        IFormatter formatter = new BinaryFormatter();
        formatter.Serialize(stream, obj);
        stream.Flush();
        byte[] bytes = stream.ToArray();
        stream.Close();
        return bytes;
    }

    // 将字节数组反序列化为对象
    public static object Deserialize(byte[] bytes)
    {
        MemoryStream stream = new MemoryStream();
        stream.Write(bytes, 0, bytes.Length);
        stream.Flush();
        stream.Position = 0;
        IFormatter formatter = new BinaryFormatter();
        object obj = formatter.Deserialize(stream);
        stream.Close();
        return obj;
    }
}

//负责解析协议
public class ProtocolResolver
{
    //递增值,用来保证发送的每条协议都有个唯一序号
    private static uint increase = 0;
    //远端IP
    public string remoteIP { get; private set; }
    //协议序号
    public uint num { get; private set; }
    //协议类型
    public uint type { get; private set; }
    //协议ID
    public uint id { get; private set; }
    //协议内容
    public byte[] data { get; private set; }

    //解码
    public void Dencode(byte[] rawBytes)
    {
        MemoryStream stream = new MemoryStream(rawBytes);
        BinaryReader reader = new BinaryReader(stream);
        remoteIP = reader.ReadString();
        num = reader.ReadUInt32();
        type = reader.ReadUInt32();
        id = reader.ReadUInt32();
        int length = reader.ReadInt32();
        data = reader.ReadBytes(length);
        reader.Close();
        stream.Close();
    }

    //编码
    public byte[] Encode(uint type, uint id, byte[] data, string localIP)
    {
        int length = (data == null) ? 0 : data.Length;
        MemoryStream stream = new MemoryStream();
        BinaryWriter writer = new BinaryWriter(stream);
        writer.Write(localIP);
        writer.Write(increase);
        writer.Write(type);
        writer.Write(id);
        writer.Write(length);
        if (length > 0)
            writer.Write(data);
        writer.Flush();
        byte[] rawBytes = stream.ToArray();
        writer.Close();
        stream.Close();
        increase = (increase + 1) % uint.MaxValue;
        return rawBytes;
    }
}
 

利用手机热点组建局域网

注意   开热点的手机必须开启移动数据,否则热点机无发向连接它的设备发数据,但连接热点机的设备可向热点机发数据。

标签: C#

Powered by emlog  蜀ICP备18021003号-1   sitemap

川公网安备 51019002001593号