《Unity3D高级编程之进阶主程》第六章,网络层(三) - 实现UDP

实现UDP

在前面介绍了如何实现TCP socket,下面要介绍下UDP的实现方式。

其实两者都是长链接的方式,很多地方都有相识之处,比如两者都需要连接和断开事件支撑,都需要做发送和接收队列缓存,都需要定义数据包协议格式,都需要加密和校验。

我们来说说TCP有而UDP没有的部分,以及需要我们在UDP上实现的功能。UDP和TCP相比,UDP基本就是TCP的阉割版本,很多TCP有的功能UDP都没有。相同的地方有,都是异步发送和接收,都需要多线程操作,发送和接收都需要缓冲队列,并且发送数据时都需要合并数据包。TCP有而UDP没有的地方有,UDP不会自己校验重发,丢包概率大,数据包接收顺序不确定,而且UDP本身没有连接断开检测机制,也没有连接确认机制。

所以直接使用UDP来做网络连接,会遇到巨大的问题,可以说直接使用而不加修饰,不确定性太大基本用不了UDP。

===

连接确认机制

TCP有连接的三次握手协议,相当于在连接过程中跟服务器端协商后敲定我们已经建立了连接这个一致预期,而UDP是无状态链接,它并没有三次握手的协议,所以UDP连接其实是一厢情愿的,客户端并不知道是否真正连接成功了。

所有发送和接受都是建立在连接确认的基础上的,所以我们必须先确认知道我们是否连接成功。能够判断连接是否成功是整个实现UDP的第一步,只有这样才能顺利得进行下面的数据包收发操作。

怎么确认连接成功呢?

首先我们来看看TCP连接的三次握手是怎么做的。

1, 首先客户端向服务器端发送一个数据包,里面包含了Seq=0的变量,表示当前发送数据包的序列号为0,也就是第一个数据包。

2, 服务器端收到客户端的数据包后,发现Seq=0,说明是第一个包,用来确认连接的,于是给客户端也发送了一个数据包,包含了Seq=0,和Ack=1,表示服务器端已经收到你的连接确认包了,并且回应包Ack序列标记为1。

3, 客户端收到服务器端给的回应数据包后,知道了服务器端已经知道了我们已经建立连接,于是向服务器端发送了一个数据包,里面包含了Seq=1,Ack=1,表示确认数据包已经收到,连接已经确认,开始发送数据。

这就是TCP的三次握手来确认连接的流程。

在UDP下并没有三次握手机制,为了建立更好的确认连接机制,我们可以模仿TCP的形式来确认连接。不过第3次握手有点多余,可以省去最后一次握手的数据包,改为2次握手。步骤如下。

首先在UDP连接后,在确认连接前不进行任何的其他类型的数据发送并且不接收任何其他数据,我们将这种发送数据包以确认连接成功与否的数据包称为握手包。

在建立连接后,客户端先向服务器端发送一个握手数据包,代表客户端向服务器端请求连接确认信号的数据包,包内的数据仅仅是一个序列号Seq=0,或者不是序列号也可以,而是一个特殊的字段。当服务器端收到这个握手数据包后,也就是实现了第一次握手。

服务器端在收到第一次握手数据包后,回应一个握手数据包,里面同样带有客户端能识别的连接确认信号。当客户端接收到时就说明,发出去给服务器端的连接确认数据包有了回应,也就是说第二次握手成功。接收到了第二次握手连接确认响应数据包时,就表明了连接已经成功建立。

UDP确认连接的整个握手过程,就相当于客户端和服务器端的一次交流,相互认识一下并且示意后面的交流将要开始。

实现UDP连接确认具体步骤(伪代码):

1, 首先使用API建立UDP连接。

SvrEndPoint = new IPEndPoint(IPAddress.Parse(host), port);
UdpClient = new UdpClient(host, port);
UdpClient.Connect(SvrEndPoint);

2, 启动接收数据线程。

UdpClient.BeginReceive(ReceiveCallback, this);
void ReceiveCallback(IAsyncResult ar)
{
    Byte[] data = (mIPEndPoint == null) ?
        UdpClient.Receive(ref mIPEndPoint) :
        UdpClient.EndReceive(ar, ref mIPEndPoint);

    if (null != data)
        OnData(data);

    if (mUdpClient != null)
    {
        // try to receive again.
        mUdpClient.BeginReceive(ReceiveCallback, this);
    }
}

3, 发送连接确认数据包,并屏蔽其他发送和接收功能。

SendConnectRequest();
StopSendNormalPackage();
StopReceiveNormalPackage();

4, 等待接收连接确认数据包。

Void OnData(data)
{
     If( !IsConnected )
    {
        If( IsConnectResponse(data) )
        {
            OnEvent( Event.ConnectSuccess );
            IsConnected = true;
        }
        Return;
    }
    ProcessNormalData(data);
}

5, 连接确认包收到,确认连接已经成功建立。开启发送和接收数据包的功能。

Void ProcessNormalData(data)
{
    If( !IsConnected ) return;

    DealNetworkData(data);
}

经过与服务器端数据包的来回,UDP完成了2次握手的确认机制,已经可以认定为连接已经成功建立。

断开连接判定机制

UDP自己不能判断是否断开,因为它是无状态的连接,需要我们自己来做断线检测,检测的方式与TCP的心跳包类似。

断开连接的判定机制,也是用数据包来回的形式,不过不是单一一个数据包,而是长时间的心跳包的形式来做持续性的判断连接状态。

首先,我们要与服务器端有个协定,每隔X秒(比如5秒)发送一个心跳数据包给服务器端,这个客户端发送的心跳数据包里包含了一些客户端信息,包括ID,角色状态,设备信息等,包体不能太大,否则会就加重了宽带负担。

当服务器端收到心跳数据包时,也立刻回复一个心跳数据回应包,里面包含了,服务器端当前时间,服务器端当前状态等信息。

当客户端收到此数据包时,说明连接尚在,也能同时同步服务器端的时间和一些基础的信息。

如果客户端很久没有收到心跳数据回应包时,就表明,连接已经断开了,比如30秒没收到心跳包,可以判断连接已经断开。

服务器端也是一样操作,当没有收到心跳包很久,就表明客户端的连接已经断开。这时客户端就可以开启相应的重连程序,或重连提示以及步骤。

具体步骤如下:

    1,  每隔X秒向服务器端发送心跳数据包。

    2,  服务器端收到心跳数据包后回复心跳响应数据包。

    3,  如果,客户端和服务器端都很久没有收到心跳数据包,比如30秒,则判定连接断开。

    4,  主动断开连接,客户端提示用户,或者重新创建连接,服务器端则是处理与之相关数据的处理。
数据包校验与重发机制

前面说UDP相当于是TCP的阉割版,而最关键的阉割部分就是校验和重发机制。

没有校验和重发机制,意味着发送端无法知道数据包的发送是否到达或者丢失,甚至丢失了也无法重新发送的机制来补充丢失的数据包。

所以我们需要自己编写增加对数据的校验和重发机制,来确保数据的可靠性。

TCP有校验和重发机制,我们可以模仿它的校验和重发机制在UDP上使用,再在此基础上加以改进。这样我们即有了UDP的速度,又有了TCP的可靠性。

我们来看TCP是如何做数据包的校验和重发的。

关于Seq和Ack。Seq即Sequence Number,为源端(source)的发送序列号;Ack即Acknowledgment Number,为目的端(destination)的接收确认序列号。

1, 首先客户端向服务器端发送数据包,里面包含Seq(sequence number)序列号为1(已经发送的数据包的累计大小),发送的数据包大小为比如264。

2, 服务器端收数据包后就知道了当前连接的这个数据包的序号为1,也就是连接后第一个数据包。于是向客户端发送了一个确认包,确认包中包含了的Ack=264(接收到的数据包的累计大小),告诉客户端我服务器端已经收到了数据包,现在累计大小为264的数据包。如果累计大小错误,就需要启动重传机制,退回到最后一次正确的的数据包位置,进行重传,以保证可靠性。

3, 当客户端再次发送数据包时,里面包含的Seq序列号为265,意思为发送累计数据大小,这次发送的数据包大小为100,服务器端接收到后向客户端发送确认包,确认包中,包含了Ack=365,意思是已经累计收到数据包大小为365。如果累计大小错误,请启动重传机制,确保可靠性。

4, 接下来由服务器端向客户端发送数据。服务器端向客户端发送数据也是同样的方法和步骤。数据包中包含了服务器端的Seq(已经发送的数据包大小),比如累计发送了1,seq=1,这次数据包大小为585,客户端接收到服务器端的数据包后,向服务器端发送确认包,包中包含了服务器端发过来的Ack =586,服务器端收到确认包后,就知道了累计接收的数据包大小已经到了585,也就是说当前的数据包已经发送成功。

若在规定的时间内收到响应Seq序列号和Ack,表明该报文发送成功,可以发送下一个报文Seq;否则重传(TCP Retransmitssion)。序列号确认机制是TCP可靠性传输的保障。

借鉴TCP的方法,UDP也可以用此方法来做检测和重传。但是大小累计的检测方式重传的效率太差,不能准确快速的定位重传的数据包,甚至有些已经到达的数据,因为中间数据包的丢包,导致已经到达的数据也需要重传,来确保可靠性。

在实现UDP的检测和重传时,我们可以进行如下的改进。

1, 客户端向服务器端发送数据包,数据包中包含Seq=1(表示数据包的发送序列),发送后将此数据包推入到已经发送但还没有确认的队列里。

如果服务器接收到Seq=1的数据包,就回应客户端一个确认包,包中Ack=1,表示Seq=1的包已经确认收到。

如果服务器端没有接收到数据,客户端X秒后发现并没有收到Seq为1的确认包,判定为Seq=1的数据包传输失败,从已经发送但未确认的数据包队列中取出Seq=1的数据包,重新发送。

2, 客户端向服务器端发送了10个数据包,分别是Seq=1,2,3,4,5,6,7,8,9,10,服务器收到的序列是,1,3,4,5,7,8,9,10,其中有2,6,没有收到数据包。

客户端在等待确认包超时后,对2,6进行重传。在服务器端接收到数据包后,在处理数据包时,如果数据包顺序有跳跃的现象就表明数据包丢失,等待客户端重传,这时就在断开的序列处停止处理数据包,进行等待操作。

3, 加快重传确认时间。客户端向服务器端发送5个数据包,分别是Seq=1,2,3,4,5,服务器端收到的包是1,3,4,5,当收到3时,发现2被跳过1次,当收到4时发现2被跳过2次,立刻启动2的重传,不在等确认超时,加快丢包重传速度。

丢包问题分析

UDP丢包多是很正常现象,这是UDP牺牲质量而提高速度的代价。UDP丢包的原因很多,我们这里做个分析。

1, 接收端处理时间过长导致丢包:

调用recv方法接收端收到数据后,处理数据花了一些时间,处理完后再次调用recv方法,在这二次调用间隔里,发过来的包可能丢失。

对于这种情况可以修改接收端,将包接收后存入一个缓冲区,然后迅速返回继续recv。或者使用前面提到的双队列机制,来缩短锁队列的时间,从而解放了处理包的时间和接收数据包的线程之间的冲突。

2, 发送的包巨大丢包:

虽然Send方法会帮你做大包切割成小包发送的事情,但包太大也不行。

例如超过50K的一个udp包,不切割直接通过send方法发送也会导致这个包丢失。

发送的包较大,超过接受者缓存导致丢包:包超过mtu size数倍,几个大的udp包可能会超过接收者的缓冲,导致丢包。

这种情况需要切割成小包再逐个send。报文过大的问题,可以通过控制报文大小来解决,使得每个报文的长度小于MTU。

以太网的MTU通常是1500 bytes,其他一些诸如拨号连接的网络MTU值为1280 bytes,如果使用speaking这样很难得到MTU的网络,那么最好将报文长度控制在1280 bytes以下。

3, 发送的包频率太快:

虽然每个包的大小都小于mtu size 但是频率太快,例如40多个mut size的包连续发送中间不sleep,也有可能导致丢包。

这种情况可以通过建立Socket接收缓冲队列解决,和建立发送缓冲队列来解决,并且在发送频率过快的时候考虑Sleep作为时间间隔。

很多人会不理解发送速度过快为什么会产生丢包,原因就是UDP的SendTo不会造成线程阻塞,也就是说,UDP的SentTo不会像TCP中的SendTo那样,直到数据完全发送才会return回调用函数,它不保证当执行下一条语句时数据是否被发送。(SendTo方法是异步的)

如果要发送的数据过多或者过大,那么在缓冲区满的那个瞬间要发送的报文就很有可能被丢失。至于对“过快”的解释,作者这样说:“A few packets a second are not an issue; hundreds or thousands may be an issue.”(一秒钟几个数据包不算什么,但是一秒钟成百上千的数据包就不好办了)。

要解决接收方丢包的问题很简单,首先要保证程序执行后马上开始监听(如果数据包不确定什么时候发过来的话),其次,要在收到一个数据包后最短的时间内重新回到监听状态,其间要尽量避免复杂的操作(比较好的解决办法是使用多线程回调机制)。

以上是UDP的实现细节,同时阐述了UDP的实现难点和注意点。

参考资料:

    1, 基于TCp的数据包传输过程

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

    本文为博主原创文章,未经允许不得转载:

    《Unity3D高级编程之进阶主程》第六章,网络层(三) - 实现UDP

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号