socket收发数据的处理

news2024/11/24 5:39:35

1. TCP 协议是一种基于数据流的协议

  • Socket的Receive方法只是把接收缓冲区的数据提取出来,当系统的接收缓冲区为空,Receive方法会被阻塞,直到里面有数据。

  • Socket的Send方法只是把数据写入到发送缓冲区里,具体的发送过程由操作系统负责。当操作系统的发送缓冲区满了,Send方法会阻塞

2.解决粘包问题的方法

一般有三种方法 可以解决粘包和半包问题,分别是

  • 长度信息法: 指在每个数据包前面加上长度信
  • 固定长度法: 每次都以相同的长度发送数据
  • 结束符号法

一般 的游戏开发会在每个数据包前面加上长度字节, 以方便解析

2.1 发送数据代码

//点击发送按钮
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);
}

2.2 定义一个缓冲区(readBuff)和一个指示缓冲区有效数据长度变量(buffCount)。

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

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

3. 大小端

3.1 使用Reverse()兼容大小端编码
如果使用BitConverter.GetBytes将数字转换成二进制数据,转换出来的数据有可能基于大端模式, 也有可能基于小端模式。

因为我们规定必须使用小端编码,一个简单的办法 是,判断系统是否是 小端编码的系统,如果不是,就使用Reverse() 方法将大端编码转换为 小端编码。以Send 为例,代码如下:

//点击发送按钮 
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); 
      //大 小 端编 码
      if(!BitConverter.IsLittleEndian) {
          Debug. Log (" [Send] Reverse lenBytes");
          lenBytes.Reverse();
      }    
      // 拼接字节
      byte[] sendBytes = lenBytes.Concat(bodyBytes) .ToArray ( );
      socket.Send(sendBytes);
}

3.2 手动还原数值
BitConverter. Toint16中根据系统大小端采用不同的编码方式,如果是小端编码,返回 的 是(pbyte) | ((pbyte+1)<< 8),如果是大端编码,返回的是(pbyte<< 8) | ((pbyte+1)。以小端为例,由于采用指针,(pbyte)指向缓冲区中指定位置的第1 个字节,(pbyte + 1)指向缓冲区中指定位置的第2个字节,((pbyte+1)<<8)表示左移8位,相当于乘以 256,返回的数字便是“第1个字节+第2字节256”,与4. 4.1节中介绍的步骤相同。

public void OnReceiveData ( ) { 
	// 消息长 度
	if (buffCount <= 2)
	return;
	// 消息长度
	Int16 bodyLength = (short) ((readBuff[1] << 8) | readBuff[0]);
	Debug.Log ( "[Recv] bodyLength=" + bodyLength); 
	// 消息体、更新 缓冲区
	// 消 息 处 理 、 继 续读 取 消 息
	.....
}

readBuff[0] 代表缓冲区的第1 个字节,readBuff[1] 代 表 缓 冲 区 的 第 2 个 字 节, ( readBuff[1] < < 8 ) 代 表 将 缓 冲 区 第 2 个 字 节 的 数 据 乘 以 2 5 6 , 中 间的“|” 代表逻辑与,在这里等同于相加。

  1. 完整的ByteArray
    ByteArray作为一种通用的byte型缓冲区结构 ,它应该支持自动扩展,支持常用的读写操作 。 同时 , 为了做到极致的效率, ByteArray 的大部分成员变量都设为
    public ,以提供灵活性
    在这里插入图片描述
    在这里插入图片描述
    readldx 代表可 读位置,即缓冲区有效数据的起始位置,writeldx 代表可写位置,即缓冲区有效数据的末尾。 成员两数remain 代表缓冲区还可以容纳的字节数
using System;

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 byte[size];
		capacity = size;
		initSize = size;
		readIdx = 0;
		writeIdx = 0;
	}

	//构造函数
	public ByteArray(byte[] defaultBytes){
		bytes = defaultBytes;
		capacity = defaultBytes.Length;
		initSize = defaultBytes.Length;
		readIdx = 0;
		writeIdx = defaultBytes.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;
	}

	//写入数据
	public int Write(byte[] bs, int offset, int count){
		if(remain < count){
			ReSize(length + count);
		}
		Array.Copy(bs, offset, bytes, writeIdx, count);
		writeIdx+=count;
		return count;
	}

	//读取数据
	public int Read(byte[] bs, int offset, int count){
		count = Math.Min(count, length);
		Array.Copy(bytes, 0, bs, offset, count);
		readIdx+=count;
		CheckAndMoveBytes();
		return count;
	}

	//检查并移动数据
	public void CheckAndMoveBytes(){
		if(length < 8){
			MoveBytes();
		}
	}

	//移动数据
	public void MoveBytes(){
		Array.Copy(bytes, readIdx, bytes, 0, length);
		writeIdx = length;
		readIdx = 0;
	} 

	//读取Int16
	public Int16 ReadInt16(){
		if(length < 2) return 0;
		Int16 ret = BitConverter.ToInt16(bytes, readIdx);
		readIdx += 2;
		CheckAndMoveBytes();
		return ret;
	}

	//读取Int32
	public Int32 ReadInt32(){
		if(length < 4) return 0;
		Int32 ret = BitConverter.ToInt32(bytes, readIdx);
		readIdx += 4;
		CheckAndMoveBytes();
		return ret;
	}
	//打印缓冲区
	public override string ToString(){
		return BitConverter.ToString(bytes, readIdx, length);
	}

	//打印调试信息
	public string Debug(){
		return string.Format("readIdx({0}) writeIdx({1}) bytes({2})",
			readIdx,
			writeIdx,
			BitConverter.ToString(bytes, 0, capacity)
		);
	}
}

4.2. 重设尺寸
在 某 此 情 况 下, 比 如 需 要 写 人 的 数 据 量 大 于 缓 冲 区 剩 余 长 度 (r e m a i n ) 时 , 就 需 要 扩 大 缓 冲 区。 例 如 要 在 图 4 - 4 2 所 示 缓 冲 区 后 面 添 加 数 据 〝0 5 h e l l o ” , 使 绥 冲 区 数 据 变 成 〝02hioshello”。此时缓冲区只剩余6 个字节,但“0shello” 是7个字节,放不下。此时的 做法是,重新申请一 个长度合适的byte 数组,然后把原byte 数组的数据复制过去,再重新 设置readldx、writeldx 等数值。
在这里插入图片描述
为了避免频繁重设尺寸,规定每次翻倍增加bytes数组长度,即长度是1、2、4、8、 1 6 、 32 、 64 、 128 、 256 、 512 、 1024 、 2048 等 值 。 如果参数 size 是1500 , 缓冲区长度会被设置成 2048 , 如果参数 size 是130 (假设size值合理), 缓冲区长度会被设 置成256。通过“ int n= 1; while(n<size) n*= 2〞 这两行代码,即可得出长度值。在图4-42 所示的缓冲区上执行Resize(6)后,缓冲区将变成图 4- 43 所 示的样式。
在这里插入图片描述
4.3 移动数据
在 某 些 情 形 下 , 例 如 有 效 数 据 长 度 很 小 ( 这 里 设 置 为 8 ), 或 者 数 据 全 部 被 读 取 时 ( readldx==writeldx),可以将数据前移,增加remain,避免bytes数组过长。由于数据很 少, 程 序 执 行 的 效 率 不 会 有 影 响 。 在 图 4 - 4 0 所 示 的 缓 冲 区 上 执 行 CheckAndMoveBytes 后 , 缓冲 区将 变成图4- 44 所示的样式。
在这里插入图片描述

  1. 把上面的ByteArray 应用到异步
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;
	//UGUI
	public InputField InputFeld;
	public Text text;
	//接收缓冲区
	ByteArray readBuff = new ByteArray();
	//发送缓冲区
	Queue<ByteArray> writeQueue = new Queue<ByteArray>();
	bool isSending = false;
	//显示文字
	string recvStr = "";


	//点击连接按钮
	public void Connection()
	{
		//Socket
		socket = new Socket(AddressFamily.InterNetwork,
			SocketType.Stream, ProtocolType.Tcp);
		//为了精简代码:使用同步Connect
		//             不考虑抛出异常
		socket.Connect("127.0.0.1", 8888);

		socket.BeginReceive( readBuff.bytes, readBuff.writeIdx, 
							readBuff.remain, 0, ReceiveCallback, socket);
	}

	//Receive回调
	public void ReceiveCallback(IAsyncResult ar){
		try {
			Socket socket = (Socket) ar.AsyncState;
			//获取接收数据长度
			int count = socket.EndReceive(ar);
			readBuff.writeIdx+=count;
			//处理二进制消息
			OnReceiveData();
			//继续接收数据
			if(readBuff.remain < 8){
				readBuff.MoveBytes();
				readBuff.ReSize(readBuff.length*2);
			}
			socket.BeginReceive( readBuff.bytes, readBuff.writeIdx, 
				readBuff.remain, 0, ReceiveCallback, socket);
		}
		catch (SocketException ex){
			Debug.Log("Socket Receive fail" + ex.ToString());
		}
	}

	//
	public void OnReceiveData(){
		Debug.Log("[Recv 1] length  =" + readBuff.length);
		Debug.Log("[Recv 2] readbuff=" + readBuff.ToString());
		if(readBuff.length <= 2) 
			return;
		//消息长度
		int readIdx = readBuff.readIdx;
		byte[] bytes =readBuff.bytes; 
		Int16 bodyLength = (Int16)((bytes[readIdx+1] << 8 )| bytes[readIdx]);
		if(readBuff.length < bodyLength)
			return;
		readBuff.readIdx+=2; 
		Debug.Log("[Recv 3] bodyLength=" +bodyLength);
		//消息体
		byte[] stringByte = new byte[bodyLength];
		readBuff.Read(stringByte, 0, bodyLength);
		string s = System.Text.Encoding.UTF8.GetString(stringByte);

		Debug.Log("[Recv 4] s=" +s);
;
		Debug.Log("[Recv 5] readbuff=" + readBuff.ToString());
		//消息处理
		recvStr = s + "\n" + recvStr;
		//继续读取消息
		if(readBuff.length > 2){
			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);
		//大小端编码
		if(!BitConverter.IsLittleEndian){
			Debug.Log("[Send] Reverse lenBytes");
			lenBytes.Reverse();
		}
		//拼接字节
		byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();
		ByteArray ba = new ByteArray(sendBytes);
		lock(writeQueue){
			writeQueue.Enqueue(ba);
		}
		//send
		socket = new Socket(AddressFamily.InterNetwork,
			SocketType.Stream, ProtocolType.Tcp);
		if(!isSending){
			socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
			isSending = true;
		}
		Debug.Log("[Send] " + BitConverter.ToString(sendBytes));

	}

	//Send回调
	public void SendCallback(IAsyncResult ar){
		//获取state
		Socket socket = (Socket) ar.AsyncState;
		//EndSend的处理
		int count = 0;
		count = socket.EndSend(ar);
			
		Debug.Log("Socket Send succ " + count);

		ByteArray ba = writeQueue.First();
		ba.readIdx+=count;
		if(ba.length == 0){
			lock(writeQueue){
				writeQueue.Dequeue();
				ba = writeQueue.First();
			}
		}
		if(ba != null){
			socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0, SendCallback, socket);
		}
		else{
			isSending = false;
		}
	}

	public void Update(){
		text.text = recvStr;
	}
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android中的消息异步处理机制及实现方案

基本介绍 当我们需要执行复杂的计算逻辑&#xff0c;网络请求等耗时操作时&#xff0c;服务器可能不会立即响应请求&#xff0c;如果不将这类操作放在子线程中运行&#xff0c;就会导致主线程被阻塞住&#xff0c;从而影响用户的使用体验如果想要更新应用程序中的UI控件&#…

idea远程调试docker容器内正在运行的线上项目

1.重新编写Dockerfile文件 在原本的Dockerfile上新增参数 就是 运行jar包增加调试参数 增加调试暴漏的端口号 -agentlib:jdwptransportdt_socket,servery,suspendn,address*:50052.在运行docker容器的时候增加暴漏端口5005 3.打开idea就是正在运行的项目 4.选择远程配置 5.配…

解决MyBatis获取刚插入数据的ID值

解决MyBatis获取刚插入数据的ID值 Mybatis获取刚插入数据的ID值有很多解决方法&#xff0c;目前采用以下方式进行获取。 添加完数据后直接返回刚添加数据的id // UserDao.java public static void addUser() throws Exception{InputStream resourceAsStream Resources.getR…

idea的java代码引用proto文件报错

尝试了四种办法&#xff0c;感觉第一个和第二个比较有效。 前提是要先安装了 proto 的idea插件。 1.修改idea配置文件编译大文件的限制 proto生成的源文件有数万行&#xff0c;源文件过大导致 idea 拒绝编译过大的源文件。 解决方案&#xff1a; 如果 protoc 生成的 class 文…

程序优化 --- arthas trace命令使用

最近在做优化,通过arthas的trace命令去观察方法内的耗时情况以便对程序进行修改. 1.启动arthas之后选择需要监测的程序 2.找到需要监测的接口,一般都是直接找service例子如下: trace 类地址.类名 方法名 (中间有空格)

智能计算系统-概述

1、人工智能技术分层 2、人工智能方向人才培养 3、课程体系的建议 4、智能系统课程对学生的价值 5、智能计算系统对老师的价值 6、什么是智能计算系统 7、智能计算系统的形态 8、智能计算系统具有重大价值 9、智能计算系统的三大困难 10、开创深度学习处理器方向 11、寒武纪的国…

MOS开关电路应用于降低静态功耗

本文主要讲述MOS开关电路的应用,过了好久突然想整理一下&#xff0c;有错误的地方请多多指出&#xff0c;在做电池类产品&#xff0c;需要控制产品的静态功耗&#xff0c;即使让芯片进入休眠状态&#xff0c;依旧功率很大&#xff0c;所以在电路中加一组软开关&#xff0c;防止…

嵌入式软件工程师入何突破瓶颈?

各位关注嵌入式软件工程师发展的朋友们&#xff0c;下面来探讨一下嵌入式软件工程师该如何突破瓶颈。首先要强调的是&#xff0c;不要仅仅将自己局限在嵌入式软件工程师这一角色定位上。 事实上&#xff0c;嵌入式软件工程师已经掌握了诸多业务层面的内容&#xff0c;完全有能力…

【C++】编译原理

三、C编译 前面给大家演示了如何从写C代码到编译代码再到执行代码的全过程。这个过程中非常重要的编译环节&#xff0c;被我们一个按钮或者一个ctrlF7快捷键就给带过了。其实这个环节非常重要&#xff0c;如果你非常了解这个环节&#xff0c;你开发源代码就会更加自信和清醒&a…

pytest + yaml 框架 -61.jenkins+allure+钉钉通知添加测试结果

前言 上一篇pytest + yaml 框架 -60.git+jenkins+allure+钉钉通知反馈 已经实现测试结果用钉钉通知。 本篇继续在钉钉通知里添加测试的汇总结果,此功能在pytest-yaml-yoyo v1.5.2版本上实现。 Environment Injector 插件 在运行完用例后会生成一个summary.json 文件,汇总…

【系统架构】REST风格

系列文章目录 第一章 系统架构的演进 第二章 REST风格架构 文章目录 系列文章目录前言一、进程间的通信普通管道&#xff08;Pipe&#xff09;或者具名管道&#xff08;Named Pipe&#xff09;信号&#xff08;Signal&#xff09;信号量&#xff08;Semaphore&#xff09;消息…

项目实战中学透Spring-业务场景驱动-Spring01(IOCDI)

软件环境 JDK1.8 Maven3.6 IDEA2022.3(Ultimate Edition) Spring5.3.29 主要知识点大纲 1.Spring简介 2.Spring整体架构 3.业务场景中理解Spring IOC(控制反转)和DI(依赖注入) 4.业务场景中理解IOC容器&#xff0c;实例化容器&#xff0c;实例化Bean的几种方式 5.业务…

在C#中对 JSON进行序列化和反序列化处理

概述&#xff1a;在现代软件开发领域&#xff0c;不同系统和平台之间的数据交换是不可或缺的方面。JSON&#xff08;JavaScript 对象表示法&#xff09;因其轻量级、人类可读和易于解析的特性而成为一种无处不在的数据格式。使用 C# &#x1f680;编程的 JSON 序列化和反序列化…

OpenCV学习(4.15) 基于 GrabCut 算法的交互式前景提取

1. 目标 在这一章当中 我们将看到 GrabCut 算法来提取图像中的前景我们将为此创建一个交互式应用程序。 2. 理论 GrabCut 算法由英国剑桥微软研究院 Carsten Rother&#xff0c;Vladimir Kolmogorov和Andrew Blake发明&#xff0c;并在他们的论文“GrabCut”&#xff1a;使…

使用 yocto 搭建 qemuarm64 环境

文章目录 前言一、ubuntu 环境准备1. 编译主机基础的环境准备2. 编译主机相关依赖软件的安装二、yocto5.0 代码的获取与编译1. 获取代码2. yocto5.0 代码的编译2.1 source 环境变量2.2 修改相关配置文件2.3 编译3. 启动 qemu总结参考资料前言 本文主要介绍如何在 ubuntu 下使用…

MySQL日志(二):MySQL抖动

一条SQL语句&#xff0c; 正常执行的时候特别快&#xff0c; 但是有时也不知道怎么回事&#xff0c; 它就会变得特别慢&#xff0c; 并且这样的场景很难复现&#xff0c; 它不只随机&#xff0c; 而且持续时间还很短。 看上去&#xff0c; 这就像是数据库“抖”了一下。 今天&…

FreeRTOS简单内核实现3 任务管理

文章目录 0、思考与回答0.1、思考一0.2、思考二0.3、思考三 1、任务控制块2、创建任务2.1、xTaskCreateStatic( )2.2、prvInitialiseNewTask( )2.3、pxPortInitialiseStack( )2.4、任务内存详解 3、就绪链表3.1、定义3.2、prvInitialiseTaskLists( ) 4、任务调度器4.1、vTaskSt…

阿里云系列产品免费用,不香吗?

阿里云系列产品免费用&#xff0c;不香吗&#xff1f; 什么是无影云电脑开启无影云下载安装客户端登录无影云桌面应用场景 开篇先发布一下阿里云产品免费体验地址&#xff1a;https://free.aliyun.com/?utm_contentg_1000370296 下面开始我的无影云电脑或者叫做无影云桌面的体…

LLM大语言模型算法特训,带你转型AI大语言模型算法工程师(完结)

LLM大语言模型算法 与AI大语言模型算法工程师的联系 LLM&#xff08;Large Language Model&#xff09;大语言模型是指像GPT这样的大型自然语言处理模型&#xff0c;而AI大语言模型算法工程师则是负责开发和优化这些模型的专业人士。它们之间的联系可以从以下几个方面来理解&a…

linux驱动学习(十二)之看门狗

一、看门狗定时器功能 1、产生复位信号&#xff1a;当系统受到由于噪声或者干扰而造成系统死机&#xff0c;看门狗产生一个复位信号。 2、普通定时器&#xff1a;16bits定时器&#xff0c;产生周期性的中断信号 二、看门狗系统框图 设置计数值以每隔10S就会产生一个复位信号&…