2025-05-04 Unity 网络基础6——TCP心跳消息
文章目录
- 1 Disconnect 方法
- 2 心跳消息
在客户端主动退出时,我们会调用 socket 的
ShutDown()
和
Close()
方法,但调用这两个方法后,服务器端无法得知客户端已经主动断开。
本文主要介绍在网络通信中,如何服务端如何判断客户端断开连接。
1 Disconnect 方法
Socket 当中有一个专门在客户端使用的方法:Disconnect 方法。
- 此方法将结束连接并将 Connected 属性设置为
false
。但是,如果reuseSocket
为true
,则可以重用套接字。 - 若要确保在关闭套接字之前发送和接收所有数据,应在调用 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;}}...
}
服务端
-
收发消息时判断 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); // 解析错误,也认为把消息断开}}... }
-
处理删除记录的 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 并立刻结束运行,服务器中可以看到如下消息:

2 心跳消息
很多情况下,客户端并不会像上述一样正常断开连接。例如
- 非正常关闭客户端时,服务器无法正常收到关闭连接消息。
- 客户端长期不发送消息,防火墙或者路由器会断开连接。
因此,在长连接中,客户端和服务端之间会定期发送的一种特殊数据包,用于通知对方自己还在线,以确保长连接的有效性。
由于其发送的时间间隔往往是固定的持续的,就像是心跳一样一直存在,所以我们称之为**“心跳消息”**。
客户端
-
定义心跳消息
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;} }
-
定时发送消息。
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,服务器中可以定时收到心跳消息:

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

相关文章:

2025-05-04 Unity 网络基础6——TCP心跳消息
文章目录 1 Disconnect 方法2 心跳消息 在客户端主动退出时,我们会调用 socket 的 ShutDown() 和 Close() 方法,但调用这两个方法后,服务器端无法得知客户端已经主动断开。 本文主要介绍在网络通信中,如何服务端如何判断客…...

word导出pdf带有目录导航栏-error记
1、打开word文档——>点击"视图"选项卡——>勾选"导航窗格" 2、点击"文件"——>导出——>创建PDF/XPS 3、点击"选项"——>勾选"创建书签时使用(C)" "标题(H)" 4、点击"确定"——>点击…...

1. 视频基础知识
1. 图像基础概念 像素:像素是一个图片的基本单位,pix是英语单词picture,加上英语单词“元素element”,就得到了pixel,简称px。所以“像素”有“图像元素”之意。分辨率:指的是图像的大小或者尺寸。比如 19…...
VTK 数据结构和算法类介绍
基本数据结构类 vtkPolyData 描述: 表示多边形几何结构 主要属性: Points: vtkPoints对象,存储顶点坐标 Verts: vtkCellArray对象,存储顶点数据 Lines: vtkCellArray对象,存储线数据 Polys: vtkCellArray对象,存储多边形数据 Strips: vtkCellArray对象,存储三角带数据 常…...
云计算的基础概论
一、云计算基础概念 1. 云计算定义 • 英文:Cloud Computing • 定义:通过互联网(Internet)按需提供可扩展的计算资源(如服务器、存储、数据库、网络、软件等),用户无需管理底层基础设施。 …...

HarmonyOS-hdc远程网络方式连接设备
hdc工具使用手册 1 hdc简介 hdc(OpenHarmony Device Connector)是为开发人员提供的用于设备连接调试的命令行工具,pc端开发机使用命令行工具hdc,该工具需支持部署在Windows/Linux/Mac等系统上与OpenHarmony设备(或模…...
【计算机网络网络层深度解析】从IP协议到路由优化
目录 前言技术背景与价值当前技术痛点解决方案概述目标读者说明 一、技术原理剖析核心概念图解核心作用讲解关键技术模块说明技术选型对比 二、实战演示环境配置要求核心实验实现实验1:IPv6地址配置实验2:OSPF路由配置实验3:NAT转换验证 运行…...
不同OS版本中的同一yum源yum list差异排查思路
问题描述: qemu-guest-agent二进制rpm包的yum仓库源和yum源仓库配置文件path_to_yum_conf, 通过yum list --available -c path_to_yum_conf 查询时,不同的OS版本出现了不同的结果 anolis-8无法识别 centos8可以识别 说明: 1 测试…...

奥威BI:AI+BI深度融合,重塑智能AI数据分析新标杆
在数字化浪潮席卷全球的今天,企业正面临着前所未有的数据挑战与机遇。如何高效、精准地挖掘数据价值,已成为推动业务增长、提升竞争力的核心议题。奥威BI,作为智能AI数据分析领域的领军者,凭借其创新的AIBI融合模式,正…...

第三节第一部分:Static修饰类变量、成员变量
总结 案例 要求 代码: User类: package com.day1_static;public class User {public static int num;public User() {User.num;} }Test类: package com.day1_static;public class Test {public static void main(String[] args) {User us…...

高级架构软考之网络OSI网络模型
高级架构软考之网络: 1.OSI网络模型: a.物理层: a.物理传输介质物理连接,负责数据传输,并监控数据 b.传输单位:bit c.协议: d:对应设备:中继器、集线器 b.数据链路层: a.…...

Kubernetes(k8s)学习笔记(六)--KubeSphere前置环境安装
1、安装 helm(master 节点执行) Helm 是 Kubernetes 的包管理器。包管理器类似于我们在 Ubuntu 中使用的apt、Centos 中使用的 yum 或者 Python 中的 pip 一样,能快速查找、下载和安装软件包。Helm由客户端组件 helm 和服务端组件 Tiller 组…...
PyTorch_张量元素类型转换
tensor.type([张量类型])torch.double() 代码 import torch import numpy as np # 使用 type() 函数进行转换 def test01():data torch.full([2,3], 10)print(data.dtype)# 注意:返回一个新的类型转换过的张量data data.type(torch.DoubleTensor)#data data.ty…...

架构思维:构建高并发读服务_异构数据的同步一致性方案
文章目录 一、引言二、全景架构回顾三、潜在问题问题1:Binlog 延迟——理想 vs 实际问题2:Binlog 格式解析问题3:高可靠消费1. 串行 ACK 消费2. 并行消费+乱序风险3. 解决方案 问题4:缓存数据结构设计1. Key–Value 冗…...

剑指大规模 AI 可观测,阿里云 Prometheus 2.0 应运而生
作者:曾庆国(悦达) Prometheus 大家应该非常熟悉,正文开始前,让我们一起来回顾开源 Prometheus 项目的发展史。Prometheus 最初由 SoundCloud 的工程师 Bjrn Rabehl 和 Julius Volz 于 2012 年开发。当时,…...

游戏引擎学习第260天:在性能分析器中实现钻取功能
昨天那个帧内存满之后触发段错误实在没找到什么原因导致的 继续研究一下为什么导致的 内存不够进来释放frame 释放frame 应该会给DebugState->FirstFreeStoredEvent 赋值吧 这段宏定义: #define FREELIST_DEALLOCATE(Pointer, FreeListPointer) \if(Pointer) {…...

【自然语言处理与大模型】使用Xtuner进行QLoRA微调实操
本文首先对Xtuner这一微调框架进行简单的介绍。手把手演示如何使用Xtuner对模型进行微调训练,包括数据准备、训练命令执行及训练过程中的监控技巧。最后,在完成微调之后,本文还将介绍如何对微调结果进行简单对话测试。 一、Xtuner微调框架 X…...

扣子创建一个应用
什么是扣子应用 扣子应用可以让你相对轻松的搭建一个具备AI功能的应用,它区别智能体,在于智能体的ui和交互相对固定,主要是以对话框聊天的方式进行交互,而扣子应用则可以让ui交互表现更加丰富。 实践一个生成图片的应用 这里我…...

SpringBoot教程(vuepress版)
Spring Boot 教程 项目介绍 这是一个系统化的 Spring Boot 学习教程,采用循序渐进的方式,帮助开发者从零开始掌握 Spring Boot 开发。 教程特点 系统化的知识结构实用的代码示例完整的实战案例丰富的练习作业 目录结构 基础入门 Spring Boot 简介…...

FiLo++的框架图介绍
FiLo框架图模块详解 1. 文本生成模块 Normal Texts 功能:生成正常样本的文本描述。输入:固定模板(如 A [domain] photo of [state][cls])和可学习模板(如 [v1][v2]...[vm][state][cls])。输出:融…...

C++--入门基础
C入门基础 1. C的第一个程序 C继承C语言许多大多数的语法,所以以C语言实现的hello world也可以运行,C中需要把文件定义为.cpp,vs编译器看是.cpp就会调用C编译器编译,linux下要用g编译,不再是gcc。 // test.cpp #inc…...

准确---Typora配置Gitee图床并实现自动图片上传
下载地址:https://github.com/Molunerfinn/picgo/releases 安装就直接下一步,下一步就行 安装完以后然后回到Typora上偏好设置指定一下路径 默认是 C:\Program Files\PicGo\PicGo.exe 并且还需要选择规则 接下来就需要去PicGo上面配置了 配置之前需要去…...

Day111 | 灵神 | 二叉树 | 验证二叉搜索树
Day111 | 灵神 | 二叉树 | 验证二叉搜索树 98.验证二叉搜索树 98. 验证二叉搜索树 - 力扣(LeetCode) 方法一:前序遍历 递归函数传入合法的左右边界,只有当前结点是合法的边界,才是二叉搜索树,否则就返回…...

Redis 8.0 正式版发布,新特性很强!
就在前两天,Redis 8.0 正式版 (GA) 来了!这并不是一次简单的更新,Redis 8.0 不仅带来了性能上的进一步提升,还带来一些实用的新特性与功能增强。并且,最重要的是拥抱 AGPLv3 重归开源! 下面,简单…...
Prompt Engineering 提示词工程学习
一、Prompt Engineering 简介 Prompt Engineering 是设计和优化输入提示(Prompt)以获得预期输出的过程。在与大型语言模型(如 GPT-4)交互时,如何构造提示会显著影响模型的回答质量。 二、Prompt 的重要性 提高生成准确性:通过正确的 Prompt 引导,模型能够更好地理解用…...

以太坊智能合约开发框架:Hardhat v2 核心功能从入门到基础教程
一、设置项目 Hardhat 项目是安装了 hardhat 包并包含 hardhat.config.js 文件的 Node.js 项目。 操作步骤: ①初始化 npm npm init -y②安装 Hardhat npm install --save-dev hardhat③创建 Hardhat 项目 npx hardhat init如果选择 Create an empty hardhat.…...

了解Dockerfile
定制docker 镜像的方式: 手动修改容器内容,导出新的镜像基于dockerfile 自行编写指令,基于指令流程创建镜像 镜像和容器的层级实现 docker拉取镜像到docker engine 之后,共享系统内核。 在内核层上有镜像层(本质上只…...
7. HTML 表格基础
表格是网页开发中最基础也最实用的元素之一,尽管现代前端开发中表格布局已被 CSS 布局方案取代,但在展示结构化数据时,表格依然发挥着不可替代的作用。本文将基于提供的代码素材,系统讲解 HTML 表格的核心概念与实用技巧。 一、表格的基本结构 一个完整的 HTML 表格由以下…...
Spring普通配置类 vs 自动配置类-笔记
1.简要版 Configuration和Bean,既可以用于普通配置类,也可以用于自动配置类。二者的区别和联系是什么呢? 区别: Configuration和Bean是Spring框架本身的注解,用于定义配置类和生成Bean。而自动配置通常是Spring Boo…...

强化学习--2.数学
强化学习--数学 1、概率统计知识1.1 随机变量与观测值1.2 概率密度函数(PDF)1.3 期望1.4 随机抽样 2、数据期望E3、正态分布4、条件概率1. **与多个条件相关**(依赖所有前置条件)2. **仅与上一个条件相关**(马尔可夫性…...