开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:1907

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 1374|回复: 34

[yukuyoulei] 超简洁,单文件,Unity友好 - UDP可靠传输算法,附赠大包拆分

[复制链接]  [移动端链接]
排名
27
昨日变化

53

主题

872

帖子

7501

积分

Rank: 9Rank: 9Rank: 9

UID
287
好友
44
蛮牛币
4311
威望
30
注册时间
2013-6-9
在线时间
1910 小时
最后登录
2017-10-20

七夕浪漫情人社区QQ达人活力之星原创先锋认证开发者

QQ
发表于 2017-10-12 13:51:32 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
本帖最后由 yukuyoulei 于 2017-10-12 14:03 编辑

大家好,好久不见了,有人想我没,如果没有的话,下个帖子我再问问


UDP大家都知道是什么东东吧,它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。[引用自互动百科]它最突出的一个优点,就是整包送达,不像TCP那样,还需要考虑粘包和拆包,并且UDP通讯是公平的,没有所谓的服务器或者客户端,想发就发,要发的漂亮。 但是它也有很严重的缺点,那就是——丢包率,因为它是不可靠的通信,发出去之后,就不管了。


所以我们如果想用它来做一些简单的东西还好,丢就丢了,不过想做成可用的游戏或者应用,又不太想用TCP(就你事儿多),就必须要想办法克服它的不可靠这个缺点了。


最简单的一个思路,就是将所有要发送的包,放到一个队列里,另外一端收到这个包之后,就告诉发包端,我收到了,你可以把包XXX从队列里删除掉了,否则发包端就会一直在一定时间后重发这个包,一般我们还会设定发送若干次之后将这个包主动丢弃,因为可能已经过了时效。当然,这个确认包我们一般设置为不可靠传输,否则这个确认包收到后再发个“确认‘确认包’包”,就没个完了。


另外一个小事情,UDP的传输是有字节数上限的,一般是65535个字节,但是UDP本身的包头(8)和IP包头(20)一共会占去28个字节,也就是说我们能用的最大上限是65507个byte。一般情况下是够用的,不过也还是有不少情况下需要将这些大包拆分成小包来发送。注意,这里和TCP协议的拆包是有本质上不同的,TCP协议的拆包是在接收端进行的,原因是TCP是个流协议,流是没有边界的,它会按照缓冲区的情况,来进行包的划分,它并不在意它划分出来的包是一个完整的包,还是一个大包的一部分,或是一堆小包的集合。而UDP协议的拆包,是在发送时要做好的,原因就是上面提到的,UDP传输是整包发送,并且有字节数上限。


闲话少说,上源码。
AUdpConnector.cs
[C#] 纯文本查看 复制代码
using System.Collections.Generic;
using System.Net.Sockets;
using System;
using System.Net;
using System.Text;
using System.Threading;

public class AUdpConnector
{
        private static AUdpConnector sinstance;
        public static AUdpConnector Instance
        {
                get
                {
                        if (sinstance == null)
                        {
                                sinstance = new AUdpConnector();
                        }
                        return sinstance;
                }
        }

        int bufferSize = 4096;
        UdpClient connector;
        Action<byte[], IPEndPoint, int> delReceive;
        public void Init(Action<byte[], IPEndPoint, int> receiveDel, int port)
        {
                delReceive = receiveDel;
                OnBind(port);
        }
        private void OnBind(int port)
        {
                connector = new UdpClient(port, AddressFamily.InterNetwork);
                OnStartReceive(connector);
                OnStartToSend();
        }

        private void OnStartToSend()
        {
                new Thread(new ThreadStart(DoSend)).Start();
        }

        public static bool bQuit = false;
        private void DoSend()
        {
                while (true)
                {
                        if (bQuit)
                        {
                                break;
                        }
                        lock (sendLock)
                        {
                                List<int> remove = new List<int>();
                                foreach (var pv in packetsToSendPool)
                                {
                                        var p = pv.Value;
                                        if (p.bOutOfTime)
                                        {
                                                remove.Add(pv.Key);
                                                continue;
                                        }
                                        if (p.NeedRetry)
                                        {
                                                byte[] buffer = new byte[p.buffer.Length + 8];
                                                byte[] packetMetaIDBuff = BitConverter.GetBytes(p.packetMetaID);
                                                byte[] packetIDBuff = BitConverter.GetBytes(p.packetID);
                                                Array.ConstrainedCopy(packetMetaIDBuff, 0, buffer, 0, packetMetaIDBuff.Length);
                                                Array.ConstrainedCopy(packetIDBuff, 0, buffer, 4, packetIDBuff.Length);
                                                Array.ConstrainedCopy(p.buffer, 0, buffer, 8, p.buffer.Length);
                                                connector.Send(buffer, buffer.Length, p.endpoint);

                                                if (!p.bReliable)
                                                {
                                                        remove.Add(pv.Key);
                                                }
                                        }
                                }
                                foreach (var r in remove)
                                {
                                        packetsToSendPool.Remove(r);
                                }
                        }

                        Thread.Sleep(1);
                }
        }

        public void OnStartReceive(UdpClient s)
        {
                if (s == null)
                {
                        s = connector;
                }

                byte[] bs = new byte[bufferSize];
                s.BeginReceive(OnReceived, new object[] { s, bs });
        }
        private void OnReceived(IAsyncResult ar)
        {
                object[] objs = ar.AsyncState as object[];
                UdpClient s = objs[0] as UdpClient;
                IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
                byte[] bs = s.EndReceive(ar, ref sender);
                int buffr = bs.Length;
                if (buffr == 0)
                {
                        if (delReceive != null)
                        {
                                delReceive(null, sender, 0);
                        }
                }
                else
                {
                        if (delReceive != null)
                        {
                                lock (sendLock)
                                {
                                        int packetMetaID = BitConverter.ToInt32(bs, 0);
                                        int packetID = BitConverter.ToInt32(bs, 4);

                                        int totalSec = BitConverter.ToInt32(bs, 8);
                                        if (totalSec == 0)
                                        {
                                                int def = BitConverter.ToInt32(bs, 16);

                                                byte[] packetBuff = new byte[bs.Length - 20];
                                                Array.Copy(bs, 20, packetBuff, 0, packetBuff.Length);
                                                if (def == (int)EPacketID.EUDPPacketReceived)
                                                {
                                                        string sp = System.Text.Encoding.UTF8.GetString(packetBuff);
                                                        int ipid = 0;
                                                        int.TryParse(sp, out ipid);
                                                        if (packetsToSendPool.ContainsKey(ipid))
                                                        {
                                                                packetsToSendPool.Remove(ipid);
                                                        }
                                                }
                                                else
                                                {
                                                        OnSend("" + packetMetaID, sender.Address, sender.Port, (int)EPacketID.EUDPPacketReceived, false);
                                                        delReceive(packetBuff, sender, def);
                                                }
                                        }
                                        else
                                        {
                                                OnSend("" + packetMetaID, sender.Address, sender.Port, (int)EPacketID.EUDPPacketReceived, false);

                                                if (!dPendingPackets.ContainsKey(packetID))
                                                {
                                                        dPendingPackets.Add(packetID, new Dictionary<int, byte[]>());
                                                }
                                                int index = BitConverter.ToInt32(bs, 12);
                                                byte[] packetBuff = new byte[bs.Length - 16];
                                                Array.Copy(bs, 16, packetBuff, 0, packetBuff.Length);
                                                if (dPendingPackets[packetID].ContainsKey(index))
                                                {
                                                        dPendingPackets[packetID][index] = packetBuff;
                                                }
                                                else
                                                {
                                                        dPendingPackets[packetID].Add(index, packetBuff);
                                                }
                                                if (dPendingPackets[packetID].Count == totalSec)
                                                {
                                                        ProcessingPacket(dPendingPackets[packetID], sender);
                                                }
                                        }
                                }
                        }
                        OnStartReceive(s);
                }
        }

        private void ProcessingPacket(Dictionary<int, byte[]> dictionary, IPEndPoint ep)
        {
                byte[] buffer = new byte[bufferSize * dictionary.Count];
                int total = 0;
                for (int i = 0; i < dictionary.Count; i++)
                {
                        Array.Copy(dictionary[i], 0, buffer, total, dictionary[i].Length);
                        total += dictionary[i].Length;
                }
                int def = BitConverter.ToInt32(buffer, 0);
                byte[] result = new byte[total - 4];
                Array.Copy(buffer, 4, result, 0, result.Length);
                delReceive(result, ep, def);
        }

        Dictionary<int, Dictionary<int, byte[]>> dPendingPackets = new Dictionary<int, Dictionary<int, byte[]>>();

        private List<byte[]> BufferProcesser(byte[] buffer)
        {
                List<byte[]> result = new List<byte[]>();
                if (buffer.Length <= bufferSize - 20)
                {
                        byte[] bs = new byte[buffer.Length + 8];
                        Array.Copy(buffer, 0, bs, 8, buffer.Length);
                        result.Add(bs);
                }
                else
                {
                        int totalBuffSec = 0;
                        totalBuffSec = (int)(buffer.Length / (bufferSize - 2));
                        if (buffer.Length % (bufferSize - 2) > 0)
                        {
                                totalBuffSec++;
                        }
                        int offset = 0;
                        for (int i = 0; ; i++)
                        {
                                byte[] bs = new byte[(buffer.Length - offset > bufferSize - 2 ? bufferSize : buffer.Length - offset) + 8];
                                byte[] lengthBuff = BitConverter.GetBytes(totalBuffSec);
                                byte[] lengthIndex = BitConverter.GetBytes(i);
                                Array.Copy(lengthBuff, 0, bs, 0, lengthBuff.Length);
                                Array.Copy(lengthIndex, 0, bs, 4, lengthIndex.Length);
                                Array.Copy(buffer, offset, bs, 8, bs.Length - 8);
                                result.Add(bs);
                                offset += bs.Length - 8;
                                if (offset >= buffer.Length)
                                {
                                        break;
                                }
                        }
                }
                return result;
        }
        public void OnSend(string s, IPAddress ip, int port, int def, bool bReliable = true)
        {
                OnSend(Encoding.UTF8.GetBytes(s), ip, port, def, bReliable);
        }
        private object sendLock = new object();
        private Dictionary<int, UDPPacket> packetsToSendPool = new Dictionary<int, UDPPacket>();
        public void OnSend(byte[] bs, IPAddress ip, int port, int def, bool bReliable = true)
        {
                lock (sendLock)
                {
                        byte[] defbuff = BitConverter.GetBytes(def);
                        byte[] buff = new byte[bs.Length + defbuff.Length];
                        Array.Copy(defbuff, buff, defbuff.Length);
                        Array.Copy(bs, 0, buff, 4, bs.Length);

                        List<byte[]> buffers = BufferProcesser(buff);
                        int packetID = UDPPacket.NewPacketID;
                        foreach (var b in buffers)
                        {
                                var p = new UDPPacket(b, new IPEndPoint(ip, port), packetID, bReliable);
                                packetsToSendPool.Add(p.packetMetaID, p);
                        }
                }
        }

         public void OnSendOnBroadcast(string s, int port)
        {
                OnSendOnBroadcast(Encoding.UTF8.GetBytes(s), port);
        }
        public void OnSendOnBroadcast(byte[] bs, int port)
        {
                OnSend(bs, IPAddress.Broadcast, port, (int)EPacketID.EBroadcast, false);
        }

        public bool server { get; set; }
        public bool client { get; set; }
}

// packetID, total section count, index
public class UDPPacket
{
        private static int _staticid;
        public static int NewPacketID
        {
                get
                {
                        if (_staticid > int.MaxValue - 2)
                        {
                                _staticid = 0;
                        }
                        return ++_staticid;
                }
        }

        public byte[] buffer { get; private set; }
        public IPEndPoint endpoint { get; private set; }
        public DateTime tryTime { get; private set; }
        public int packetID { get; private set; }
        public int packetMetaID { get; private set; }
        public int packetDef { get; private set; }
        public int retryCount { get; private set; }
        const int retryMS = 200;
        const int maxRetryCount = 10;
        public bool bReliable { get; private set; }
        public UDPPacket(byte[] b, IPEndPoint ep, int pid, bool reliable)
        {
                buffer = b;
                endpoint = ep;
                tryTime = DateTime.Now.AddMilliseconds(-retryMS);
                packetID = pid;
                bReliable = reliable;
                packetMetaID = NewPacketID;
        }
        public bool bOutOfTime { get; private set; }
        public bool NeedRetry
        {
                get
                {
                        if ((DateTime.Now - tryTime).TotalMilliseconds < retryMS)
                        {
                                return false;
                        }
                        tryTime = DateTime.Now;
                        retryCount++;
                        if (retryCount > maxRetryCount)
                        {
                                bOutOfTime = true;
                                return false;
                        }
                        return true;
                }
        }
}
public enum EPacketID
{
        EUDPPacketReceived = 1,
        EBroadcast,
}



注意事项:
因为我们的发送是新建了一个进程然后死循环来进行的(代码第40行),
6.png

所以如果在Unity内使用的时候,编辑器模式下要在退出运行后将这个线程主动break掉,只要在一个app运行时不会被销毁的脚本上(例如:标记了DontDestroyOnLoad)的OnDestroy函数里,调用一下

AUdpConnector.bQuit = true;

否则退出运行后,再重新运行Unity会卡死(万年BUG仍未解)。


本帖被以下淘专辑推荐:

  • · I Like|主题: 83, 订阅: 7

回复

使用道具 举报

排名
27
昨日变化

53

主题

872

帖子

7501

积分

Rank: 9Rank: 9Rank: 9

UID
287
好友
44
蛮牛币
4311
威望
30
注册时间
2013-6-9
在线时间
1910 小时
最后登录
2017-10-20

七夕浪漫情人社区QQ达人活力之星原创先锋认证开发者

QQ
 楼主| 发表于 2017-10-12 13:53:45 | 显示全部楼层
忘了说了,Enjoy~

回复

使用道具 举报

3偶尔光临
246/300
排名
8779
昨日变化
3

0

主题

55

帖子

246

积分

Rank: 3Rank: 3Rank: 3

UID
17208
好友
0
蛮牛币
207
威望
0
注册时间
2014-3-11
在线时间
96 小时
最后登录
2017-10-23
发表于 2017-10-12 15:53:57 | 显示全部楼层
谢谢分享

回复

使用道具 举报

排名
1275
昨日变化
5

9

主题

114

帖子

1953

积分

Rank: 9Rank: 9Rank: 9

UID
686
好友
5
蛮牛币
2238
威望
0
注册时间
2013-7-4
在线时间
708 小时
最后登录
2017-10-22

专栏作家社区QQ达人

QQ
发表于 2017-10-12 17:22:49 | 显示全部楼层
/// <summary>         /// 当应用程序退出或编辑器结束运行         /// </summary>         private void OnApplicationQuit()

回复 支持 反对

使用道具 举报

排名
14505
昨日变化
7

0

主题

14

帖子

77

积分

Rank: 2Rank: 2

UID
226019
好友
0
蛮牛币
85
威望
0
注册时间
2017-6-9
在线时间
28 小时
最后登录
2017-10-21
发表于 2017-10-12 19:42:49 | 显示全部楼层
666感谢分享

回复

使用道具 举报

排名
1329
昨日变化
1

61

主题

335

帖子

2330

积分

Rank: 9Rank: 9Rank: 9

UID
35466
好友
20
蛮牛币
3794
威望
0
注册时间
2014-7-21
在线时间
782 小时
最后登录
2017-10-21

专栏作家

发表于 2017-10-13 09:19:23 | 显示全部楼层
听起来很牛b的样子

回复 支持 反对

使用道具 举报

3偶尔光临
185/300
排名
32192
昨日变化
22

0

主题

38

帖子

185

积分

Rank: 3Rank: 3Rank: 3

UID
75790
好友
2
蛮牛币
182
威望
0
注册时间
2015-2-26
在线时间
142 小时
最后登录
2017-10-17
发表于 2017-10-13 09:26:12 | 显示全部楼层
有时间能否分享下  udp的p2p模式

回复 支持 反对

使用道具 举报

4四处流浪
444/500
排名
5827
昨日变化
2

1

主题

89

帖子

444

积分

Rank: 4

UID
209909
好友
0
蛮牛币
305
威望
0
注册时间
2017-3-4
在线时间
178 小时
最后登录
2017-10-20
发表于 2017-10-13 09:27:41 | 显示全部楼层
ganxie分享。。。。。。。。。。

回复

使用道具 举报

7日久生情
1638/5000
排名
1866
昨日变化
2

7

主题

269

帖子

1638

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
159103
好友
6
蛮牛币
4409
威望
0
注册时间
2016-8-29
在线时间
738 小时
最后登录
2017-10-20

活力之星迈向小康

发表于 2017-10-13 09:49:57 | 显示全部楼层
别的不管先问答  想你了  好久不见

点评

mmd  发表于 2017-10-13 10:43

回复 支持 反对

使用道具 举报

排名
27
昨日变化

53

主题

872

帖子

7501

积分

Rank: 9Rank: 9Rank: 9

UID
287
好友
44
蛮牛币
4311
威望
30
注册时间
2013-6-9
在线时间
1910 小时
最后登录
2017-10-20

七夕浪漫情人社区QQ达人活力之星原创先锋认证开发者

QQ
 楼主| 发表于 2017-10-13 10:43:01 | 显示全部楼层
菜菜汪 发表于 2017-10-13 09:26
有时间能否分享下  udp的p2p模式

UDP就是天生的P2P模式。

回复 支持 反对

使用道具 举报

5熟悉之中
642/1000
排名
3333
昨日变化

0

主题

137

帖子

642

积分

Rank: 5Rank: 5

UID
147717
好友
0
蛮牛币
734
威望
0
注册时间
2016-5-5
在线时间
165 小时
最后登录
2017-10-18
发表于 2017-10-13 11:29:10 | 显示全部楼层
听起来很牛b的样子

回复 支持 反对

使用道具 举报

3偶尔光临
189/300
排名
9240
昨日变化
4

2

主题

40

帖子

189

积分

Rank: 3Rank: 3Rank: 3

UID
43365
好友
2
蛮牛币
93
威望
0
注册时间
2014-9-3
在线时间
60 小时
最后登录
2017-10-13
发表于 2017-10-13 12:22:18 | 显示全部楼层
用法呢?

回复

使用道具 举报

排名
27
昨日变化

53

主题

872

帖子

7501

积分

Rank: 9Rank: 9Rank: 9

UID
287
好友
44
蛮牛币
4311
威望
30
注册时间
2013-6-9
在线时间
1910 小时
最后登录
2017-10-20

七夕浪漫情人社区QQ达人活力之星原创先锋认证开发者

QQ
 楼主| 发表于 2017-10-13 15:25:14 | 显示全部楼层

只有两个接口,Init和Send,看着用嘛。

回复 支持 反对

使用道具 举报

排名
27
昨日变化

53

主题

872

帖子

7501

积分

Rank: 9Rank: 9Rank: 9

UID
287
好友
44
蛮牛币
4311
威望
30
注册时间
2013-6-9
在线时间
1910 小时
最后登录
2017-10-20

七夕浪漫情人社区QQ达人活力之星原创先锋认证开发者

QQ
 楼主| 发表于 2017-10-13 15:25:16 | 显示全部楼层

只有两个接口,Init和Send,看着用嘛。

回复 支持 反对

使用道具 举报

7日久生情
1608/5000
排名
1148
昨日变化
6

1

主题

212

帖子

1608

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
157507
好友
1
蛮牛币
2716
威望
0
注册时间
2016-7-18
在线时间
575 小时
最后登录
2017-10-22
QQ
发表于 2017-10-14 16:20:20 | 显示全部楼层
本帖最后由 lesterlzy 于 2017-10-14 16:21 编辑

不用啊 老铁。什么万年bug。
你设置线程退出的条件为 while(app_is_need_to_stop == false)
{
//do your job
}

然后重载 OnApplicationQuit(){connector.close();connector = null;app_is_need_to_stop = true;}
完事,优雅的退出。你值得拥有。

点评

一个意思,还是要在一个MonoBehaviour里头处理一下。  发表于 7 天前

回复 支持 1 反对 0

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表