【Unity】uNet游戏服务端框架(三)心跳机制
更新日期:2025年10月13日。
项目源码:获取源码。
索引
- uNet游戏服务端框架
- 一、心跳机制
- 1.心跳包
- 2.服务端发送心跳包
- 3.客户端响应心跳包
uNet游戏服务端框架
uNet
游戏服务端框架为使用.Net Core
开发的高性能、高并发网络游戏服务端框架(最佳适配Unity客户端),基于async / await
的多线程架构天然适应IO高并发
环境,使用Protobuf
进行网络数据交换能够极大的降低带宽和网络延迟,同时使用对象池
技术管理服务端中的大多数实例能够显著的降低GC开销。
一、心跳机制
心跳机制(Heartbeat Mechanism)
是一种用于监控和维护网络应用中连接状态的技术,通过定期发送信号(心跳包)来确认系统组件之间的活跃性和可用性。
在uNet
中心跳包由服务端
作为发送方,客户端
作为响应方,在每一个心跳轮询周期内,如果客户端未在超时时间内响应心跳包,则判定为心跳超时
,会被标记为已断线客户端。
目前针对已断线客户端,会被直接踢出服务器,然后客户端检测到断开连接后,自行走断线重连逻辑。
1.心跳包
在心跳包的设计上,必须遵循极简的策略,毕竟服务端会频繁的向所有玩家客户端发送心跳包,心跳包也无需携带任何信息,所以越小越好,哪怕设计为一个字节也可以。
在uNet
中心跳包即为消息数据包的校验码
(大小4字节),在MMORPG
服务端程序中,MMO_Player
为网络玩家类,其定义的心跳包校验码如下:
namespace uNet.Example.MMO
{/// <summary>/// 玩家/// </summary>internal class MMO_Player : NetworkPlayer{/// <summary>/// 心跳包校验码/// </summary>public override int HEARTBEAT => 0;/// <summary>/// 常规信息校验码/// </summary>public override int NORMAL => 65536;}}
2.服务端发送心跳包
NetworkPlayer
即维护了与一个玩家客户端的连接,所以心跳包(以及常规数据包)的发送与接收都由其来完成,在其Awake
方法中:
Awake:
uNet
中几乎所有实例都有该方法,作为实例被唤醒时的初始化方法,由于几乎所有实例都由对象池进行管理,所以此方法的调用时机为实例创建时(从对象池中取出)调用一次。
Dispose:作为实例被销毁时的回调方法,此方法的调用时机为实例销毁时(存入对象池)调用一次。
private static long SelfIncreasingID = 1;private static readonly object _lockObj = new object();private byte[]? _heartbeatPackage;private CancellationTokenSource? _cancel;private Socket? _socket;private TimeSpan _heartbeatInterval;private TimeSpan _heartbeatTimeout;private DateTime _lastHeartbeat;/// <summary>/// 初始化/// </summary>/// <param name="socket">Socket实例</param>/// <param name="heartbeatInterval">心跳检测间隔时间(秒)</param>/// <param name="heartbeatTimeout">心跳检测超时时间(秒)</param>public virtual void Awake(Socket socket, float heartbeatInterval, float heartbeatTimeout){//ID自增(lock防止多线程资源竞争,以防止SelfIncreasingID值异常)lock (_lockObj){ID = SelfIncreasingID;SelfIncreasingID++;}Log.LogInfo($"玩家【ID:{ID}】登入服务器!");//是否已发送心跳包(单次心跳轮询)IsSendHeartbeat = false;//是否心跳验证通过(单次心跳轮询)IsHeartbeatPass = false;//创建心跳包(也即是将心跳校验码转为字节数组即可,因为心跳包只包含一个心跳校验码)_heartbeatPackage = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(HEARTBEAT));//创建自身的异步取消Token,用于关联所有由自身启动的异步操作_cancel = new CancellationTokenSource();_socket = socket;_socket.SendBufferSize = 128 * 1024;_socket.ReceiveBufferSize = 64 * 1024;//每次心跳轮询的间隔时间_heartbeatInterval = TimeSpan.FromSeconds(heartbeatInterval);//心跳检测的超时时间_heartbeatTimeout = TimeSpan.FromSeconds(heartbeatTimeout);//注册并启动心跳检测HeartbeatTestAsync(_cancel.Token);}
在心跳检测的核心方法HeartbeatTestAsync
中,使用while循环使其永久轮询(除非_cancel
取消),因其为异步async
方法,且使用了ConfigureAwait(false)
配置,所以每次await
后都会由新的空闲线程来接替,所以不用担心其性能问题:
/// <summary>/// 心跳检测/// </summary>/// <param name="token">用于取消异步的token</param>private async void HeartbeatTestAsync(CancellationToken token){//只要token没取消,就一直轮询while (!token.IsCancellationRequested){if (!IsConnect){//如果玩家客户端已断开连接,则通过服务器入口销毁该玩家(踢出服务器)ServerEntry.Current.DestroyPlayer(this);break;}IsHeartbeatPass = false;//向玩家客户端发送心跳包(单次心跳轮询)IsSendHeartbeat = await SendDataAsync(_heartbeatPackage, token).ConfigureAwait(false);//如果发送成功(发送失败则等待进入下一次轮询,理论上不会一直失败)if (IsSendHeartbeat){//记录发送时间_lastHeartbeat = DateTime.Now;//IsHeartbeatPass = false,代表客户端还未响应心跳包while (!IsHeartbeatPass && !token.IsCancellationRequested){//判断当前是否已超时if ((DateTime.Now - _lastHeartbeat) > _heartbeatTimeout){Log.LogWarning($"玩家【ID:{ID}】心跳超时,已被踢出服务器!");ServerEntry.Current.DestroyPlayer(this);break;}try{//1秒进行一次超时判断await Task.Delay(1000, token).ConfigureAwait(false);}catch (Exception){ }}}try{//单次心跳轮询结束,等待间隔时间后进行下一次await Task.Delay(_heartbeatInterval, token).ConfigureAwait(false);}catch (Exception) { }}}
需注意的是,
Task.Delay
方法的使用几乎都包裹在try-catch
块内,这是因为其传入了可取消的token参数,当token在外部被取消时,Task.Delay
方法会抛出一个TaskCanceledException
异常,表明任务被取消,所以我们只需要捕获并抛弃他即可,否则程序会崩溃。
3.客户端响应心跳包
IsHeartbeatPass = true
即代表了客户端在当前心跳轮询中响应了心跳包,也即是心跳检测通过。
在上一篇网络通信协议中,有提到将IsHeartbeatPass
设置为true的时机,即在服务端接收到客户端的消息时:
/// <summary>/// 从客户端接收数据/// </summary>/// <param name="token">用于取消异步的token</param>/// <returns>网络消息</returns>protected async Task<NetworkMessage?> ReceiveDataAsync(CancellationToken token){if (_socket == null || !IsConnect)return null;try{//接收消息校验码(4字节)await ReceiveFixedBytes(_socket, _receiveBuffer, _receiveCodeData, token).ConfigureAwait(false);int code = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(_receiveCodeData, 0));//通过消息校验码,判断当前接收到的是心跳包if (code == HEARTBEAT){//设置心跳检测通过IsHeartbeatPass = true;return null;}//......}}
然后回到客户端代码中,客户端在收到服务端的心跳包后,会立即做出响应:
namespace HT.Framework
{/// <summary>/// 默认的TCP协议通道/// </summary>public sealed class TcpChannel : ProtocolChannelBase{/// <summary>/// 接收消息/// </summary>/// <param name="client">客户端</param>/// <returns>接收到的消息对象</returns>protected override INetworkMessage ReceiveMessage(Socket client){try{//接收消息校验码(4字节)ReceiveFixedBytes(client, _receiveBuffer, _receiveCodeData);int code = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(_receiveCodeData, 0));//通过消息校验码,判断当前接收到的是心跳包if (code == HEARTBEAT){//响应心跳包//将心跳包注入到待发送消息队列InjectMessage(_heartbeatPackage);return null;}//......}}}
}
设置IsHeartbeatPass = true
后,一次心跳检测轮询就算完成了,且心跳检测通过,服务端会认为该客户端处于正常连接中,等待进行下一次心跳轮询。