当前位置: 首页 > news >正文

粘包/半包及解决方案

一、粘包/半包介绍

1:粘包

粘包(Packet Concatenation)通常发生在基于流式传输协议(如 TCP)的通信中,因为 TCP 是面向流的传输协议,它不保证数据包的边界,而是将数据视为连续的字节流,它表示客户端发送多条消息,服务端只收到了一条消息

2:半包

半包(Half Packet)与粘包问题相反。在半包问题中,接收端接收到的数据包不完整,即接收到的数据包只是完整数据包的一部分,无法完整地解析和处理。

3:原因

①网络延迟/阻塞

②发送方连续发送数据

③接收端缓冲区大小限制

④数据包丢失

二、粘包/半包解决方案

1:长度信息法

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

①客户端第一次发送包含长度信息的内容

②客户端第二次发送包含长度信息的内容

③服务端第一次接收到了4个字节,存入缓冲区,但是这时候并不处理,因为收到了10,所以要等到11个字节完整再处理

④服务端等到客户端发送剩下的7个字节,但是第二次接收到了9个字节,服务端把之前的6个字节再读取,然后拼接,把10helloworld进行处理,读取到标志长度4,等待下次处理

⑤服务端最后一次收到ove,就把之前的l一起拼接,返回完整的4love

*一般的游戏是16位的整型数来存放长度信息

2:固定长度法

每次都发送相同长度的数据,一次不足的数据用.来补充,.位补充字符,没有实际意义

如果接收到的字符数大于10,就只提取前10个字符

3:结束符号法

规定一个结束符号作为消息的分隔符,比如Hello$World就是两条信息

三、代码示例

1:发送数据
        byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(SendStr);Int16 len = (Int16)bodyBytes.Length;//把长度转化为Int16byte[] lenBytes = BitConverter.GetBytes(len);//此时SendBytes包含长度字符串和内容字符串byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();

2:接收数据

定义一个接收缓冲区和接收缓冲区长度。缓冲区会保存尚未处理的数据

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

之前的BeginReceive函数原型如下:

public IAsyncResult BeginReceive (  byte[] buffer,  int offset,  int size,  SocketFlags socketFlags,  AsyncCallback callback, object state )

现在的参数应该写成这个样子:

socket.BeginReceive(readBuff, buffCount 1024-buffCount, 0,  ReceiveCallback, socket);

readBuff 是缓冲区

buffCount 是开始读取的位置

1024 - buffCount 是剩余多少可读取的大小

3:处理数据

public void OnReceiveData(){//消息长度小于2,直接返回等待下一次接收if(buffCount <= 2)return;//消息的长度Int16 bodyLength = BitConverter.ToInt16(readBuff, 0);//消息体//如果消息长度小于我消息内容和长度的字节,就返回继续读取if(buffCount < 2+bodyLength)return;//如果长度够用,就转化为string类型string s = System.Text.Encoding.UTF8.GetString(readBuff, 2,buffCount);//更新缓冲区int start = 2 + bodyLength;int count = buffCount - start;//Copy函数把缓冲区后面的内容提到前面Array.Copy(readBuff, start, readBuff, 0, count);buffCount -= start;//继续读取消息if(readBuff.length > 2){OnReceiveData();}
}

Copy的原型函数如下:

public static void Copy(Array sourceArray,//源数组long sourceIndex,//目标数据Array destinationArray,//目标数组long destinationIndex,//目标数组起始位置long length//复制消息的长度
)

四、完整示例

1:客户端
using System;
using System.Linq;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;public class SendScr : MonoBehaviour
{Socket socket;public InputField inputField;public Text text;byte[] readBuff = new byte[1024];int buffCount = 0;//缓冲区数据长度string recvStr = "";public void Connection(){socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);socket.Connect("127.0.0.1", 8888);socket.BeginReceive(readBuff, buffCount, 1024 - buffCount, 0, ReceiveCallback, socket);}private 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());}}private 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 = inputField.text;byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);Int16 len = (Int16)bodyBytes.Length;byte[] lenBytes = BitConverter.GetBytes(len);//此时SendBytes包含长度字符串和内容字符串byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();socket.Send(sendBytes);Debug.Log("[Send]" + BitConverter.ToString(sendBytes));}private void Update(){text.text = recvStr;}}

捋一下客户端的功能:

当进入场景点击Connection,创建新的Socket连接,绑定服务端口,开始接收信息

这里的BeginReceive就是我们接受消息的函数,它传入我们的缓冲区readBuff,缓冲区长度buffCount,1024 - buffCount表示我们还剩多少字节数据,然后进入回调函数

Receive回调函数中创建新的Socket对象来解析获取到的socket对象,count用于跟踪每次异步接收操作实际接收到的数据长度,然后把count加到buffCount上,这样更新了缓冲区的长度

然后使用OnReceiveData处理数据,继续读取数据,点击发送就调用Send()

2:服务端
using System.Net.Sockets;
using System.Net;
internal class ClientState
{public Socket socket;public byte[] readBuff = new byte[1024];
}
class Class
{static Socket listenfd;static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();public static void Main(string[] args){listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPAddress ipAdr = IPAddress.Parse("127.0.0.1");IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);listenfd.Bind(ipEp);listenfd.Listen(0);Console.WriteLine("[服务器]启动成功");List<Socket> checkRead = new List<Socket>();while (true){//填充checkRead列表checkRead.Clear();checkRead.Add(listenfd);foreach (ClientState s in clients.Values){checkRead.Add(s.socket);}//selectSocket.Select(checkRead, null, null, 1000);//检查可读对象foreach (Socket s in checkRead){if (s == listenfd){ReadListenfd(s);}else{ReadClientfd(s);}}}}public static void ReadListenfd(Socket listenfd){Console.WriteLine("Accept");Socket clientfd = listenfd.Accept();ClientState state = new ClientState();state.socket = clientfd;clients.Add(clientfd, state);}public static bool ReadClientfd(Socket clientfd){ClientState state = clients[clientfd];//接收int count = clientfd.Receive(state.readBuff);if (count == 0){clientfd.Close();clients.Remove(clientfd);Console.WriteLine("Socket Close");return false;}//广播string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 2, count - 2);Console.WriteLine("Receive" + recvStr);byte[] sendBytes = new byte[count];Array.Copy(state.readBuff, 0, sendBytes, 0, count);foreach (ClientState cs in clients.Values){cs.socket.Send(sendBytes);}return true;}
}

这里的服务端代码和Select部分的代码基本相同

3:测试

打开服务器,客户端连接后输入UNITY

①客户端反馈

Recv1的buffCount就是接收到服务端所返回的消息,服务端所返回的信息是客户端发送的拼接数组,就是UNITY+lenBytes,就是7

Rec2的readbuff就是缓冲区存储的内容

Recv3是当消息没有接受完整的时候的消息本体长度

Recv4是完整的消息内容

Recv5是接受完成UNITY后更新后的新buffCount新长度,因为没有后续的,所以是0

②服务端反馈

服务端接受到的消息

四、模拟粘包

客户端修改

using System;
using System.Linq;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;public class SendScr : MonoBehaviour
{Socket socket;public InputField inputField;public Text text;byte[] readBuff = new byte[1024];int buffCount = 0;//缓冲区数据长度string recvStr = "";public void Connection(){socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);socket.Connect("127.0.0.1", 8888);socket.BeginReceive(readBuff, buffCount, 1024 - buffCount, 0, ReceiveCallback, socket);}private void ReceiveCallback(IAsyncResult ar){try{Socket socket = (Socket)ar.AsyncState;//获取接收数据长度int count = socket.EndReceive(ar);buffCount += count;//处理二进制消息OnReceiveData();System.Threading.Thread.Sleep(1000*30);//继续接收数据socket.BeginReceive(readBuff, buffCount, 1024 - buffCount, 0, ReceiveCallback, socket);}catch (SocketException ex){Debug.Log("Socket Receive fail" + ex.ToString());}}private 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 = inputField.text;byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);Int16 len = (Int16)bodyBytes.Length;byte[] lenBytes = BitConverter.GetBytes(len);//此时SendBytes包含长度字符串和内容字符串byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();socket.Send(sendBytes);Debug.Log("[Send]" + BitConverter.ToString(sendBytes));}private void Update(){text.text = recvStr;}}

*在接收数据的时候,强制等待30s再进行下一次接收,ReceiveCallback是在子线程执行,调用Sleep函数并不会卡住主线程,客户端不会被卡住,在30s内多次发送数据,经由服务端转发,再次调用BeginReceive的时候,缓冲区有很多数据,会产生粘包。

客户端快速发送三条消息,发送不会堵塞,每次接收都会等待30s,但是接收到的消息不是在一起的,而是分开的

相关文章:

粘包/半包及解决方案

一、粘包/半包介绍 1&#xff1a;粘包 粘包&#xff08;Packet Concatenation&#xff09;通常发生在基于流式传输协议&#xff08;如 TCP&#xff09;的通信中&#xff0c;因为 TCP 是面向流的传输协议&#xff0c;它不保证数据包的边界&#xff0c;而是将数据视为连续的字节…...

2024华为软件精英挑战赛记录

前言 本次主要是记录自己第一次参加华为软件挑战赛的经历。第一次参加比赛还是缺少经验&#xff0c;训练赛中拿到赛区的20多名&#xff0c;最后在正式赛中被反超了&#xff0c;只拿了40多名&#xff0c;实在是感到可惜。 题目&#xff1a;本次题目是一个智慧港口的问题。10个机…...

数据可视化艺术:Matplotlib与Seaborn实战

目录 1.Matplotlib基础绘图与定制化 1.1. 基础绘图 1.2. 定制化 2.Seaborn高级图表类型与样式设定 2.1. 高级图表类型 2.2. 样式设定 3.实战&#xff1a;绘制多维度数据可视化报告 4.总结 1. 前言 在数据科学领域&#xff0c;数据可视化扮演着至关重要的角色。通过图形化…...

python初级第一次作业

一、 dayint(input("enter today day")) fdayint(input("enter num of day since today")) c((fday%7)day)%7 if c0:print("sunday") elif c1:print("monday") elif c2:print("tuesday") elif c3:print("wendnsday&quo…...

Spring Boot整合Camunda打造高效工作流程

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是尘缘&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f449;点击这里&#xff0c;就可以查看我的主页啦&#xff01;&#x1f447;&#x…...

2.8、下拉刷新与上拉加载

页面的下拉刷新与上拉加载功能在移动应用中十分常见,例如,新闻页面的内容刷新和加载。这两种操作的原理都是通过响应用户的触摸事件,在顶部或者底部显示一个刷新或加载视图,完成后再将此视图隐藏。 实现思路 以下拉刷新为例,其实现主要分成三步: 监听手指按下事件,记录…...

java Web餐馆订单管理系统用eclipse定制开发mysql数据库BS模式java编程jdbc

一、源码特点 JSP 餐馆订单管理系统是一套完善的web设计系统&#xff0c;对理解JSP java 编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,eclipse开发&#xff0c;数据库为Mysql5.0&#xff0c;使…...

小程序从入门到入坑:事件系统

前言 哈喽大家好&#xff0c;我是 SuperYing&#xff0c;本文是小程序从入门到入坑系列的第 3 篇&#xff0c;将比较详尽的讲解 小程序事件系统 的相关知识点&#xff0c;欢迎小伙伴阅读。 读完本文您将收获&#xff1a; 了解小程序事件及基础使用。了解小程序事件分类及多种的…...

Windows蓝牙驱动开发之模拟HID设备(二)(把Windows电脑模拟成蓝牙鼠标和蓝牙键盘等设备)

by fanxiushu 2024-03-24 转载或引用请注明原作者 接上文,当我们建立了蓝牙链接请求之后,就该传输数据了, 其实传输数据比起上章阐述的创建SDP和建立连接要简单许多。 使用类型 BRB_L2CA_ACL_TRANSFER 的BRB请求,就可以实现接收和发送操作, 至于具体是接收还是发送,根据设…...

快速区分清楚图形渲染中的AABB,KD树和BVH这些概念

快速区分清楚图形渲染中的AABB&#xff0c;KD树和BVH这些概念 主要想形象去区分好这些术语&#xff0c;目的是扫盲&#xff0c;先开好坑&#xff0c;内容持续填充。 0.先摆出这些词的全称 AABB&#xff1a; 原名&#xff1a;axis aligned bounding box&#xff1b;中文直译名…...

Rust 的 HashMap 特定键值元素值的累加方法

在Rust中&#xff0c;如果你想要对HashMap中特定键对应的值进行累加操作&#xff0c;你需要首先检查该键是否已存在。如果存在&#xff0c;则取出其值&#xff0c;进行累加&#xff0c;然后将结果存回HashMap。如果不存在&#xff0c;则可能需要插入一个新的键值对&#xff0c;…...

Java后端项目性能优化实战-群发通知

背景 公司群发通知模块性能存在问题&#xff0c;我进行全面的系统调优&#xff0c;系统处理能力大幅提升。 原发送流程 优化后的发送流程 优化的点 说明&#xff1a;以下问题基本都是压测过程遇到的&#xff0c;有些问题普通的功能测试暴露不了。优化目标&#xff1a;保证高…...

5、Jenkins持续集成-Maven和Tomcat的安装与配置

文章目录 一、Maven的安装与配置1、安装maven并配置环境2、全局工具配置关联jdk和maven3、添加Jenkins全局变量4、修改settings.xml文件5、测试是否配置成功二、Tomcat的安装与配置1、安装tomcat8+2、配置Tomcat用户角色权限3、测试是否配置成功一、Maven的安装与配置 在Jenki…...

Qt教程 — 3.7 深入了解Qt 控件: Layouts部件

目录 2 如何使用Layouts部件 2.1 QBoxLayout组件-垂直或水平布局 2.2 QGridLayout组件-网格布局 2.3 QFormLayout组件-表单布局 在Qt中&#xff0c;布局管理器&#xff08;Layouts&#xff09;是用来管理窗口中控件位置和大小的重要工具。布局管理器可以确保窗口中的控件在…...

自动驾驶的几种名词

1. 自适应巡航控制&#xff08;ACC&#xff09; 自适应巡航控制&#xff08;Adaptive Cruise Control&#xff0c;ACC&#xff09;是一种汽车驾驶辅助系统&#xff0c;它可以根据前方车辆的速度和距离自动调整车辆的速度&#xff0c;以保持与前车的安全距离。ACC系统由控制层和…...

华为全套企业管理资料合集(21专题)

华为全套企业管理资料合集-知识星球下载 1.绩效考核 华为内训绝密资料:绩效管理与绩效考核.ppt 华为绩效管理与绩效考核制度.docx 华为公司实用性各种绩效图表汇总.doc 华为公司考勤管理制度.doc 华为IPD模式中跨部门团队成员的考核激励制度.doc 2.企业管理 华为公司人力资源…...

LeetCode Python - 74. 搜索二维矩阵

目录 题目描述解法方法一&#xff1a;二分查找方法二&#xff1a;从左下角或右上角搜索 运行结果方法一方法二 题目描述 给你一个满足下述两条属性的 m x n 整数矩阵&#xff1a; 每行中的整数从左到右按非严格递增顺序排列。每行的第一个整数大于前一行的最后一个整数。 给…...

如何安全地添加液氮到液氮罐中

液氮是一种极低温的液体&#xff0c;它在许多领域广泛应用&#xff0c;但在处理液氮时需谨慎小心。添加液氮到液氮罐中是一个常见的操作&#xff0c;需要遵循一些安全准则以确保操作人员的安全和设备的完整性。 选择合适的液氮容器 选用专业设计用于存储液氮的容器至关重要。…...

LGBM算法 原理

简介 GBDT (Gradient Boosting Decision Tree) 是机器学习中一个长盛不衰的模型&#xff0c;其主要思想是利用弱分类器&#xff08;决策树&#xff09;迭代训练以得到最优模型&#xff0c;该模型具有训练效果好、不易过拟合等优点。GBDT不仅在工业界应用广泛&#xff0c;通常被…...

【WPF应用5】WPF中的TextBlock控件:属性与事件详解及示例

在WPF&#xff08;Windows Presentation Foundation&#xff09;开发中&#xff0c;TextBlock控件是一个常用的元素&#xff0c;用于显示静态或动态文本内容。它提供了丰富的属性和事件&#xff0c;使得开发者能够灵活地控制文本的显示样式和响应用户的交互行为。本文将详细介绍…...

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站&#xff0c;会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后&#xff0c;网站没有变化的情况。 不熟悉siteground主机的新手&#xff0c;遇到这个问题&#xff0c;就很抓狂&#xff0c;明明是哪都没操作错误&#x…...

【Linux】shell脚本忽略错误继续执行

在 shell 脚本中&#xff0c;可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行&#xff0c;可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令&#xff0c;并忽略错误 rm somefile…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)

文章目录 1.什么是Redis&#xff1f;2.为什么要使用redis作为mysql的缓存&#xff1f;3.什么是缓存雪崩、缓存穿透、缓存击穿&#xff1f;3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

MMaDA: Multimodal Large Diffusion Language Models

CODE &#xff1a; https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA&#xff0c;它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构&#xf…...

如何在看板中有效管理突发紧急任务

在看板中有效管理突发紧急任务需要&#xff1a;设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP&#xff08;Work-in-Progress&#xff09;弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中&#xff0c;设立专门的紧急任务通道尤为重要&#xff0c;这能…...

Spring AI 入门:Java 开发者的生成式 AI 实践之路

一、Spring AI 简介 在人工智能技术快速迭代的今天&#xff0c;Spring AI 作为 Spring 生态系统的新生力量&#xff0c;正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务&#xff08;如 OpenAI、Anthropic&#xff09;的无缝对接&…...

k8s业务程序联调工具-KtConnect

概述 原理 工具作用是建立了一个从本地到集群的单向VPN&#xff0c;根据VPN原理&#xff0c;打通两个内网必然需要借助一个公共中继节点&#xff0c;ktconnect工具巧妙的利用k8s原生的portforward能力&#xff0c;简化了建立连接的过程&#xff0c;apiserver间接起到了中继节…...

Spring AI与Spring Modulith核心技术解析

Spring AI核心架构解析 Spring AI&#xff08;https://spring.io/projects/spring-ai&#xff09;作为Spring生态中的AI集成框架&#xff0c;其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似&#xff0c;但特别为多语…...

短视频矩阵系统文案创作功能开发实践,定制化开发

在短视频行业迅猛发展的当下&#xff0c;企业和个人创作者为了扩大影响力、提升传播效果&#xff0c;纷纷采用短视频矩阵运营策略&#xff0c;同时管理多个平台、多个账号的内容发布。然而&#xff0c;频繁的文案创作需求让运营者疲于应对&#xff0c;如何高效产出高质量文案成…...

Golang——6、指针和结构体

指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...