.NET上位机踩坑:用Pipelines替代环形缓冲区(番外篇)
目录前言Pipelines管道实例后记前言大家好我是 wacky。书接上回我们上回探讨了如何解决TCP粘包和半包的问题并在最后引入了环形缓冲区的概念。虽然环形缓冲区主要是通过固定数组读写索引模运算来实现循环但是实际开发过程中还存在手动管理 offset、count数组扩容、数据移动容易越界、内存拷贝多等一系列的问题。而实际上在.NET中依然有更简洁高效的API来代替手写环形缓冲区它就是Pipelines管道。如果还想回顾一下手写环形缓冲区的概念可以从传送门出发.NET上位机踩坑为什么有时读取数据需要SleepPipelines管道命名空间为System.IO.Pipelines它是.NET内置的高性能内存缓冲组件。之前在和一些技术大佬聊环形缓冲区的过程中他们很多人已经把这个组件用于实际生产环境中了那我们今天就来讲一讲在解决Modbus协议TCP粘包和半包的问题中这个组件要怎么用。它主要包含以下几部分内容Pipe内置一对PipeWriter 和PipeReaderWriter 负责往管道塞网络收到的数据Reader 负责从管道读取、解析报文。Pipe 内部自带环形缓冲。Socket 异步接收用Socket.ReceiveAsync 持续接收 PLC 下发的字节流写入PipeWriter。TCP 是字节流没有边界这一步只管收字节不关心是不是完整帧。PipeReader 循环解析ReadAsync()从管道拿一段可用内存无需拷贝直接内存切片尝试在这段内存里查找完整 ModbusTCP 帧MBAP 头固定 7 字节事务 ID (2) 协议 ID (2) 长度 (2) 单元 ID (1)后面跟 N 个功能码数据如果找到完整帧AdvanceTo 标记已经消费掉的字节交给业务处理剩下半包留在管道缓冲区下次继续解析如果不够一帧停止解析等待后续 Socket 继续收到数据写入管道4. 断开 / 异常Complete PipeReader/PipeWriter释放资源。实例现在我们来结合C#实例继续讲解internal class ModbusTcpPipelinesClient { private readonly string _ip; private readonly int _port; private Socket _socket; private Pipe _pipe; private Task _readTask; private Task _writeTask; private CancellationTokenSource _cts; // 收到完整Modbus报文回调上位机在这里解析数据 public Actionbyte[] OnModbusFrameReceived { get; set; } public ModbusTcpPipelinesClient(string ip, int port 502) { _ip ip; _port port; } public async Task ConnectAsync() { _cts new CancellationTokenSource(); _socket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await _socket.ConnectAsync(_ip, _port, _cts.Token); _pipe new Pipe(new PipeOptions(pauseWriterThreshold: 4096, resumeWriterThreshold: 2048)); // 两个独立任务1. Socket收数据写入PipeWriter2. PipeReader解析报文 _writeTask FillPipeFromSocketAsync(_socket, _pipe.Writer, _cts.Token); _readTask ParsePipeFramesAsync(_pipe.Reader, _cts.Token); } // 从Socket接收字节写入PipeWriter相当于填充缓冲区 private async Task FillPipeFromSocketAsync(Socket socket, PipeWriter writer, CancellationToken ct) { try { while (!ct.IsCancellationRequested) { // 获取一块内存最小分配512字节可根据PLC调大小 Memorybyte memory writer.GetMemory(512); int readLen await socket.ReceiveAsync(memory, SocketFlags.None, ct); if (readLen 0) break; // 远端关闭连接 writer.Advance(readLen); // 告诉writer实际收到多少字节 FlushResult flushResult await writer.FlushAsync(ct); if (flushResult.IsCompleted) break; } } catch (Exception ex) { Console.WriteLine($接收异常:{ex.Message}); } finally { writer.Complete(); } } // PipeReader 循环解析Modbus TCP报文核心替代环形缓冲区解析 private async Task ParsePipeFramesAsync(PipeReader reader, CancellationToken ct) { try { while (!ct.IsCancellationRequested) { ReadResult result await reader.ReadAsync(ct); ReadOnlySequencebyte buffer result.Buffer; SequencePosition? consumedPos null; // 循环解析缓冲区里所有完整Modbus帧处理粘包一次多个报文 while (TryParseModbusFrame(buffer, out var frame, out var consumed)) { OnModbusFrameReceived?.Invoke(frame.ToArray()); consumedPos consumed; buffer buffer.Slice(consumed); // 切掉已经解析完的数据 } // AdvanceTo标记消费位置、查看位置Pipelines自动回收内存 reader.AdvanceTo(consumedPos ?? buffer.Start, buffer.End); if (result.IsCompleted) break; } } catch (Exception ex) { Console.WriteLine($解析异常:{ex.Message}); } finally { reader.Complete(); } } /// summary /// 尝试从ReadOnlySequence解析ModbusTCP帧 /// MBAP: 7字节 [TransId(2)ProtoId(2)Len(2)UnitId(1)] PDU /// /summary private bool TryParseModbusFrame(in ReadOnlySequencebyte seq, out ReadOnlySequencebyte frame, out SequencePosition consumed) { frame default; consumed seq.Start; if (seq.Length 7) return false; // 不足MBAP头半包等待更多数据 // 读取MBAP第5、6字节PDU长度 var headerReader new SequenceReaderbyte(seq); headerReader.Advance(4); headerReader.TryReadBigEndian(out short pduLen); int totalFrameLen 7 pduLen; if (seq.Length totalFrameLen) return false; // 收到的数据不够完整报文等待 frame seq.Slice(0, totalFrameLen); consumed seq.GetPosition(totalFrameLen); return true; } // 发送Modbus请求 public async Task SendAsync(byte[] data) { if (_socket null || !_socket.Connected) throw new InvalidOperationException(未连接PLC); int sendTotal 0; while (sendTotal data.Length) { int sent await _socket.SendAsync(data.AsMemory(sendTotal), SocketFlags.None); sendTotal sent; } } public async Task CloseAsync() { _cts?.Cancel(); try { await Task.WhenAll(_readTask, _writeTask); } catch { } _socket?.Close(); _pipe null; } }在上述代码段中我们把写入和解析作为2个异步方法分开来执行用于职责分离。socket在异步拿到数据后会告诉PipeWriter实际收到了多少字节然后把数据提交给PipeReader用于读取。在这一步socket的职责只有把TCP字节流输入管道中不做任何的协议解析。而在解析的方法中只负责拿出管道中当前可用的所有数据进行解析这里引入了ReadOnlySequence的概念这个类型是代表跨多个内存块的连续逻辑字节流不需要合并数组。其中又包含一个我们自定义的TryParseModbusFrame方法用于解析ModbusTCP帧。在TryParseModbusFrame方法内部我们处理了半包或者粘包的场景由于一个完整的MBAP(Modbus TCP)报文头的长度是7字节因此我们在不足7字节的时候直接return false判断此为半包不会继续进行解析。然后我们会计算整个帧的总长度totalFrameLen 7 pduLen这里我们会继续判断收到的数据是否为完整的报文长度如果不足那么依然return false继续等待数据完整后将buffer切片输出给调用方。最后我们通过AdvanceTo来标记消费位置、查看位置用于下次继续从指定的位置继续读取这样我们就可以有效解决TCP存在粘包和半包的问题。后记现代编程语言API的进化总是让人欣喜直接引入Pipelines就可以避免手写RingBuffer存在的很多问题可谓是大大提升了效率。但是我们依然还是需要知其然知其所以然明白了存在什么问题再去根据问题的本质进行解决懂得怎么解决之后再做改进各位不知今天看懂了吗引入地址

相关新闻

Antigravity+Blender MCP:AI代理对话式搭建智慧仓储数字孪生场景

Antigravity+Blender MCP:AI代理对话式搭建智慧仓储数字孪生场景

前阵子接手一个智慧仓储的前期原型,客户要得其实很朴素:把仓库里的货架、托盘、AGV通道做成一个3D场景,让业务方在浏览器里能直观看到整体布局,后期还要叠加上库存数据和传感器状态。按老路子走,要么上UE5、Unity从零搭…

2026/9/30 21:24:30 阅读更多 →
同一张切片,同时看蛋白和RNA:空间多组学为什么需要PCF?

同一张切片,同时看蛋白和RNA:空间多组学为什么需要PCF?

更新于2026年9月29日空间多组学的发展,让研究者开始同时关注RNA、蛋白、细胞状态以及组织结构。但在实际研究中,“同时拥有多组学数据”并不一定意味着真正实现了空间上的多模态整合。如果蛋白和RNA分别来自不同组织切片,即使两张切片位置相邻…

2026/9/30 21:23:28 阅读更多 →
视频融合平台:语音对讲、云台控制、电子地图和平台级联功能介绍

视频融合平台:语音对讲、云台控制、电子地图和平台级联功能介绍

国标视频云平台SkeyeVSS平台基于云边端协同架构,可支持多协议、多类型的海量设备接入与分发。平台可提供的视频能力包括:视频监控直播、云端录像、云存储、录像检索与回看、智能告警、平台级联、云台控制、语音对讲、智能分析等。视频融合平台的核心价值…

2026/9/30 21:23:28 阅读更多 →

最新新闻

用trae自动生成有趣互动的登录页面:TaoToken统一Key接入与配置验证

用trae自动生成有趣互动的登录页面:TaoToken统一Key接入与配置验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/30 22:06:17 阅读更多 →
CentOS7 Docker daemon.json配置实战:从加速到日志轮转避坑指南

CentOS7 Docker daemon.json配置实战:从加速到日志轮转避坑指南

用 CentOS7 装机装了无数次 Docker 之后,我越来越确认一个观点:Docker 装好只是开始,真正决定你后面运维省不省心的,是你有没有把/etc/docker/daemon.json写对。很多人遇到拉镜像慢、容器日志把磁盘塞满、daemon 一重启业务就断、…

2026/9/30 22:06:17 阅读更多 →
OpenClaw自托管AI Agent实战速查:部署、配置与排障指南

OpenClaw自托管AI Agent实战速查:部署、配置与排障指南

OpenClaw 是我最近几周重度使用的一款自托管式 AI Agent 运行时,核心特点是本地优先、通道可扩展、能直接把手上的工具链串起来干活。这篇内容不是重新讲一遍官方文档,而是我根据实际部署、配置、接入 Teams、联动 Obsidian 以及处理各种报错时踩坑的记录…

2026/9/30 22:06:17 阅读更多 →
UltraEdit 取消自动备份:TaoToken 统一 Key 通道下的配置文件骨架与验证动作

UltraEdit 取消自动备份:TaoToken 统一 Key 通道下的配置文件骨架与验证动作

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/30 22:06:17 阅读更多 →
LeetCode 26删除有序数组重复项:双指针C语言实现与细节剖析

LeetCode 26删除有序数组重复项:双指针C语言实现与细节剖析

LeetCode 26题,删除有序数组中的重复项,在LeetCode上难度标的是"简单",但真去刷过一遍的人心里都清楚:这题第一遍能一次写对的人并不多。原因很简单,它考的不是什么高深算法,而是一个特别容易被想…

2026/9/30 22:06:17 阅读更多 →
DeepSeek写的论文AI率太高?2026年降AI工具横评与TaoToken统一Key实测

DeepSeek写的论文AI率太高?2026年降AI工具横评与TaoToken统一Key实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/30 22:05:16 阅读更多 →

日新闻

Base64 图片头部特征识别:从文件头到格式判断的完整指南

Base64 图片头部特征识别:从文件头到格式判断的完整指南

1. 项目概述:为什么说看懂 base64 图片头部是基本功这几年跟 base64 打交道的机会越来越多,后端接口返回图片、前端渲染验证码、小程序里存小图、还有一些老系统导出报表,动不动就给你一段长到怀疑人生的 base64 字符串。很多人拿到字符串就直…

2026/9/30 0:00:35 阅读更多 →
Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

简介:本资源是一份面向Java初学者与课程设计学生的公交站牌广告灯箱管理系统毕业设计文档,聚焦城市公共广告资源信息化管理痛点,提供从需求分析到技术实现的完整方案。文档采用标准学术论文结构,含摘要、英文摘要、目录及五章正文…

2026/9/30 0:00:35 阅读更多 →
用 Redis Lua 构建大模型 API 多租户原子配额治理体系

用 Redis Lua 构建大模型 API 多租户原子配额治理体系

我去年年底接了一个内部 AI 平台的治理需求,背景很直接:公司把 DeepSeek、MiniMax 这类大模型 API 统一封装成内部网关,开放给几个业务团队用。结果第一个月账单出来,额度直接超了 4 倍。仔细查日志,发现原因并不复杂—…

2026/9/30 0:00:35 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/30 13:14:22 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/30 18:13:06 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/9/30 13:14:49 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/29 19:29:29 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/29 5:58:00 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/30 15:27:04 阅读更多 →