分包和黏包的基本概念和逻辑实现

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson9 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 什么是分包、黏包?
        //分包、黏包指在网络通信中由于各种因素(网络环境、API规则等)造成的消息与消息之间出现的两种状态
        //分包:一个消息分成了多个消息进行发送
        //黏包:一个消息和另一个消息黏在了一起
        //注意:分包和黏包可能同时发生
        #endregion
        #region 知识点二 如何解决分包、黏包的问题?
        //现在的处理:
        //我们收到的消息都是以字节数组的形式在程序中体现
        //目前我们的处理规则是默认传过来的消息就是正常情况
        //前4个字节是消息ID
        //后面的字节数组全部用来反序列化
        //如果出现分包、黏包会导致我们反序列化报错
        //思考:
        //那么通过接收到的字节数组我们应该如何判断收到的字节数组处于以下状态
        //1.正常
        //2.分包
        //3.黏包 
        //突破点:
        //如何判断一个消息没有出现分包或者黏包呢?
        //答案——>消息长度
        //我们可以如同处理 区分消息类型 的逻辑一样
        //为消息添加头部,头部记录消息的长度
        //当我们接收到消息时,通过消息长度来判断是否分包、黏包
        //对消息进行拆分处理、合并处理
        //我们每次只处理完整的消息
        #endregion 
        #region 知识点三 实践解决
        //1.为所有消息添加头部信息,用于存储其消息长度 
        //2.根据分包、黏包的表现情况,修改接收消息处的逻辑
        #endregion
        #region 总结
        //处理分包、黏包问题首先要了解什么是分包和黏包
        //解决该问题的逻辑实现的写法可能有很多种
        //采用最节约性能的方式解决问题就行
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
//网络管理器(单例模式)
public class NetMgr : MonoBehaviour
{
    private static NetMgr instance;

    //用于发送消息的队列 公共容器 主线程往里边放 发送线程从里边取
    private Queue sendMgsQueue = new Queue();
    //用于接收消息的队列 公共容器 存放线程往里放 主线程从里边取
    private Queue receiveMgsQueue = new Queue(); 
    public static NetMgr Instance => instance;
    用于收消息的水桶(容器)
    //private byte[] bytes = new byte[1024 * 1024];
    用于返回字节数组大小
    //private int receiveNum;
    //用于处理分包时缓存的字节数组和字节数组的长度
    private byte[] cacheBytes = new byte[1024 * 1024];
    private int cacheNum = 0;
    //客户端Socket
    public Socket socket;
    private bool IsConnect=false;

    void Awake()
    {
        instance = this;    
    }
    void Update()
    {
        if(receiveMgsQueue .Count >0)
        {
            BaseMsg msg = receiveMgsQueue.Dequeue();
            if(msg is PlayerMsg)
            {
                PlayerMsg playerMsg = (msg as PlayerMsg);
                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)
    {
        sendMgsQueue.Enqueue(msg);
    }
    public void SendMsg(object obj)
    {
        if(sendMgsQueue .Count >0)
        {
            socket.Send(sendMgsQueue.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);
                解析字符数组成字符串,放入公共容器中
                首先解析收到字节数组的前四个字节 读取消息的ID
                //int msgID = BitConverter.ToInt32(bytes, 0);
                //BaseMsg baseMsg = null;
                //switch (msgID)
                //{
                //    case 1001:
                //        PlayerMsg msg = new PlayerMsg();
                //        msg.Reading(bytes,4);
                //        baseMsg = msg;
                //        break;
                //}
                如果消息为空,证明是不知道类型的消息,没有解析
                //if (baseMsg == null) continue;
                //receiveMgsQueue.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循环解决黏包问题
        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(receiveBytes, nowIndex);
                        baseMsg = msg;
                        break;
                }
                if (baseMsg != null)
                    receiveMgsQueue.Enqueue(baseMsg);
                nowIndex += msgLength;
                if (nowIndex == cacheNum)
                {
                    break;
                }
            }
            else
            {
                //如果不满足,证明有分包
                //那么我们需要把当前收到的消息存下来
                //有待下次接收到消息后再做处理
                //receiveBytes.CopyTo(cacheBytes, 0);
                //cacheNum = receiveNum;
                //如果进行了id和长度的解析,但是没有成功解析消息体,nowindex需要减去移动的位置
                if (msgLength != -1)
                    nowIndex -= 8;
                //就是把剩余没有解析的字节数组的内容 移到前面来 用于缓存下次继续解析
                Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
                cacheNum = cacheNum - nowIndex;
                break;
            }
        }
  
    }
    public void Close()
    {
        if(socket !=null)
        {
            IsConnect  = false;
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
        }
    }
    private void OnDestroy()
    {
        Close();
    }
}

你可能感兴趣的:(Unity网络开发基础,unity,网络,网络协议,tcp/ip)