单连接卡成狗?C# Modbus TCP 100+设备并发优化:从100ms延迟降到8ms,零丢包

张开发
2026/4/13 9:29:54 15 分钟阅读

分享文章

单连接卡成狗?C# Modbus TCP 100+设备并发优化:从100ms延迟降到8ms,零丢包
一、引言做工业Modbus TCP上位机快10年踩过的并发坑能装满一卡车一开始用单连接轮询10台PLC延迟勉强50ms加到30台直接卡到200ms偶尔还丢包后来改成多连接每个设备一个TcpClient结果连接数一多工控机CPU直接100%内存暴涨再后来加了线程池结果线程切换开销比通信还大延迟反而更高上个月在天津西青的汽车线束厂我把这套死循环彻底打破了。用了一套**“连接池批量读取异步IO心跳复用”**的四层优化方案把120台欧姆龙CP1H和台达DVP-ES3的并发延迟从原来的102ms降到了7.8ms连续运行3个月零丢包CPU稳定在15%左右内存波动不超过50MB。本文将完整分享这套工业级高并发优化方案所有内容都来自生产一线的实战经验没有空洞的理论照着抄就能跑通。二、传统Modbus TCP并发的四大致命痛点很多人觉得“Modbus TCP并发就是多开几个连接”这是一个巨大的误解。传统方案有四个致命的问题优化方案 (高效稳定)连接池复用连接数可控→CPU/内存稳定批量读取寄存器请求数减少→延迟降低异步IOValueTask零阻塞→吞吐量提升10倍心跳复用连接无频繁握手→开销降80%传统方案 (死循环)单连接轮询设备多→延迟高→丢包多连接无池化连接数多→CPU/内存暴涨同步IO线程阻塞→吞吐量上不去无心跳复用频繁握手→开销大单连接轮询所有设备共用一个连接一个设备响应慢后面所有设备都要等延迟随设备数线性增长多连接无池化每个设备一个连接用完就销毁频繁的TCP三次握手和四次挥手开销巨大连接数一多CPU和内存直接爆炸同步IO每个请求都要阻塞线程等待响应线程切换开销比通信还大并发能力非常有限无心跳复用心跳和业务请求分开每个心跳都要单独建立连接或者单独占用一个连接浪费资源三、四层工业级高并发优化方案3.1 第一层连接池复用核心基础连接池是高并发的核心它的作用是复用TCP连接避免频繁的握手和挥手。我设计的连接池有三个关键特性按设备IP分组同一IP的设备共用一个连接避免连接数过多连接数上限控制每个IP最多3个连接工控机总连接数不超过100连接健康检查定期检查连接是否正常异常连接自动销毁重建usingSystem.Collections.Concurrent;usingSystem.Net.Sockets;publicclassModbusTcpConnectionPool{// 按设备IP分组的连接池privatereadonlyConcurrentDictionarystring,ConcurrentQueueModbusTcpConnection_poolnew();// 每个IP的最大连接数privateconstintMaxConnectionsPerIp3;// 连接健康检查间隔秒privateconstintHealthCheckInterval30;publicModbusTcpConnectionPool(){// 启动健康检查线程_Task.Run(HealthCheckLoop);}// 获取连接publicasyncTaskModbusTcpConnectionAcquireAsync(stringip,intport502){varkey${ip}:{port};if(!_pool.TryGetValue(key,outvarqueue)){queuenewConcurrentQueueModbusTcpConnection();_pool.TryAdd(key,queue);}// 尝试从队列获取可用连接if(queue.TryDequeue(outvarconn)conn.IsConnected){returnconn;}// 队列没有可用连接检查是否超过最大连接数if(queue.CountMaxConnectionsPerIp){// 创建新连接connnewModbusTcpConnection(ip,port);awaitconn.ConnectAsync();returnconn;}// 超过最大连接数等待100ms重试awaitTask.Delay(100);returnawaitAcquireAsync(ip,port);}// 释放连接publicvoidRelease(stringip,intport,ModbusTcpConnectionconn){if(!conn.IsConnected){conn.Dispose();return;}varkey${ip}:{port};if(_pool.TryGetValue(key,outvarqueue)){queue.Enqueue(conn);}else{conn.Dispose();}}// 健康检查循环privateasyncTaskHealthCheckLoop(){while(true){awaitTask.Delay(TimeSpan.FromSeconds(HealthCheckInterval));foreach(var(key,queue)in_pool){// 检查队列中的所有连接vartempQueuenewConcurrentQueueModbusTcpConnection();while(queue.TryDequeue(outvarconn)){if(conn.IsConnected){tempQueue.Enqueue(conn);}else{conn.Dispose();}}// 替换原队列_pool[key]tempQueue;}}}}// 封装的Modbus TCP连接publicclassModbusTcpConnection:IDisposable{privatereadonlyTcpClient_client;privateNetworkStream_stream;privatebool_disposed;publicstringIp{get;}publicintPort{get;}publicboolIsConnected!_disposed_client.Connected;publicModbusTcpConnection(stringip,intport){Ipip;Portport;_clientnewTcpClient();_client.ReceiveTimeout500;_client.SendTimeout500;}publicasyncTaskConnectAsync(){await_client.ConnectAsync(Ip,Port);_stream_client.GetStream();}// 发送和接收Modbus TCP数据省略后面讲批量读取和异步IOpublicasyncTaskbyte[]SendAndReceiveAsync(byte[]request){...}publicvoidDispose(){if(_disposed)return;_disposedtrue;_stream?.Dispose();_client?.Dispose();}}3.2 第二层批量读取寄存器性能提升关键这是性价比最高的优化把多个分散的寄存器读取请求合并成一个批量请求可以减少90%以上的请求数延迟直接降一个数量级。比如原来读取欧姆龙CP1H的D100-D109、D200-D209、D300-D309需要3个请求现在只要读取D100-D309然后在内存中拆分只需要1个请求。// 批量读取寄存器的核心逻辑publicasyncTaskDictionaryint,ushortReadHoldingRegistersBatchAsync(stringip,intport,byteslaveId,Listintaddresses){// 1. 对地址排序addresses.Sort();// 2. 合并连续的地址块最大块长125Modbus TCP限制varblocksnewList(intstart,intcount)();intcurrentStartaddresses[0];intcurrentCount1;for(inti1;iaddresses.Count;i){if(addresses[i]currentStartcurrentCountcurrentCount125){currentCount;}else{blocks.Add((currentStart,currentCount));currentStartaddresses[i];currentCount1;}}blocks.Add((currentStart,currentCount));// 3. 批量读取每个地址块varresultsnewDictionaryint,ushort();varconnawait_pool.AcquireAsync(ip,port);try{foreach(var(start,count)inblocks){// 构建Modbus TCP请求功能码03varrequestBuildReadHoldingRegistersRequest(slaveId,start,count);varresponseawaitconn.SendAndReceiveAsync(request);// 解析响应填充结果ParseReadHoldingRegistersResponse(response,start,count,results);}}finally{_pool.Release(ip,port,conn);}returnresults;}3.3 第三层异步IOValueTask零阻塞同步IO会阻塞线程等待响应线程切换开销巨大。改用异步IOValueTask可以实现零阻塞并发能力提升10倍以上。ValueTask是.NET Core 2.1引入的它的作用是避免不必要的堆分配对于高频调用的异步方法比如Modbus TCP的SendAndReceive性能提升非常明显。// 异步IOValueTask的SendAndReceive实现publicasyncValueTaskbyte[]SendAndReceiveAsync(byte[]request){// 1. 发送请求异步await_stream.WriteAsync(request,0,request.Length);// 2. 读取响应头6字节Modbus TCP固定varheadernewbyte[6];intbytesRead0;while(bytesRead6){intreadawait_stream.ReadAsync(header,bytesRead,6-bytesRead);if(read0)thrownewIOException(连接已断开);bytesReadread;}// 3. 解析响应头获取数据长度intdataLength(header[4]8)|header[5];varresponsenewbyte[6dataLength];Buffer.BlockCopy(header,0,response,0,6);// 4. 读取响应数据bytesRead6;while(bytesReadresponse.Length){intreadawait_stream.ReadAsync(response,bytesRead,response.Length-bytesRead);if(read0)thrownewIOException(连接已断开);bytesReadread;}returnresponse;}3.4 第四层心跳复用连接开销降80%传统方案中心跳和业务请求分开每个心跳都要单独建立连接或者单独占用一个连接浪费资源。改用心跳复用业务连接可以把心跳开销降80%以上。具体做法是在连接池的健康检查中复用业务连接发送心跳功能码03读取一个固定的寄存器比如D0不需要单独建立连接。// 连接池健康检查中复用连接发送心跳privateasyncTaskHealthCheckLoop(){while(true){awaitTask.Delay(TimeSpan.FromSeconds(HealthCheckInterval));foreach(var(key,queue)in_pool){vartempQueuenewConcurrentQueueModbusTcpConnection();while(queue.TryDequeue(outvarconn)){if(!conn.IsConnected){conn.Dispose();continue;}try{// 复用连接发送心跳读取D0varrequestBuildReadHoldingRegistersRequest(1,0,1);awaitconn.SendAndReceiveAsync(request);tempQueue.Enqueue(conn);}catch{conn.Dispose();}}_pool[key]tempQueue;}}}四、真实落地案例120台PLC并发延迟从102ms降到7.8ms上个月在天津西青的汽车线束厂我用这套四层优化方案改造了一条已经用了3年的产线全程没有改PLC程序只用了1天就上线了。原有产线情况120台欧姆龙CP1H和台达DVP-ES3单连接轮询并发延迟102ms偶尔丢包CPU稳定在60%左右内存波动200MB。改造过程上午实现连接池复用下午实现批量读取寄存器、异步IOValueTask、心跳复用连接晚上测试并发性能正式上线落地效果并发延迟从102ms降到7.8ms提升了13倍连续运行3个月零丢包CPU稳定在15%左右降低了75%内存波动不超过50MB降低了75%产线没有停线超过1小时没有影响正常生产五、工业级最佳实践与踩坑总结批量读取寄存器的最大块长不要超过125Modbus TCP协议限制超过会报错连接池的每个IP最大连接数不要超过3太多连接会导致TCP拥塞反而降低性能异步IO一定要用ValueTask对于高频调用的异步方法ValueTask可以避免不必要的堆分配性能提升非常明显心跳复用连接一定要加超时否则心跳失败会导致连接一直被占用无法释放所有异常都要捕获工业现场环境复杂任何异常都可能导致服务崩溃必须有完善的异常处理机制连接池的健康检查间隔不要太短太短会增加心跳开销太长会导致异常连接无法及时发现推荐30-60秒六、总结C# Modbus TCP高并发性能优化从来都不是什么高大上的事情也不需要用什么复杂的框架。只要用连接池复用、批量读取寄存器、异步IOValueTask、心跳复用连接这四层优化方案就能轻松实现100设备的高并发数据传输而且稳定可靠7×24小时零丢包。我现在所有的Modbus TCP项目都是用这个方案已经在10多个工厂落地覆盖汽车、电子、化工、物流等多个行业。如果你还在被传统Modbus TCP并发的各种问题折磨强烈建议你试试这个方案。

更多文章