☰
C#上位机与MCGS触摸屏TCP通信全指南:模式选型、组帧解析与排障
2026/10/8 2:23:28 网站建设 项目流程

简介:一套基于C#与MCGS(昆仑通态)的TCP通信范例代码,面向工控上位机开发与组态软件联调场景,解决C#程序如何通过网络读取MCGS内部数据的问题。示例基于VS2013编写,演示了通信建立、数据请求与解析的完整流程,适合具备一定C#基础、想熟悉MCGS TCP接口的工程师参考。整个压缩包共36个文件,以9个C#源码文件为核心,同时包含工程解决方案、配置文件、界面资源、可执行程序等辅助内容,整体大小仅92KB,结构精简,便于直接打开工程查看调用关系和调试运行。目前该范例已有2504人学习下载,代码覆盖通信关键步骤,并提供可运行的示例工程和看板人机界面,可帮助读者快速理解双方通信参数与代码实现的对应关系,也能作为后续功能扩展或项目复用的基础模板。

1. C#与MCGS用TCP通信,先把“谁来连谁、谁问谁答”定下来

C#上位机要和MCGS昆仑通态触摸屏走TCP通信,第一步不是写代码,而是先想清楚通信模式。触摸屏在系统里往往不只是显示界面,它肚里装着PLC实时变量,上位机盯上的是这些变量。而MCGS的TCP驱动通常给两条路:要么C#当客户端,按触摸屏的报文格式问一句答一句;要么让触摸屏主动往C#监听端口里推数据。选错了,后面代码全推倒重来。更麻烦的是,MCGS不同产品线的组态环境不一样,老TPC用McgsTpc,新TPC用McgsPro,同一个关键词在不同版本里菜单和驱动能力都不同。这一篇就把模式选型、C#侧代码骨架和真正耗时间的坑一次讲完。

2. MCGS侧先打好地基:TCP驱动模式、变量规划与跨网段设置

动手写C#之前,先把MCGS工程里的网络驱动配置好。常见做法是在设备窗口挂一个“通用TCP/IP父设备”,下面再挂具体协议子设备。父设备负责决定连谁、谁监听、走哪个tcp端口号,子设备决定报文怎么组织。父设备模式选错,上位机写得再漂亮也白搭。

2.1 先确认产品线和组态环境:TPC老型号与McgsPro的差异

昆仑通态触摸屏按产品线分,老一批TPC型号通常在McgsTpc嵌入版组态软件里做工程,新TPC、TPC-Com以及多数带“Pro”后缀的型号用McgsPro或新版McgsTpc。同一个“TCP/IP父设备”在不同环境里长得不一样:McgsTpc里是“设备窗口 → 添加设备 → 网络设备 → 通用TCP/IP父设备”,McgsPro里可能叫“TCP/IP通讯父设备”或在“采集设备”分类下。你拿到一台屏,第一件事不是翻代码,是把组态软件版本和触摸屏型号对上。

父设备里需要确认三个东西:本机IP或远端IP、端口号、工作模式。工作模式一般有“客户端”和“服务器(服务端)”两个选项。选“服务器”时,触摸屏在本机监听一个端口,C#上位机作为tcp连接主动找上门,这是最常见的“上位机主连”场景;选“客户端”时,触摸屏会主动去连上位机开的监听端口程序,适合触摸屏数量多、上位机需要统一接收的场景。端口号上,不少MCGS TCP驱动模板默认填8888,Modbus TCP则常是502,具体以你工程里驱动实际显示为准,别凭记忆硬填。

2.2 变量规划:把PLC变量映射成便于TCP读写的量

MCGS里所有变量都放在实时数据库里,C#通过TCP读写它们的本质,是向MCGS驱动发请求,由驱动去映射对应的PLC地址。变量规划直接影响后续代码复杂度。建议单独开一块区域,用统一命名前缀,比如SCADA_Level、SCADA_Flow、SCADA_Temp。这样做有实际好处:C#端收到报文后,可以直接按变量名后缀做字典映射,不用频繁改解析代码;MCGS侧做变量导出时也省事。

同时要注意“原始值”和“工程值”的差别。触摸屏组态里一个液位变量可能来自PLC的4字节浮点,也可能来自整数的量程转换。C#读到的数据到底是原始值还是工程值,取决于MCGS侧变量设置和驱动协议。最容易踩的是浮点字节序:MCGS不同版本对float的排列可能不同,有的低字节在前,有的高字节在前。建议在触摸屏组态里建立一个只读的“测试区”,放几个已知数值的变量,专门给上位机联调用。

跨网段通讯也是现场常事。上位机在192.168.1.x,触摸屏在192.168.2.x,不少工程师先改IP发现还是不通,就开始怀疑MCGS驱动。其实TCP/IP协议栈不负责跨网段路由,触摸屏的“本机设置”里要正确填写网关地址,三层交换机或路由器要有一条通往对方网段的静态路由。这个排障顺序很重要:先ping通,再谈tcp连接,最后才谈报文解析。

3. C#做TCP客户端读MCGS:连接、组帧、解析与断线重连的实现

模式定好了,这一章给出一套可直接抄走的C# Socket客户端骨架。用的是System.Net.Sockets下的TcpClient,不引第三方库。这套代码在.NET Framework 4.7.2和.NET 6/8下都能编译,适合老工控机也适合新系统。

3.1 TcpClient连接与超时参数设置

工控现场最怕的不是连不上,而是连不上时卡死。默认TcpClient.Connect在网络上没回应时可能阻塞很久,必须自己做超时控制。常见做法是BeginConnect配合WaitOne,短超时快速失败,再交给上层重连逻辑。这里有个细节:Connect成功不等于通信正常,所以收发超时也要单独设置。

using System.Net.Sockets; /// 建立TCP连接,失败快速抛出,不做重试 public TcpClient ConnectWithTimeout(string ip, int port, int timeoutMs = 3000) { TcpClient client = new TcpClient(); try { // BeginConnect + WaitOne 实现可控制的连接超时 IAsyncResult ar = client.BeginConnect(ip, port, null, null); if (!ar.AsyncWaitHandle.WaitOne(timeoutMs)) { client.Close(); throw new TimeoutException($"连接 {ip}:{port} 超时,请检查MCGS服务端是否监听"); } client.EndConnect(ar); // 收发超时设为2000毫秒,避免读半包后永久阻塞 client.SendTimeout = 2000; client.ReceiveTimeout = 2000; return client; } catch { client?.Close(); throw; } }

这段代码解决了两个常见问题:一是连接超时可控,二是收发有上限。SendTimeout和ReceiveTimeout的取值要根据现场调,PLC扫描周期快、并发量大的现场建议1500~2000毫秒;经过多级交换机或无线桥接时,网络延迟本身高,超时放宽到3000~5000毫秒更合理,否则会出现“明明通着却超时误报”的假故障。

3.2 按报文帧格式组帧:头部、长度、功能码、数据、校验

MCGS TCP驱动的私有协议在不同组态版本里字节布局不完全一致。这里给一个通用组帧骨架,用MemoryStream按“帧头 + 长度段 + 载荷 + 校验”拼装,你只需要拿抓包结果替换头部的固定字节和校验算法。

using System.IO; /// 按MCGS报文模板组帧 /// 注意:不同MCGS驱动版本帧结构不同,头部、长度段字节序和校验算法以实际抓包为准 public static byte[] BuildFrame(byte[] payload) { using MemoryStream ms = new MemoryStream(); // 帧头:常见为固定引导字节,示例用 AA 55,实际按驱动手册替换 ms.WriteByte(0xAA); ms.WriteByte(0x55); // 长度段:2字节,示例为低字节在前,高字节在后 int len = payload.Length; ms.WriteByte((byte)(len & 0xFF)); ms.WriteByte((byte)((len >> 8) & 0xFF)); // 载荷:功能码 + 变量名 + 数量 等,由具体协议决定 ms.Write(payload, 0, payload.Length); // 校验:示例用异或累加,很多驱动用CRC16,需要替换成匹配的算法 byte xor = 0; foreach (byte b in payload) { xor ^= b; } ms.WriteByte(xor); return ms.ToArray(); }

把这个方法理解成“造帧车间”:你只需要确定MCGS侧的报文格式,把写帧头、长度、校验这三处替换掉,剩下的发送和解析逻辑可以复用。特别注意长度段是包含功能码还是只包含数据,多算一字节或少算一字节,对端会认为校验失败直接丢帧。建议在MCGS侧加一个调试按钮,C#端每发一帧后把原始十六进制打印出来,肉眼对齐一次心里就踏实。

3.3 发送请求并等待应答:一次请求对应一次响应

TCP是流式协议,不存在“一次发送就是一次接收”的保证,所以读应答不能简单地Read一次就完事。需要一个循环:先读2字节帧头,再读长度段,知道载荷长度后按需读取,拼成一帧再返回。这样才能扛住粘包和半包。

public static byte[] ReadFullFrame(NetworkStream stream) { using MemoryStream ms = new MemoryStream(); int b; // 第一步:找帧头 while (true) { b = stream.ReadByte(); if (b == -1) throw new IOException("连接已关闭"); ms.WriteByte((byte)b); // 示例帧头是 AA 55,读到第二个头字节就跳出 if (ms.Length == 2 && ms.GetBuffer()[0] == 0xAA && ms.GetBuffer()[1] == 0x55) break; // 如果连续读取没匹配帧头,清空重来 if (ms.Length > 2) ms.SetLength(0); } // 第二步:读长度段(2字节) byte[] lenBytes = new byte[2]; stream.ReadExactly(lenBytes, 0, 2); int payloadLen = lenBytes[0] | (lenBytes[1] << 8); // 低字节在前 byte[] payload = new byte[payloadLen]; stream.ReadExactly(payload, 0, payloadLen); // 第三步:读校验字节 int checksum = stream.ReadByte(); // 这里按实际协议规则校验 payload 与 checksum,不匹配要丢掉整帧 return payload; }

这段代码里的ReadExactly在.NET Core里直接可用,在.NET Framework里需要自己写循环补读,核心思想是一次读不够就继续读,直到凑够字节为止。做完这些,主流程就是:连接 → 组帧 → 发送 → ReadFullFrame → 解析。解析结果用Dictionary<string, double>存起来,交给界面或数据库去消费。

4. 反向通信:让MCGS当TCP服务端主动推送,C#只做监听与拆包

很多项目里,触摸屏数量不止一台,每台屏还要周期性上报数据。这时让C#挨个去问,逻辑繁琐不说,还要管N份连接状态。更省事的办法是让MCGS侧当TCP客户端,把屏上的变量周期性地往C#开的监听端口程序里推。C#那边只需要一个监听端口程序,等着数据自己送上门。

4.1 TcpListener开监听:异步接受多个触摸屏连接

用TcpListener开启监听,接受连接后每个客户端单独开一个Task处理。这里不需要多线程并发争用,主线程只负责Accept,每个连接的任务各自读自己的数据。

using System.Net; using System.Net.Sockets; public async Task StartMcgsListenerAsync(int port, CancellationToken ct) { TcpListener listener = new TcpListener(IPAddress.Any, port); listener.Start(); Console.WriteLine($"监听端口 {port} 已启动"); while (!ct.IsCancellationRequested) { TcpClient client = await listener.AcceptTcpClientAsync(ct); // 每个触摸屏连接单独处理,避免一台屏掉线拖垮整个接收流程 _ = Task.Run(() => HandleClientAsync(client, ct), ct); } }

端口选择上,建议避开常用端口和MCGS其他驱动默认端口,选9000~10000区间的空闲端口。防火墙里要放行该端口,否则触摸屏侧显示连接成功,C#这边却一直看不到数据。监听端口程序的核心价值不是接收,而是把每条连接状态、最近一帧时间记录下来,方便现场排查掉线问题。

4.2 处理TCP粘包与半包:缓冲区拆帧

触摸屏按周期推数据,C#接收端遇到最常见的问题是粘包:两个数据帧连在一起被一次读出来。如果直接把读到的字节当一帧解析,后面全错位。解决办法是定义一个TryExtractFrame函数,把收到的字节先放进MemoryStream缓冲,能拆出完整帧就返回,拆不出来就等下一批数据。

private readonly MemoryStream _pending = new MemoryStream(); // 从缓冲流中尝试拆出一帧,拆不出返回false private bool TryExtractFrame(byte[] incoming, int length, out byte[]? frame) { _pending.Write(incoming, 0, length); // 检查缓冲里是否已有完整帧头 if (_pending.Length < 2) { frame = null; return false; } byte[] buffer = _pending.GetBuffer(); int startIndex = -1; for (int i = 0; i < _pending.Length - 1; i++) { if (buffer[i] == 0xAA && buffer[i + 1] == 0x55) // 帧头示例 { startIndex = i; break; } } if (startIndex < 0) { _pending.SetLength(0); frame = null; return false; } // 丢弃帧头之前的脏数据 if (startIndex > 0) { byte[] remain = _pending.ToArray().Skip(startIndex).ToArray(); _pending.SetLength(0); _pending.Write(remain, 0, remain.Length); } // 至少要有 帧头2 + 长度2 才能判断整帧长度 if (_pending.Length < 4) { frame = null; return false; } int lenLow = buffer[2]; int lenHigh = buffer[3]; int payloadLen = lenLow | (lenHigh << 8); int totalLen = 2 + 2 + payloadLen + 1; // 帧头+长度+载荷+校验 if (_pending.Length < totalLen) { frame = null; return false; } // 半包,继续收 byte[] fullFrame = new byte[totalLen]; _pending.Read(fullFrame, 0, totalLen); frame = fullFrame; return true; }

拆帧的关键在于“不够就等,够了再切”。每收到一段数据,调一次TryExtractFrame,返回true就处理一帧,继续循环直到返回false为止。这个写法比“判断数据尾字节”可靠得多,因为你没法保证触摸屏一次只发一帧,更没法保证网络延迟不会把一帧切成两半。处理完的帧交解析函数,按变量编号和值更新内存字典。

这套反向推送模式还有一个优势:触摸屏侧发生变量变化时,组态里可以配置条件触发上送,报警类数据能做到准实时。C#侧代码完全不用关心到底谁变化了,只做被动接收。前提是MCGS组态工程的TCP客户端配置里,把目标IP和端口填对,并且采集周期设置在一个合理范围内,比如500毫秒到1秒一次,别小到让触摸屏驱动忙不过来。

5. 参数设置与常见问题排查:连不上、卡死、数据错位的5个根因

不管是C#主动连接还是监听推送,最终都会在调试现场碰到几类典型故障。下面这5条是我反复见过的,每一条都按“现象 → 原因 → 解决”拆开写。

5.1 端口、心跳和超时:先别急着改代码,按这个顺序查

现象:C#客户端连接报“目标计算机积极拒绝”,或者触摸屏推送端显示已连接但C#收不到数。

原因:多数是三个层面之一。第一,MCGS侧服务端没有正常启动监听,父设备没保存或组态没下载到触摸屏,屏上跑的其实还是旧工程。第二,防火墙拦了端口,Windows默认会拦外部连接。第三,C#端口号和MCGS驱动配置不一致,比如屏上配的8888,代码里写的502。

解决:先做排除法。在触摸屏上用自带调试窗口或在线模拟确认驱动运行状态;在上位机上用Test-NetConnection ip -Port 端口命令探测tcp连接是否通;最后再把报文内容打出来看。顺序不要反,很多人一上来就怀疑代码,结果查半天是屏上的工程没下载对。

5.2 连接正常但读几个帧就卡死,收发线程互相等待

现象:C#能连上触摸屏,读第一帧正常,第二帧直接卡住,程序假死。

原因:最常见的是同步Socket在单线程里收发,发送一个请求后,接收端没有在预期时间内返回下一帧,Receive阻塞住整个界面线程。另一个常见原因是半包:上次读到的数据没拼完,下一次读到的是后半段,导致解析错位,请求永远等不到正确应答。

解决:把收发挪到后台线程,界面只订阅结果。另外,所有Read操作都要走完整帧读取逻辑,也就是前面3.3节的ReadFullFrame,不要用“读一次就当一帧”的省事写法。再加上ReceiveTimeout兜底,超时后扔出异常,由上层做重连而不是卡死。

5.3 读到的数值和触摸屏上显示的对不上

现象:C#读回来的整数和触摸屏界面显示的变量值完全对不上,或者float读出来是天文数字。

原因:要么是字节序问题,MCGS里float变量在TCP报文里是高字节在前,C#的BitConverter默认是低字节在前;要么是读写地址映射错位,MCGS侧变量列表里的索引和C#请求里的变量序号错了一行。还有一个很隐蔽的情况:读的是原始值,触摸屏界面显示的是工程值,中间差了一个量程换算系数。

解决:写一段一次性解析测试代码,连接后连续读已知变量,逐一对照。float翻转字节序后打印,再看是否是工程值换算问题。项目初期多花半小时调字节序,能省掉试机现场一晚上的折腾。

5.4 触摸屏和上位机不在同一网段,ping都不通

现象:IP改了,网线插了,但C#始终连不上触摸屏,ping也不通。

原因:跨网段通讯需要网关参与,可触摸屏的网关设置是空的,或者上位机本身开着多个网卡让路由走了错误网关。MCGS触摸屏系统设置里有本机IP、子网掩码、网关三项,少填一项都跨不了网段。

解决:先在上位机cmd里用route print看看默认路由指向哪个网卡,再核对触摸屏的网关填的是不是和现场路由器管理地址同一段。如果中间隔着三层交换机,交换机上要有去往对方网段的静态路由。这类问题跟C#代码无关,但最容易让人在代码里白找半天。

5.5 触摸屏断电重启后,上位机连不回来了

现象:现场工人把触摸屏断电重启,之后C#程序再也连不上,直到手动重启上位机软件。

原因:TCP连接是两端状态,一端断掉后另一端不会立刻感知。C#的TcpClient对象还在,但底层连接已经死了。发送数据时可能触发异常,但如果程序一直只是“等数据”,就永远等不到。

解决:建立心跳机制。C#侧每2~3秒检测一次连接状态,通过发送一帧空读命令或心跳帧来确认连接是否还活着,一旦失败立即销毁旧连接,按指数退避重连。重连时要注意:触摸屏重启后可能没有立刻准备好监听,前几次连接失败是正常的,所以要重试,不能第一次失败就放弃。

6. 把通信代码封装成上位机的“元器件”:抓包验证和日志先行

到最后这一步,我习惯把整套通信逻辑封装成一个内部状态机式的组件,而不是散落在窗体事件里。组件对外暴露Connect、Disconnect、读取变量、变量更新事件,对内管理着连接状态、重连退避、收帧缓冲区和最近帧时间。好处是上位机页面代码永远干干净净,出问题只查这一个类。

重连退避是个值得做的细节:连续失败时,不要每200毫秒疯狂重连,那会把触摸屏驱动拖垮。常见做法是记录失败次数,按5秒、10秒、20秒、最大30秒递增,成功一次后重置。判断连接健康的依据不应该是TcpClient.Connected属性,它只反映Socket状态,不代表对端还活着;要定义一个旧数据保护阈值,如果超过3个心跳周期没有收到任何有效帧,自动判定掉线并触发重连。

还有一个习惯我吃了亏才养成:先抓包再写解析。新到一个MCGS工程,不管对方给的协议文档写得多清楚,先用抓包工具把触摸屏发出的原始帧抓下来,对着十六进制逐字节研究。很多项目的协议文档和实际固件版本差了一两个版本,长度段算法和帧头都可能不一样。抓包后把每一帧的用途、字段偏移、字节序记到一个固定表格里,代码照着这个表写。这个习惯后来救过我很多次,尤其是老TPC设备升级固件后报文悄悄变化的情况。

日志也一定要在通信层做:记录每一次连接成功、连接失败、帧超时、重连动作、未知帧头。日志级别分开,平时只记连接状态和异常,调试时打开完整帧记录。宁可日志多写几个字段,不要等屏幕数据突然停下来才想起没有任何日志可查。这套组件做完,C#与MCGS昆仑通态触摸屏的TCP通信就有了一个稳定、可替换、可测的底座,后面加协议、加屏、加数据点只是配置层面的活儿。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询