前言


前几篇内容

在Unity中实现TCP通信(一)

在Unity中实现TCP通信(二)

在Unity中实现TCP通信(三)

分别阐述了Unity中实现TCP通信的客户端、服务端的实现以及引入ProtoBuf进行数据序列化的方式。这篇来写一下心跳机制。


心跳机制原理


在第二篇内容中,对于客户端是否断开连接,是通过判断接受到的数据流是否是空来判断连接是否断开。

代码是这样的:

1
2
3
4
5
6
7
8
int length = m_Socket.EndReceive(ir);
if(length < 1)
{
IPEndPoint endPoint = m_Socket.RemoteEndPoint as IPEndPoint;
Console.WriteLine("客户端:" + endPoint.Address.ToString() + "已断开连接");
Close();
return;
}

显然用这种方式是不足以说明客户端真的已经断开连接的,因此这里就需要引入心跳机制来解决这个断开问题。


心跳机制的概念


所谓心跳机制,就是服务端每隔一段时间向客户端发送一个空包(根据实际需求,也可以带上当前时间戳,总之包体不能太大),若客户端不能在规定时间内做出应答则认为连接已断开。而且心跳机制这个名字也非常具有象征意义,它就像人的心脏跳动一样,按照一定频率不断的去确认客户端是否还“活着”。

人的心脏如果不跳了,那么人肯就是死了,而客户端如果不能做出应答,也就像人的心脏停止跳动一样,“死了”。

PS:因为游戏开发中客户端需要不断更新服务器时间,并且若玩家正在游戏中活跃势必会产生发包行为,所以在这篇博文中采用服务端向客户端发送心跳包并带上当前时间戳的方式。而反过来客户端定时给服务端发心跳包也是更为常见的方式,但不管谁主动谁被动两边互通数据不可避免。

基于这个逻辑,下面将在第二篇代码的基础上实现心跳机制。


实现心跳机制


心跳计时器


既然是按照一定频率来发送空包,肯定就要使用计时器去触发了,这里直接构造一个System.Threading.Timer的计时器对象,每隔15000毫秒(15秒)触发一次:

1
m_HeartBitTimer = new Timer(HeartBit, 0, 0, 15000);

使用时间戳来计算心跳间隔时间


心跳需要每隔一段时间触发一次,因此这里需要采用时间戳来计算触发时间,时间戳就是从过去到现在的一段时间,使用两个时间戳相减即可得到这中间流逝的时间。

比如服务器启动时间是2020年6月9日14点30分0秒,那么:从服务器启动到当前所经过的时间 = 从1970年1月1日0分0秒到当前的时间 - 从1970年1月1日0分0秒到2020年6月9日14点30分0秒。

若心跳每隔15秒触发一次,则这个差值大于等于15的时候就开始执行心跳逻辑。

采用时间戳的好处就是,不需要实时累计,在需要的时候计算一下即可得到精确的差值。而实时累加由于精度问题,往往会产生一定误差,且需要不断累计比较繁琐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void HeartBit(object state)
{
TimeSpan ts = DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0, 0);

if (m_TimeStamp == 0)
{
m_TimeStamp = ts.TotalSeconds;
}

if(ts.TotalSeconds - m_TimeStamp > DIS_CONNECT_TIME)
{
if (!m_IsCheckHeart)
{
m_IsCheckHeart = true;
m_TimeStamp = ts.TotalSeconds;
Send(1, new byte[0]);
}
else
{
Close();
}
}
}

这里DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0, 0)即是从1970年1月1日0点0分0秒开始到目前为止的一段时间,使用秒做单位

m_TimeStamp是一个标志位,它记录的是一次心跳发生之前的时间,ts.ToalSecondes - m_TimeStamp也就是当前时间和一次心跳发生前的时间的差值,即从一次心跳之前到现在过去了多久。 DIS_CONNECT_TIME就是一个触发时间的常量,这里定义为15秒。


收包处理


因为这里把编号为1的包作为心跳包,所以在收包的时候要对编号为1的包体进行特殊处理:收到1号包的时候把时间戳归0,等待下一次心跳触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void CheckReceiveBuffer(object state)
{
lock (m_ReceiveQueue)
{
if (m_ReceiveQueue.Count < 1)
{
return;
}

byte[] buffer = m_ReceiveQueue.Dequeue();
byte[] msgContent = new byte[buffer.Length - 2];
ushort msgCode = 0;

using (MemoryStream ms = new MemoryStream(buffer))
{
byte[] msgCodeBuffer = new byte[2];
ms.Read(msgCodeBuffer, 0, msgCodeBuffer.Length);
msgCode = BitConverter.ToUInt16(msgCodeBuffer, 0);
ms.Read(msgContent, 0, msgContent.Length);
}

if (msgCode == 1)//若收到编号为1的心跳包,则把时间戳归0,等待下一次心跳触发
{
m_IsCheckHeart = false;
m_TimeStamp = 0;
}
else
{
text content = ProtoBufUtil.BytesToObject<text>(msgContent, 0, msgContent.Length);
Console.WriteLine("消息编号:" + msgCode + ",内容:" + content.content);
}
}
}

结语


好了,心跳机制到这里也写完了。Unity TCP部分我写了4篇文章,算是把整个流程说了大概。其实在实际的开发工作中,网络部分要远远比这几篇文章所提到的内容复杂得多,而笔者能力有限无法将这些实际问题和解决方案一一进行细致的阐述。当然,不管写的好与坏,至少这对于我自己关于这部分知识也起到了梳理和夯实的作用。

接下来我会写一些关于Unity中的行为树,状态机,UI框架,资源管理框架,优化等内容的文章,期待和大家共同进步。