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

Socket丢包粘包的处理方案

一、粘包

粘包产生的原因:TCP 是流式协议,无消息边界,操作系统缓冲区会积攒数据一次性发送,UDP就不会。

粘包的原因:

1.发送方连续多次 Send,内核缓冲区合并成一个 TCP 包发出。

2.接收方一次 Recv 读取到多条消息拼接在一起。

3.发送数据包过小,Nagle 算法自动合并小包。

针对不同场景有不同的解决方法:

针对第一个,一次发送完成之后,等对方回复,同时建立超时机制,设置一个阈值,假如超时3次以内可以重发,3次以上对方没有回复则报警。

针对第二个,首先双方定义交互的报文协议,一种是有帧头,帧尾+校验位;或是加上“特殊分隔符”。 

针对第三个,关闭Nagle算法(减少主动粘包,会略微增加网络包数量,大数据文件传输不建议开)

// TcpNoDelay=true 关闭Nagle,小包立即发送
socket.NoDelay = true;

  

二、丢包

TCP 底层有重传、校验、滑动窗口,不会丢字节,一般都是业务层丢包:
丢包的原因:

1.接收缓冲区读取不全,只读了一部分数据,剩余数据被丢弃

2.多线程并发处理逻辑错误、缓冲区覆盖、异步回调被冲掉(独立缓冲区)

3.网络中断、断开未捕获异常,残留数据丢失(这个时候就要报警,日志做好记录)。

 

发送方:如果字节内容很长,需要循环发送。

View Code

接收方

建立2个缓冲区:一个是list<>的滑动缓冲区;一个是单次循环接收的byte数组;

 

 

TCP 帧头 + 帧尾 + 校验和 自定义协议循环接收解析完整方案 

整体流程

  1. 全局私有缓冲区(List<byte> 最方便操作删除截取)接收所有 TCP 字节
  2. 循环扫描缓冲区,先匹配帧头
  3. 匹配到帧头后,判断剩余字节是否足够一整帧长度
  4. 够长度则截取完整帧 → 校验 CRC → 校验帧尾
  5. 校验通过:执行业务处理,从缓冲区删除已消费的所有字节
  6. 校验失败:删除当前帧头第一个字节,回到步骤 2 重新遍历(抗脏数据)
  7. 缓冲区剩余不足一帧,退出循环,等待下一次接收追加数据

关键规则

  • 缓冲区每个 Socket 连接独立一个,多客户端不能共用
  • 校验不通过直接丢弃 1 字节回溯查找,防止死循环卡死
  • 禁止用 MemoryStream 频繁重建,List<byte> 插入删除性能更友好

极简口诀

  • 每连接独立List<byte>滑动缓冲区,TCP 流全部往里堆;
  • 循环找帧头,裁掉前面垃圾字节;
  • 读长度算整帧总大小,缓冲区够长再截取;
  • 先验帧尾再验 CRC,双校验防篡改、防错包;
  • 校验成功删缓冲区已消费数据,失败删一字节回溯重找;
  • 发送端严格对齐帧头、长度、校验、帧尾四段结构。

协议常量定义

using System;
using System.Collections.Generic;
using System.Linq;public static class ProtocolDef
{// 帧头 2字节 AA BBpublic static readonly byte[] Header = { 0xAA, 0xBB };// 帧尾 2字节 CC DDpublic static readonly byte[] Tail = { 0xCC, 0xDD };// 帧头长度public const int HeaderLen = 2;// 长度字段占用2字节public const int LengthFieldLen = 2;// CRC校验占用2字节 CRC16public const int CrcLen = 2;// 帧尾长度public const int TailLen = 2;// 单帧最小固定头部+尾部开销 = 2+2+2+2=8字节(不含载荷)public const int FixedOverhead = HeaderLen + LengthFieldLen + CrcLen + TailLen;
}
View Code

单连接缓冲区封装类(每个ClientSocket实例化一个对象)

/// <summary>
/// 带帧头帧尾CRC校验的TCP粘包解析器
/// </summary>
public class FrameParser
{// 滑动接收缓冲区private readonly List<byte> _recvBuf = new List<byte>();// 单次读取临时数组private readonly byte[] _tempBuf = new byte[4096];#region 外部调用入口:塞入TCP收到的字节,返回解析成功的完整帧列表public List<byte[]> AppendDataAndParse(Socket socket){List<byte[]> resultFrames = new List<byte[]>();try{// 读取TCP数据int readCount = socket.Receive(_tempBuf);if (readCount <= 0)return resultFrames; // 连接关闭lock (_recvBuf){// 写入缓冲区
                _recvBuf.AddRange(_tempBuf.Take(readCount));// 循环反复解析,直到拆不出完整帧while (ParseOneFrame(out byte[] frame)){resultFrames.Add(frame);}}}catch (Exception ex){Console.WriteLine($"接收异常:{ex.Message}");}return resultFrames;}#endregion#region 核心单帧解析方法/// <summary>/// 尝试解析一帧,成功返回true,并输出完整帧原始字节/// </summary>private bool ParseOneFrame(out byte[] fullFrame){fullFrame = null;lock (_recvBuf){int bufLen = _recvBuf.Count;// 1. 缓冲区不足最小固定开销,直接返回if (bufLen < ProtocolDef.FixedOverhead)return false;// ========== 步骤1:查找帧头 AA BB ==========int headerIndex = FindHeaderIndex();if (headerIndex == -1){// 没有找到帧头,清空缓冲区无效脏数据
                _recvBuf.Clear();return false;}// 移除帧头前面所有无效垃圾字节if (headerIndex > 0){_recvBuf.RemoveRange(0, headerIndex);}// 修剪后重新获取长度bufLen = _recvBuf.Count;// 帧头占2字节,接下来至少还要 2(长度)+2(CRC)+2(尾) =6if (bufLen < ProtocolDef.FixedOverhead)return false;// ========== 步骤2:读取载荷长度(第2、3位两个字节) ==========byte[] lenBytes = new byte[2];lenBytes[0] = _recvBuf[2];lenBytes[1] = _recvBuf[3];// 网络统一大端序ushort payloadLen = BitConverter.ToUInt16(lenBytes.Reverse().ToArray(), 0);// 计算本帧总字节数:帧头2 + 长度2 + 载荷N + CRC2 + 帧尾2int totalFrameLen = ProtocolDef.FixedOverhead + payloadLen;// 缓冲区不够完整一帧,等待下次接收if (bufLen < totalFrameLen)return false;// ========== 步骤3:截取完整一帧 ==========fullFrame = _recvBuf.Take(totalFrameLen).ToArray();// ========== 步骤4:校验帧尾 ==========bool tailOk = CheckFrameTail(fullFrame, totalFrameLen);if (!tailOk){// 帧尾错误,丢弃当前帧第一个字节,重新查找帧头_recvBuf.RemoveAt(0);fullFrame = null;return false;}// ========== 步骤5:校验CRC校验和 ==========bool crcOk = CheckCrc(fullFrame, payloadLen);if (!crcOk){_recvBuf.RemoveAt(0);fullFrame = null;return false;}// ========== 全部校验通过,删除缓冲区已消费数据 ==========_recvBuf.RemoveRange(0, totalFrameLen);return true;}}#endregion#region 辅助私有方法// 在缓冲区查找帧头AA BB起始位置private int FindHeaderIndex(){for (int i = 0; i <= _recvBuf.Count - ProtocolDef.HeaderLen; i++){if (_recvBuf[i] == ProtocolDef.Header[0] && _recvBuf[i + 1] == ProtocolDef.Header[1]){return i;}}return -1;}// 校验帧尾是否为CC DDprivate bool CheckFrameTail(byte[] frame, int totalLen){int tailStart = totalLen - ProtocolDef.TailLen;return frame[tailStart] == ProtocolDef.Tail[0] && frame[tailStart + 1] == ProtocolDef.Tail[1];}// CRC16校验示例(根据你的设备实际校验算法替换)private bool CheckCrc(byte[] frame, ushort payloadLen){// CRC所在位置:帧头2 + 长度2 + 载荷N = 偏移位置int crcOffset = ProtocolDef.HeaderLen + ProtocolDef.LengthFieldLen + payloadLen;byte[] crcBytes = new byte[2] { frame[crcOffset], frame[crcOffset + 1] };// 参与CRC计算的区域:长度字段 + 载荷数据int calcDataLen = ProtocolDef.LengthFieldLen + payloadLen;byte[] calcData = new byte[calcDataLen];Array.Copy(frame, ProtocolDef.HeaderLen, calcData, 0, calcDataLen);// 此处替换为你真实的CRC16算法(Modbus CRC16/Xmodem等)ushort calcCrc = CalcCRC16(calcData);ushort recvCrc = BitConverter.ToUInt16(crcBytes.Reverse().ToArray(), 0);return calcCrc == recvCrc;}// 示例Modbus CRC16算法private ushort CalcCRC16(byte[] data){ushort crc = 0xFFFF;for (int i = 0; i < data.Length; i++){crc ^= data[i];for (int j = 0; j < 8; j++){if ((crc & 1) != 0)crc = (ushort)((crc >> 1) ^ 0xA001);elsecrc >>= 1;}}return crc;}#endregion
}
View Code

服务端接收循环调用示例

// 每一个客户端连接单独实例化一个解析器
private void ClientReceiveLoop(Socket clientSock)
{FrameParser parser = new FrameParser();while (true){// 塞入Socket数据,自动解析出所有完整合法帧var frames = parser.AppendDataAndParse(clientSock);foreach (var frame in frames){// frame 就是校验通过的完整一帧原始字节数组Console.WriteLine($"收到合法帧,长度:{frame.Length}");// 在这里解析载荷、执行业务逻辑
        }}
}
View Code

 

http://www.jsqmd.com/news/1388157/

相关文章:

  • JavaScript 中的定时器与动画基础
  • TVA智能体:物理AI的“虚实连接器”
  • podman管理redis集群
  • 基于el-input实现数字输入框:从原理到实战的完整指南
  • 机房搬迁别只拆机柜:算力卡迁移隐性损伤与防护指南
  • Python离线安装tar.gz包全攻略:原理、流程与避坑指南
  • 长沙理财网站建设:传统金融机构数字化转型的破局之路
  • TLA+实战指南:用形式化验证构建可靠的分布式系统设计
  • Llama大模型本地部署实战:从Ollama快速体验到生产级微调
  • AI工程师成长指南:从Python基础到Transformer工程化落地
  • 王虹、邓煜、张益唐、韦东奕四位数学家研究方向预测
  • 深度解析江苏省徐州市建设银行网站如何赋能当地居民生活与企业发展
  • 构建AI Agent评估体系:从六个维度量化智能体性能
  • 「钢联国贸」2026年8月13日成都地区工字钢销售有限公司最新价格行情 - 四川盛世钢联营销中心
  • 0809周考
  • Unity开发者求职能力地图:从技术栈到项目实战的完整指南
  • RAG索引优化实战:摘要与父子索引提升检索质量
  • 深入理解变量作用域:编程基础与实战技巧
  • Dism++ 免费 Windows 系统优化工具实战:6 个步骤让一台老电脑重新流畅起来
  • 探索高校 门户网站 建设背景下的数字化转型与校园品牌重塑深层逻辑
  • 2026年7月南京市溧水区二手房价格深度分析报告
  • 计算机毕业设计之基于Spark的7K7K小游戏可视化系统设计与实现
  • Linux入门指南:从内核到发行版,掌握开源操作系统的核心与应用
  • PC上使用QEMU虚拟化运行树莓派系统:跨架构模拟实战指南
  • 2026年SPF动物房建设厂家选择:屏障环境设计、净化工程、实验动物房施工一站式服务实力之选 - 卓企推荐
  • NumPy范数计算全解析:从L1、L2到矩阵范数与应用实战
  • 社恐高敏感专属陪伴测评 头部两大树洞低压力治愈优选 - nuanyin
  • 浙江商会网站建设策划方案:打造连接浙商精神与全球商业机遇的数字化桥梁
  • 五华网站建设怎么选?揭秘优帮云高性价比建站方案与企业数字化突围指南
  • MySQL字典表设计:从单表到混合型,构建高性能系统基石