简介面向物流、仓储、制造等行业中需要对接基恩士扫码枪的C#开发人员这套示例围绕扫码枪网口TCP通讯的完整实现展开覆盖服务端监听、客户端连接、指令触发、数据回传与异步重连等关键环节帮助快速打通扫码设备与上位机之间的数据链路。压缩包共51个文件、约291KB主体为13个C#源码文件及配套工程.sln/.csproj另含可直接运行的exe、依赖dll与config配置文件并带少量pdb调试符号和资源文件便于直接部署和二次修改。目前已有3992人浏览学习印证了其在工业扫码场景中的实用参考价值。程序以计算机为服务端、扫码枪为客户端的交互模式可通过指令触发扫码并实时获取条形码/二维码数据异步重连机制能在网络波动时自动恢复连接降低数据丢失风险。源码结构清晰继承基类即可按业务扩展适合入门TCP通讯或快速集成基恩士扫码设备。1. 扫码枪网口TCP通讯先分清“谁连谁”不然demo跑起来就是黑匣子很多生产线上扫码枪的网线都插上了指示灯也亮但上位机软件就是连不上。这通常不是扫码枪坏了而是“扫码枪网口TCP通讯demo及源码”里最关键的一个问题没搞清楚这台扫码枪到底是 TCP Server 还是 TCP Client。网口扫码枪内部带有一套完整的 TCP/IP 协议栈通过网口暴露一个端口上位机要做的事情只有两种——主动连它或者等它来连。选错模式socket 永远握不上手后面写的读取、入库、大屏显示全都白搭。这篇文章先帮你把通讯模型立住再给一个不依赖第三方库的 Python demo最后把产线上最容易翻车的几个坑挨个拆开。2. 网口扫码枪的TCP协议栈选型Server/Client模式与串口透传的差别2.1 扫码枪内置的TCP服务端上位机主动连端口拿到一款网口扫码枪第一步不是打开 IDE而是看设备说明书里的“上位机通讯”章节。绝大多数固定式扫码枪出厂时处于 TCP Server 模式也就是说扫码枪上电后会在某个 TCP 端口上监听等待上位机连接。常见的默认端口是 2001 或 9001。这种设计的理由是扫码枪的数据是“扫一下发一帧”上位机不一定每时每刻准备收让扫码枪一直挂着监听上位机随时可以连上来拉数据产线调度最方便。验证方法也很快把扫码枪的 IP 配成192.168.1.20上位机网段改成192.168.1.x然后在 CMD 里执行一条命令。如果端口通黑窗口会变成空白而不是报“无法打开到主机的连接”说明这把枪就是服务端上位机要写 client。如果 telnet 报连接失败也别急着判定枪坏了可能模式反了。ping 192.168.1.20 telnet 192.168.1.20 2001这种模式的优点是实现直接一条socket.connect就能拿到数据流缺点是扫码枪的 TCP 连接数通常有限如果上位机程序崩了没有正常 close端口会进入 TIME_WAIT重启时容易被卡住所以上位机代码一定要做好重连和端口释放。2.2 扫码枪作为TCP客户端上位机需要先起监听另一类扫码枪尤其是无线扫码枪和需要由上位机统一调度的设备配置方式反过来扫码枪里填一个服务器的 IP 和端口上电后主动向上位机发起 TCP 连接。这种模式下上位机程序必须提前在指定端口监听不然扫码枪会反复尝试建立连接设备管理页面里会不停报“连接失败”。上位机监听这类连接时我一般会把SO_REUSEADDR打开否则程序重启时经常撞上Address already in use。如果产线上有多把枪最好用客户端 IP 或者独立的连接对象来区分每把枪单独建一条会话。需要注意的是这种模式里一条条码发完连接可能保持也可能马上断开取决于扫码枪设置里的“保持心跳”选项。如果连接反复断开就需要在应用层做心跳保活。有些扫码枪的说明书写“TCP Client”但实际是“TCP Server 自动发送”很容易误导人。快速分辨方法给扫码枪断电重启回到上位机这边用netstat -ano看端口状态。如果上位机这边出现 LISTEN说明扫码枪确实在等连接如果另一侧先发起 SYN 包说明它是主动连接方。2.3 IP、端口、触发指令与数据帧格式连接前先读说明书我把这类设备上手必查的参数固定成一张表每次接到新项目先按表核对能省掉大半天的瞎试。配置项常见取值影响IP 地址192.168.1.20上位机必须与它同网段TCP 端口2001 或 9001配错直接连不上通讯模式TCP Server / TCP Client决定谁 connect、谁 accept触发方式光感 / 手动 / 指令决定上位机是否需要发触发命令输出格式前缀 条码 后缀直接影响分帧解析逻辑字符编码GBK / UTF-8编码不一致就会出现中文乱码这里特别说一句 TCP 和 UDP 的区别。扫码枪不要用 UDP 做条码上报UDP 是面向无连接的发出去不保证到达TCP 有握手、重传和确认机制条码这种不能丢的数据就该走 TCP。有些扫码枪配置里默认是 UDP你连 TCP 端口自然一直失败先到配置页面把协议切到 TCP。触发方式也很容易忽略。有的扫码枪是“指令触发”也就是说扫枪上电后不主动出光要上位机发一条 ASCII 指令才开始扫描。这种情况下就算 TCP 连接是通的扫码枪也不会有数据推送。先读清楚说明书里的命令列表在连接建立后发一条TRIGGER之类的指令再进入读取循环。输出格式决定了你收到的数据长什么样。很多国产扫码枪默认输出CODE_123456789\r\n也就是“类型前缀 内容 回车换行”。你在代码里按\r\n分帧时前缀也会被带进条码字段如果后续要精确匹配记得把CODE_去掉。编码也是同理出厂多为 GBK上位机用 UTF-8 解码中文会出现乱码这个后面避坑章节会专门讲。3. 最小可复现的Python TCP客户端demo依赖标准库不装第三方包3.1 先跑通“上位机主动连扫码枪”的服务端模式不要一上来就上框架先用 Python 标准库socket把连接建立起来。这个脚本针对的是扫码枪做 TCP Server 的模式上位机作为客户端去连它的 2001 端口阻塞等待一条条码超时就返回。import socket def receive_barcode(host192.168.1.20, port2001, timeout3.0): # 扫码枪一般以回车换行作为一帧结束 frame_end b\r\n buffer b sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: sock.connect((host, port)) print(f已连接扫码枪 {host}:{port}) while True: chunk sock.recv(4096) if not chunk: break buffer chunk # 收到完整一帧才返回 if frame_end in buffer: line, buffer buffer.split(frame_end, 1) code line.strip().decode(utf-8, errorsignore) if code: return code except socket.timeout: return None finally: sock.close() if __name__ __main__: result receive_barcode() print(条码:, result)这段代码的核心逻辑不是recv一次就拿到一条完整数据而是把每次收进来的字节先追加到buffer里直到发现\r\n才认为一帧结束。TCP 是字节流协议不是消息流协议扫码枪发过来的一个条码可能被拆成两个 TCP 分包也可能两个条码粘在一个包里不按分隔符分帧解析一定出错。参数说明host和port改成你扫码枪背后的实际配置timeout控制等待扫码的时间产线环境建议 3 到 5 秒太短会误报超时太长会让上位机界面卡住。recv(4096)的缓冲区对条码完全够用不需要去调特别大的值。如果扫码枪输出的是 GBK 编码把decode(utf-8)改成decode(gbk)否则中文条码会变成一串乱码。3.2 扫码枪主动连上位机时demo程序倒过来监听要是你的扫码枪工作在 TCP Client 模式上位机就要反过来做监听端。这个脚本先监听 9000 端口等扫码枪连上来然后循环读数据。import socket def start_listener(port9000): server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((0.0.0.0, port)) server.listen(5) print(f上位机监听 {port}等待扫码枪接入) conn, addr server.accept() frame_end b\r\n buffer b with conn: print(f扫码枪接入: {addr}) while True: chunk conn.recv(4096) if not chunk: break buffer chunk while frame_end in buffer: line, buffer buffer.split(frame_end, 1) code line.strip().decode(gbk, errorsignore) if code: print(条码:, code)这里bind((0.0.0.0, port))表示监听本机所有网卡避免扫码枪从另外一块网卡连过来时找不到监听端口。SO_REUSEADDR是必须的否则程序 CtrlC 之后立刻重启系统还会认为端口被占着报Address already in use。accept 是阻塞的扫码枪不在线时程序会一直挂在这所以这种模式要求你先启动上位机再给扫码枪上电。如果产线不止一把枪这段单连接的代码就不够用了。常见做法是把读包循环丢进threading.Thread或者用socketserver.ThreadingTCPServer处理多连接。在 demo 阶段先把单连接跑通再谈并发。3.3 一枪多码的粘包分帧读取循环与超时参数详解条码数据在上位机里最常见的处理难题就是粘包和半包。扫码枪在一秒内连续扫了两个码TCP 可能一次性把两个帧都送过来反过来一个长的 Data Matrix 码也可能拆成两个 chunk 到达。所以分帧逻辑必须独立成一个类不能写在某个if分支里。class BarcodeFrameDecoder: def __init__(self, frame_endb\r\n, encodinggbk): self.frame_end frame_end self.encoding encoding self._buffer b def feed(self, data): self._buffer data frames [] while self.frame_end in self._buffer: line, self._buffer self._buffer.split(self.frame_end, 1) code line.strip().decode(self.encoding, errorsignore) if code: frames.append(code) return frames这个类只做一件事把网络字节流切成一条条完整的条码帧。调用方每次recv拿到 chunk就直接decoder.feed(chunk)它会返回一个列表可能是空、一个帧或多个帧。剩余的半包数据留在_buffer里等下一块数据到了再继续拼。这样不管 TCP 怎么切分上层逻辑永远按“一条条码”来处理不会丢也不会串。参数上frame_end必须和扫码枪设置一致。有些枪默认后缀是\r有些是\n有些是\r\n一帧里还可能有 STX/ETX。你要是发现条码能收到但经常截断先看枪的数据格式再回来改这个类。4. 从demo到可交付的源码工程断线重连、数据入库与配置文件4.1 用配置文件把IP、端口、扫码枪模式拆出去demo 里把 IP 和端口写死在函数参数上自己能跑但交付给现场就得拆出来。产线换了一把枪或者调试时临时改端口总不能让人去翻代码。我一般用一个config.json放在程序同级目录代码启动时读一下。{ mode: client_to_scanner, scanner_ip: 192.168.1.20, scanner_port: 2001, listen_port: 9000, encoding: gbk, frame_end: \\r\\n, timeout: 3.0, auto_reconnect: true, log_file: barcode.log }注意frame_end在 JSON 文件里写的是字面量\\r\\n不是真正的回车换行符这样人工编辑时不容易误解。Python 里读取后再转成字节串。import json def load_config(pathconfig.json): with open(path, r, encodingutf-8) as f: cfg json.load(f) # 把 \\r\\n 转换为 b\r\n cfg[frame_end] cfg[frame_end].encode(utf-8).decode(unicode_escape).encode(latin-1) return cfg cfg load_config()这里用unicode_escape做一次转义把字符串里的两个字符\r\n变成真实回车换行再编码成字节串解码分帧就能直接用。mode字段用来区分是主动连扫码枪还是监听等扫码枪连程序启动时按这个字段决定走哪条分支。把这个文件也交付给现场他们就能自己改 IP不用来找你改代码。4.2 断线重连与看门狗产线不能接受拔电后不恢复产线设备最讨厌的就是断一次线得上位机重启才能恢复。扫码枪电源是独立供电的有时候操作工拔插一下TCP 连接就断了如果你只做一次性连接程序就再也收不到数据。所以连接对象必须有重试机制而且重试不能阻塞主流程。import socket import time def connect_with_retry(scanner_ip, scanner_port, timeout, max_retry-1): retry 0 while max_retry 0 or retry max_retry: s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(timeout) try: s.connect((scanner_ip, scanner_port)) return s except OSError: time.sleep(2) retry 1 finally: pass return None注意这里连接失败后要关闭未成功的 socket否则会漏句柄。重试间隔 2 秒比较合适太短会刷爆日志太长会让操作工误以为系统坏了。连接成功后一旦recv返回空字节或者抛出异常就说明连接断了需要跳回重连循环。while True: sock connect_with_retry(cfg[scanner_ip], cfg[scanner_port], cfg[timeout]) if sock is None: time.sleep(5) continue try: while True: data sock.recv(4096) if not data: raise ConnectionError(扫码枪断线) for code in decoder.feed(data): print(code) except Exception: sock.close() time.sleep(2)这样即使扫码枪断电重启上位机也会在 2 秒后重新建立连接不需要人工干预。看门狗不是非要额外线程主循环里的connect_with_retry就是最简单的看门狗只要程序不崩连接就一直在。4.3 条码流向写到CSV、数据库还是转成Modbus TCP条码读出来了下一步往哪存取决于现场需求。我见过三种常见去向本地文件留痕、数据库长期追溯、PLC 实时读取。用一个表格直接对比去向适用场景实现成本关键点CSV 文件小产线简单留痕低多线程写入要加锁SQLite / MySQL长期追溯、报表查询中批量插入减少 commit 次数MQTT上报车间大屏、云端平台中QoS 设为 1防丢失Modbus TCPPLC 直接读取条码中把条码转 ASCII 写入保持寄存器如果用 CSV最简单也最容易踩坑。多个线程同时写文件会丢行必须加文件锁或线程锁。import csv import threading from datetime import datetime csv_lock threading.Lock() def append_barcode_to_csv(barcode, csv_pathbarcodes.csv): with csv_lock: with open(csv_path, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([datetime.now().isoformat(), barcode])with csv_lock保证同一时间只有一条线程在写文件。如果你的扫码枪读取线程和数据入库线程是分开的不加这个锁CSV 里就会出现半行或相互穿插的数据。数据库方案也是同理建议用队列把读取线程和入库线程解耦读取线程只负责往queue.Queue里放条码入库线程统一取走并写库这样扫码高峰不会卡住网络读取。如果现场要求 PLC 读到条码常规做法是把条码字符串转成 ASCII 码再按字或字节写入 Modbus 保持寄存器。这个方案需要引入一个 Modbus TCP 从站库把扫码枪 demo 里的输出接到从站寄存器区。思路不复杂但一定要提前确认 PLC 的寄存器访址方式和字节序。5. 扫码枪TCP通讯避坑清单连不上、掉线、串码的五条血泪经验5.1 收不到数据先查触发方式不是所有枪都自动扫描现象TCP 连接已经建立了用调试工具能看到链路是通的但扫几下枪上位机一个字节都收不到。原因扫码枪的触发方式被设成了“指令触发”或者启用了“光感只在特定角度触发”上位机没有发任何命令扫枪当然不出光。这是新手最容易误判的故障很多人会怀疑网线、网卡甚至扫码枪硬件坏了。解决先读说明书里触发指令那一页找到类似TRIGGER、SCAN_START的命令在连接建立后先sock.sendall(bTRIGGER\r\n)再进入读取循环。如果枪支持持续扫描模式直接在配置软件里打开“自动感应 连续扫描”让上位机只需要被动收数。我遇到过一个项目光感灵敏度调太低条码颜色浅一点就扫不到误以为是 TCP 的问题折腾了半小时才想起看触发日志。5.2 ping得通TCP连接却被防火墙或端口占用拦掉现象扫码枪和上位机同一网段ping 一直通但socket.connect报Connection refused或者timed out。原因ping 用的是 ICMP 协议走的是网卡和 IP 协议栈跟 TCP 端口没关系。连接被拒绝要么是扫码枪的监听端口没起来要么端口被本机其他程序占用要么 Windows 防火墙把入站端口拦了。产线上一些杀毒软件还会拦非白名单程序的 socket 连接这个很难从代码层看出来。解决先在本机执行netstat -ano | findstr 2001看端口有没有被监听再临时关闭 Windows 防火墙或添加端口入站规则最后用 telnet 从上位机去连。如果 telnet 都不通问题一定在扫码枪或网络设备不在你的代码。注意用完防火墙测试后要恢复默认规则不要为了省事把整个防火墙关掉再交付。5.3 中文条码变乱码GBK、UTF-8和“看不见的后缀”在打架现象ASCII 条码一切正常扫到包含中文的条码时上位机打印出来是鏉庢桩或者???这样的内容数据库里也存成了乱码。原因扫码枪出厂字符集通常是 GBK/GB2312Python 的recv拿到字节流后如果按 UTF-8 解码GBK 编码的两个或三个字节就会产生 Unicode 解码错误即使用了errorsignore也会把中文吞掉。更隐蔽的是有些枪会在数据末尾追加一个不可见的NULL或\r导致条码长度对不上。解决先确认扫码枪配置页的字符集能改 UTF-8 就直接改成 UTF-8上位机统一decode(utf-8)。不能改就用decode(gbk)并且在分帧时把常见的尾部不可见字符都去掉。code.strip().strip(b\x00.decode())这种连续 strip 可以作为通用兜底。我在现场吃过一次亏那时候把 GBK 数据硬按 ASCII 处理条码存进数据库之后彻底无法追溯最后只能重新扫。所以编码问题一定要在联调第一天就确认。5.4 长连接频繁掉线keepalive没开交换机把空闲连接老化了现象上位机连着扫码枪长时间不扫码时连接看起来正常但下一次扫码时recv直接抛异常或者等待超时。检查物理链路网线、交换机都正常重新连接后又能用一阵。原因TCP 长连接空闲一段时间后中间交换机或防火墙会认为连接已经失效把它静默回收。扫码枪那头还在维护着这条连接自己不知道上位机要发送数据时才发现链路已经断了。Windows 上位机默认的 TCP keepalive 周期太保守等它发现断线可能要两个小时。解决在 socket 建立后立刻打开 keepalive并缩短探测时间。sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) try: sock.ioctl(socket.SIO_KEEPALIVE_VALS, (1, 60000, 5000)) except (AttributeError, OSError): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 5) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)SIO_KEEPALIVE_VALS在 Windows 上可以把空闲探测调整为 60 秒、每 5 秒重试。Linux 上则用TCP_KEEPIDLE控制空闲时间。不要设成 10 秒这种过短值否则正常不扫码时也会制造大量探测包反而增加网络负担。如果抓包看到大量重传且两端都是 Windows管理员 CMD 里执行netsh int tcp set global timestampsenabled再用netsh int tcp show global确认开启状态这能解决某些 TCP 时间戳引发的高延迟丢包但这是全局参数不要盲目开先拿抓包结果佐证。5.5 一枪多码粘包不加分帧条码数量永远对不上现象扫码枪一次扫到多个条码上位机收到的一整包数据里混了两三个码中间没有分隔符或者一个条码被拆成两半上位机把它当成两条数据处理产线产量虚高。原因TCP 是面向字节流的不是消息边界协议。扫码枪应用层发的是code1\r\ncode2\r\nTCP 可能用一个报文全送过来也可能分两次。你如果直接对recv返回的字节做 strip很容易把一个帧截断。解决用前面写的BarcodeFrameDecoder每次收到数据就feed一次由它按frame_end分割。同时把扫码枪的输出格式配置成“每条码后追加 CRLF”并且把“无条码时不上报”开关打开避免空帧干扰。如果分帧后还是偶尔出现两个条码拼接多半是枪的“条码间延时”设了 0最好在配置里加 20 到 50 毫秒的帧间隔给上位机一点喘气的空间。6. 进阶技巧帧校验与去重队列把demo改成连续扫码不串码从 demo 到产线还有一个很容易被忽略的坑重复上报。扫码枪在弱光、反光或者条码印刷不清晰时经常会在一帧里重复发送同一条码有些枪还会在连续触发时把上一次的条码再补发一次。上位机如果没有去重成品计数就会虚高这比连不上更隐蔽。我一般会在分帧之后接一个去重队列只保留最近 N 个条码新的条码如果在队列里就直接丢弃。窗口大小根据现场节奏调通常在 10 条以内。from collections import deque class BarcodeGuard: def __init__(self, window10): self.window window self.seen deque(maxlenwindow) def accept(self, code): if code not in self.seen: self.seen.append(code) return True return Falsedeque(maxlenwindow)会在队列满时自动丢掉最旧的记录内存占用固定不会越跑越大。对于贴着传送带的高速扫码场景窗口里保存最近几个条码就够了对于人工扫描可以适当放大到几十个防止操作工扫完 A 又快速扫 A。如果扫码枪协议里自带校验位建议把帧校验也加上。有些高端扫码枪的网口协议是STX DATA ETX CHECKSUM的二进制帧这种就不用\r\n分帧了要按固定包头和长度来解析。def extract_payload(data, start0x02, end0x03): # 帧格式: STX DATA ETX CHECKSUM if len(data) 4: return None if data[0] ! start or data[-2] ! end: return None checksum sum(data[1:-2]) % 256 if checksum ! data[-1]: return None return data[1:-2].decode(gbk, errorsignore)注意不是所有枪都有这个格式只有说明书里标明“带校验”的协议才需要用这段。贸然套到纯文本帧上会误杀所有数据反而把 demo 改废了。校验方法也很直接拿一把枪在配置软件里打开“调试输出”或者用 Wireshark 在工控机网口抓包看 TCP payload 的十六进制内容一个字节一个字节地对协议。还有一个实用的本地验证技巧没有扫码枪也能测试代码。用 Python 开一个模拟服务端绑到同样的端口然后往这个端口发TEST001\r\nTEST002\r\n观察你的解析函数是否能稳定分出两条。先自动化验证逻辑再拿去接真机能省掉很多来回插拔网线的麻烦。我最早调扫码枪时以为 TCP 连上就万事大吉结果被触发方式、GBK 乱码、粘包三件事各绊了一次尤其那个编码问题排查了一个下午才发现是枪的出厂字符集没改。后来拿到不熟悉的型号我都会先确认三件事枪是 Server 还是 Client、帧结束符是什么、编码是 GBK 还是 UTF-8。这三件事敲定剩下就是套这个 demo 往上加逻辑。希望帮到你。本文还有配套的精品资源点击获取