
一、 核心组件与依赖安装在 .NET 8.0 环境中我们选择开源社区最稳定、使用最广泛的S7netplus。通过 NuGet 包管理器或控制台安装dotnetaddpackage S7netplus二、 配置文件与选项模式 (Options Pattern)在现代 .NET 开发中硬编码是大忌。我们使用 Options 模式来管理 PLC 的连接参数。1.S7PlcOptions.cs(配置实体类)usingS7.Net;namespaceIIoT.PlcIntegration;/// summary/// PLC 连接配置选项/// /summarypublicclassS7PlcOptions{/// summary/// 配置文件中的节点名称/// /summarypublicconststringPositionS7PlcConfig;/// summary/// CPU 类型 (例如: S71200, S71500, S7300)/// 默认使用 S7-1500/// /summarypublicCpuTypeCpu{get;set;}CpuType.S71500;/// summary/// PLC 的 IP 地址/// /summarypublicstringIpAddress{get;set;}127.0.0.1;/// summary/// 机架号 (对于 S7-1200/1500 通常为 0)/// /summarypublicshortRack{get;set;}0;/// summary/// 插槽号 (对于 S7-1200/1500 通常为 1S7-300 通常为 2)/// /summarypublicshortSlot{get;set;}1;/// summary/// 连接超时时间毫秒/// /summarypublicintTimeoutMs{get;set;}3000;}三、 接口抽象定义遵循依赖倒置原则DIP我们先定义清晰的接口。这不仅方便后续通过依赖注入使用也极其方便单元测试Mock。2.IS7PlcService.cs(接口定义)usingS7.Net;namespaceIIoT.PlcIntegration;/// summary/// S7 PLC 通信服务接口/// /summarypublicinterfaceIS7PlcService{/// summary/// 获取当前是否处于连接状态/// /summaryboolIsConnected{get;}/// summary/// 泛型读取方法 (适用于读取单个变量如 DB1.DBD4)/// /summary/// typeparam nameT期望转换的数据类型 (如 float, short, bool)/typeparam/// param namevariablePLC 变量地址/param/// param namecancellationToken取消令牌/param/// returns读取成功返回对应类型的值失败返回默认值/returnsTaskT?ReadAsyncT(stringvariable,CancellationTokencancellationTokendefault);/// summary/// 写入单个变量/// /summary/// param namevariablePLC 变量地址/param/// param namevalue要写入的值/param/// param namecancellationToken取消令牌/param/// returns是否写入成功/returnsTaskboolWriteAsync(stringvariable,objectvalue,CancellationTokencancellationTokendefault);/// summary/// 批量读取连续的字节 (高性能推荐用法)/// 工业现场为了节约带宽和降低延迟通常一次性读取整个 DB 块的字节数组然后在 C# 中解析/// /summary/// param namedataType数据块类型 (如 DataType.DataBlock)/param/// param namedbDB 块号/param/// param namestartByteAdr起始字节偏移量/param/// param namecount读取的字节长度/param/// param namecancellationToken取消令牌/param/// returns字节数组失败返回 null/returnsTaskbyte[]?ReadBytesAsync(DataTypedataType,intdb,intstartByteAdr,intcount,CancellationTokencancellationTokendefault);}四、 核心服务实现灵魂代码这是整篇文章的核心所在。需要向读者强调工控通信的命门在于并发控制和异常处理。这里使用了 C# 12 的主构造函数Primary Constructors。3.S7PlcService.cs(核心实现)usingMicrosoft.Extensions.Logging;usingMicrosoft.Extensions.Options;usingS7.Net;namespaceIIoT.PlcIntegration;/// summary/// 高可用 S7 PLC 服务实现/// /summary/// remarks/// 设计要点/// 1. 使用 SemaphoreSlim 保证线程安全防止多线程同时发起 TCP 请求导致 S7 协议报文错乱。/// 2. 采用惰性连接 (Lazy Connection) 与断线自愈机制读写前自动检查并重建连接。/// 3. 捕获底层异常时主动释放底层 Socket避免死锁或半连接状态。/// /remarkspublicclassS7PlcService(IOptionsS7PlcOptionsoptions,ILoggerS7PlcServicelogger):IS7PlcService,IDisposable{privatereadonlyS7PlcOptions_optionsoptions.Value;privatePlc?_plc;// 【核心组件】信号量只允许 1 个线程同时进入底层读写防止串线privatereadonlySemaphoreSlim_semaphorenew(1,1);// 判断内部 PLC 对象是否实例化且处于连接状态publicboolIsConnected_plcis{IsConnected:true};/// summary/// 内部核心方法确保连接就绪。/// 注意调用此方法前必须确保已经获得了 _semaphore 锁/// /summaryprivateasyncTaskboolEnsureConnectionAsync(CancellationTokencancellationToken){// 如果已经连接直接返回避免重复开销if(IsConnected)returntrue;try{// 彻底清理旧的连接实例防止内存泄漏或 Socket 端口占用_plc?.Close();// 重新实例化 PLC 对象_plcnewPlc(_options.Cpu,_options.IpAddress,_options.Rack,_options.Slot);_plc.ReadTimeout_options.TimeoutMs;_plc.WriteTimeout_options.TimeoutMs;// 异步发起 TCP 连接await_plc.OpenAsync(cancellationToken);logger.LogInformation(成功连接到西门子 PLC [{IpAddress}]. Rack:{Rack}, Slot:{Slot},_options.IpAddress,_options.Rack,_options.Slot);returntrue;}catch(Exceptionex){logger.LogError(ex,连接西门子 PLC [{IpAddress}] 失败请检查网络状态或机架/插槽配置。,_options.IpAddress);returnfalse;}}publicasyncTaskT?ReadAsyncT(stringvariable,CancellationTokencancellationTokendefault){// 等待进入临界区await_semaphore.WaitAsync(cancellationToken);try{// 检查并建立连接如果连接失败直接返回默认值if(!awaitEnsureConnectionAsync(cancellationToken))returndefault;// 调用底层读取varresultawait_plc!.ReadAsync(variable,cancellationToken);if(resultnull)returndefault;// 类型转换处理处理底层返回类型与泛型 T 不完全一致的情况returnresultisTtypedResult?typedResult:(T)Convert.ChangeType(result,typeof(T));}catch(Exceptionex){logger.LogError(ex,读取 PLC 变量 {Variable} 异常。将强制断开连接触发下一次重连。,variable);// 【防御性编程】一旦发生异常立刻关闭当前连接强迫下次请求重建连接_plc?.Close();returndefault;}finally{// 无论成功失败必须释放锁_semaphore.Release();}}publicasyncTaskboolWriteAsync(stringvariable,objectvalue,CancellationTokencancellationTokendefault){await_semaphore.WaitAsync(cancellationToken);try{if(!awaitEnsureConnectionAsync(cancellationToken))returnfalse;await_plc!.WriteAsync(variable,value,cancellationToken);logger.LogDebug(成功向变量 {Variable} 写入值: {Value},variable,value);returntrue;}catch(Exceptionex){logger.LogError(ex,写入 PLC 变量 {Variable} 异常,variable);_plc?.Close();returnfalse;}finally{_semaphore.Release();}}publicasyncTaskbyte[]?ReadBytesAsync(DataTypedataType,intdb,intstartByteAdr,intcount,CancellationTokencancellationTokendefault){await_semaphore.WaitAsync(cancellationToken);try{if(!awaitEnsureConnectionAsync(cancellationToken))returnnull;returnawait_plc!.ReadBytesAsync(dataType,db,startByteAdr,count,cancellationToken);}catch(Exceptionex){logger.LogError(ex,批量读取 PLC 字节数据失败。DB:{Db}, 起始地址:{Start}, 长度:{Count},db,startByteAdr,count);_plc?.Close();returnnull;}finally{_semaphore.Release();}}/// summary/// 释放非托管资源/// /summarypublicvoidDispose(){_plc?.Close();_semaphore.Dispose();GC.SuppressFinalize(this);}}五、 在项目中的注册与实战运用在appsettings.json中配置 PLC 信息{S7PlcConfig:{Cpu:S71500,IpAddress:192.168.0.200,Rack:0,Slot:1,TimeoutMs:3000}}在Program.cs中注册服务注册为单例因为底层做了并发控制和重连全应用共用一个实例即可节约连接数varbuilderWebApplication.CreateBuilder(args);// 1. 绑定配置到 IOptionsbuilder.Services.ConfigureS7PlcOptions(builder.Configuration.GetSection(S7PlcOptions.Position));// 2. 注册为单例服务builder.Services.AddSingletonIS7PlcService,S7PlcService();// 3. 注册测试用的后台工作服务builder.Services.AddHostedServicePlcDataCollectorWorker();varappbuilder.Build();app.Run();编写一个后台采集服务 (PlcDataCollectorWorker.cs) 测试效果usingIIoT.PlcIntegration;usingMicrosoft.Extensions.Hosting;usingMicrosoft.Extensions.Logging;publicclassPlcDataCollectorWorker(IS7PlcServiceplcService,ILoggerPlcDataCollectorWorkerlogger):BackgroundService{protectedoverrideasyncTaskExecuteAsync(CancellationTokenstoppingToken){logger.LogInformation(PLC 数据采集服务已启动...);while(!stoppingToken.IsCancellationRequested){try{// 示例 1读取简单的单变量 (DB1 块偏移量 0 的双字转换为 Float)vartemperatureawaitplcService.ReadAsyncfloat(DB1.DBD0,stoppingToken);logger.LogInformation(当前设备温度: {Temp} ℃,temperature);// 示例 2心跳写入 (将 M0.0 置为 true 告诉 PLC 上位机在线)awaitplcService.WriteAsync(M0.0,true,stoppingToken);}catch(Exceptionex){// 防止主循环崩溃logger.LogError(ex,采集周期发生未处理异常);}// 间隔 1 秒采集一次awaitTask.Delay(1000,stoppingToken);}}}