news 2026/9/8 13:46:35

C#实现SICK RFID读卡器TCP异步通信:从协议解析到断线重连实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#实现SICK RFID读卡器TCP异步通信:从协议解析到断线重连实践

简介:面向需与德国SICK RFID读卡器RFU630通信的C#开发者,这份资源提供了一套基于TCP客户端的异步读取程序,可应用于自动化、物流、生产流程中的物体识别与追踪场景,解决工业设备数据采集、多线程处理和界面卡顿等问题。压缩包共32个文件,整体仅60KB,涵盖10个C#源文件(.cs)、可执行程序(.exe)、配置文件(.config)、项目工程(.csproj/.sln)、资源与调试符号(.resx/.resources/.pdb)等;其中源文件承载核心逻辑,exe可直观演示,config便于调整连接参数。已有1296人学习下载。通过分析源码,可重点掌握TcpClient双向通信、async/await异步读取NetworkStream、Task/Thread并发管理,以及委托配合Control.BeginInvoke安全更新UI的完整思路。工程内还单独对委托进行示例说明,帮助理解多线程跨线程机制,适合中高级.NET研发人员快速上手SICK RFID二次开发。 最近在搞一个产线信息化项目,设备端用的是德国SICK的RFID读卡器,上位机程序用C#实现,整体架构就是读卡器作为TCP服务端,我这边写一个TCP客户端,通过异步方式把标签数据读取下来,解析完再交给MES系统。需求听起来很常规,但真正动手做的时候会发现,设备通信协议、半包粘包处理、断线重连、界面卡顿这几点,每一个都能让你多花一个晚上。

这篇文章把我实际写过的SICK RFID读卡程序方案完整捋一遍,包括为什么选TCP客户端、为什么必须异步读取、报文是怎么解析的,以及我在现场排障时踩过的坑。如果你是做仓储、AGV调度、产线追溯这类上位机开发的,应该能直接抄作业。

1. 项目背景与整体方案拆解

1.1 读卡场景:RFID读卡器在产线里干什么

SICK的RFID读卡器在工厂里最常见的使用方式,是给“移动的物体”做身份识别。比如AGV小车走到某个站点,地面上的读卡器自动识别车底标签;或者工装托盘经过工位时,读卡器读到托盘上的标签,系统就知道这个托盘绑定的是哪张工单、该执行什么工序。

在这个项目里,读卡器被固定在一个装配工位旁边,当装有RFID标签的载具经过时,读卡器把标签ID、信号强度这些数据读出来,再通过网络发送给上位机。读卡器本身有点像扫码枪,但区别是它不需要人去扣扳机——标签一进入射频场,设备就会自动读取。触发源在设备端,上位机要做的就是“等事件”。

标题里提到的“读卡程序”,本质上就是上位机和读卡器之间的通信模块。这个模块要解决的问题包括:建立网络连接、接收设备主动推送的数据、把数据解析成业务能用的字段、以及在链路中断后自动恢复。

1.2 为什么选“C# + TCP客户端 + 异步读取”

先说选型的理由。SICK的RFID读卡器,例如RFU63x系列,本身就带以太网口,支持上位机通过socket直接连接,不需要额外转接硬件。相比于串口或者IO-Link,TCP方式有几个明显好处:传输距离远、一台 PC 可以同时挂多台读卡器、数据量也更大。

串口方案虽然简单,但距离短、一台工控机要接多个读卡器时还得扩展串口卡,实在不方便。TCP方案只要交换机端口够,理论上可以带几十台设备,而且SICK设备直接暴露一个固定端口,上位机作为TCP客户端去连接它,逻辑非常清晰。

再看为什么用C#。做产线上位机,C#的优势在于开发效率高,WinForm/WPF做界面方便,和MES、数据库、PLC通信的生态都很成熟。对于这种中等数据量的物联网通信场景,.NET的异步机制完全够用。

至于“异步读取”,这是必须选的。你想象一下,如果用一个同步Read方法去读读卡器数据,当没有标签经过时,Read会一直阻塞在那里,界面线程直接卡死,用户一看就是“程序未响应”。而且同步Read在断线场景下很难感知,通常要等到超时才能反应过来,这在产线上是不可接受的。异步读取则能让网络接收在后台持续运行,数据到了就回调,UI线程始终保持流畅。

方案优点缺点
串口同步读取简单直接距离短、单台设备、界面易卡顿
TCP同步读取+独立线程能跑通断线感知差、线程关闭麻烦
TCP异步读取(async/await)不占线程、可优雅取消、UI不卡需要理解状态机,代码稍散
SocketAsyncEventArgs高性能、低延迟代码复杂度高,上位机场景一般用不上

1.3 数据从标签到系统,完整链路是怎么走的

别看最终效果就是“刷一下卡,数据进系统”,这条链路其实有六个环节:RFID标签本身、读卡器天线射频场、读卡器内部基带处理、以太网口、交换机或网线、上位机TCP客户端。其中任何一个环节出问题,都会表现为“读不到数据”或“数据不对”。

我在项目里排障的习惯是:先从射频端查,确认标签能被读卡器识别,再去查网络,最后查软件解析。因为软件解析问题往往最隐蔽,比如收到一串数据但拆错了帧,程序不报错,但数据就是不对。后面我会专门讲这块。

2. 协议细节与关键设计

2.1 SICK RFID读卡器的通信配置:IP、端口、报文格式

SICK的RFID读卡器,在以太网通信方式下,常见的是XML-over-TCP协议。拿RFU63x举例,上位机作为TCP客户端去连接读卡器,默认端口一般是21111。当然,不同型号、不同固件版本的默认端口可能不一样,正式开发前一定要先查你手头设备的手册,别上来就默认。

设备端的IP地址一般可以通过SICK的配置工具SOPAS ET来设定,也可以直接在设备参数里改。建议上线前把IP固定下来,比如设成192.168.1.10,不要用DHCP,否则设备IP一变,上位机就连不上了。

链接建立之后,双方通过XML文本消息交互。SICK的Host Interface协议里,有连接建立、看门狗设置、读写配置、标签数据上报等不同消息类型。因为是文本协议,调试起来比纯二进制协议直观得多。

2.2 报文具体长什么样:命令与数据上报

我用一个实际项目里的例子来说明。上位机连接设备后,需要按协议发一条连接指令,类似:

<sick><connect/></sick>

设备会回复连接成功的确认消息。之后如果读卡器处于自动读取模式,一旦有标签进入射频场,设备就会主动通过TCP连接推送标签数据,格式大概是下面这样:

<sick> <radio> <tag> <id code="2">E28011606000020100001A5B</id> <rssi>-46</rssi> <antenna>1</antenna> </tag> </radio> </sick>

这串XML里,<id>是标签的EPC编码,现在UHF超高频标签通常以十六进制字符串形式展示,比如E280开头的这串就是EPC码;<rssi>是信号强度,单位是dBm,数值越大表示信号越强,比如-46dBm比-65dBm要好;<antenna>是天线编号,多天线设备用来区分标签是从哪个天线读到的。

要特别说明的是,上面的XML结构是根据项目经验整理的,不同固件版本的字段名和嵌套层级可能有差异。所以写解析代码之前,一定要先抓几包真实的设备数据,看清楚你的设备到底输出什么格式,再按实际情况写解析逻辑。

2.3 异步读取的底层逻辑

C#的async/await并不是“开了一个后台线程”这么简单,它本质上是编译成一个状态机。当执行到await stream.ReadAsync()时,当前线程会立即返回,不阻塞。数据到达后,操作系统IO完成端口触发回调,状态机会从await的下一行继续执行。整个过程里没有线程被白白占着等数据。

这在工业上位机里非常重要。你想想,如果每个读卡器都占一个线程,线程池里的线程大部分时间都在休眠,一旦设备多了,线程上下文切换的开销也会上来。异步读写让少量线程就可以管理大量网络连接,而且UI线程永远不会因为网络等待而卡顿。

另外,C#里做异步网络接收有两种常见写法:一种是NetworkStream.ReadAsync配合CancellationToken,代码简洁直观,适合大多数上位机项目;另一种是SocketAsyncEventArgs,性能更好,但代码复杂度明显上升。除非你有极高的吞吐量要求,否则我建议用前者,把精力放在数据解析和业务逻辑上。

3. C#实现:从连接建立到数据落地

3.1 一个最小的TCP客户端骨架

我习惯把一个读卡器的通信逻辑封装成一个独立的类,叫它SickRfidClient。这样上层WinForm界面不需要关心socket细节,只要订阅事件就行。下面是最基础的结构:

public class SickRfidClient { private TcpClient _tcp; private CancellationTokenSource _cts; public event Action<string> FrameReceived; public event Action Disconnected; public async Task ConnectAsync(string ip, int port) { _tcp = new TcpClient(); await Task.Run(() => _tcp.Connect(ip, port)); _cts = new CancellationTokenSource(); _ = ReceiveLoopAsync(_cts.Token); // 启动后台接收 } private async Task ReceiveLoopAsync(CancellationToken token) { var stream = _tcp.GetStream(); var buffer = new byte[4096]; try { while (!token.IsCancellationRequested) { int read = await stream.ReadAsync(buffer, 0, buffer.Length, token); if (read == 0) break; AppendData(buffer, 0, read); } } catch (OperationCanceledException) { } catch (IOException) { } finally { Disconnected?.Invoke(); } } }

ReceiveLoopAsync里那个while循环就是核心:每当有数据到达,ReadAsync返回读取的字节数,然后我们把字节追加到缓冲区。如果read == 0,说明对端关闭了连接,循环退出。任何IO异常都会跳出循环,再由上层决定是否重连。

3.2 半包与粘包:按结束标签切帧

TCP是流协议,没有消息边界。SICK那边发出的一整条XML消息,到了你这边可能被拆成几个包到达,也可能几条消息粘在一起一次到达。如果不做分帧处理,解析出来的数据一定是坏的。

我的做法是维护一个字节列表,先把收到的数据追加进去,然后尝试从中提取完整帧。因为SICK的数据是XML文本,而一条消息通常以</sick>结束,所以可以用这个闭合标签作为切帧依据:

private readonly List<byte> _pending = new List<byte>(); private const string FrameEndTag = "</sick>"; private void AppendData(byte[] data, int offset, int count) { for (int i = 0; i < count; i++) _pending.Add(data[offset + i]); TryExtractFrame(); } private void TryExtractFrame() { while (true) { string text = Encoding.UTF8.GetString(_pending.ToArray()); int endIdx = text.IndexOf(FrameEndTag, StringComparison.OrdinalIgnoreCase); if (endIdx < 0) return; int frameLen = endIdx + FrameEndTag.Length; string frame = text.Substring(0, frameLen); _pending.RemoveRange(0, frameLen); FrameReceived?.Invoke(frame); } }

这段代码逻辑不复杂,但解决了一个关键问题:无论网络层把数据切得多碎,只要设备发的是完整XML消息,最终都能正确拼出来。如果标签上报频率很高,比如每秒几十次,这样全量转字符串再查找会有点浪费,但一般产线场景完全够用。真遇到高频场景,可以改造成逐字节匹配结束标签的内存流方式。

3.3 心跳与看门狗:为什么连接会莫名断开

SICK的设备端有一个看门狗机制。连接建立后,如果上位机在一段时间内没有发送任何指令,设备可能会主动断开连接,目的是防止僵尸连接占用资源。所以上位机要定时发送心跳消息保活。

我用一个System.Timers.Timer,每隔5秒发一次心跳。不同设备的保活消息格式不一样,有的设备是发看门狗配置命令,有的是发一个空指令,具体以手册为准。

private System.Timers.Timer _heartbeatTimer; private void StartHeartbeat(int intervalMs = 5000) { _heartbeatTimer = new System.Timers.Timer(intervalMs); _heartbeatTimer.Elapsed += async (s, e) => { try { await SendAsync("<sick><heartbeat/></sick>"); } catch { // 发送失败交给断线重连逻辑处理 } }; _heartbeatTimer.Start(); }

发送命令的方法也很简单,把字符串转成UTF8字节,写到网络流里:

public async Task SendAsync(string xml) { if (_tcp == null || !_tcp.Connected) return; byte[] data = Encoding.UTF8.GetBytes(xml); await _tcp.GetStream().WriteAsync(data, 0, data.Length); }

3.4 断线重连:产线程序的基本修养

上位机程序跑在产线上,一跑就是几个月,网络抖动、交换机重启、读卡器断电都是会发生的。如果断线后不自动重连,系统就等着宕机吧。我在Disconnected事件触发后,会启动一个重连循环:

private async Task ReconnectLoopAsync() { while (!_cts.IsCancellationRequested) { try { await Task.Delay(3000, _cts.Token); if (_tcp == null || !_tcp.Connected) { await ConnectAsync(_ip, _port); break; // 重连成功,退出循环 } } catch { } } }

重连必须加延时,不然设备还没恢复,你这边就以毫秒级频率疯狂尝试连接,把自己的CPU和日志都刷爆了。我一般用3秒到5秒的固定延时,如果项目要求高,可以做指数退避,比如第一次等2秒,第二次等4秒,直到上限。

3.5 封装成上位机能直接用的模块

把上面这些组合起来,对外暴露的数据事件可以再细化。比如解析完XML后,直接抛出一个TagReadEventArgs,里面带上标签ID、RSSI、天线号,WinForm里只要一行代码就能订阅:

client.TagRead += (s, e) => { textBox1.Invoke(() => { listBox1.Items.Add($"{e.TagId} RSSI: {e.Rssi} Ant: {e.Antenna}"); }); };

这里注意,事件回调是在异步接收线程里触发的,UI控件更新一定要用InvokeBeginInvoke,否则会报跨线程操作异常。很多新手在这里卡很久,其实就是一个线程切换问题。

4. 实操过程与首次联调记录

4.1 第一次把SICK读卡器跑通,我做了这几步

拿到读卡器,别急着写代码。我强烈建议先花半小时,用SICK官方的配置软件SOPAS ET把设备摸一遍,确认硬件和网络都正常,再回过来写C#。

具体步骤是这样的:先给读卡器接上电源和网线,网线可以直接插到电脑网口,也可以进交换机。然后用SOPAS ET扫描网段里的设备,找到读卡器后进入参数配置界面,把IP地址设成固定IP,比如192.168.1.10,电脑这边设成192.168.1.100,保证两个IP在同一网段。

接下来在配置软件里把读卡器的工作模式设置成自动读取,也就是说不需要上位机发读卡指令,只要标签进射频场,设备就主动上报。这个模式在上位机集成时最省事。还要注意看一下天线功率,功率太低,标签离得远一点就完全读不到。

网络和射频都确认无误后,再用最小化的代码验证TCP通信。我这里说的最小化代码,就是一个控制台程序,连接读卡器,把所有收到的数据打印出来。放一张卡进去,如果控制台里能看到类似XML的文本,说明整条链路已经通了,剩下的就是解析和封装。

4.2 上线前必须确认的几个设备参数

代码写完后,真正上线前,有几个设备参数我会再检查一遍。

第一个是天线功率。读取距离不够时,首先想到的不是改代码,而是去配置软件里把输出功率调大,同时检查标签的安装位置,是不是被金属遮挡了。第二个是读卡区域。有的设备支持多天线,要确认你读的是不是正确的天线区域,别出现A天线的数据走到B天线的逻辑里。第三个是EPC过滤。如果现场同时有多个标签,可以设置只上报符合某个前缀的标签,减少无用数据。

还有一个容易被忽略的点:SICK读卡器的输出格式。有些设备可以配置输出为SICK默认的XML格式,也可以配置输出为自定义二进制格式。一定要保证设备配置和你上位机解析逻辑是一致的。我见过有项目改了半天代码,最后发现是设备输出格式被人改掉了。

4.3 调试工具:没有模拟器就自己造

做这类项目,手头有几个调试工具能省很多事。第一是SICK的SOPAS ET,前面说过,调参数、看状态必备。第二是Wireshark,抓包看TCP数据流,确认设备到底发了什么过来,这是排查半包粘包和协议格式的利器。第三是自己写一个TCP调试小工具,可以手动连接读卡器,发任意指令,看返回数据,方便验证心跳和看门狗逻辑。

我还会在程序里加一个原始数据日志开关。调试阶段把所有收到的字节同时以十六进制和UTF8文本两种形式写进日志文件。十六进制可以确认底层数据是否有异常字节,文本则方便直接阅读XML内容。正式上线之后,我会把这个开关关掉,避免日志文件无限膨胀。

5. 常见问题排查与避坑经验

5.1 连不上、连上就断:网络与参数排查

很多人搜索“RFID数据连接错误”时,其实遇到的都是类似下面这些情况。我根据自己的现场经验整理了一个速查表:

现象可能原因解决方法
TCP连接失败IP不在同一网段、防火墙拦截先ping设备IP,再telnet端口测通
连上后几秒就断未处理设备看门狗按协议设置看门狗或定时发心跳
连接正常但无标签数据设备未开自动读取、天线功率过低在配置软件里开启自动读取、调高功率
数据不完整或乱码半包粘包没处理、编码不一致按结束标签切帧,统一使用UTF8
标签读到了但重复报送标签长时间停在射频场业务层按标签ID+时间戳去重

其中“连上后几秒就断”这个问题,我印象最深。第一次调试时,连接建立成功,但不到30秒就被设备主动断开,查了半天才发现是看门狗机制。后来加上心跳保活,问题马上消失。这类问题光看代码看不出来,一定要先了解设备协议的特性。

5.2 半包和粘包是常态,不是异常

TCP分帧问题值得单独拿出来说。读卡器上报数据时,网络层可能把一条完整的XML消息切成两次发送,第一次收到前半段,第二次才收到后半段。如果代码里拿到数据就立刻解析,那前半段肯定是解析失败的。

正确做法就是我第3.2节写的缓冲切帧:收到数据先放进缓冲区,每次都尝试从缓冲区里提取完整帧,提取不到就继续等后面的数据。这样无论网络怎么切包,最终都能正确拼出完整消息。

还有一种情况是多条消息粘在一起。比如标签读取频率很高,几条XML消息背靠背到达。这时候切帧逻辑要放在while循环里,一次把所有完整帧都提取出来,而不是只取一条。我见过有人只取了一条帧,剩下半条留在缓冲区里,导致下一条消息解析错位,整个数据流全乱了。

5.3 现场踩坑清单

最后整理几条我做这类项目的经验,每一条都是用加班换来的。

先说TcpClient.Connected属性。这个属性不可靠,它只表示最后一次通信时连接是通的,不代表现在仍然正常。我判断连接是否存活的可靠方式,是看接收循环是否还在运行,一旦ReadAsync抛异常或者返回0,就说明连接已经断了。

再说日志。上位机程序一定要有完善的日志框架,除了业务数据,还要记录连接建立、断开、重连这些状态变化。否则程序在产线上跑几个月后突然出现问题,你连当时发生了什么都不知道,排障会非常痛苦。

还有UI线程的坑。接收数据的事件回调发生在后台线程,如果你直接去更新WinForm控件,会触发跨线程异常。解决方案就是Invoke或者使用SynchronizationContext。这个不难,但几乎每个新手都会忘。

另外就是重连逻辑。不要做无退避的疯狂重连,那会把CPU打满,还会让日志文件变得巨大。合适的做法是固定延时3到5秒,或者指数退避。

如果项目里有多台SICK读卡器,建议每个读卡器实例化一个独立的SickRfidClient对象,不要共用一个socket。每个实例自己维护连接、接收循环和重连逻辑,互不干扰,排查问题也方便。

最后再分享一个经验:协议细节一定要以实际抓包为准,不要迷信网上找的资料。不同型号、不同固件版本,SICK的XML结构可能会有细微差别。我现在的习惯是接到项目后,先用SOPAS ET和Wireshark抓几帧真实数据,把字段确认清楚再写解析代码。这套流程走顺了,从零到一个稳定运行的读卡程序,基本一天之内能搞定。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 13:46:30

AI编程工具选型实测:免费与付费方案如何组合最划算?

我前前后后把市面上叫得出名字的AI编程工具都试了个遍&#xff0c;从免费插件到按月订阅的付费方案&#xff0c;踩过不少坑&#xff0c;也总结出了一套自己的选型逻辑。今天这篇不是给你罗列一堆官网介绍&#xff0c;而是说点实际使用的真话&#xff1a;哪些钱值得花&#xff0…

作者头像 李华
网站建设 2026/9/8 13:42:49

从PR淹没到自动化评审:Hermes如何用大语言模型重塑代码质量门禁

最近团队把代码评审的活儿交给了一个自动化工具&#xff0c;叫 Hermes。一开始只是抱着试试看的心态&#xff0c;想着能帮我们少点重复劳动&#xff0c;结果跑了一段时间&#xff0c;这玩意儿确实把 GitHub 上的 PR 审查流程捋顺了不少。今天就把我们接入 Hermes 做自动化代码评…

作者头像 李华
网站建设 2026/9/8 13:41:11

S7-1200与WinCC立体车库控制系统实战:从PLC程序到PLCSIM仿真

先说个事&#xff1a;我见过不少人把立体车库的电气控制系统想得很简单&#xff0c;觉得无非就是几个电机正反转、几个限位开关、一块触摸屏。真把项目接到手里才明白&#xff0c;麻烦的不是单个动作&#xff0c;而是“一排车位、两层甚至三层、还要防止人和车同时出问题”的调…

作者头像 李华
网站建设 2026/9/8 13:41:04

视频加载技术实现与用户体验优化全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 13:40:03

Java目录监控实战:基于WatchService实现文件变动实时监听

简介&#xff1a;一个基于 Java 的文件系统监控工具资源包&#xff0c;适合想掌握目录监听、文件变更检测与事件驱动编程的开发者学习参考。程序采用 java.io.File 获取文件属性&#xff0c;并结合 java.nio.file.WatchService 或 commons-io 的 FileAlterationObserver 实现目…

作者头像 李华
网站建设 2026/9/8 13:38:19

RAID配置实战:各品牌主板BIOS与阵列卡开启全攻略

简介&#xff1a;这是一份面向IT运维、装机爱好者及数据存储用户的RAID配置入门图解资料&#xff0c;针对“不同主板上如何开启RAID”这一常见实操痛点&#xff0c;系统讲解RAID 0/1/5/6四种级别的工作原理&#xff0c;以及从BIOS进入、SATA模式切换、创建阵列到初始化格式化的…

作者头像 李华