ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C# MES源码拆解:C/S架构工单与通信机制实战

C# MES源码拆解:C/S架构工单与通信机制实战

简介:这是一套面向 C# 开发者与制造业信息化实施人员的 MES 系统完整源代码,基于 Mes.Client 与 Mes.Server 前后端分离结构,适合用于学习工业软件架构、二次开发或搭建生产管理原型。压缩包共约 2000 个文件,以 1025 个 css、520 个 cs、415 个 js 为主,辅以 dll、csproj、sln 等工程文件,整体约 35.87MB,涵盖界面样式、业务逻辑与前端交互各层。客户端按 Model、Service、Utility、UI、Web、UnitTest 分层,服务端则包含 BE 宿主、Objects 数据操作与 Services 对外接口,目录划分清晰,便于按模块研读。内容涉及订单、产品、排程、用户与合作伙伴等数据操作类,可帮助读者理解实体建模、服务引用与单元测试的组织方式。目前已有 1561 人学习,适合具备一定 C# 基础、希望深入 MES 业务与分层设计的开发者参考。

1. 拿到一套 C# MES 源码,先别急着点“启动”

一套标着“完整源代码程序”的 C# MES 系统,拆开只有两个工程:Mes.Client 和 Mes.Server。很多人下载完第一反应是双击 sln、F5 跑起来,结果十有八九卡在数据库连接串或者端口占用上,然后就开始怀疑这套代码是不是“半成品”。其实 C/S 架构的 MES 源码,价值不在能不能一键跑通,而在于它把“车间工单怎么下发、设备数据怎么回传、服务端怎么扛并发”这条链路用可读的 C# 代码摊开给你看。适合谁?做上位机、做产线数字化、接私活要快速搭一套工单/报工原型的 C# 开发者,以及想理解 MES 客户端与服务端职责边界的技术负责人。下面按“这套东西是什么 → 怎么把它跑起来 → 哪里会翻车”的顺序拆。

2. 拆开 Mes.Client 与 Mes.Server:C/S 两端各扛什么活

2.1 为什么 MES 选 C/S 而不是纯 B/S

车间现场的网络条件、外设依赖和响应要求,决定了 MES 的客户端往往不能只靠浏览器。产线工位机要接扫码枪、要读 PLC、要在断网瞬间还能缓存报工记录,这些事 B/S 做起来要么靠一堆浏览器插件,要么靠本地代理进程,反而更重。这套源码把客户端独立成 Mes.Client,本质是把“现场交互 + 本地缓存 + 设备通信”收在一个常驻的 WinForms/WPF 进程里,服务端 Mes.Server 只负责业务规则、数据落库和跨工位协同。

从代码组织上看,这种分法的直接好处是:客户端可以按工位裁剪功能,服务端可以按车间横向扩容。你如果做过纯 B/S 的 MES,应该体会过“扫码枪焦点丢失”“打印标签要装控件”这类玄学问题,C/S 客户端把这些不确定性关进了一个可控进程里。代价是部署和升级麻烦,所以这套源码里通常会有自动更新或版本校验的逻辑,这也是后面要重点看的地方。

选型上没有绝对优劣,只有场景匹配。单车间、工位机固定、外设多,C/S 更稳;多厂区、移动端也要用,B/S 更省心。这套源码给的是前者的完整骨架,你拿它改造成混合架构(客户端保留、管理端走 Web)也是常见做法。

2.2 两端通信模型:从连接建立到消息分发

C/S 的通信层是这套代码里最值得先读的部分。常见做法是 TCP 长连接 + 自定义消息头,或者走 WCF/Remoting 这类老牌 RPC。不管哪种,你都要先找到三个东西:连接管理类、消息协议定义、心跳与重连逻辑。

以 TCP 自定义协议为例,典型的消息结构是“长度 + 命令字 + 序列化体”。服务端收到后按命令字路由到对应的 Handler,客户端收到响应后按序列号回填到等待队列。读代码时重点看序列化用的是 BinaryFormatter、Json 还是 Protobuf——如果是 BinaryFormatter,注意它在较新 .NET 版本里已被标记不安全,迁移时要换掉。

// 典型的消息帧封装示意(按你源码里的实际协议调整) public class MessageFrame { public int Length { get; set; } // 整帧长度,用于粘包拆包 public short Command { get; set; } // 命令字,决定服务端路由到哪个 Handler public int Sequence { get; set; } // 请求序号,客户端用它匹配响应 public byte[] Payload { get; set; } // 序列化后的业务体 }

上面这段不是让你照抄,而是给你一个“读代码的锚点”。拿到源码后先在 Server 端搜Command或Handler关键字,把命令字和业务功能的映射表列出来,这张表就是你理解整套系统的地图。参数上要留意 Length 字段的字节序(大端小端)和是否包含头自身长度,这两点搞错,粘包拆包必翻车。

2.3 数据访问层与工单主流程

MES 的核心业务绕不开工单:下发、开工、报工、质检、完工。这套源码里这条主流程通常落在 Server 端的 Service 层,客户端只做界面和调用。读的时候建议顺着“工单下发”这一个动作走完全程:客户端点按钮 → 组装请求 → 服务端校验 → 写工单表 → 回推给相关工位。

数据访问层常见是手写 DAL + 存储过程,或者轻量 ORM。手写 DAL 的好处是 SQL 可控、性能好调,坏处是改字段要动好几处。你接手后如果要加字段,先确认有没有代码生成器或者统一的实体映射,否则容易漏改导致“界面有、库里没有”的经典问题。

-- 工单主表与报工记录的典型关联,按你源码实际表名替换 SELECT w.OrderNo, w.ProductCode, w.PlanQty, ISNULL(r.DoneQty, 0) AS DoneQty FROM WorkOrder w LEFT JOIN ( SELECT OrderNo, SUM(Qty) AS DoneQty FROM ReportRecord GROUP BY OrderNo ) r ON w.OrderNo = r.OrderNo WHERE w.Status = 1; -- 1 表示生产中

这段 SQL 的作用是快速核对“计划数 vs 已完成数”,排查报工数据对不上时非常有用。参数上注意 Status 的枚举值要和源码里的定义一致,别自己猜。如果源码用的是存储过程,就去数据库里找对应的 proc 定义,逻辑是一样的。

3. 把源码跑起来:环境、配置与首次联调

3.1 开发环境与依赖版本对齐

这套代码能不能跑,第一道坎是 .NET 版本。老项目常见是 .NET Framework 4.x,新一点可能是 .NET 6/8。打开 sln 后先看两个 csproj 的 TargetFramework,再决定用 Visual Studio 哪个版本打开。Framework 4.x 的项目用 VS2022 也能开,但要注意工作负载里勾上“.NET 桌面开发”。

依赖方面,重点看 packages.config 或 PackageReference 里有没有第三方控件(报表、图表、串口库)。这类包如果来源不明或者版本太老,还原时容易失败。常见做法是先把能还原的还原,还原不了的单独找替代或注释掉相关界面,先保证主流程能编译。

检查项看哪里不对时的表现
.NET 版本两个 csproj 的 TargetFramework编译报大量语法/API 不存在
第三方包packages.config / PackageReference还原失败、缺 DLL
数据库类型Server 端配置文件连接串报错、驱动缺失
端口占用Server 端监听配置启动即崩、客户端连不上

3.2 服务端配置:连接串、监听端口、初始化脚本

服务端启动前必须改的是数据库连接串。找到 Server 端的 App.config 或 appsettings.json,把连接串指向你本机的实例。如果源码附带建库脚本,先执行脚本再启动,顺序反了会报“表不存在”。

<!-- App.config 里的典型配置,按你本机实例改 --> <connectionStrings> <add name="MesDb" connectionString="Server=.;Database=MesDemo;User Id=sa;Password=你的密码;" providerName="System.Data.SqlClient" /> </connectionStrings> <appSettings> <add key="ListenPort" value="8899" /> </appSettings>

连接串里Server=.表示本机默认实例,用命名实例就写Server=.\SQLEXPRESS。密码别用明文提交到仓库,本地调试无所谓,上线前务必换成配置中心或加密。ListenPort 要和客户端配置里的服务端地址端口一致,这是联调失败最高频的原因之一。

启动顺序建议:先起数据库 → 执行初始化脚本 → 起 Server → 看日志确认监听成功 → 再起 Client。Server 启动后如果控制台没有明显输出,去日志文件或事件查看器里找,别干等。

3.3 客户端首次连接与一个最小联调动作

客户端配置里通常有服务端 IP、端口、工位号。工位号很关键,很多业务逻辑按工位过滤数据,填错会出现“登录成功但看不到工单”。改完配置后先做一个最小动作:登录 → 查工单列表。这一步通了,说明通信层、认证、数据访问三层都活着。

// 客户端发起登录请求的调用链示意,按源码实际方法名调整 var client = ConnectionManager.Instance; var resp = client.Send<LoginResponse>(new LoginRequest { UserName = "operator01", Password = "123456", StationNo = "ST01" // 工位号,影响后续数据过滤 }); if (!resp.Success) { // 先看 resp.Message,再看 Server 端日志,两边对照 Log.Error($"登录失败: {resp.Message}"); }

这段代码的重点不是语法,而是排查思路:客户端失败时,先拿 resp.Message,再去 Server 端同一时间的日志里找对应记录,两边时间戳一对,就能判断是请求没到、到了没处理、还是处理了没回。这个习惯能省掉大量瞎猜。

4. 避坑排查:这套 C/S 源码最容易翻车的五个地方

4.1 现象:客户端一直“连接中”,服务端日志无请求

原因通常是端口没通或协议不匹配。先确认 Server 真的在监听(netstat -ano | findstr 8899),再确认客户端连的 IP 不是 127.0.0.1 而服务端在另一台机器。如果端口通但日志无请求,多半是消息头格式对不上,比如客户端按小端写长度、服务端按大端读,第一帧就解析失败被丢弃。

解决:先用一个最简单的 ping 类命令字做连通性测试,别一上来就发业务请求。协议字节序在两端各写一个单元测试对齐,比肉眼比对靠谱。

4.2 现象:报工数据偶发丢失,重试后重复

原因是客户端本地缓存与服务端确认机制没闭环。断网时客户端把报工写本地队列,恢复后重发,但服务端没有做幂等,同一条记录被写两次。这类问题在车间网络抖动时特别常见,属于血泪经验级别的坑。

解决:给报工请求带一个客户端生成的唯一流水号,服务端落库前先查流水号是否存在。源码里如果没有这个字段,加一张去重表或给业务表加唯一索引,成本不高但能救命。

4.3 现象:换台机器部署,界面正常但打印/扫码全失效

原因是外设调用写死了设备名或路径。C/S 客户端常见把打印机名、串口号硬编码在配置或代码里,换机器就找不到。扫码枪如果是键盘模拟模式一般没事,如果是串口模式,COM 口号一变就哑。

解决:把所有外设标识提到配置文件,启动时做一次可用性检测并给出明确提示,而不是等到用户点打印才报错。这个改动不大,但能显著降低现场部署的沟通成本。

4.4 现象:服务端跑几天内存持续上涨

原因是长连接管理或事件订阅没释放。C/S 服务端维护大量客户端连接,如果断开的连接对象没从字典里移除,或者事件处理器重复注册,内存就会缓慢泄漏。这类问题不会立刻崩,属于黑匣子式的慢性病。

解决:给连接字典加定期清理,事件注册用弱引用或显式反注册。上线前用压力工具模拟反复连接断开,观察内存曲线,比等现场反馈强。

4.5 现象:升级客户端后老版本连不上

原因是协议或接口不兼容,且没有版本协商。C/S 架构升级最怕客户端和服务端版本错配,老客户端发老格式,新服务端按新格式解析,直接失败。

解决:在握手阶段带上协议版本号,服务端对不兼容版本返回明确提示并引导升级。源码里如果有自动更新模块,把版本校验放在登录之前,别让用户登录成功后再莫名其妙报错。

5. 二次开发落点:从这套源码里抠出可复用的三块骨头

这套代码真正值钱的地方,不是它现在能跑多少功能,而是你能从中拆出三块可复用的骨架:连接与消息层、工单状态机、客户端本地缓存队列。前两块决定系统稳不稳,第三块决定断网时现场慌不慌。

先说工单状态机。MES 的工单状态流转如果写成散落各处的 if-else,后面加一个“暂停”状态就要改十几处。读源码时留意有没有集中的状态定义和流转校验,如果没有,建议你二次开发时先补一个状态机,把“下发→开工→报工→质检→完工”的合法迁移列成表,非法迁移直接拒绝。这一步做完,后面加业务规则会轻松很多。

// 工单状态流转校验的最小实现思路 private static readonly Dictionary<OrderStatus, OrderStatus[]> Allowed = new() { { OrderStatus.Created, new[]{ OrderStatus.Started } }, { OrderStatus.Started, new[]{ OrderStatus.Reporting, OrderStatus.Paused } }, { OrderStatus.Reporting,new[]{ OrderStatus.QcPending } }, { OrderStatus.QcPending,new[]{ OrderStatus.Finished, OrderStatus.Reporting } } }; public bool CanTransit(OrderStatus from, OrderStatus to) => Allowed.TryGetValue(from, out var next) && next.Contains(to);

这段代码的参数就是状态枚举和允许的迁移数组,改业务规则只动这张表,不动调用方。验证方法也简单:写一组单元测试,把每条合法和非法迁移都跑一遍,比手工点界面可靠。

再说本地缓存队列。断网续传是 C/S MES 的刚需,实现上建议用“本地表 + 发送游标”而不是内存队列,进程重启也不丢。发送成功后更新游标,失败则保留并退避重试。验证时直接拔网线,报工十几条再插回,看服务端最终条数和本地是否一致,这个动作我每次交付前都会强制走一遍。

最后提醒一句:这套源码适合当骨架改,不适合原样上生产。数据库权限、日志、异常兜底这些“不性感”的部分,往往才是现场能不能睡好觉的关键。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进