大家好我是威哥。前两年做过一个汽配厂的设备监控项目一开始图快代码写得很“糙”通信断了就报错异常没捕获直接崩日志只有一行“出错了”现场排查问题要翻几小时文本。后来痛定思痛花了一周把代码重构了一遍加上了工业级的通信重试、全局异常兜底和结构化日志之后项目稳得一批现场工人再也没凌晨给我打过电话。今天把这套封装方案分享出来都是能直接抄的干货。一、先说说工业现场的“三大痛点”做工业上位机最头疼的就是这三件事通信不稳定车间网线被叉车碰、无线Wi-Fi信号弱PLC/传感器经常断连没重试机制的话数据直接丢。异常没兜底一个小异常比如解析数据越界就能让整个程序崩生产线停一分钟损失几千块。日志难排查普通文本日志只有时间和错误信息没有设备ID、通信地址这些上下文出问题要翻几万行日志头都大了。所以这次的封装目标很明确通信断了自动重连/重试不用人工干预。任何异常都不崩程序记录日志后尝试恢复。日志结构化、带上下文出问题5分钟内定位到原因。二、核心封装1通信重试断路器Polly是神器工业现场通信重试不能用简单的while(true)循环要考虑重试策略指数退避比如第一次等1秒第二次等2秒第三次等4秒避免网络拥堵时一直重试。断路器模式连续失败10次就“跳闸”暂停通信5分钟避免把PLC/传感器“冲死”。不同异常不同处理比如“连接超时”要重试“地址无效”就不用重试是配置问题。这里我用了Polly.NET生态最火的重试/熔断库配合S7netplus和FluentModbus封装了一个通用的通信服务。2.1 安装PollyInstall-Package Polly Install-Package Polly.Extensions.Http2.2 封装S7-1200通信服务带重试断路器直接上代码重点看注释里的踩坑点usingPolly;usingPolly.CircuitBreaker;usingS7.Net;usingSystem.Net.Sockets;publicclassS7PlcService{privatereadonlyILoggerS7PlcService_logger;privatePlc_plc;privatereadonlystring_ip;privatereadonlyint_rack;privatereadonlyint_slot;// 重试策略处理SocketException和PlcException指数退避最多重试5次privatereadonlyAsyncPolicy_retryPolicy;// 断路器策略连续失败10次跳闸暂停5分钟privatereadonlyAsyncCircuitBreakerPolicy_circuitBreakerPolicy;publicS7PlcService(ILoggerS7PlcServicelogger,stringip,intrack0,intslot1){_loggerlogger;_ipip;_rackrack;_slotslot;_plcnewPlc(CpuType.S71200,ip,rack,slot);// 1. 配置重试策略_retryPolicyPolicy.HandleSocketException()// 处理网络异常.OrPlcException(exex.ErrorCode!ErrorCode.WrongVarFormat)// 处理PLC异常但排除“地址无效”这种配置错误.WaitAndRetryAsync(retryCount:5,sleepDurationProvider:attemptTimeSpan.FromSeconds(Math.Pow(2,attempt)),// 指数退避1s, 2s, 4s, 8s, 16sonRetry:(ex,ts,attempt,ctx){_logger.LogWarning(ex,PLC通信失败{Attempt}次重试等待{Ts}秒后重试IP{Ip},attempt,ts.TotalSeconds,_ip);});// 2. 配置断路器策略_circuitBreakerPolicyPolicy.HandleSocketException().OrPlcException().CircuitBreakerAsync(exceptionsAllowedBeforeBreaking:10,// 连续10次失败跳闸durationOfBreak:TimeSpan.FromMinutes(5),// 暂停5分钟onBreak:(ex,ts){_logger.LogError(ex,PLC通信连续失败断路器跳闸暂停{Ts}分钟IP{Ip},ts.TotalMinutes,_ip);},onReset:(){_logger.LogInformation(PLC通信恢复断路器重置IP{Ip},_ip);});}// 连接PLC带重试断路器publicasyncTaskConnectAsync(CancellationTokenstoppingToken){await_retryPolicy.ExecuteAsync(async(){await_circuitBreakerPolicy.ExecuteAsync(async(){if(!_plc.IsConnected){_logger.LogInformation(正在连接PLCIP{Ip},_ip);await_plc.OpenAsync();_logger.LogInformation(✅ PLC连接成功IP{Ip},_ip);}});});}// 读取DB块带重试断路器publicasyncTaskbyte[]ReadBytesAsync(DataTypedataType,intdb,intstartByte,intcount,CancellationTokenstoppingToken){returnawait_retryPolicy.ExecuteAsync(async(){returnawait_circuitBreakerPolicy.ExecuteAsync(async(){if(!_plc.IsConnected)awaitConnectAsync(stoppingToken);_logger.LogDebug(读取PLC数据类型{DataType}DB{Db}起始地址{StartByte}长度{Count}IP{Ip},dataType,db,startByte,count,_ip);returnawait_plc.ReadBytesAsync(dataType,db,startByte,count);});});}// 写入数据带重试断路器省略和读取类似publicasyncTaskWriteAsync(stringaddress,objectvalue,CancellationTokenstoppingToken){...}publicvoidDispose(){if(_plc.IsConnected)_plc.Close();_plc.Dispose();}}踩坑点总结不要无限重试最多5-10次不然网络拥堵时会雪上加霜。断路器很重要连续失败后暂停避免把PLC/传感器“冲死”工业设备通信端口处理能力有限。区分异常类型配置错误比如地址写错不用重试直接报错让人工处理。三、核心封装2全局异常处理程序永远不崩工业上位机最忌讳的就是“一报错就崩”哪怕是一个小的解析异常也可能导致生产线停。所以我们要做全局异常兜底捕获所有未处理的异常记录日志后尝试恢复。3.1 Worker Service后台服务的全局异常处理Worker Service是工业上位机常用的后台服务模式全局异常处理要覆盖这三个地方ExecuteAsync里的异常用try/catch包裹。AppDomain.CurrentDomain.UnhandledException非托管异常。TaskScheduler.UnobservedTaskException未观察到的Task异常。直接上代码publicclassProgram{publicstaticasyncTaskMain(string[]args){// 1. 先配置Serilog后面会讲Log.LoggernewLoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().WriteTo.File(logs/device-monitor-.txt,rollingInterval:RollingInterval.Day).CreateLogger();try{Log.Information( 设备监控服务启动 );// 2. 注册全局异常处理AppDomain.CurrentDomain.UnhandledException(sender,e){Log.Fatal((Exception)e.ExceptionObject,捕获到非托管全局异常服务即将重启);// 这里可以加代码记录日志后自动重启服务比如用systemd或任务计划程序};TaskScheduler.UnobservedTaskException(sender,e){Log.Error(e.Exception,捕获到未观察到的Task异常);e.SetObserved();// 标记为已观察避免程序崩};// 3. 启动HostvarhostHost.CreateDefaultBuilder(args).UseSerilog()// 用Serilog替换默认日志.ConfigureServices((context,services){// 注册S7PlcService单例services.AddSingletonS7PlcService(spnewS7PlcService(sp.GetRequiredServiceILoggerS7PlcService(),192.168.1.100));// 注册Workerservices.AddHostedServiceDeviceMonitorWorker();}).Build();awaithost.RunAsync();}catch(Exceptionex){Log.Fatal(ex,服务启动失败);}finally{Log.CloseAndFlush();}}}// Worker里的ExecuteAsync也要用try/catch包裹publicclassDeviceMonitorWorker:BackgroundService{privatereadonlyS7PlcService_plcService;privatereadonlyILoggerDeviceMonitorWorker_logger;publicDeviceMonitorWorker(S7PlcServiceplcService,ILoggerDeviceMonitorWorkerlogger){_plcServiceplcService;_loggerlogger;}protectedoverrideasyncTaskExecuteAsync(CancellationTokenstoppingToken){while(!stoppingToken.IsCancellationRequested){try{// 正常的业务逻辑读取PLC数据、解析、上传vardataawait_plcService.ReadBytesAsync(DataType.DataBlock,1,0,20,stoppingToken);_logger.LogInformation(读取到PLC数据{Data},BitConverter.ToString(data));}catch(Exceptionex){// 捕获Worker里的所有异常记录日志后继续循环不要崩_logger.LogError(ex,Worker业务逻辑异常继续运行);}awaitTask.Delay(1000,stoppingToken);}}}3.2 WinForms/WPF的全局异常处理如果是WinForms/WPF界面程序还要加这两个// WinForms的Program.cs里[STAThread]staticvoidMain(){Application.SetHighDpiMode(HighDpiMode.SystemAware);Application.EnableVisualStyles();Application.SetCompatibleTextRenderingDefault(false);// 注册UI线程异常Application.ThreadException(sender,e){Log.Fatal(e.Exception,捕获到UI线程异常);MessageBox.Show($程序发生异常{e.Exception.Message}\n请联系管理员,错误,MessageBoxButtons.OK,MessageBoxIcon.Error);};// 注册非UI线程异常AppDomain.CurrentDomain.UnhandledException(sender,e){Log.Fatal((Exception)e.ExceptionObject,捕获到非UI线程全局异常);MessageBox.Show($程序发生严重异常{((Exception)e.ExceptionObject).Message}\n程序即将退出,严重错误,MessageBoxButtons.OK,MessageBoxIcon.Stop);};Application.Run(newMainForm());}踩坑点总结Worker里的循环一定要用try/catch包裹不然一个异常就会让Worker退出服务虽然没崩但业务停了。TaskScheduler.UnobservedTaskException一定要加很多异步异常没被观察到会直接导致程序崩。异常后不要直接退出记录日志后尝试恢复比如重连PLC、继续循环工业现场“能跑就行”。四、核心封装3结构化日志Serilog上下文信息普通的文本日志是这样的2026-02-20 10:00:00 读取PLC数据 2026-02-20 10:00:01 出错了出问题根本不知道是哪个设备、哪个地址出的错。结构化日志是这样的JSON格式{Timestamp:2026-02-20T10:00:00.123456708:00,Level:Error,MessageTemplate:PLC通信失败IP{Ip}, 地址{Address},Properties:{Ip:192.168.1.100,Address:DB1.DBW0,Exception:System.Net.Sockets.SocketException: 连接超时...}}有了上下文信息出问题5分钟内就能定位到原因。这里我用了Serilog.NET生态最火的结构化日志库。4.1 安装SerilogInstall-Package Serilog.AspNetCore Install-Package Serilog.Sinks.Console Install-Package Serilog.Sinks.File Install-Package Serilog.Sinks.Elasticsearch // 可选输出到Elasticsearch方便后续分析4.2 配置Serilog工业现场版在Program.cs里配置Serilog重点是按日期分文件每天一个日志文件方便归档。保留30天日志避免日志占满磁盘。输出JSON格式结构化方便分析。加上下文信息比如设备ID、环境开发/生产。Log.LoggernewLoggerConfiguration().MinimumLevel.Debug().MinimumLevel.Override(Microsoft,LogEventLevel.Information)// 微软的日志设为Info减少噪音.Enrich.FromLogContext()// 允许从LogContext添加上下文.Enrich.WithProperty(DeviceId,Device-Monitor-001)// 全局上下文设备ID.Enrich.WithProperty(Environment,Production)// 全局上下文环境// 输出到控制台开发时用.WriteTo.Console(outputTemplate:[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception})// 输出到文件生产时用按日期分保留30天JSON格式.WriteTo.File(path:logs/device-monitor-.json,rollingInterval:RollingInterval.Day,retainedFileCountLimit:30,formatter:newSerilog.Formatting.Json.JsonFormatter())// 可选输出到Elasticsearch后续用Kibana分析// .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri(http://192.168.1.200:9200))// {// AutoRegisterTemplate true,// IndexFormat device-monitor-logs-{0:yyyy.MM.dd}// }).CreateLogger();4.3 在代码里使用Serilog加上下文在业务代码里用LogContext添加临时上下文比如当前的PLC地址、传感器IDusing(LogContext.PushProperty(PlcAddress,DB1.DBW0))using(LogContext.PushProperty(SensorId,Sensor-001)){_logger.LogInformation(开始读取传感器数据);try{vardataawait_plcService.ReadBytesAsync(DataType.DataBlock,1,0,2,stoppingToken);_logger.LogInformation(读取成功数据{Data},BitConverter.ToString(data));}catch(Exceptionex){_logger.LogError(ex,读取失败);}}这样日志里就会带上PlcAddress和SensorId出问题直接定位到是哪个传感器、哪个地址。踩坑点总结日志级别要合理Debug开发调试、Info正常业务、Warning通信波动、Error异常、Fatal严重错误不要全用Info。日志不要太啰嗦正常业务只记录关键信息比如“读取成功”不要记录每一步细节不然日志量太大。一定要加全局上下文比如设备ID、环境后续分析日志时能区分是哪个设备出的问题。五、实战整合设备监控项目跑起来把上面三个模块整合起来一个工业级的设备监控项目就完成了通信层S7PlcService带重试断路器通信稳定。业务层DeviceMonitorWorker带try/catch异常不崩。日志层Serilog结构化带上下文排查问题快。部署后的效果通信稳定性之前每天断连10次现在断连后自动重连工人根本感觉不到。程序稳定性之前每周崩2次现在连续运行3个月没崩过。排查问题效率之前翻几小时日志现在5分钟内定位到原因。六、总结工业级封装的“三大原则”最后总结几个能直接落地的经验稳字当头工业上位机不需要花里胡哨的功能稳定是第一位的重试、异常兜底、日志都是为了“稳”。不要重复造轮子Polly、Serilog这些库都是经过千锤百炼的直接用不要自己写重试逻辑、自己写日志框架。日志是排查问题的关键一定要结构化、带上下文不然出问题哭都来不及。异常要兜底任何异常都不要让程序崩记录日志后尝试恢复工业现场“能跑就行”。如果大家还有工业级封装的问题或者需要完整的项目代码欢迎在评论区交流。后续我打算讲讲怎么把这套封装和OPC UA结合起来做更通用的设备监控到时候再写文章分享。