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

news2025/5/9 22:19:54

文章目录

  • 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]; // 缓冲区,大小为 1MB
        private 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]; // 缓冲区,大小为 1MB
        private 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]; // 缓冲区,大小为 1MB
    private 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)
            {
                // 解析 Id
                messageId =  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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2371827.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

word导出pdf带有目录导航栏-error记

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

1. 视频基础知识

1. 图像基础概念 像素&#xff1a;像素是一个图片的基本单位&#xff0c;pix是英语单词picture&#xff0c;加上英语单词“元素element”&#xff0c;就得到了pixel&#xff0c;简称px。所以“像素”有“图像元素”之意。分辨率&#xff1a;指的是图像的大小或者尺寸。比如 19…

HarmonyOS-hdc远程网络方式连接设备

hdc工具使用手册 1 hdc简介 hdc&#xff08;OpenHarmony Device Connector&#xff09;是为开发人员提供的用于设备连接调试的命令行工具&#xff0c;pc端开发机使用命令行工具hdc&#xff0c;该工具需支持部署在Windows/Linux/Mac等系统上与OpenHarmony设备&#xff08;或模…

奥威BI:AI+BI深度融合,重塑智能AI数据分析新标杆

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

第三节第一部分:Static修饰类变量、成员变量

总结 案例 要求 代码&#xff1a; User类&#xff1a; package com.day1_static;public class User {public static int num;public User() {User.num;} }Test类&#xff1a; package com.day1_static;public class Test {public static void main(String[] args) {User us…

高级架构软考之网络OSI网络模型

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

Kubernetes(k8s)学习笔记(六)--KubeSphere前置环境安装

1、安装 helm&#xff08;master 节点执行&#xff09; Helm 是 Kubernetes 的包管理器。包管理器类似于我们在 Ubuntu 中使用的apt、Centos 中使用的 yum 或者 Python 中的 pip 一样&#xff0c;能快速查找、下载和安装软件包。Helm由客户端组件 helm 和服务端组件 Tiller 组…

架构思维:构建高并发读服务_异构数据的同步一致性方案

文章目录 一、引言二、全景架构回顾三、潜在问题问题1&#xff1a;Binlog 延迟——理想 vs 实际问题2&#xff1a;Binlog 格式解析问题3&#xff1a;高可靠消费1. 串行 ACK 消费2. 并行消费&#xff0b;乱序风险3. 解决方案 问题4&#xff1a;缓存数据结构设计1. Key–Value 冗…

剑指大规模 AI 可观测,阿里云 Prometheus 2.0 应运而生

作者&#xff1a;曾庆国&#xff08;悦达&#xff09; Prometheus 大家应该非常熟悉&#xff0c;正文开始前&#xff0c;让我们一起来回顾开源 Prometheus 项目的发展史。Prometheus 最初由 SoundCloud 的工程师 Bjrn Rabehl 和 Julius Volz 于 2012 年开发。当时&#xff0c;…

游戏引擎学习第260天:在性能分析器中实现钻取功能

昨天那个帧内存满之后触发段错误实在没找到什么原因导致的 继续研究一下为什么导致的 内存不够进来释放frame 释放frame 应该会给DebugState->FirstFreeStoredEvent 赋值吧 这段宏定义&#xff1a; #define FREELIST_DEALLOCATE(Pointer, FreeListPointer) \if(Pointer) {…

【自然语言处理与大模型】使用Xtuner进行QLoRA微调实操

本文首先对Xtuner这一微调框架进行简单的介绍。手把手演示如何使用Xtuner对模型进行微调训练&#xff0c;包括数据准备、训练命令执行及训练过程中的监控技巧。最后&#xff0c;在完成微调之后&#xff0c;本文还将介绍如何对微调结果进行简单对话测试。 一、Xtuner微调框架 X…

扣子创建一个应用

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

SpringBoot教程(vuepress版)

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

FiLo++的框架图介绍

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

C++--入门基础

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

准确---Typora配置Gitee图床并实现自动图片上传

下载地址&#xff1a;https://github.com/Molunerfinn/picgo/releases 安装就直接下一步&#xff0c;下一步就行 安装完以后然后回到Typora上偏好设置指定一下路径 默认是 C:\Program Files\PicGo\PicGo.exe 并且还需要选择规则 接下来就需要去PicGo上面配置了 配置之前需要去…

Day111 | 灵神 | 二叉树 | 验证二叉搜索树

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

Redis 8.0 正式版发布,新特性很强!

就在前两天&#xff0c;Redis 8.0 正式版 (GA) 来了&#xff01;这并不是一次简单的更新&#xff0c;Redis 8.0 不仅带来了性能上的进一步提升&#xff0c;还带来一些实用的新特性与功能增强。并且&#xff0c;最重要的是拥抱 AGPLv3 重归开源&#xff01; 下面&#xff0c;简单…

以太坊智能合约开发框架:Hardhat v2 核心功能从入门到基础教程

一、设置项目 Hardhat 项目是安装了 hardhat 包并包含 hardhat.config.js 文件的 Node.js 项目。 操作步骤&#xff1a; ①初始化 npm npm init -y②安装 Hardhat npm install --save-dev hardhat③创建 Hardhat 项目 npx hardhat init如果选择 Create an empty hardhat.…

了解Dockerfile

定制docker 镜像的方式&#xff1a; 手动修改容器内容&#xff0c;导出新的镜像基于dockerfile 自行编写指令&#xff0c;基于指令流程创建镜像 镜像和容器的层级实现 docker拉取镜像到docker engine 之后&#xff0c;共享系统内核。 在内核层上有镜像层&#xff08;本质上只…