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

server/2024/11/15 3:52:19/

目录

前言

概念

解决方案

具体代码

总结 

分包黏包概念

分包

黏包

解决方案概述


前言

        在探讨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;//客户端Socketprivate 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){//解析IDmsgID = 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'tiif(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. 消息处理逻辑
    • 在接收消息时,需要维护一个缓存区,用于存储接收到的字节数据。
    • 每次接收到新的字节数据时,将其追加到缓存区中。
    • 然后,根据消息头部中记录的长度,从缓存区中逐个解析出完整的消息。
    • 如果缓存区中的数据不足以构成一个完整的消息,则继续等待接收新的字节数据。
    • 如果缓存区中的数据可以构成一个或多个完整的消息,则依次解析出这些消息,并将其从缓存区中移除。

http://www.ppmy.cn/server/142011.html

相关文章

【go从零单排】Regular Expressions正则表达式

&#x1f308;Don’t worry , just coding! 内耗与overthinking只会削弱你的精力&#xff0c;虚度你的光阴&#xff0c;每天迈出一小步&#xff0c;回头时发现已经走了很远。 &#x1f4d7;概念 Go 中的正则表达式是通过 regexp 包提供的&#xff0c;允许开发者使用强大的模式…

【AI声音克隆整合包及教程】第二代GPT-SoVITS V2:创新与应用

一、引言 随着科技的迅猛发展&#xff0c;声音克隆技术已经成为一个炙手可热的研究领域。SoVITS&#xff08;Sound Voice Intelligent Transfer System&#xff09;&#xff0c;作为该领域的先锋&#xff0c;凭借其卓越的性能和广泛的适用性&#xff0c;正在为多个行业带来前所…

Java LeetCode练习

3194. 最小元素和最大元素的最小平均值 package JavaExercise;import java.util.Arrays;public class Exercise {public static void main(String[] args) {int[] array {1,2,3,7,8,9};System.out.println(Solution.minimumAverage(array));} }class Solution {public static…

力扣-Hot100-技巧【算法学习day.31】

前言 ###我做这类文档一个重要的目的还是给正在学习的大家提供方向&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非常非常高滴&am…

Window.history API学习笔记

Window.history API学习笔记 在现代前端开发中&#xff0c;单页应用&#xff08;SPA&#xff09;的流行让我们对于页面的浏览历史管理需求愈加明显。window.history API作为浏览器提供的原生API&#xff0c;能够帮助开发者更加细致地控制用户的导航体验。本文将介绍window.his…

通过SpringTask模拟打印机定时向数据库传入模拟数据

目录 一、SpringTask简介及使用方法 1.简介 2.使用方法 1&#xff09;在启动类上加上EnableScheduling注解 2&#xff09;编写定时任务类Task 3.注意事项 二、实现模拟打印机定时向数据库传入数据 1.用到的DTO类 2.用到的Mapper 3.Task类 一、SpringTask简介及使用方…

SpringBoot(十五)springboot集成Sentinel

目前负责的业务一直有一个小问题没有太好的解决办法,这个小问题是什么呢?传说中的学生选课。 这玩意跟商城的秒杀还不一样,商城的秒杀是萝卜多,坑少,最后不一定能秒杀的到。学生选课是一个萝卜一个坑,最后你一定能选上课。这么相对来说,商城秒杀要比学生选课的难度要高一…

Vuex 与 Pinia:Vue 状态管理库的选择与对比

1. Vuex 与 Pinia 概述 Vuex Vuex 是 Vue 官方的状态管理库&#xff0c;首次发布于 Vue 2.x&#xff0c;专门为 Vue 应用设计的全局状态管理工具。Vuex 将所有的状态放在一个全局 store 中&#xff0c;组件通过分发&#xff08;dispatch&#xff09;动作&#xff08;actions&…