Unity3D游戏制作学习记录03——丛林战争_橘猫吸薄荷的博客-程序员秘密

技术标签: unity3d  游戏开发  

Unity3D游戏制作学习记录03——丛林战争

Siki学院的视频教程指路牌:http://www.sikiedu.com/course/61.

粘包和分包

粘包

我们要发送出去的数据成为包,当我们发送的数据包很小,发送间隔又很短的时候,这对于性能的消耗是很大的,Socket就会自动优化,把这些数据组合起来作为一条消息发送到服务器端。
出现的情况一般是在大型网络游戏中,要一直更新定位,血量等等的数据,就很容易出现粘包的问题了。

分包

当我们发送的数据包很大的时候,Socket就会把包分开去传输。一方面,如果整个包去发送的话,如果发送失败,就要重新来过,十分耗费时间;另一方面,把大的数据包分成小包去发送,发送的也会更快,如果有某几个包发送失败了,只需要单独重新发送那几个包就可以了。

解决方法

在发送的数据包取几位,记录数据包的长度,当所接收到的数据包长度小于记录的长度,我们就暂时不处理,直到接收到的数据包长度和记录长度相等,再统一处理。
解决的主要是由于强制分包导致每个包最后几位或者最前几位识别错误(byte转string)的问题。
(有点像我以前学过的课《计算机网络》里面的内容,有学过的朋友应该比较好理解吧o( ̄▽ ̄)ブ)

那么具体记录长度的字节要有多大呢?
因为记录长度的肯定是整型int,那么我们根据所选的int所占用的字节长度,来作为数据包的长度存储。
int有int16,int32,int64,这里选的是int32,即32位,4个字节。

字节转换方式
说到转成字节,那我们按照之前消息发送那样子用,把数据包的长度转换成字节:

int count = 10000;
System.Text.Encoding.UTF8.GetBytes(count.ToString());

它的转换原理是针对字符串(引用类型)的,它转换是先将字符串一个个字符转换成对应的字节来存储,比如说字符串“10000”,转换出来的字节数组就是“49;48;48;48;48;”(一个分号一个字节)
那如果是“100”,转换出来的就是:“49;48;48;”
在这里插入图片描述
至于为什么1对应49
是因为在UTF-8中:1的UTF-8编码对应的十进制数是49,
在这里插入图片描述
可见,转换出来的int所占字节的位数是不固定的,它和这个数据的字符串对应的长度有关,因为是一个数字对应一个字符转换成字节。

这显然是不满足我们采取固定字节长度来存储数据包大小的要求啦。

所以,我们转换字节的方式不能用以前的办法,应该要用针对值类型的字节转换方式:
BitConverter.GetBytes(count);
在这里插入图片描述
至于为什么233直接是233;0;0;0;
是因为233可以用一个字节表示完全,一个字节是8位,
二进制:11101001

同理,10000
十进制:10000
二进制:00100111;00010000

这是我转成二进制输出的效果:
在这里插入图片描述
我的问题:
我不理解的是,根据输出,是从左往右填写,在后面补0,为什么不是从右往左填值,在左边补0?

客户端——消息整合发送

  1. 我们给客户端添加一个新建项:MessagePackup
    将处理发送的消息的数据拼接方法封装在这个类里面
    在这里插入图片描述
    2.先处理消息,转换为字节数组;
    然后获取到数组的长度,同样转换称字节数组;
    然后利用byte数组的内置方法将两个数组合并,返回即可。
namespace TCPClient
{
    
  /// <summary>
  /// 处理消息的拼接和组装,添加长度信息等
  /// </summary>
  public class MessagePackup
  {
    
      /// <summary>
      /// 将消息字符串转换成字节数组
      /// </summary>
      /// <param name="msg">要发送的消息</param>
      /// <returns></returns>
      static public byte[] GetByte(string msg)
      {
    
          // 数据转字节
          byte[] dataBytes = System.Text.Encoding.UTF8.GetBytes(msg);
          // 长度转字节
          byte[] dataLength = BitConverter.GetBytes(dataBytes.Length);
          // 拼接
          byte[] msgBytes = dataLength.Concat(dataBytes).ToArray();

          return msgBytes;
      }

  }
}
  1. 我们修改一下客户端发送消息的转换
             while (true)
             {
          
                 // 客户端向服务器发送消息,发送的消息由用户在控制台输入
                 string msgSend = Console.ReadLine();
                 socketClient.Send(MessagePackup.GetByte(msgSend));
             }
    

服务器端——消息解析接收

  1. 为服务器端添加新项,用于消息解析:MessageUnpack
    在这里插入图片描述
  2. 用于消息解析,因此我们需要有存储消息的字节数组 receiveBytes;
    了解决粘包的问题并防止数据包覆盖原来未处理的数据,我们需要一个记录下一个数据包可以追击的下标 addIndex,那这个字段同时也可以表示当前存储的数据长度。
    由于这两个字段是和读取到的消息有关,并不能随意更改,我们让它私有,但为了外部的获取,同时为它们添加属性。
    		// 存储接收到的byte消息
            private byte[] receiveBytes = new byte[1024];
    
            // 填充位置的标记,记录上一次数据存储的位置
            // 也相当于当前数组内存储的数据长度
            private int addIndex = 0;
    
            /// <summary>
            /// 提供给外部 存储消息的字节数组
            /// </summary>
            public byte[] receiveMssage
            {
    
                get {
     return receiveBytes; }
            }
    
            /// <summary>
            /// 提供给外部 获取消息存储的开始位置
            /// </summary>
            public int AddIndex
            {
    
                get {
     return addIndex; }
            } 

由于粘包情况,我们需要判断当前这个数据还是否能存得下数据,所以我们添加一个remainSize来记录剩余大小。

    		/// <summary>
           /// 提供给外部 获取当前数组剩余长度
           /// </summary>
           public int RemainSize
           {
    
               get {
     return receiveBytes.Length - addIndex; }
           } 

目前这些字段的关系如下图辣:在这里插入图片描述
2.服务器消息处理:
由于服务器的消息接收处理是在AcceptCallback()回调函数中,我们就在那里面进行修改。
先声明一个静态全局的MessageUnpack类的对象:msgUnpack
在开启异步接收BeginAceive()的参数修改:

	// 异步接收客户端的消息
    // 读取的数据存储数组,存储的起始位置,接收数据的长度,套接字的标志位,接收到消息时要执行的函数,要给函数传递的信息
    clientSocket.BeginReceive(msgUnpack.receiveMssage, msgUnpack.AddIndex, msgUnpack.RemainSize, SocketFlags.None, ReceiveCallback, clientSocket); 

我们把存储的起始位置改为了追加的下标addIndex;
可接收数据的长度是remainSize,用于防止字节数组无法存储的情况;

3.由于接收消息的操作在BeginReceive()的回调函数:ReceiveCallback()中:
① 在接收到新消息的时候,更新追加下标的值addIndex,让其向后移

② 对消息进行解析,我们将这步操作封装在MessageUnpack类里的ReadMessage()发方法内:
Ⅰ.判断消息有效性,即是否长度小于一个int32所占字节的长度
Ⅱ. 判断数据是否接收完毕:
1. 获取消息长度:读取第一个int32字节:
int msgSize = BitConverter.ToInt32( receiveMssage, 0);
2. 当前字符数据长度大于接收到的该条数据:(addIndex>=msgSize+sizeof(int32))
2.1 则可以处理:
string msgRecv = System.Text.Encoding.UTF8.GetString(receiveMssage, 4, msgSize);
消息处理结束后,消息字节数组前移,追加下标addIndex前移:
int moveSize = msgSize + 4;
Array.Copy(receiveBytes, moveSize, receiveBytes, 0, addIndex - moveSize);
addIndex -= moveSize;
2.2 否则等待下一条消息,直到满足2.1

代码

		/// <summary>
        /// 接收新消息 更新追加下标后移
        /// </summary>
        /// <param name="addSize"></param>
        public void UpdateAddIndex(int addSize)
        {
    
            addIndex += addSize;
        }

		/// <summary>
        /// 消息解析
        /// </summary>
        public void ReadMessage()
        {
    
            while (true)
            {
    
                // 接收数据无效
                if (addIndex <= 4) return;

                //  有效数据 消息解析
                // 1. 获取消息长度,从消息数据的第0位开始读取一个int32的大小的数据
                int msgSize = BitConverter.ToInt32(receiveMssage, 0);
                // 2.处理长度后的消息内容
                // 2.1 消息长度是否足够 【粘包和正常情况时】
                if ((addIndex - 4) >= msgSize)
                {
    
                    // 数据长度足够,从第4位开始读取
                    string msgRecv = System.Text.Encoding.UTF8.GetString(receiveMssage, 4, msgSize);

                    Console.WriteLine("消息解析\n" +"size:"+ msgSize+"  内容:"+ msgRecv);

                    // 将后半部分的数据往前移
                    int moveSize = msgSize + 4;
                    Array.Copy(receiveBytes, moveSize, receiveBytes, 0, addIndex - moveSize);
                    // 追加下标前移
                    addIndex -= moveSize;
                }
                // 2.2 接收的消息比消息长度短【分包的情况时】
                else
                {
    
                    break;
                }
            }
        }

运行结果

客户端实验代码:

            for (int i = 0; i < 2333; ++i)
            {
    
                socketClient.Send(MessagePackup.GetByte(i.ToString()));
            }

粘包解决效果:
在这里插入图片描述
实验结果显示没有出现粘包现象。

由于我没有保留原来的代码,只好截视频的粘包结果了:
视频循环是0-100,在第二次发送可以看到包背结合了在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/LAUGINJI/article/details/106129867

智能推荐

Android Studio 开发环境搭建 & 配置_android studio环境配置_Wriprin的博客-程序员秘密

前言 开发环境:JDK 开发语言:Java 开发工具 Android Studio JDK 安装配置 HelloWorld AS 默认配置修改 AVD 安装配置

时间单位的换算(秒,毫秒,微秒,纳秒,皮秒)_秒,毫秒,微秒,纳秒,皮秒_lucky_zbaby的博客-程序员秘密

时间单位:  秒(second),时间单位 : s,  毫秒(millisecond),时间单位:ms  微秒(microsecond),时间单位:μs时间换算:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1s【秒】 = 1000ms【毫秒】  1ms【毫秒】 = 1000μs【微秒】   1μs【微秒】 = 1000ns【纳秒】  1ns 【纳秒】= 1000ps【皮秒】1秒(s) = 1000 毫秒(ms) = 1,000,000 微秒(μs) = 1,0

May 18th Monday (五月 十八日 月曜日)_Lu_ming的博客-程序员秘密

  There are three interesing C sources.// specsym.c#include extern char __executable_start[];extern char etext[], _etext[], __etext[];extern char edata[], _edata[];extern char end[], _end[];int main()

【论文阅读】Is Graph Structure Necessary for Multi-hop Question Answering?_没有胡子的猫的博客-程序员秘密

Is Graph Structure Necessary for Multi-hop Question Answering?论文:EMNLP 2020-Is Graph Structure Necessary for Multi-hop Question Answering任务探讨多步推理问答任务中是否需要图结构的问题。验证自注意力或者Transformer可能更加擅长处理多步推理问答任务。方法(模型)本文的实验使用Distractor设定。基线模型使用一个检索模型检出候选段落中

C语言—char*和字符数组的区别_张文琪2023的博客-程序员秘密

近期做了一道C语言的题,里面用到了字符指针来使用字符串,以前也遇到过这种问题,今天也是好不容易了解了其中的道道声明字符指针并赋值字符串:char *s1 = &quot;abc&quot;其实实际物理存储模式和字符数组是一样的,一个地址存一个字符,但是有两个和字符数组完全不同的地方。1、赋值后不可修改数据2、移动指针输出字符时,是从当前指针位置开始输出,到‘\0’停止 char *s1 = &quot;abc&quot;;//...

随便推点

Android:view体系总结_cdh516的博客-程序员秘密

Android:View体系一. 示图坐标系1.View简介View是Android所有控件的基类,同时ViewGroup也是继承自View,看下面这张图我们就会有一个直观的了解:2.Android坐标系在Android中,将屏幕的左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,原点向下是Y轴正方向。3.View坐标系View坐标系以父视图到左上角为坐标原点,过...

ajax跨域问题:No ‘Access-Control-Allow-Origin‘ header is present on the requested resource_$.ajax has been blocked by cors policy: no 'access_折本咲焰的博客-程序员秘密

今天在学习Ajax时,客户端发送请求总是报错。No ‘Access-Control-Allow-Origin’ header is present on the requested resource.我是用的是nodemon 和 express 框架来测试ajax请求。以下是代码部分:/* app.js *///引入express框架const express = require('express');//引入路径处理模块const path = require('path')//创建we

ESP32 Arduino使用内部霍尔传感器_arduino esp32 磁性传感器_perseverance52的博客-程序员秘密

ESP32Arduino使用内部霍尔传感器开发板:ESP32 Dev Module实例代码/******************************************************* ESP32 使用内部霍尔传感器 功能:使用内部霍尔传感器获取磁场强度的值打印到串口监视器 引脚: *******************************************************/const int LED = 2;cons

【小白专用】K-最近邻法(KNN)原理及其简单应用_最近邻法原理_李仔全会的博客-程序员秘密

原理:如果一个样本在特征空间中的K个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性,KNN方法在类别决策上仅仅依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。图解: (其中“” 代表A类别,“” 代表B类别 ,“”代表待判断的样本) 通过计算“” 与“和“”各点的距离(),选择距离“”最邻近的一个或者几个样本(也就是“和...

linux下如何使用命令连接wifi_linuxwifi_M小马M的博客-程序员秘密

(1)首先ifconfig命令查看一下1|[email protected]_6dq:/ # ifconfigeth0 Link encap:Ethernet HWaddr 12:21:12:12:12:34 inet addr:192.168.1.1 Bcast:192.168.1.255 Mask:255.255.255.0 UP BROAD...

量化交易100个知识点 重新认识 ROE(净资产收益率)的风险和竞争优势_净资产收益率 风险[email protected]的博客-程序员秘密

量化交易100个知识点 重新认识 ROE(净资产收益率)的风险和竞争优势知识点: ROE(净资产收益率) 企业经营模式和经济周期净资产收益率ROE(Rate of Return on Common Stockholders’ Equity),净资产收益率又称股东权益报酬率/净值报酬率/权益报酬率/权益利润率/净资产利润率,是净利润与...

推荐文章

热门文章

相关标签