资讯动态

C#上位机实战:通信协议、多线程调度与代码复用

发布时间:2026/10/21 4:47:06 来源:尧图企业网站定制
C#上位机项目实战做过的都清楚真正决定项目成败的往往不是界面拖得有多快而是底层通信稳不稳、代码能不能跨项目复用、现场出问题时能不能快速定位。我这些年接手过的设备数据采集、远程控制、产线监控类上位机项目几乎每个都会踩到类似的坑——协议各写各的、线程一多界面就卡、换一个设备型号就要改掉半套业务代码。这篇文章不聊空泛的概念直接讲我在几个真实场景里反复验证过的做法串口仪表批量采集、TCP多设备联动、多线程调度下的UI刷新以及一套可以沉淀复用的代码骨架。适用的人是刚接触上位机开发的初学者也适合写了不少接口但总觉得代码一换设备就得重写的开发者。看完你至少能少踩一半的坑。1. 接需求后先别画界面通信协议、线程模型与状态机这三件事很多人拿到上位机需求第一反应是打开设计器开始拖控件。我的建议是忍一忍。上位机项目里界面只是最外层的一层皮真正的工作量集中在三件事上设备与程序之间怎么对话通信协议、对话过程中谁来负责什么事线程模型、以及设备在什么状态下能做什么操作状态机。这三件事没想清楚界面做得再漂亮联调时一样会被现实按在地上摩擦。1.1 上位机项目的真实工作量都在设备和程序的对话规则上位机本质上是一台电脑PC作为监控和管理端去和一台或多台下位机PLC、仪表、传感器、控制板卡通信。通信方式常见的就那几种串口RS-232/RS-485、TCP/UDP、USB、蓝牙。但无论物理链路是什么双方都必须遵守同一个对话规则也就是通信协议。一个典型的协议帧长这样帧头 设备地址 功能码 数据区 校验码。比如读取仪表当前温度可能需要发送AA 55 01 03 00 01 7E 9A仪表的回包则是AA 55 01 03 02 1F 40 88 2C。看起来只是一串十六进制数但里面的每个字节都对应一个含义AA 55是帧头、01是设备地址、03是功能码读寄存器、02是后面数据区的长度、1F 40是温度原始值、88 2C是CRC校验。我见过不少项目开发时很默契地用串口助手调通了但写代码阶段发现协议里有个字节的校验算法理解错了结果现场联调好几个小时都在对数据。所以我现在的习惯是动手写任何代码之前先把协议文档逐字节列出来用串口调试助手或者网络调试工具先手工发一帧、收一帧确认每一个字段都对应上了。这就像请了一个翻译你得先试音确认他真懂这门语言再让他正式上场。1.2 先画一张线程分工图比先画UI更值钱上位机程序至少会涉及四个角色UI线程负责界面显示和用户交互接收线程负责从串口/网口读数据业务处理线程负责解析数据、判断状态、触发动作IO线程负责把指令写出去。很多新手卡在界面卡死问题上原因就是把这几件事全塞到了一个线程里。比如串口有数据到达理论上触发DataReceived事件有人图省事直接在事件里更新界面上的TextBox。这样不是不行但read事件是系统线程池里触发的不归UI线程管高频数据一来界面会频繁跨线程访问轻则闪、卡重则直接崩溃报线程间操作无效。而且如果这个事件里还做了数据库写入、Excel导出这种耗时操作串口接收缓冲区会被堵住后面到的数据就会丢。正确的心态是接收线程只负责把原始字节搬进一个共享缓冲区业务线程从这个缓冲区取数据、解析、判断UI线程只订阅业务线程抛出的处理结果来刷新界面。每一层各干各的线程之间的交接物用线程安全集合来传递。后面我会给出具体代码这里先把分工图刻在脑子里——UI不碰IO、IO不碰控件、业务逻辑单独跑。1.3 状态机把开机、运行、暂停、报警落到代码里设备不是任何时刻都允许你下发任何指令的。比如电机运行中你点击打开阀门可能是安全的但如果设备已经处于报警停机状态你再下发点击启动可能会带着故障继续跑。为了避免这种误操作上位机里要维护一个设备状态机。状态机说白了就是用一个枚举表示设备当前处于哪个状态再用一张表告诉系统在状态A下哪些操作是允许的。我一般这样写public enum DeviceState { Disconnected, Connecting, Ready, Running, Paused, Alarm, Stopped } public class DeviceStateMachine { private DeviceState _currentState DeviceState.Disconnected; public DeviceState Current _currentState; public bool CanChange(DeviceState next) { return _currentState switch { DeviceState.Disconnected next is DeviceState.Connecting, DeviceState.Connecting next is DeviceState.Ready or DeviceState.Disconnected, DeviceState.Ready next is DeviceState.Running or DeviceState.Stopped, DeviceState.Running next is DeviceState.Paused or DeviceState.Stopped or DeviceState.Alarm, DeviceState.Paused next is DeviceState.Running or DeviceState.Stopped, DeviceState.Alarm next is DeviceState.Ready or DeviceState.Stopped, DeviceState.Stopped next is DeviceState.Disconnected, _ false }; } public bool TryChange(DeviceState next) { if (!CanChange(next)) return false; _currentState next; return true; } }状态机的好处是界面上所有按钮的可用性可以直接绑定到CanChange的结果比如启动按钮只有在Ready和Paused状态下才可点击。设备管理逻辑因此变得非常清晰即使三个月后回来维护看代码也能知道什么时候能操作什么而不是靠项目经理口头交代。2. 场景一串口仪表批量参数采集——把串口收发封装成可复用的帧管理器第一个常见场景车间里面有几十台仪表每台仪表通过RS-485总线接到USB转串口模块上位机需要轮询采集每台仪表的电流、电压、温度等参数并且能批量下发设置参数。设备厂商给的协议往往很简单但代码如果写得随意很容易出现偶尔少一帧某些地址读不到的毛病。2.1 协议怎么定帧头、命令、校验和超时串口通信是全双工或半双工RS-485一般是半双工同一时刻只能有一方发送。所以轮询时上位机发完一帧请求后必须等设备回包收到完整帧后才知道该发下一帧如果在一定时间内没收到回包或回包校验错误就要重试或者跳到下一个设备。一个简单可靠的协议设计可以这样定字段长度说明帧头2字节固定0xAA 0x55地址1字节设备地址1~247命令1字节如0x03读参数、0x06写单个参数数据长度1字节数据区字节数数据区N字节要读写的寄存器地址和值校验2字节CRC16从地址到数据区结尾统一计算关键点在于帧头必须选设备正常数据里不容易出现的值比如AA 55校验必须覆盖到地址、命令、数据长度和数据区而不是只校验数据区。我接手过一个项目厂商文档里只校数据区结果设备地址都发错了还照样能算出合法校验排查很久才发现地址字节根本没参与校验。超时时间也要分场景如果是RS-485总线轮询通常给设备100~300ms的响应窗口就够了如果设备内部需要执行复杂动作比如启动加热返回时间可能到秒级这时候超时参数应该可以通过配置文件改而不是写死在代码里。2.2 可复用代码串口帧收发器的核心结构串口通信的解析有一个核心思路收到一个字节就判断当前是不是帧头是帧头才继续往后面收不是帧头要么重新找帧头要么丢弃。我习惯封装一个SerialFrameReceiver它不关心业务命令是什么只负责把一串字节流拆成一帧一帧的完整数据。public class SerialFrameReceiver { private readonly byte[] _frameHeader { 0xAA, 0x55 }; private readonly Listbyte _buffer new(); private readonly int _maxFrameLength; public event Actionbyte[] FrameReceived; public SerialFrameReceiver(int maxFrameLength 256) { _maxFrameLength maxFrameLength; } public void Push(byte[] data) { if (data null || data.Length 0) return; _buffer.AddRange(data); // 永远只处理完整帧帧头 地址 命令 长度 数据 CRC while (_buffer.Count _frameHeader.Length 4) { if (_buffer[0] ! _frameHeader[0] || _buffer[1] ! _frameHeader[1]) { _buffer.RemoveAt(0); continue; } int dataLength _buffer[3]; int totalLength 2 1 1 1 dataLength 2; // 帧头 地址 命令 长度 数据 CRC if (totalLength _maxFrameLength) { _buffer.RemoveRange(0, 2); // 帧头错误或长度异常跳过帧头重新找 continue; } if (_buffer.Count totalLength) { return; // 数据不够等待下一批 } byte[] frame _buffer.Take(totalLength).ToArray(); _buffer.RemoveRange(0, totalLength); if (CheckCrc(frame)) { FrameReceived?.Invoke(frame); } else { // 校验失败很可能这一帧的帧头是数据中间碰巧出现的跳过一字节再找 _buffer.RemoveAt(0); } } } private static bool CheckCrc(byte[] frame) { // 根据厂商协议实现对应CRC16算法 ushort crc Crc16.Compute(frame, 0, frame.Length - 2); ushort expect (ushort)((frame[^2] 8) | frame[^1]); return crc expect; } }这个类只需要一个Push方法把串口DataReceived里读到的原始字节塞进去它就会自动拼帧、找头、校验、抛给你一个完整的数据帧。无论协议是读温度还是读电压这个帧接收器都不需要改天然可复用。设备侧的业务解析放到上层去处理。使用的时候要注意一点Push是在串口接收线程里被调用的所以FrameReceived事件也会在接收线程触发。如果上层收到帧后要更新界面必须再借助队列切到UI线程或者让接收器只负责往队列里丢数据别直接碰UI控件。2.3 这个场景最容易踩的坑换行符、IO超时、串口被占用串口场景我踩过几个印象很深的坑。第一个是换行符。很多仪表协议是ASCII码 换行结尾比如发READ\r\n但有些设备只发\n有些发\r\n还有些在每帧数据后面跟一个空格。如果用ReadLine()去读经常会因为换行符不匹配而读不到完整行。老老实实按字节读、自己找帧边界比依赖ReadLine靠谱得多。第二个是SerialPort.ReadTimeout。读超时设得太大轮询周期会被拖长设得太小在设备偶尔慢一拍时会误报超时。我一般把读超时设置成设备正常响应时间的三倍比如设备一般在50ms内回包读超时设150ms就够了执行类操作单独延长不跟普通查询混用。第三个是串口被占用。程序异常退出后SerialPort没来得及释放端口会被系统占用下次启动直接抛异常。解决方式是在finally里关闭串口或者用using管理串口生命周期。另外调试时如果串口助手还开着串口再由上位机去打开同一个端口也会失败这个连老手都会偶尔中招。3. 场景二TCP多设备联动——粘包拆包、心跳保活与断线重连怎么设计第二个高频场景是多台设备通过网络连接上位机。比如现场有若干台可联网的测控设备每台设备作为TCP Server、被动等待连接也有设备作为Client、主动连接上位机的情况上位机需要同时维持几十个长连接定时采集状态、下发控制指令。3.1 为什么TCP比串口更容易出现看似通实际不通串口的数据是串行字节流一般不会出现信息边界问题因为协议里的帧头基本够用。但TCP是流协议应用层发给Socket的数据会被底层拆分成任意大小的包接收端可能一次收到半个帧也可能一次收到好几个帧混在一起这就是粘包和半包问题。举个例子上位机发了三帧指令给设备设备端可能在一个Receive里同时收到这三帧粘包也可能第一帧只收到前半部分半包。如果不做拆包处理直接拿收到的字节当完整报文去解析数据处理必然错乱。我曾经用一个开源测试工具模拟设备端发现上位机发了一帧设备端收到的数据却把下一帧的内容也带进来了。排查到最后不是通信链路错乱而是上位机用的Send方法连续写入两块数据底层TCP优化机制把两次写入合并成了一次看起来就像多发了东西。这就是典型的粘包。3.2 粘包/半包处理用长度字段而不是ReadLine串口协议里可以用帧头识别TCP场景里我更推荐在协议里加一个长度字段帧头2字节 包长2字节 命令 数据 校验。接收端每收到一批数据先看能不能凑够帧头和包长如果长度不够就直接等下一批如果够了就从缓冲区里取完整的一帧。public class TcpFrameDecoder { private readonly Listbyte _buffer new(); private readonly int _maxFrameLength; public event Actionbyte[] FrameDecoded; public TcpFrameDecoder(int maxFrameLength 4096) { _maxFrameLength maxFrameLength; } public void Push(byte[] data) { _buffer.AddRange(data); int index 0; while (true) { if (_buffer.Count - index 4) { break; // 帧头 包长都凑不齐继续收 } // 这里假设协议0xAA 0x55 包长(2字节小端) if (_buffer[index] ! 0xAA || _buffer[index 1] ! 0x55) { index; continue; } int bodyLength _buffer[index 2] | (_buffer[index 3] 8); int totalLength 2 2 bodyLength 2; // 帧头 包长 数据 CRC if (totalLength _maxFrameLength) { index; continue; } if (_buffer.Count - index totalLength) { break; // 完整帧还没到齐等待更多数据 } byte[] frame _buffer.GetRange(index, totalLength).ToArray(); _buffer.RemoveRange(0, index totalLength); index 0; // 校验通过后再抛出去 if (CheckFrameCrc(frame)) { FrameDecoded?.Invoke(frame); } } } }这个TcpFrameDecoder和串口的SerialFrameReceiver思路几乎一样Push数据进缓冲按长度字段把完整帧拆出来。有了这层上层代码就不用关心底层收到的包是宽是窄只需要处理完整帧即可。这是我认为最值得复用的套路。3.3 心跳与重连优雅地识别假连接TCP建立连接后如果网络断了但操作系统还没发现应用层可能还以为连接是好的这种情况叫假连接。设备可能已经重启或者中间交换机断电但上位机的TcpClient还处于Connectedtrue状态此时你去发指令发现发得出去、没有报错但实际上没有设备在听这个坑非常隐蔽。解决办法是应用层心跳机制上位机每隔几秒发送一个心跳包比如AA 55 00 01 00 FC设备收到后回一个心跳应答。如果连续N次没有收到心跳应答就判定连接失效主动关闭旧连接并执行重连。重连策略我建议用指数退避而不是每500ms疯狂重连。比如第一次重连等待1秒第二次2秒第三次4秒最多不超过30秒防止程序崩溃式狂刷日志。private async Task RunReconnectLoopAsync() { int retryDelayMs 1000; const int maxDelayMs 30000; while (!_cancellationToken.IsCancellationRequested) { if (_tcpClient?.Connected true await HasHeartBeatAsync()) { retryDelayMs 1000; // 正常重置间隔 await Task.Delay(2000); continue; } CloseInternalSocket(); await TryConnectAsync(); await Task.Delay(retryDelayMs); retryDelayMs Math.Min(retryDelayMs * 2, maxDelayMs); } }这里的HasHeartBeatAsync()可以设计成发一个心跳包等心跳应答事件出现如果在3秒内没等到就算失联。另一种更省心的做法是每次收到任何设备数据都视为活着只有连续超过N秒没有任何数据才判定失联并主动重连。现场网速波动大时建议把超时阈值放宽到5秒以上否则偶尔的网络抖动也会触发重连。4. 场景三多台设备并行调度与UI实时刷新——从卡界面到顺滑曲线当设备数量从一台变成几十台你会立刻发现代码的并发能力成了瓶颈。每一个Socket/串口都有独立的接收线程每个线程都在产生数据同时UI需要实时更新几十个标签和曲线。如果每个线程都直接去碰控件程序会稳定地卡死或崩溃。这个场景其实是生产者和消费者问题。4.1 直接在后台线程操作控件为什么会崩Windows的UI控件并不线程安全。WinForms和WPF都要求对控件的访问必须在创建这个控件的线程也就是UI线程里进行。后台线程里直接给textBox.Text ...;有两种后果要么抛异常要么偶发性的界面错乱、文字消失、状态栏闪烁。有人会想到用Control.Invoke或Dispatcher.Invoke切线程。这确实是正确方向但如果设备太多、数据量太大每条数据都Invoke一次UI线程会被高频调用淹没界面依然卡。更好的方式是合并通知按节奏刷新。4.2 用BlockingCollection做生产者消费者队列替代一堆锁我的习惯是在通信层和UI层中间放一个BlockingCollectionT通信层接收线程只往队列里放处理结果对象UI层用一个后台消费者线程不断取数据。这个集合自带线程安全不需要自己写锁。public class DataQueueT { private readonly BlockingCollectionT _queue new(new ConcurrentQueueT()); public void Produce(T item) { _queue.Add(item); } public T Consume(CancellationToken cancellationToken) { try { return _queue.Take(cancellationToken); } catch (OperationCanceledException) { return default; } } public bool TryConsumeBatch(int batchSize, CancellationToken cancellationToken, out ListT batch) { batch new ListT(batchSize); T item Consume(cancellationToken); if (EqualityComparerT.Default.Equals(item, default)) return false; batch.Add(item); while (batch.Count batchSize _queue.TryTake(out T extra)) { batch.Add(extra); } return true; } }UI刷新逻辑会起一个独立线程循环调用TryConsumeBatch比如一次最多取20条数据再统一更新界面。这样既不会丢数据又减少了UI跨线程调用的次数。生产者只管往队列里丢消费者只管批量取解耦非常干净。4.3 实时曲线和报表刷新的节流与批量提交实时曲线是UI卡顿的重灾区。如果仪表的采集频率是50ms一次也就是说每秒产生20个点画曲线时如果数据点一旦到达就立刻刷新界面刷新频率就失控了。实用做法是节流无论中间来了多少数据UI最多每100ms或200ms刷新一次曲线中间的数据合并处理。我常用一个简单的节流器利用毫秒时间戳判断该不该刷新private DateTime _lastUiRefresh DateTime.MinValue; private readonly TimeSpan _refreshInterval TimeSpan.FromMilliseconds(150); public void OnDataPacketReceived(DataPacket packet) { // 先把数据塞进显示模型的集合里这步很快 _chartSeries.Add(packet.Timestamp, packet.Value); // 真正的刷新动作统一交给UI线程并做节流 if (DateTime.Now - _lastUiRefresh _refreshInterval) { _lastUiRefresh DateTime.Now; uiContext.Post(_ { RefreshChart(); UpdateDeviceLabels(); }, null); } }这个模式我百试不爽。如果数据出得特别快界面最多每150ms重绘一次如果数据出得慢就完全不受影响。批量报表导出同理不要在每次收到数据时就去写Excel而是先把数据累积到一个内存列表等一轮采集结束或者用户点击导出时一次批量写。这样Excel写入造成的卡顿只会出现一次而不是每分钟卡一次。5. 避坑全指南三类典型故障的完整排查链路踩坑不可怕可怕的是不知道怎么定位。下面三个是我遇到最多、也最典型的故障类型我把完整的排查链路写出来下次你遇到类似的可以照这个顺序查。5.1 串口数据偶尔错位先怀疑协议接口对没对齐而不是硬件现象串口助手单独发指令给设备回包正常上位机一跑起来偶尔会读到错误数据或者漏帧。新手很容易先去怀疑USB转串口线不稳、干扰大最后也没查出所以然。我的排查顺序是先看接收端收到的原始字节流用日志记录每一批DataReceived收到的十六进制字符串不要只看解析后的结果。检查是否出现了帧头错位把收到的字节和协议帧逐字节对照看是不是有某一帧少了前面几个字节导致后续所有帧都错位。再检查发送端上位机连续发送多帧请求时有没有和别人共用同一个SerialPort实例又没有加锁导致两帧数据交错写入。确认校验算法是否完全按厂商文档实现包括字节序。CRC16的高低字节顺序反了在解析时不会报错但会造成偶发校验失败。最后才考虑硬件把波特率降到9600试试如果错帧概率明显下降多半是线缆过长或干扰否则问题更可能在软件。这里有个容易被忽略的点上位机里如果用SerialPort.Write连续发送两帧而发送间隔极短底层可能把这两帧合并成一个TCP段串口也有缓冲设备端收到后可能无差别输出。所以发送端最好用同一个锁串起整个发送动作不要允许两个业务线程同时写同一个串口。5.2 TCP连接连上了但设备不应答从端口到防火墙逐层排除现象设备TCP Server起来了上位机显示连接成功但发指令过去设备没反应。此时TcpClient.Connected还是true容易让人懵。排查链路先确认TCP连接是否真的建立用系统自带的网络连接查询工具看有没有ESTABLISHED状态的连接。排除连接是假象TcpClient.Connected只代表上次操作后连接没有被显式关闭网络断开时它并不立刻变false。发一个心跳包过去看是否有回包。确认发送的字节是否真正到了设备端在设备端打日志或者在设备上用回环工具抓包。如果设备端根本没收到看是不是发送的IP/端口写错或者数据被本地防火墙拦了。如果设备端收到了但没回检查协议帧是否满足设备要求帧头对不对、CRC对不对、命令码是否在设备支持范围内。如果前面都正确再检查设备是否规定一次只能处理一个连接。有些设备作为Server时只允许一个上位机连接你拿着调试工具连了一次没断开上位机再连接虽然能连上但不一定能通信。这一类问题最忌讳乱猜最好的方式是把发送和接收的完整字节流都记日志。我甚至会在调试阶段专门写一个协议回显窗口所有发出的帧和收到的帧原样打在界面上方便比对上线前再隐藏。5.3 交付现场才出现的差异中文环境、DPI缩放、杀毒软件和USB驱动有些问题在开发机上永远复现不了一拿到客户现场就出幺蛾子通常集中在几个地方。一是操作系统区域设置。现场如果用的是英文版Windows而你的代码里用了Encoding.Default去解析中文字符串大概率乱码。串口数据如果是ASCII倒是没事但设备日志、文件名、数据库连接字符串里带中文路径的编码不对会直接报错。解决办法是统一用UTF-8或显式指定编码千万别依赖系统默认编码。二是DPI缩放。WinForms高分辨率屏幕下如果程序没有声明DPI感知界面会模糊、控件重叠看起来就跟没做完似的。在程序入口的app.manifest里声明高DPI支持能省掉不少现场截图界面变形的反馈。三是杀毒软件或安全策略。有些现场设备上位机要监听TCP端口或者往本机写日志文件杀毒软件可能把监听端口或写入操作拦截导致程序看起来正常但连接失败。这类问题只能靠分阶段排查先关掉杀毒软件试一次确认后再决定是加白名单还是改实现方式。四是USB转串口驱动。现场设备经常用不同品牌的USB转串口线不同驱动给系统分配的COM口号会变。程序里如果把COM口写死成COM3换一台设备就失效。解决办法是把串口号、波特率等通信参数全部放到配置文件里程序启动时自动探测可用COM口让用户勾选而不是硬编码。6. 沉淀可复用代码资产把项目拆成协议层、设备层、业务层等一个项目交付之后如果你发现代码里到处是某仪表专用的解析逻辑下个项目大概率还是要重写。我测试过很多次把上位机代码拆成协议层、设备层、业务层三层是复用性最高的结构。6.1 分层思想与依赖方向我理想的分层是这样的传输层负责串口、TCP等物理通道的打开、关闭、读写。这一层只关心字节流不关心协议内容。上面写的SerialFrameReceiver、TcpFrameDecoder就属于这一层。协议层负责字节流和业务数据对象的互相转换。比如收到一帧AA 55 01 03 02 1F 40...协议层把它解析成一个TemperaturePacket对象。这一层依赖协议文档但不依赖具体设备和UI。设备层代表具体的物理设备。把读温度读电压设置范围这些操作封装成一个个方法内部调用协议层去发命令、等回包。换一个型号的设备时只要协议变了主要改这一层。业务层和UI层业务层负责调度逻辑什么时候该采集、什么时候该报警UI层只做展示。依赖方向必须是单向的UI依赖业务层业务层依赖设备层设备层依赖协议层协议层依赖传输层。千万不要让协议解析代码里冒出MessageBox.Show(数据错误)这种UI污染否则换个界面框架就要大改。6.2 日志、配置和异常处理三件套可复用的项目骨架里三件套必不可少。日志方面我建议从一开始就接入一个标准的日志库输出到文件和控制台并且按日期滚动。日志的级别至少要Info、Warn、Error三种Info记录连接成功、指令下发、回包内容Warn记录重试、超时Error记录未处理异常和断线次数。不要只打Error没有Info日志你根本排不了现场问题。配置方面所有会变的东西都要进配置文件COM口号、波特率、设备地址列表、TCP端口、心跳间隔、超时时间、重试次数。下个项目复用骨架时只需要改配置和协议层代码一行不用动。配置文件的加载建议在程序启动时统一做加载失败就直接报错退出避免运行到一半才发现配置有问题。异常处理方面有一条原则后台线程的异常不能静默消失。你可以在UnhandledException或TaskScheduler.UnobservedTaskException里统一记录并兜底但在业务代码里还是要尽量捕获预期异常。比如重连失败、发送超时、解析错误这些都不该让整个程序崩掉应该捕获后转成日志和重试机制。另外跨线程更新UI时设备状态变化最好抛给UI层的Dispatcher不要在后台线程里直接操作异常处理也尽量放到统一调度中心去做。6.3 从能跑到可交付的收尾检查清单最后一个项目结束前我会下意识地过一遍这个清单全是踩坑换来的经验程序关闭时是否主动关闭串口、Socket、后台线程、数据库连接且不报正在占用的异常设备断线重连过程中界面会不会卡死是不是有重连日志长时间运行比如连续48小时后内存会不会持续增长如果数据缓存队列无限增长一定要设置上限或者定期清理。协议解析里遇到帧头碰巧出现在数据区中间的场景是否处理是否靠CRC兜底配置文件参数改动后是否需要重启程序有没有一个参数设置界面日志文件是否定期归档或清理会不会把磁盘写满客户如果临时改了一个字段的解析规则我需要动几层代码如果只改协议层就能满足说明分层合格。按照这个骨架去沉淀你会发现下一个上位机项目真正要写的代码只剩协议解析和设备操作逻辑其他都是复用。这也是我个人这几年做C#上位机项目最大的体会——把时间留给真正有差异的部分而不必每个项目都从零搭一遍通信和线程框架。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑