《Unity3D高级编程之进阶主程》第六章,网络层(三) - 实现UDP
实现UDP
===
在前面介绍了如何实现TCP socket,下面要介绍下UDP的实现方式。其实两者都是长链接的方式,很多地方都有相识之处,比如两者都需要连接和断开事件支撑,都需要做发送和接收队列缓存,都需要定义数据包协议格式,都需要加密和校验。
我们先来说说TCP有而UDP没有的部分,再看看需要我们在UDP上实现的有哪些功能。和TCP相比,UDP基本就是TCP的阉割版本,很多TCP本身自带有的功能UDP都没有,虽然它们最大的区别是数据传输的方式,但在接口和逻辑层上很难感觉到它们的区别,不过至少我们得知道它们传输的不同方式。相同的地方包括,大都以异步发送和接收的方式进行,以及都需要理解多线程同步的操作机制,还有发送和接收都需要缓冲队列,并且发送数据时都需要合并数据包(其实TCP可以不用,因为它本身就有确认和重发机制)。TCP有而UDP没有的地方包括,UDP不会自己校验重发,由于是数据报的发送模式丢包概率大,数据包接收顺序不确定,而且UDP本身没有连接状态导致没有连接断开这个机制,也没有连接确认机制。
前面说了这么多关于TCP和UDP在程序上的区别就是要大家明白,直接使用UDP来做网络发送和接收会遇到诸多问题,可以说如果直接使用而不加修饰,使得不确定性太大,导致基本用不了UDP。因此这节我们来说说应该怎么封装才能让UDP顺利的使用在我们的项目上。
连接确认机制
TCP有连接的三次握手协议,相当于在连接过程中跟服务器端协商后敲定我们已经建立了连接这个一致预期,而UDP是无状态链接,它并没有三次握手的协议,所以可以说UDP的连接其实是一厢情愿的,客户端并不知道是否真正连接成功了,如果由于网络异常原因没有连接上,就会导致收发失败。其实怎么确认UDP连接上了服务器,这才是我们想要得到的反馈,我们需要准确的得知UDP连接是否已经成功连接上服务器,因此这个连接确认机制必不可少。
发送和接收如果建立在连接确认的基础上则会更加牢靠,因此我们必须先确认知道我们是否连接成功。能够判断连接是否成功是整个实现UDP的第一步,只有这样才能顺利得进行下面的数据包收发操作。
我们应该怎么确认连接成功
很简单我们可以模仿TCP的确认连接机制,我们来看看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);
上述代码为使用C#的UDP接口对指定的IP和端口打开链接。
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);
UPD是无状态连接,打开连接就相当于只是一厢情愿的自我意识,因此可以立刻开始接收数据的接口并开启线程。
3.发送连接确认数据包,并屏蔽其他发送和接收功能。
SendConnectRequest();
StopSendNormalPackage();
StopReceiveNormalPackage();
这三个函数是自定义的,第一个表示发送握手包,可以认为是为握手包特意定制的数据装载函数,第二个和第三个只是把普通的接收数据包的开关给关了,在连接还没确认前不接收任何其他形式的数据包,实际上接收数据包仍在继续,只是不加入到数据处理队列且也不处理数据的具体句柄。
4.等待接收连接确认数据包:
Void OnData(byte[] 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连接状态的心跳包类似,因为这种方式是花费最小的代价并且能够及时准确的确认连接的方式。因此UDP检测连接的判定机制,也可以用数据包来回的形式,不过这次不像握手数据包那样只是单一一个数据包,而是持续的心跳包的形式来做持续的判断连接状态。
首先,我们要与服务器端有个协定,每隔X秒(比如5秒)发送一个心跳数据包给服务器端,这个客户端发送的心跳数据包里包含了一些客户端信息,包括ID,角色状态,设备信息等,包体不能太大,否则会就加重了宽带负担。
当服务器端收到心跳数据包时,也立刻回复一个心跳数据回应包,里面包含了,服务器端当前时间,服务器端当前状态等信息。客户端收到此数据包时,说明连接尚在,也能同时同步服务器端的时间和一些基础的信息。
如果客户端很久没有收到心跳数据回应包时,就表明,连接已经断开了,比如30秒没收到心跳包,可以判断连接已经断开。服务器端也是一样操作,当没有收到心跳包很久,就表明客户端的连接已经断开。这时客户端就可以开启相应的重连程序,或重连提示以及步骤。
步骤可以分为如下4步:
1.每隔X秒向服务器端发送心跳数据包。
2.服务器端收到心跳数据包后回复心跳响应数据包。