《Unity3D网络游戏实战》正确收发数据流

TCP数据流

系统缓冲区

当收到对端数据时,操作系统会将数据存入到Socket的接收缓冲区中

操作系统层面上的缓冲区完全由操作系统操作,程序并不能直接操作它们,只能通过socket.Receive、socket.Send等方法来间接操作。当系统的接收缓冲区为空,Receive方法会被阻塞,直到里面有数据。同样地,Socket的Send方法只是把数据写入到发送缓冲区里,具体的发送过程由操作系统负责。当操作系统的发送缓冲区满了,Send方法将会阻塞

粘包半包现象

如果发送端快速发送多条数据,接收端没有及时调用Receive,那么数据便会在接收端的缓冲区中累积

解决粘包半包现象

一般有三种方法可以解决粘包和半包问题,分别是长度信息法、固定长度法和结束符号法

长度信息法

长度信息法是指在每个数据包前面加上长度信息。每次接收到数据后,先读取表示长度的字节,如果缓冲区的数据长度大于要取的字节数,则取出相应的字节,否则等待下一次数据接收。

游戏程序一般会使用16位整型数或32位整型数来存放长度信息 。16位整型数的取值范围是0~65535,32位整型数的取值范围是0~4294967295。对于大部分游戏,网络消息的长度很难超过65535字节,使用16位整型数来存放长度信息较合适

固定长度法

每次都以相同的长度发送数据,假设规定每条信息的长度都为10个字符,那么发送“Hello”​“Unity”两条信息可以发送成“He llo... ”​“Unity... ”​,其中的“. ”表示填充字符,是为凑数,没有实际意义,只为了每次发送的数据都有固定长度。接收方每次读取10个字符,作为一条消息去处理。如果读到的字符数大于10,比如第1次读到“He llo...Un”​,那它只要把前10个字节“Hello... ”抽取出来,再把后面的两个字节“Un”存起来,等到再次接收数据,拼接第二条信息。

结束符号法

规定一个结束符号,作为消息间的分隔符

实现

发送数据

        //点击发送按钮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();//为了精简代码:使用同步Send//不考虑抛出异常socket.Send( sendBytes);}

接收数据

游戏程序一般会使用“长度信息法”处理粘包问题,核心思想是定义一个缓冲区(readBuff)和一个指示缓冲区有效数据长度变量(buffCount)​。 

        //接收缓冲区byte[] readBuff = new byte[1024];//接收缓冲区的数据长度int buffCount = 0;

比如,readBuff中有5个字节的数据“world”​(其余为byte的默认值0)​,那么buffCount的值应是5

因为存在粘包现象,缓冲区里面会保存尚未处理的数据。所以接收数据时不再从缓冲区开头的位置写入,而是把新数据放在有效数据之后

如果使用异步Socket, BeginReceive的参数应填成下面的样子:

        socket.BeginReceive(readBuff,          //缓冲区buffCount,        //开始位置1024-buffCount,   //最多读取多少数据0,                  //标志位,设成0即可ReceiveCallback, //回调函数socket);           //状态

在收到数据后,程序需要更新buffCount,以使下一次接收数据时,写入到缓冲区有效数据的末尾

        public void ReceiveCallback(IAsyncResult ar){Socket socket = (Socket) ar.AsyncState;//获取接收数据长度int count = socket.EndReceive(ar);buffCount+=count;……}

处理数据

收到数据后,如果缓冲区的数据足够长,超过1条消息的长度,就把消息提取出来处理。如果数据长度不够,不去处理它,等待下一次接收数据。

        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();}}

读取出的缓冲区数据已经没有用了,需要删除它。一个直观的办法是将缓冲区后面的数据向前移位

移动缓冲区数据可使用Array.Copy方法,它的原型如下:

        public static void Copy(Array sourceArray,long sourceIndex,Array destinationArray,long destinationIndex,long length)

sourceArray代表源数组,destinationArray代表目标数据,sourceIndex代表源数组的起始位置,destinationIndex代表目标数组的起始位置,length代表要复制的消息的长度。

        public void OnReceiveData(){//处理一条消息(略)//更新缓冲区int start = 2 + bodyLength;int count = buffCount - start;Array.Copy(readBuff, start, readBuff, 0, count);buffCount -= start;//如果有更多消息,就处理它}

完整示例

        using System.Collections;using System.Collections.Generic;using UnityEngine;using System.Net.Sockets;using UnityEngine.UI;using System;using System.Linq;public class Echo : MonoBehaviour {//定义套接字Socket socket;//UGUIpublic InputField InputFeld;public Text text;//接收缓冲区byte[] readBuff = new byte[1024];//接收缓冲区的数据长度int buffCount = 0;//显示文字string recvStr = "";//点击连接按钮public void Connection(){//Socketsocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);//为了精简代码:使用同步Connect//不考虑抛出异常socket.Connect("127.0.0.1", 8888);socket.BeginReceive( readBuff, buffCount, 1024-buffCount, 0,ReceiveCallback, socket);}//Receive回调public void ReceiveCallback(IAsyncResult ar){try {Socket socket = (Socket) ar.AsyncState;//获取接收数据长度int count = socket.EndReceive(ar);buffCount+=count;//处理二进制消息OnReceiveData();//继续接收数据socket.BeginReceive( readBuff, buffCount, 1024-buffCount, 0,ReceiveCallback, socket);}catch (SocketException ex){Debug.Log("Socket Receive fail" + ex.ToString());}}public void OnReceiveData(){Debug.Log("[Recv 1] buffCount=" +buffCount);Debug.Log("[Recv 2] readbuff=" + BitConverter.ToString(readBuff));//消息长度if(buffCount <= 2)return;Int16 bodyLength = BitConverter.ToInt16(readBuff, 0);Debug.Log("[Recv 3] bodyLength=" +bodyLength);//消息体if(buffCount < 2+bodyLength)return;string s = System.Text.Encoding.UTF8.GetString(readBuff, 2, buffCount);Debug.Log("[Recv 4] s=" +s);//更新缓冲区int start = 2 + bodyLength;int count = buffCount - start;Array.Copy(readBuff, start, readBuff, 0, count);buffCount -= start;Debug.Log("[Recv 5] buffCount=" +buffCount);//消息处理recvStr = s + "\n" + recvStr;//继续读取消息OnReceiveData();}//点击发送按钮public void Send(){string sendStr = InputFeld.text;//组装协议byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);Int16 len = (Int16)bodyBytes.Length;byte[] lenBytes = BitConverter.GetBytes(len);byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();//为了精简代码:使用同步Send//不考虑抛出异常socket.Send(sendBytes);Debug.Log("[Send]" + BitConverter.ToString(sendBytes));}public void Update(){text.text = recvStr;}}
  • 使用buffCount记录缓冲区的数据长度,使缓冲区可以保存多条数据;
  • 接收数据(BeginReceive)的起点改为buffCount,由于缓冲区总长度为1024,所以最大能接收的数据长度变成了1024-buffCount;
  • 通过OnReceiveData处理消息
  • 给发送的消息添加长度信息。

大端小端问题

下面是经过简化的BitConverter.ToInt16源码,其中的IsLittleEndian代表这台计算机是大端编码还是小端编码,不同的计算机编码方式会有不同。

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

完整发送数据

如何解决发送不完整问题

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

        //定义发送缓冲区byte[] sendBytes = new byte[1024];//缓冲区偏移值int readIdx = 0;//缓冲区剩余长度int length = 0;//点击发送按钮public void Send(){sendBytes = 要发送的数据;length = sendBytes.Length;       //数据长度readIdx = 0;socket.BeginSend(sendBytes, 0, length, 0, SendCallback, socket);}//Send回调public void SendCallback(IAsyncResult ar){//获取stateSocket socket = (Socket) ar.AsyncState;//EndSend的处理int count = socket.EndSend(ar);readIdx + =count;length -= count;//继续发送if(length > 0){socket.BeginSend(sendBytes,readIdx,  length, 0, SendCallback, socket);}}
        socket.BeginSend(sendBytes,       //发送缓冲区readIdx,        //从索引为6的数据开始发送length,         //因为缓冲区只剩下4个数据,最多发送4个数据0,              //标志位,设置为0即可SendCallback,   //回调函数socket);        //传给回调函数的对象

上面的方案解决了一半问题,因为调用BeginSend之后,可能要隔一段时间才会调用回调函数,如果玩家在SendCallback被调用之前再次点击发送按钮,按照前面的写法,会重置readIdx和length, SendCallback也就不可能正确工作了。为此我们设计了加强版的发送缓冲区,叫作写入队列(writeQueue)​,它的结构如图

图展示了一个包含三个缓冲区的写入队列,当玩家点击发送按钮时,数据会被写入队列的末尾,比如一开始发送“08hellolpy”​,那么就在队列里添加一个缓冲区,这个缓冲区和本节前面介绍的缓冲区一样,包含一个bytes数组,以及指向缓冲区开始位置的readIdx、缓冲区剩余长度的length。Send方法会做这样的处理,示意代码如下:

        public void Send() {sendBytes = 要发送的数据;writeQueue.Enqueue(ba);     //假设ba封装了readbuff、readIdx、length等数据if(writeQueue只有一条数据){socket.BeginSend(参数略);}}public void SendCallback(IAsyncResult ar){count = socket.EndSend(ar);ByteArray ba = writeQueue.First();  //ByteArray后面再介绍ba.readIdx+=count;  //length的处理略if(发送不完整){取出第一条数据,再次发送}else if(发送完整,且writeQueue还有数据){删除第一条数据取出第二条数据,如有,发送}}

ByteArray 和 Queue

ByteArray是封装byte[​]​、readIdx和length的类,可以这样定义它(添加文件ByteArray.cs)​: 

        using System;public class ByteArray  {//缓冲区public byte[] bytes;//读写位置public int readIdx = 0;public int writeIdx = 0;//数据长度public int length { get { return writeIdx-readIdx; }}//构造函数public ByteArray(byte[] defaultBytes){bytes = defaultBytes;readIdx = 0;writeIdx = defaultBytes.Length;}}

        byte[] sendBytes = new byte[]{'0', '3', 'c', 'a', 't'};ByteArray ba = new ByteArray(sendBytes);socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0, SendCallback, socket);

Queue

        Queue<ByteArray> writeQueue = new Queue<ByteArray>();ByteArray ba = new ByteArray(sendBytes);writeQueue.Enqueue(ba);                //将ba放入队列ByteArray ba2 = writeQueue.First();  //获取writeQueue的第一个元素,队列保持不变be2 = writeQueue.Dequeue();            //弹出队列的第一个元素

Enqueue代表把元素放入到队列中,该元素会放到队列的末尾;Dequeue代表出列,队列的第一个元素被弹出来;First代表获取队列的第一个元素

解决线程冲突

由异步的机制可以知道,BeginSend和回调函数往往执行于不同的线程,如果多个线程同时操作writeQueue,有可能引发些问题。

玩家连续点击两次发送按钮,假如运气特别差,第二次发送时,第一次发送的回调函数刚好被调用。如果线程1的Send刚好走到writeQueue.Enqueue(ba)这一行(t2时刻)​,按理说writeQueue.Count应为2,不应该进入if(writeQueue.Count == 1)的真分支去发送数据(因为此时writeQueue.Count== 2)​。但假如在条件判断之前,回调线程刚好执行了writeQueue.Dequeue()(t3时刻)​,由于writeQueue里只有1个元素,在t4时刻主线程判断if(writeQueue.Count == 1)时,条件成立,会发送数据。但SendCallback中ba = writeQueue.First()也会获取到队列的第一条数据,也会把它发送出去。第二次发送的数据将会被发送两次,显然不是我们需要的。

为了避免线程竞争,可以通过加锁(lock)的方式处理。当两个线程争夺一个锁的时候,一个线程等待,被阻止的那个锁变为可用

        //发送缓冲区Queue<ByteArray> writeQueue = new Queue<ByteArray>();//点击发送按钮public void Send(){//拼接字节,省略组装sendBytes的代码byte[] sendBytes = 要发送的数据;ByteArray ba = new ByteArray(sendBytes);int count = 0;lock(writeQueue){writeQueue.Enqueue(ba);count = writeQueue.Count;}//sendif(count == 1){socket.BeginSend(sendBytes, 0, sendBytes.Length,0, SendCallback, socket);}Debug.Log("[Send]" + BitConverter.ToString(sendBytes));}//Send回调public void SendCallback(IAsyncResult ar){//获取state、EndSend的处理Socket socket = (Socket) ar.AsyncState;int count = socket.EndSend(ar);ByteArray ba;lock(writeQueue){ba = writeQueue.First();}ba.readIdx+=count;if(count == ba.length){lock(writeQueue){writeQueue.Dequeue();ba = writeQueue.First();}}if(ba ! = null){socket.BeginSend(ba.bytes, ba.readIdx, ba.length,0, SendCallback, socket);}}

以上代码把临界区设计得很小,拥有较高的执行效率。

参考书籍:《Unity3D网络游戏实战(第2版)》 (豆瓣) (douban.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/399609.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RCE绕过技巧

目录 EVAL长度限制突破技巧 1.使用反引号 2.file_put_contents写入文件 3.php5.6变长参数usort回调后门 命令长度限制突破技巧 1.拼接文件名 无字母数字的webshell命令执行 1.取反码 2.上传临时文件 EVAL长度限制突破技巧 分析代码&#xff1a;首先传递一个param参数&…

OceanBase V4.3 列存引擎之场景问题汇总

在OceanBase 4.3版本发布后&#xff08;OceanBase社区版 V4.3 免费下载&#xff09;&#xff0c;其新增的列存引擎&#xff0c;及行列混存一体化的能力&#xff0c;可以支持秒级实时分析&#xff0c;引发了用户、开发者及业界人士的广泛讨论。本文选取了这些讨论中较为典型的一…

企业应该如何准备 EcoVadis 审核?

企业准备 EcoVadis 审核可以参考以下步骤&#xff1a; 注册&#xff1a;在网上注册并提供公司的相关信息&#xff0c;包括法律实体名称、国家和地区、企业规模和行业等。如果是受客户邀请参加评估&#xff0c;需按照邀请邮件中的链接进行注册&#xff0c;并确保客户能随时获知评…

安卓默认混淆规则文件的区别

在 Android 项目中&#xff0c;ProGuard 是一个优化和混淆代码的工具。proguard-android-optimize.txt 和 proguard-android.txt 是两个用于配置 ProGuard 的默认规则文件&#xff0c;如图下 它们有以下区别&#xff1a; proguard-android-optimize.txt: 优化&#xff1a;这个配…

Django中事务的基本使用

1. Django事务处理 事务(Transaction): 是一种将多个数据库操作组合成一个单一工作单元的机制. 如果事务中的所有操作都成功完成, 则这些更改将永久保存到数据库中. 如果事务中的某个操作失败, 则整个事务将回滚到事务开始前的状态, 所有的更改都不会被保存到数据库中. 这对于…

系统编程 day10 进程2

进程创建之后&#xff1a; 1.任务-----子进程与父进程干的活差不多 2.父进程创建出子进程之后&#xff0c;子进程做的与父进程完全不同 shell程序-----bash----- 以上为进程运行的过程中&#xff0c;典型的两种应用场景 能够改变子进程的执行效果的函数是exec函数族 l和v&a…

【网盘系统3.0版本】百度云盘手动cookie获取,添加到扫码系统管理平台。

一.获取cookie步骤 1.谷歌浏览器选择开发者模式。 2.选择网路&#xff0c;过滤接口main 3.选择request head&#xff0c;cookie列表里面可查看二.添加到管理平台。 1.登录管理平台&#xff0c;输入账户和密码 2.选择账户设置&#xff0c;添加cookie。 4.复制卡密链接&#xf…

LVS实验的三模式总结

文章目录 LVS的概念叙述NAT工作模式实战案例**思想&#xff1a;**NAT工作模式的优点NAT工作模式的缺点 NAT工作模式的应用场景大致配置 route&#xff1a;打开路由内核功能 部署DR模式集群案例工作思想&#xff1a;大致工作图如下思路模型 具体配置与事实步骤补充 防火墙标签解…

c++编程(20)——类与对象(6)继承

欢迎来到博主的专栏——c编程 博主ID&#xff1a;代码小豪 文章目录 继承继承与权限访问 基类和派生类基类和派生类的赋值兼容转换基类与派生类的类作用域派生类与基类的构造函数基类与派生类拷贝构造函数 继承与静态成员final关键字 面向对象编程的核心思想是封装、继承和多态…

计算机网络408考研 2021

2021 计算机网络408考研2021年真题解析_哔哩哔哩_bilibili 1 1 11 1 1 11

解决No module named ‘tensorflow‘

import tensorflow as tf ModuleNotFoundError: No module named tensorflow 安装合适的tensorflow版本 先查看自己的python版本 或者输入指令&#xff1b;python --version 安装兼容的tensorflow版本&#xff0c;安装指定版本的tensorflow pip install tensorflow-gpu2.3.0…

Qt | QSQLite内存数据库增删改查

点击上方"蓝字"关注我们 01、演示 参数随便设置 查询 修改 右键菜单是重点 手动提交,点击Submit All

【Docker】基础篇

系列综述&#xff1a; &#x1f49e;目的&#xff1a;本系列是个人整理为了云计算学习的&#xff0c;整理期间苛求每个知识点&#xff0c;平衡理解简易度与深入程度。 &#x1f970;来源&#xff1a;材料主要源于–Docker视频教程从入门到进阶&#xff0c;docker视频教程详解–…

【云原生】高可用集群KEEPALIVED(理论篇)

一、高可用集群 1.1 集群类型 LB:Load Balance 负载均衡 LVS/HAProxy/nginx(http/upstream, stream/upstream)HA:High Availability 高可用集群数据库、RedisSPoF: Single Point of Failure&#xff0c;解决单点故障HPC: High Performance computing 高性能集群 1.2 系统可用…

车身域测试学习、CANoe工具实操学习、UDS诊断测试、功能安全测试、DTC故障注入测试、DBC数据库、CDD数据库、CAN一致性测试、ECU刷写测试

每日直播时间&#xff1a;&#xff08;直播方式&#xff1a;腾讯会议&#xff09;周一到周五&#xff1a;20&#xff1a;00-23&#xff1a;00周六与周日&#xff1a;9&#xff1a;00-17&#xff1a;00 进腾讯会议学习的&#xff0c;可以关注我并后台留言 直播内容&#xff1a;&…

OKnews加密货币资讯:现货比特币ETF市场动荡,价格大幅下跌

OKnews加密货币资讯网8月13日讯&#xff1a;现货比特币ETF 市场近期的动荡给加密货币行业带来了冲击&#xff0c;导致比特币 (BTC) 未能维持其在关键的60,000美元大关之上的地位。该数字货币在过去24 小时内下跌了 3.65%&#xff0c;跌至58,515 美元。市场波动加剧以及对美国经…

人脸操作:从检测到识别的全景指南

人脸操作&#xff1a;从检测到识别的全景指南 在现代计算机视觉技术中&#xff0c;人脸操作是一个非常重要的领域。人脸操作不仅包括检测图像中的人脸&#xff0c;还涉及到人脸识别、表情分析、面部特征提取等任务。这些技术在各种应用中发挥着关键作用&#xff0c;从社交媒体…

中国科技统计年鉴,数据覆盖1991-2022年多年份

基本信息. 数据名称: 中国科技统计年鉴 数据格式: excel 数据时间: 1991-2022年 数据几何类型: xlsx 数据坐标系: WGS84 数据来源&#xff1a;国家统计局 数据预览&#xff1a; 数据可视化.

(kali关怀版)kali调整字体图标显示大小

字体大小调整(图标字体) 字体在左上角搜apprence 图标大小调整 图标在桌面右键点apprence 任务栏大小调整 任务栏在上面右键&#xff0c;选择panel preference 终端字体大小调整 终端字体用ctrl和加号减号进行缩放 属于是kali关怀版了:) 还可指定锁屏和休眠时间&#…

waveInAddBuffer死锁的大雷解决

项目场景&#xff1a; 从来没有一个bug让我这么抓狂&#xff0c;足足查了3天3夜&#xff0c;官方文档翻了一遍说的基本无用。具体项目就是使用waveIn系列函数获取windows系统麦克风数据&#xff0c;虽然windows上有好几种方法获取麦克风数据&#xff0c;我最终还是选择了它。 …