欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 艺术 > 2025-05-04 Unity 网络基础6——TCP心跳消息

2025-05-04 Unity 网络基础6——TCP心跳消息

2025/5/5 18:41:33 来源:https://blog.csdn.net/zheliku/article/details/147691790  浏览:    关键词:2025-05-04 Unity 网络基础6——TCP心跳消息

文章目录

  • 1 Disconnect 方法
  • 2 心跳消息

​ 在客户端主动退出时,我们会调用 socket 的 ShutDown()Close() 方法,但调用这两个方法后,服务器端无法得知客户端已经主动断开。

​ 本文主要介绍在网络通信中,如何服务端如何判断客户端断开连接。

1 Disconnect 方法

​ Socket 当中有一个专门在客户端使用的方法:Disconnect 方法。

  • 此方法将结束连接并将 Connected 属性设置为 false。但是,如果 reuseSockettrue,则可以重用套接字。
  • 若要确保在关闭套接字之前发送和接收所有数据,应在调用 Disconnect 方法之前调用 Shutdown。

客户端

​ 在程序退出时,主动断开连接。

public class NetManager : MonoBehaviour
{...public void OnDestroy(){if (_socket != null){Debug.Log("客户端主动断开连接...");_isConnected = false;_socket.Shutdown(SocketShutdown.Both);_socket.Disconnect(false);_socket.Close();_socket = null;}}...
}

服务端

  1. 收发消息时判断 socket 是否已经断开。

    namespace NetLearningTcpServerExercise2;using System.Net.Sockets;public class ClientSocket
    {private static int _ClientBeginId = 1;private Socket _socket;private byte[] _cacheBytes = new byte[1024 * 1024]; // 缓冲区,大小为 1MBprivate int    _cacheBytesLength;public int Id;public bool Connected{get => _socket == null ? false : _socket.Connected;}...public void ReceiveMessage(){if (!Connected) // 判断是否连接{Program.ServerSocket.AddDelSocket(this);return;}try{if (_socket.Available > 0){var buffer        = new byte[1024 * 5];var receiveLength = _socket.Receive(buffer);HandleReceiveMessage(buffer, receiveLength);}}catch (Exception e){Console.WriteLine("ReceiveMessage Wrong: " + e);Program.ServerSocket.AddDelSocket(this); // 解析错误,也认为把消息断开}}...
    }
    
  2. 处理删除记录的 socket 的相关逻辑(使用线程锁)。

namespace NetLearningTcpServerExercise2;using System.Net;
using System.Net.Sockets;public class ServerSocket
{private readonly Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);private Dictionary<int, ClientSocket> _clientSockets = new Dictionary<int, ClientSocket>();private List<ClientSocket> _delList = new List<ClientSocket>(); // 待移除列表private bool _Running;...private void ReceiveMessage(object? state){while (_Running){lock (_clientSockets){if (_clientSockets.Count > 0){foreach (var clientSocket in _clientSockets.Values){clientSocket.ReceiveMessage();}ClearDelSocket(); // 每次循环,检查是否有待移除的 socket}}}}private void ClearDelSocket(){// 移除for (int i = 0; i < _delList.Count; i++){CloseClientSocket(_delList[i]);}_delList.Clear();}public void CloseClientSocket(ClientSocket socket){lock (_clientSockets){Console.WriteLine("ClientSocket Close: " + socket.Id);_clientSockets.Remove(socket.Id);socket.Close();}}public void AddDelSocket(ClientSocket socket){if (!_delList.Contains(socket)){_delList.Add(socket);// Console.WriteLine(socket);}}
}

测试

​ 启动服务器后,运行 Unity 并立刻结束运行,服务器中可以看到如下消息:

image-20250504092837596

2 心跳消息

​ 很多情况下,客户端并不会像上述一样正常断开连接。例如

  1. 非正常关闭客户端时,服务器无法正常收到关闭连接消息。
  2. 客户端长期不发送消息,防火墙或者路由器会断开连接。

​ 因此,在长连接中,客户端和服务端之间会定期发送的一种特殊数据包,用于通知对方自己还在线,以确保长连接的有效性。

​ 由于其发送的时间间隔往往是固定的持续的,就像是心跳一样一直存在,所以我们称之为**“心跳消息”**。

客户端

  1. 定义心跳消息

     public class HeartMessage : INetMessage
    {public int MessageId { get => 999; }public int BytesLength { get => sizeof(int) + sizeof(int); }public byte[] ToBytes(){var length = BytesLength;var bytes  = new byte[length];var index  = 0;index = this.Write(bytes, index, MessageId);// 写入消息长度index = this.Write(bytes, index, length - sizeof(int) * 2); // 减去消息长度和消息 Id 的长度return bytes;}public int FromBytes(byte[] bytes, int index){return index;}
    }
    
  2. 定时发送消息。

    public class NetManager : MonoBehaviour
    {public static NetManager Instance { get; private set; }private Socket _socket;/// <summary>/// 发送消息的公共队列,主线程塞消息,发送线程拿消息进行发送/// </summary>private Queue<INetMessage> _sendMessages = new Queue<INetMessage>();/// <summary>/// 接收消息的公共队列,主线程拿消息,接收线程获取消息塞进去/// </summary>private Queue<INetMessage> _receiveMessages = new Queue<INetMessage>();private bool _isConnected{get => _socket == null ? false : _socket.Connected;}private byte[] _cacheBytes = new byte[1024 * 1024]; // 缓冲区,大小为 1MBprivate int    _cacheBytesLength;private static readonly int _SEND_HEART_MSG_TIME = 2;private void Awake(){Instance = this;// 循环定时给服务端发送心跳消息InvokeRepeating(nameof(SendHeartMsg), 0, _SEND_HEART_MSG_TIME);}public void SendHeartMsg(){if (_isConnected){Send(new HeartMessage());}Debug.Log("发送心跳消息: " + _isConnected);}...
    }
    

服务器

​ 不停检测上次收到某客户端消息的时间,如果超时则认为连接已经断开

namespace NetLearningTcpServerExercise2;using System.Net.Sockets;public class ClientSocket
{private static int _ClientBeginId = 1;private Socket _socket;private byte[] _cacheBytes = new byte[1024 * 1024]; // 缓冲区,大小为 1MBprivate int    _cacheBytesLength;public int Id;private        long _frontTime     = -1; // 上次收到的心跳时间private static int  _TIME_OUT_TIME = 5;public bool Connected{get => _socket == null ? false : _socket.Connected;}public ClientSocket(Socket socket){Id      = _ClientBeginId++;_socket = socket;ThreadPool.QueueUserWorkItem(CheckTimeOut, null);}/// <summary>/// 间隔一段时间检测超时/// </summary>/// <param name="state"></param>private void CheckTimeOut(object? state){while (Connected){if (_frontTime != -1 &&DateTime.Now.Ticks / TimeSpan.TicksPerSecond - _frontTime > _TIME_OUT_TIME){Program.ServerSocket.AddDelSocket(this);break;}Thread.Sleep(1000);}}public void Close(){if (_socket != null){_socket.Shutdown(SocketShutdown.Both);_socket.Close();_socket = null!;}}public void SendMessage(INetMessage message){if (!Connected){Program.ServerSocket.AddDelSocket(this);return;}try{_socket.Send(message.ToBytes());}catch (Exception e){Console.WriteLine("SendMessage Wrong: " + e);Program.ServerSocket.AddDelSocket(this);}}public void ReceiveMessage(){if (!Connected){Program.ServerSocket.AddDelSocket(this);return;}try{if (_socket.Available > 0){var buffer        = new byte[1024 * 5];var receiveLength = _socket.Receive(buffer);HandleReceiveMessage(buffer, receiveLength);}}catch (Exception e){Console.WriteLine("ReceiveMessage Wrong: " + e);Program.ServerSocket.AddDelSocket(this); // 解析错误,也认为把消息断开}}private void MessageHandle(object? state){if (state == null) return;var msg = (INetMessage) state;if (msg is PlayerMessage playerMsg){Console.WriteLine($"Receive message from client {_socket} (ID {Id}): {playerMsg}");}else if (msg is QuitMessage quitMsg){Program.ServerSocket.AddDelSocket(this); // 客户端断开连接}else if (msg is HeartMessage heartMsg){_frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;Console.WriteLine($"Receive heart message from client {_socket} (ID {Id}): {heartMsg}");}}private void HandleReceiveMessage(byte[] receiveBytes, int receiveNum){var messageId = 0;var index     = 0;// 收到消息时看之前有没有缓存// 如果有,直接拼接到后面receiveBytes.CopyTo(_cacheBytes, _cacheBytesLength);_cacheBytesLength += receiveNum;while (true){var messageLength = -1;// 处理前置信息if (_cacheBytesLength - index >= 8){// 解析 IdmessageId =  BitConverter.ToInt32(_cacheBytes, index);index     += sizeof(int);// 解析长度messageLength =  BitConverter.ToInt32(_cacheBytes, index);index         += sizeof(int);}// 处理消息体if (messageLength != -1 && _cacheBytesLength - index >= messageLength){// 解析消息体INetMessage message = default;switch (messageId){case 1001:message = new PlayerMessage();message.FromBytes(_cacheBytes, index);break;case 1003:message = new QuitMessage();message.FromBytes(_cacheBytes, index);break;case 999:message = new HeartMessage();message.FromBytes(_cacheBytes, index);break;}if (message != default){ThreadPool.QueueUserWorkItem(MessageHandle, message);}index += messageLength;// 如果消息体长度等于缓存长度,证明缓存已经处理完毕if (index == _cacheBytesLength){_cacheBytesLength = 0;break;}}else // 消息体还没有接收完毕{// 解析了前置信息,但是没有成功解析消息体if (messageLength != -1){index -= 8; // 回退到解析 Id 的位置}// 缓存剩余的数据_cacheBytesLength -= index;Array.Copy(_cacheBytes, index, _cacheBytes, 0, _cacheBytesLength);break;}}}
}

测试

​ 启动服务器后,运行 Unity,服务器中可以定时收到心跳消息:

image-20250504093828255

​ 结束运行 Unity,等待 5s 后,可看到服务器显示断开连接:

image-20250504095602541

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词