首页 > 编程语言 >Unity 网络编程-正确收发数据流

Unity 网络编程-正确收发数据流

时间:2023-07-20 20:34:28浏览次数:35  
标签:int 发送 收发 Unity readIdx 数据流 长度 byte public

1.TCP数据流

  我们知道在使用Socket网络程序时,操作系统会将数据存到发送接收缓存中。程序不能直接操作它们,只能通过socket.Receive, socket.Send等方法来间接操作。

  在使用以上方法时,如果接收缓存为空,那Receive方法会阻塞。如果发送缓存满了则Send方法会阻塞。

  粘包半包现象

    如果发送端快速发送多跳信息,但是接收端没有及时的调用Receive,那数据就会在接收缓存中累计。

    粘包:假设发送队列中一开始发送 {1,2,3,4}这四个字节的数据,然后发送{5,6,7,8}。等到服务端调用Receive时只调用了一次,那么此时的接收缓存就会变成{1,2,3,4,5,6,7,8}。

    半包/分包: 假设发送{HelloWorld},但是接收队列可能要接收两次,分别接收{Hel},{loWorld}。

    由于TCP是基于流发送的,所以以上现象是很正常的现象,但是我们不想要这样,直觉告诉我们我们应该发一个包就收一个包才不会弄混淆数据。

2.解决粘包问题

  一般有三种方法可以解决该问题:长度信息法,固定长度法,结束符法。

  1.长度信息法

    长度信息法的意思是在每个数据包之前加上长度信息,每次接收数据包后先读取表示长度的字节。然后取出响应的字节,否则继续等待数据接受。

  2.固定长度法

    每次都以相同的长度发送数据。。例如发送{Hello}{World},发送长度为5。如果接收到{Hello...Wo},...Wo就会被存起来等待下次接收数据。而又因为...为填充符,所以舍弃。即只保留Wo等待下一个包再拼接在一起。

  3.结束符法

    规定一个结束字符,用来分割消息。例如$ ,发送{Hello$}{World$}。接收到$为止,有多余的数据等到下一次接收后再拼接。

实现:游戏一般会使用长度信息法用于解决该问题

  发送: 假设要发送{HelloWorld},实际发送变成{0AHelloWorld},0A表示长度10。

public void Send(string sendStr){
    // 组装协议
    byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);
    Int16 len = (Int16)bodyBytes.Length;
    byte[] lenBytes = BitConverter.GetBytes(len);
    byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();
    // 简洁代码同步发送
    socket.Send(sendBytes)
}

  接收:接收的情况比较复杂,首先应该定义一个接收缓存区(readBuff)和缓存区有效数据长度(buffCount)。

    原因是有可能会出现粘包问题,例如接收到{HelloWo},这是有效数据长度就是7。这样我们既可以处理分包问题,用7减去长度5。或者是解决分包问题,下一次接收数据从7开始接收。

    对于缓存区的长度也有一下问题:

    1.缓冲区长度小于等于2(2字节代表长度位):消息太短就不处理,直接return。

    2.缓冲区长度大于2,但还不足以构成一条消息:如果不足以构成完整的信息,那就等待下一次接收。

    3.缓冲区大于等于一条长度:那就解析出消息,然后让后面的字节往前移动。

public void OnReceiveData(){
    //消息长度
    if(buffCount <=2) return;
    Int16 bodyLength = BitConverter.ToInt16(readBuff,0);
    //消息体
    if(buffCount <2+bodyLength) return;
    string s = System.Text.Encoding.UTF8.GetString(readBuff,2,buffCount);
    // s 消息内容
    // 更新缓冲区
    int start = 2 +bodyLength;
    int count = buffCount - start;
    Array.Copy(readBuff,start,readBuff,0,count);
    buffCount -=start;
    // 继续读取消息
    if(readBuff.length>2){
        OnReceiveData();
    }
}

3.大端小端问题

   我们在前面解决粘包分包问题,使用,在计算数据长度时我们使用的是BitConvert.ToInit16。

而.Net中该方法的底层简化为如下:

public static short ToInt16(byte[] value,int startindex){
    if(startIndex%2 ==0){
       return *((short*)pbyte);
    }else{
        if(IsLittleEndian) return (short)((*pbyte)|(*(pbyte+1)<<8));
    }
    ....    
}

  其中,IsLittleEndian代表计算机是大端编码还是小端编码。不同的编码方式也会不同。所以使用该方法计算出来的消息长度也会不同。

  解决:为了兼容所有的设备,我们一般规定写入的数字必须按照小端的模式来存储。

public void Send(string sendStr){
    byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);
    Int16 len = (Int16)bodyBytes.Length;
    byte[] lenBytes = BitConverter.GetBytes(len);
    // 大端小端编码
    if(!BitConverter.IsLittleEndian){
        Debug.Log("Change")
        lenBytes = lenBytes.Reverse();
    }  
    byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();
    socket.Send(sendBytes)
}

4.发送完整数据

   在Send方法中,会把发送的数据存入操作系统的发送缓冲区,然后返回成功写入的字节数。即对于那些没有成功发送的数据,程序需要保存起来,再适当的时机再次发送。在大部分情况下,Send发送部分数据的情况并不是很多,但是以防万一,我们也需要对这种情况处理。

  为了让数据能够发送完整,需要在发送前将数据保存起来;如果发送不完整,在Send回调函数中继续发送数据。

byte[] sendBytes = new byte[2014];
// 缓冲区偏移量
int readIdx =0;
//缓冲区剩余长度
int length =0;

//点击发送数据
public void Send(){
    sendBytes = 数据
    length = sendByte.Length;
    readIdx = 0;
    socket.BeginSend(sendBytes,0,length,0,SendCallback,socket);  //
}

public void SendCallback(IAsyncResult ar){
    Socket socket = (Socket)ar.AsyncState;
    int count = socket.EndSend(ar);
    readIdx +=count;
    length -= count;
    if(length>0){
      socket.BeginSend(sendBytes,readIdx,length,0,SendCallback,socket);  //
    }
}

  以上的方式,只解决了一半的问题,因为调用BeginSend之后,可能要隔一段时间才能调用回调函数SendCallback。此时,如果玩家在回调函数调用前再次点击发送按钮,按照这里的写法,readIdx和length都会被重置,那SendCallback可能就不能再继续工作。所以我们要解决这个问题就要设计一个加强版的缓冲区,叫做写入队列。(队列的写入操作是O(1),如果使用大数组实现性能也没有队列高)

  即采用一个队列的形式存放写入缓存。当回调函数返回成功时才会将一个缓存推出队列。

  数据结构定义如下:

public class ByteArray {
    public byte[] bytes;
    public int readIdx =0;
    public int writeIdx =0;
    public int length{ get{return writeIdx-readIdx; } };

    public ByteArray(byte[] defaultArray){
        bytes = defaultBytes;
        readIdx = 0;
        writeIdx = defaultArray.Length;
    }
}

  线程冲突问题:通过异步的机制我们可以知道,BeginSend和回调不在一个线程上,那就有可能会发生线程冲突的问题。要解决该问题也很简单,我们可以通过加锁(Lock)的方式解决。使用时注意把临界区设置的尽可能小,以提高性能。

5.高效的接收数据

  在之前的代码中,我们接收数据使用了Copy函数,这个函数的时间复杂度是On。加入缓冲区的数据很多,那移动全部数据会花费比较长的时间。

  可行的解决办法:使用ByteArray作为缓冲区,当读取数据结束时只用移动readIdx。当缓冲区长度不够时才会再使用Array.Copy重置readIdx和writeIdx。同时还需要为缓冲区设置自动扩展的功能,以防网络堵塞导致缓冲区满。

  为满足上述,数据结构如下:

public class ByteArray {    
// 默认大小 const int DEFAULT_SIZE = 1024; // 初始大小 int initSize = 0; // 缓冲区 public byte[] bytes; // 读写位置 public int readIdx =0; public int writeIdx =0; // 容量 private int capacity = 0; // 剩余空间 public int remain { get{ return capacity - writeIdx}} // 数据长度 public int length{ get{return writeIdx-readIdx; } }; // 构造函数 public ByteArray(int size = DEFAULT_SIZE){ bytes = new bytes[size]; capacity = size; initSize = size; readIdx = 0; writeIdx = defaultArray.Length; } // 重写 构造函数 public ByteArray(byte[] defaultBytes){ bytes = defaultBytes; capacity = defaultBytes.Length; initSize = defaultBytes.Length; readIdx = 0; writeIdx = defaultArray.Length; } // 重设尺寸 public void Resize(int size){   if(size<length) return; if(size<initSize) return;      int n =1; while(n<size) n*=2; capacity = n; byte[] newBytes = new byte[capacity]; Array.Copy(bytes,readIdx,newBytes,0,writeIdx-readIdx); bytes = newBytes; writeIdx = length; readIdx =0; } }

 

标签:int,发送,收发,Unity,readIdx,数据流,长度,byte,public
From: https://www.cnblogs.com/CatSevenMillion/p/17568615.html

相关文章

  • unity打开内置网页(UniWebView插件)
    UniWebView支持IOS和安卓,window不行UniWebView5|网络|UnityAssetStore安卓demo场景打包就能用,IOS可能得改下配置url必须加协议前缀http://、https:// ......
  • Unity UGUI的AspectRatioFitter(宽高比适应器)组件的介绍及使用
    UnityUGUI的AspectRatioFitter(宽高比适应器)组件的介绍及使用1.什么是AspectRatioFitter组件?AspectRatioFitter(宽高比适应器)是UnityUGUI中的一个组件,用于控制UI元素的宽高比例,使其能够根据父容器的大小进行自适应调整。2.为什么要使用AspectRatioFitter组件?AspectRatioFitte......
  • Unity UGUI的ContentSizeFitter(内容尺寸适应器)组件的介绍及使用
    UnityUGUI的ContentSizeFitter(内容尺寸适应器)组件的介绍及使用1.什么是ContentSizeFitter组件?ContentSizeFitter是UnityUGUI中的一个组件,用于自动调整UI元素的大小,以适应其内容的大小变化。它可以根据内容的大小自动调整UI元素的宽度和高度,确保内容不会被截断或溢出。2.Cont......
  • unity3d unitywebrequest
    Unity3D中的UnityWebRequest是用于发送HTTP请求和处理HTTP响应的类。它提供了一种方便的方式来从服务器获取数据,例如从Web服务器的数据库中检索数据,并通过HTTP请求将数据发送回Unity3D应用程序。使用UnityWebRequest类,您可以执行以下操作:创建请求:您可以使用UnityWebRequest类......
  • UART——通用异步收发传输器
    特点:发送—并转串;接收—串转并;全双工传输1、发送:常见设置包含:起始位、数据位、波特率、奇偶校验类型、停止位、空闲位(1)起始位先发一个逻辑0,表示传输字符开始,依靠检测起始位来实现发送与接收方的时间同步。(2)数据位单个UART数据传输的数据位数,可以是5、6、7或8(默认)(3......
  • esp32笔记[4]-基于ESP-NOW协议的点对点数据收发
    摘要基于ESP-NOW协议的点对点数据收发,用两片ESP8266/ESP32实现远程控制小灯亮灭。硬件平台ESP8266小灯:IO2开发平台ArduinoIDEESP-NOW协议简述[https://www.zhihu.com/tardis/zm/art/344109867?source_id=1002]ESP-NOW是由乐鑫开发的另一款无线通信协议,可以使多个设......
  • 为什么unity里的异步加载要配合协程使用
      在Unity中,异步加载资源时需要配合协程使用的原因是为了避免阻塞主线程。在游戏开发中,资源加载通常是一个耗时的操作,如果在主线程中进行同步加载,会导致游戏卡顿或者无响应,影响用户体验。使用协程可以将资源加载操作放在后台线程中进行,然后在加载完成后再将结果返回到主线程......
  • Unity资源&&配置存档路径问题
    stringdir=Application.persistentDataPath;//万能路径,打包前打包后移动端都可用,该路径可读、可写,但是只能在程序运行时才能读写操作,不能提前将数据放入这个路径。#ifUNITY_EDITORdir=Application.streamingAssetsPath;//打包前可用#endif#if(UNITY_ANDRO......
  • Unity游戏存档读档的几种方式
    1.二进制privatestaticvoidSaveByBinary(){//创建二进制格式化程序BinaryFormatterbf=newBinaryFormatter();//创建一个文件流FileStreamfs=File.Create(GetFilePath(SaveDataType));//二进制方法序列化对象......
  • Unity 协程详解
    在程序开发时,光是了解协程怎么用是远远不够的,因为当程序出现一些有关于协程的错误时,理解协程的原理就十分有必要性了。1.协程使用的一些问题我们知道如果在Unity中编写一个死循环,会造成运行游戏时整个Unity编辑器卡死,而协程函数在使用时好像是可以与Update函数并行不斥......