Web API集成与数据上报
这是「从零搭建工业控制系统」系列第25篇。前面讲了本地数据存储,这篇讲远程上报——怎么把采集的数据推送到Web服务器,怎么处理网络断开。
为什么需要数据上报
工业设备产生的数据不只是本地用。远程监控、数据分析、报表生成都需要数据集中到服务器。
项目里的气体流量数据需要上报到Web API:
- 远程监控页面实时显示流量
- 数据分析平台统计历史趋势
- 管理层报表自动生成
HttpClient使用
.NET的HttpClient用于HTTP请求。关键原则:HttpClient必须是单例或静态,不能每次new。
// 错误:每次new(会导致端口耗尽)publicasyncTaskPostDataAsync(objectdata){usingvarclient=newHttpClient();// ❌awaitclient.PostAsync(url,content);}// 正确:单例复用privatestaticreadonlyHttpClient_httpClient=newHttpClient(){Timeout=TimeSpan.FromSeconds(10)};publicasyncTaskPostDataAsync(objectdata){varjson=JsonSerializer.Serialize(data);varcontent=newStringContent(json,Encoding.UTF8,"application/json");await_httpClient.PostAsync(url,content);}每次new HttpClient会创建新的TCP连接,频繁创建会导致端口耗尽(TIME_WAIT状态堆积)。单例复用连接池。
数据推送格式
publicclassGasFlowReport{publicstringDeviceId{get;set;}publicDateTimeTimestamp{get;set;}publicList<ChannelData>Channels{get;set;}}publicclassChannelData{publicstringChannelName{get;set;}publicdoubleFlowValue{get;set;}publicstringUnit{get;set;}}序列化成JSON推送:
varreport=newGasFlowReport{DeviceId="Device-A",Timestamp=DateTime.Now,Channels=channels.Select(c=>newChannelData{ChannelName=c.Name,FlowValue=c.CurrentValue,Unit="sccm"}).ToList()};varjson=JsonSerializer.Serialize(report);await_httpClient.PostAsync(apiUrl,newStringContent(json,Encoding.UTF8,"application/json"));重试机制
网络不稳定,推送可能失败。用指数退避重试:
publicasyncTask<bool>PostWithRetryAsync(stringurl,stringjson,intmaxRetries=3){for(inti=0;i<maxRetries;i++){try{varresponse=await_httpClient.PostAsync(url,newStringContent(json,Encoding.UTF8,"application/json"));if(response.IsSuccessStatusCode)returntrue;}catch(HttpRequestException){// 网络异常,等一会再试}awaitTask.Delay(TimeSpan.FromSeconds(Math.Pow(2,i)));// 1s, 2s, 4s}returnfalse;}3次重试,间隔1秒→2秒→4秒。3次都失败就放弃这批数据(下次采集时再推)。
网络断开时数据不丢
如果网络断开,推送失败的数据不能丢。存到本地队列,网络恢复后补发:
privatereadonlyQueue<GasFlowReport>_pendingQueue=new();publicasyncTaskReportAsync(GasFlowReportdata){boolsuccess=awaitPostWithRetryAsync(url,JsonSerializer.Serialize(data));if(!success){_pendingQueue.Enqueue(data);// 失败的存队列_logger.Warn($"数据上报失败,已加入待发队列(队列长度{_pendingQueue.Count})");}}// 定时检查队列privateasyncTaskFlushPendingQueueAsync(){while(_pendingQueue.Count>0&&awaitIsApiAvailableAsync()){vardata=_pendingQueue.Peek();if(awaitPostWithRetryAsync(url,JsonSerializer.Serialize(data)))_pendingQueue.Dequeue();// 成功了才出队elsebreak;// 还是失败,下次再试}}API可用性检测
定期探活,确认API服务在线:
privateasyncTask<bool>IsApiAvailableAsync(){try{varresponse=await_httpClient.GetAsync($"{apiBaseUrl}/health");returnresponse.IsSuccessStatusCode;}catch{returnfalse;}}每60秒探一次。在线才补发队列数据,离线就不浪费请求。
批量上报 vs 实时上报
| 模式 | 优点 | 缺点 |
|---|---|---|
| 实时上报 | 数据及时 | 请求频繁,网络压力大 |
| 批量上报 | 网络压力小 | 数据有延迟 |
| 混合模式 | 平衡 | 实现复杂 |
实际用混合模式:正常情况下每30秒批量推一次,报警时立即推。
踩坑记录
坑1:HttpClient频繁new导致端口耗尽
上线后发现程序运行几小时后无法发HTTP请求。排查发现是HttpClient用完就Dispose,TCP连接进入TIME_WAIT状态,端口耗尽。改成单例后解决。
坑2:序列化大对象慢
一次推5000条数据,JSON序列化花了2秒,阻塞了采集线程。改成在后台线程序列化,不阻塞采集。
坑3:网络断开时内存涨
网络断开后数据全存内存队列,断了一上午内存涨了200MB。加了队列上限——超过1000条就丢弃最老的(保最新数据优先)。
本篇小结
| 概念 | 关键做法 |
|---|---|
| HttpClient | 单例复用,不每次new |
| 数据格式 | JSON序列化 |
| 重试 | 指数退避,最多3次 |
| 断网不丢 | 失败存队列,恢复后补发 |
| 探活 | 60秒一次health检查 |
| 上报策略 | 正常30秒批量,报警立即推 |
数据上报的核心:网络不稳定是常态。重试+本地队列+探活,保证数据不丢、不阻塞采集。
下期预告
第26篇:调试标记系统
数据上报讲完了,下篇讲调试技巧——怎么用.tag文件切换开发模式和生产行为。