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在发送过程中黏在了一起,形成了一个新的字节数组,其长度为两者之和。
解决方案概述
- 添加消息头部:
- 为每个消息添加头部信息,头部中记录该消息的长度。
- 当接收到消息时,首先读取头部信息,根据头部中记录的长度来判断消息是否完整,以及是否出现了分包或黏包的情况。
- 消息处理逻辑:
- 在接收消息时,需要维护一个缓存区,用于存储接收到的字节数据。
- 每次接收到新的字节数据时,将其追加到缓存区中。
- 然后,根据消息头部中记录的长度,从缓存区中逐个解析出完整的消息。
- 如果缓存区中的数据不足以构成一个完整的消息,则继续等待接收新的字节数据。
- 如果缓存区中的数据可以构成一个或多个完整的消息,则依次解析出这些消息,并将其从缓存区中移除。