Unity网络通信(part7.分包和黏包)

目录

前言

概念

解决方案

具体代码

总结 

分包黏包概念

分包

黏包

解决方案概述


前言

        在探讨Unity网络通信的深入内容时,分包和黏包问题无疑是其中的关键环节。以下是对Unity网络通信中分包和黏包问题前言部分的详细解读。

概念

        在网络通信中,分包和黏包是常见的问题。分包是指将一个较大的数据包拆分成多个小的数据包进行传输,黏包则是指将多个小的数据包合并成一个较大的数据包。

        产生分包和黏包的原因主要有两个:一是网络传输的不可靠性,数据包在传输过程中可能会丢失、重复或乱序;二是数据发送方将多个数据包连续发送,而接收方可能不会立即处理完一个数据包,而是先处理下一个数据包,导致多个数据包被合并成一个。

        解决分包和黏包问题的方法有多种,其中一种常见的方法是在数据包中添加特殊的标记,来标识数据包的边界,以便接收方能够正确地解析出每个数据包。

        在Unity中,可以使用自定义的消息协议来解决分包和黏包问题。具体的实现方式可以根据实际需求来确定,比如可以在数据包的头部添加一个表示数据包长度的字段,或者在数据包之间添加一个特定的分隔符来标记数据包的边界。

        除了使用自定义的消息协议,还可以使用Unity提供的网络组件来解决分包和黏包问题。比如可以使用Unity自带的网络库UNet,或者使用第三方网络库如Photon Unity Networking(PUN)来进行网络通信。这些网络库都提供了相应的接口和方法来处理分包和黏包问题。

        总之,分包和黏包是网络通信中常见的问题,但可以通过合适的方法和工具来解决。在开发网络游戏或其他网络应用时,需要注意处理分包和黏包问题,以确保数据的正确传输和解析。

注意:分包和黏包可能同时发生


解决方案

1.为所有消息添加头部信息,用于存储其消息长度
2.根据分包、黏包的表现情况,修改接收消息处的逻辑

具体代码

此代码在之前的客户端管理模块基础上将接收消息的方法做了改动。

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
using UnityEngine.XR;

public class NetMgr : MonoBehaviour
{
    private static NetMgr instance;

    public static NetMgr Instance => instance;

    //客户端Socket
    private Socket socket;
    //用于发送消息的队列 公共容器 主线程往里面放 发送线程往里面取
    private Queue<BaseMsg> sendMsgQueue = new Queue<BaseMsg>();
    //用于接收消息的队列 公共容器 子线程往里面放 主线程往里面取
    private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();

    用于收消息的容器
    //private byte[] receiveBytes = new byte[1024*1024];
    返回收到的字节数
    //private int receiveNum;

    //用于处理分包时缓存的字节数
    private byte[] cacheBytes = new byte[1024*1024];
    private int cacheNum;

    //是否连接
    private bool isConnect=false;

    private void Awake()
    {
        instance = this; 
        DontDestroyOnLoad(this.gameObject);
    }

    private void Update()
    {
        if(receiveQueue.Count>0)
        {
            BaseMsg msg = receiveQueue.Dequeue();
            if(msg is PlayerMsg)
            {
                PlayerMsg playerMsg = (PlayerMsg)msg;
                print(playerMsg.playerID);
                print(playerMsg.playerData.name);
                print(playerMsg.playerData.lev);
                print(playerMsg.playerData.atk);
            }
        }
    }




    //连接服务端
    public void Connect(string ip,int port)
    {
        //如果是连接状态 直接返回
        if(isConnect)
        {
            return;
        }


        if(socket==null)
        {
            socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
        }
        //连接服务端
        IPEndPoint iPPoint = new IPEndPoint(IPAddress.Parse(ip),port);
        try
        {
            socket.Connect(iPPoint);
            isConnect=true;

            //开启发送线程
            ThreadPool.QueueUserWorkItem(SendMsg);

            //开启接收线程
            ThreadPool.QueueUserWorkItem(ReceiveMsg);
        }
        catch(SocketException e)
        {
            if(e.ErrorCode == 10061)
            {
                print("服务器拒绝连接");
            }
            else
            {
                print("连接失败"+e.ErrorCode+e.Message);
            }
        }
    }
    //发送消息
    public void Send(BaseMsg msg)
    {
        sendMsgQueue.Enqueue(msg);
    }

    private void SendMsg(object obj)
    {
        while(isConnect)
        {
            if(sendMsgQueue.Count>0)
            {
                socket.Send(sendMsgQueue.Dequeue().Writing());
            }
        }
    }
    //接收消息
    public void ReceiveMsg(object obj)
    {
        while(isConnect)
        {
            if (socket.Available > 0)
            {
                //申明为临时变量,节约内存空间
                byte[] receiveBytes = new byte[1024 * 1024];
                int receiveNum = socket.Receive(receiveBytes);
                HandleReceiveMsg(receiveBytes,receiveNum);
                首先把收到字节数组的前4个字节  读取出来得到ID
                //int msgID = BitConverter.ToInt32(receiveBytes, 0);
                //BaseMsg baseMsg = null;
                //switch (msgID)
                //{
                //    case 1001:
                //        PlayerMsg msg = new PlayerMsg();
                //        msg.Reading(receiveBytes, 4);
                //        baseMsg = msg;
                //        break;
                //}
                如果消息为空 那证明是不知道类型的消息 没有解析
                //if (baseMsg == null)
                //{
                //    continue;
                //}
                收到消息 解析消息为字符串 并放入公共容器
                //receiveQueue.Enqueue(baseMsg);
            }
        }
    }

    //处理接收消息 分包、黏包问题的方法
    private void HandleReceiveMsg(byte[] receiveBytes,int receiveNum)
    {
        int msgID=0;
        int msgLength = 0;
        int nowIndex = 0;

        //收到消息时,应该看看之前有没有缓存的 如果有的话 直接拼接到后面
        receiveBytes.CopyTo(cacheBytes,cacheNum);
        cacheNum += receiveNum;

        while(true)
        {
            //每次将长度设置为-1,是为了避免上一次解析的数据影响这一次的判断
            msgLength = -1;
            //处理解析一条消息
            if(cacheNum-nowIndex >= 8)
            {
                //解析ID
                msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
                nowIndex += 4;
                //解析长度
                msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
                nowIndex += 4;
            }
            if(cacheNum - nowIndex>=msgLength&&msgLength!=-1)
            {
                //解析消息体
                BaseMsg baseMsg = null;
                switch (msgID)
                {
                    case 1001:
                        PlayerMsg msg = new PlayerMsg();
                        msg.Reading(cacheBytes, nowIndex);
                        baseMsg = msg;
                        break;
                }
                if (baseMsg != null)
                {
                    receiveQueue.Enqueue(baseMsg);
                }
                nowIndex += msgLength;
                if(nowIndex == cacheNum)
                {
                    cacheNum = 0;
                    break;
                }
            }
            else//保存消息体,等下一次收到消息时进行拼接
            {
                //receiveBytes.CopyTo(cacheBytes, 0);
                //cacheNum = receiveNum;

                //如果进行了id和长度的解析 但是 没有成功jie'xi'xiao'xi'ti
                if(msgLength !=-1)
                {
                    nowIndex -= 8;
                }

                //就是把剩余没有解析字节数组内容 移到前面来 用来缓存下次继续解析
                Array.Copy(cacheBytes,nowIndex,cacheBytes,0,cacheNum-nowIndex);
                cacheNum = cacheNum - nowIndex;
                break;
            }
        }
    }

    //关闭连接
    public void Close()
    {
        if(socket!=null)
        {
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();

            isConnect = false;
        }
    }

    private void OnDestroy()
    {
        Close();
    }
}

总结 

分包黏包概念

分包

        分包是指一个完整的消息在发送过程中被拆分成了多个消息包进行发送。例如,原本的一个字节数组B,被分成了两段(或更多),如字节数组B1和字节数组B2。

黏包

        黏包则是指多个消息在发送过程中合并成了一个消息包进行发送。例如,消息A的字节数组A和消息B的字节数组B在发送过程中黏在了一起,形成了一个新的字节数组,其长度为两者之和。

解决方案概述

  1. 添加消息头部
    • 为每个消息添加头部信息,头部中记录该消息的长度。
    • 当接收到消息时,首先读取头部信息,根据头部中记录的长度来判断消息是否完整,以及是否出现了分包或黏包的情况。
  2. 消息处理逻辑
    • 在接收消息时,需要维护一个缓存区,用于存储接收到的字节数据。
    • 每次接收到新的字节数据时,将其追加到缓存区中。
    • 然后,根据消息头部中记录的长度,从缓存区中逐个解析出完整的消息。
    • 如果缓存区中的数据不足以构成一个完整的消息,则继续等待接收新的字节数据。
    • 如果缓存区中的数据可以构成一个或多个完整的消息,则依次解析出这些消息,并将其从缓存区中移除。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

64位PE壳编写指南

文章目录 前记x64壳后记reference 文章首发于微信公众号《渗透测试安全攻防》 前记 开源的关于PE压缩和加密壳几乎都是32位&#xff0c;于是学习写一个64位的壳供参考&#xff0c;其原理差别不大学写PE壳是熟悉PE结构很好的方式项目已开源&#xff0c;求个stars嘻嘻嘻 https:…

3D意识(3D Awareness)浅析

一、简介 3D意识&#xff08;3D Awareness&#xff09;主要是指视觉基础模型&#xff08;visual foundation models&#xff09;对于3D结构的意识或感知能力&#xff0c;即这些模型在处理2D图像时是否能够理解和表示出图像中物体或场景的3D结构&#xff0c;其具体体现在编码场景…

Web安全之SQL注入---基础

文章目录 SQL注入简介SQL注入基础SQL注入分类SQL注入流程 SQL注入简介 什么是SQL注入&#xff1f; SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严&#xff0c;攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句&#xff0c;在管理…

黄仁勋对话孙正义:日本的AI新饼、Arm的AI野心与英伟达的东亚新机会

2020 年的软银世界大会上&#xff0c;孙正义与黄仁勋围绕「What’s Next for AI」展开了一次围炉对谈。黄仁勋穿着标志性的皮夹克坐在火堆旁&#xff0c;畅谈了将 Arm 纳入麾下的重要价值&#xff0c;孙正义也毫不吝啬赞美之词&#xff0c;称老黄在未来 10 年会达到史蒂夫 乔布…

【案例】Excel使用宏来批量插入图片

一、场景介绍 我有一个excel文件&#xff0c;需要通过一列的文件名称&#xff0c;按照规则给批量上传图片附件。 原始文件&#xff1a; 成功后文件&#xff1a; 二、实现方法 1. 使用【wps】工具打开Excel文件&#xff0c;将其保存为启用宏的文件。 2.找到编辑宏的【VB编辑器…

Kubernetes-ArgoCD篇-01-简介

1、什么是Argo CD Argo CD 是针对 Kubernetes 的声明式 GitOps 持续交付工具。 Argo CD官方文档地址&#xff1a;https://argo-cd.readthedocs.io Argo CD源码地址&#xff1a;https://github.com/argoproj/argo-cd 1.1 关于Argo Argo是一个开源的项目&#xff0c;主要是扩…

odoo17 前端 在头像下拉 dropdown 自定义菜单

odoo17 前端 在头像下拉 dropdown 自定义菜单 其实很简单, 我们先找到原来已经创建好的, 找到代码位置 使用 我的资料 为例 odoo-17.0\addons\hr\static\src\user_menu\my_profile.js /** odoo-module **/import { _t } from "web/core/l10n/translation"; import …

时序预测 | Python基于CNN-transformer时间序列预测

时序预测 | Python基于CNN-transformer时间序列预测 目录 时序预测 | Python基于CNN-transformer时间序列预测预测效果基本介绍参考资料 预测效果 基本介绍 时序预测 | Python基于CNN-transformer时间序列预测 Cnn-transformer-自适应稀疏自注意力ASSA-对比归一化contranorm预…

(干货)Jenkins使用kubernetes插件连接k8s的认证方式

#Kubernetes插件简介 Kubernetes 插件的目的是能够使用 Kubernetes 配合&#xff0c;实现动态配置 Jenkins 代理&#xff08;使用 Kubernetes 调度机制来优化负载&#xff09;&#xff0c;在执行 Jenkins Job 构建时&#xff0c;Jenkins Master 会在 kubernetes 中创建一个 Sla…

Python学习25天

# 切片语法&#xff1a;序列[起始索引:结束索引:步长]&#xff0c;起始不写默认为0&#xff0c;结束不写默认取到结尾&#xff0c;步长不写默认为1(步长为-反向截取&#xff0c;最后一位起始序列为-1),左闭右开&#xff0c;切片后原序列不变 str "lx,hahaha,呵呵" s…

API接口精准获取商品详情信息案例

在当今数字化时代&#xff0c;电子商务平台的蓬勃发展&#xff0c;使得商品信息的获取变得尤为重要。API&#xff08;Application Programming Interface&#xff0c;应用程序编程接口&#xff09;作为连接前端用户界面与后端服务的桥梁&#xff0c;扮演着至关重要的角色。本文…

MySQL算数运算符基础:详解与入门

目录 背景&#xff1a; 过程&#xff1a; 1.加法与减法运算符 1.2扩展&#xff1a; 1.3运算结果得出结论 &#xff1a; 2.乘法和除法运算 ​2.1练习&#xff1a; 2.2运算结果得出结论 &#xff1a; 3.求模取余运算符 3.1练习&#xff1a; 总结&#xff1a; 背景&a…

【vue2.0入门】vue基本语法

目录 引言一、页面动态插值1. 一般用法 二、计算属性computed三、动态class、style绑定四、条件渲染与列表渲染五、事件处理六、表单输入绑定七、总结 引言 本系列教程旨在帮助一些零基础的玩家快速上手前端开发。基于我自学的经验会删减部分使用频率不高的内容&#xff0c;并不…

丹摩征文活动 | 丹摩智算平台:服务器虚拟化的璀璨明珠与实战秘籍

丹摩DAMODEL&#xff5c;让AI开发更简单&#xff01;算力租赁上丹摩&#xff01; 目录 一、引言 二、丹摩智算平台概述 &#xff08;一&#xff09;平台架构 &#xff08;二&#xff09;平台特点 三、服务器虚拟化基础 &#xff08;一&#xff09;虚拟化的概念 &#xf…

蓝牙5.0模块助力闹钟升级,开启智能生活第一步

随着智能家居产业的快速发展&#xff0c;智能闹钟作为其中一个重要的品类&#xff0c;逐渐从单一的时间提醒功能演变为集音频播放、语音交互、智能控制等多种功能于一体的智能设备。而在这些功能的实现中&#xff0c;蓝牙音频模组扮演着核心角色。 1、蓝牙音频模组的功能概述 …

POI word转pdf乱码问题处理

1.使用poi 转换word文档成pdf 导入依赖 <dependency><groupId>com.aspose</groupId><artifactId>words</artifactId><version>16.8.0</version></dependency>2.代码实现: SneakyThrowspublic void wordToPdf(String docPath,…

有趣的Midjourney作品赏析(附提示词)

中文提示词&#xff1a;国风少年 C4D软件,高分辨率,超细节,超现实主义, 英文提示词&#xff1a;National Style Youth Cinema4D,high resolution,hyper detailed,surrealism, --niji 6 --ar 1:1 中文提示词&#xff1a;粘土模型&#xff0c;男性穿着中世纪欧洲蓝色盔甲&#x…

猫头虎分享: 小米大模型升级第二代MiLM2:从一代到二代,能力飞跃提升

小米大模型升级第二代MiLM2&#xff1a;从一代到二代&#xff0c;能力飞跃提升 大家好&#xff0c;我是猫头虎&#xff0c;今天给大家带来一篇关于小米大模型MiLM2的深度解读。作为技术圈的重磅消息&#xff0c;小米的第二代大模型&#xff08;MiLM2&#xff09;在多项领域实现…

解决Anaconda出现CondaHTTPError: HTTP 000 CONNECTION FAILED for url

解决Anaconda出现CondaHTTPError: HTTP 000 CONNECTION FAILED for url 第一类情况 在anaconda创建新环境时&#xff0c;使用如下代码 conda create -n charts python3.7 错误原因&#xff1a; 默认镜像源访问速度过慢&#xff0c;会导致超时从而导致更新和下载失败。 解决方…