当前位置: 首页 > news >正文

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

相关文章:

  • Android第三次面试总结之Java篇补充
  • NV214NV217美光闪存固态NV218NV225
  • 基于Hive + Spark离线数仓大数据实战项目(视频+课件+代码+资料+笔记)
  • 【LeetCode Hot100】动态规划篇
  • 二叉搜索树实现删除功能 Java
  • 初识 iOS 开发中的证书固定
  • EasyExcel使用总结
  • 【Linux系统】第二节—基础指令(2)
  • 【ArcGIS微课1000例】0144:沿线或多边形要素添加折点,将曲线线段(贝塞尔、圆弧和椭圆弧)替换为线段。
  • Spring MVC设计与实现
  • 【Java JUnit单元测试框架-60】深入理解JUnit:Java单元测试的艺术与实践
  • 架构思维:利用全量缓存架构构建毫秒级的读服务
  • 【C++ Qt】输入类控件(上) LineEdit、QTextEdit
  • 仓颉编程语言快速入门:从零构建全场景开发能力
  • 主成分分析(PCA)与逻辑回归在鸢尾花数据集上的实践与效果对比
  • PyTorch_张量索引操作
  • 【C++】 —— 笔试刷题day_25
  • [硬件电路-12]:LD激光器与DFB激光器功能概述、管脚定义、功能比较
  • Qwen2.5模型性能测评 - 速度指标
  • 【Linux】命令行参数与环境变量
  • 重庆市大渡口区区长黄红已任九龙坡区政协党组书记
  • 《水饺皇后》:命运如刀,她以饺子还击
  • 魔都眼|西岸国际咖啡生活节:连接艺术、音乐与宠物
  • 5月人文社科中文原创好书榜|巫蛊:中国文化的历史暗流
  • 神舟十九号航天员乘组平安抵京
  • 北京银行一季度净赚超76亿降逾2%,不良贷款率微降