多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

扫码枪网口TCP通讯原理与Python Socket服务端demo实践

扫码枪网口TCP通讯原理与Python Socket服务端demo实践 简介针对基恩士扫码枪的网口 TCP 通讯 demo 及源码面向需要快速实现扫码枪与上位机实时数据交互的工业软件开发人员适用于物流、仓储、制造等自动化场景。方案采用客户端与服务端模型计算机作为服务端等待扫码枪连接可下发触发指令并接收条码或二维码回传内置异步重连机制连接异常断开后可自动恢复降低网络闪断造成的数据丢失风险。压缩包共 51 个文件以 13 个 cs 源码文件为核心另有 3 个可直接运行的 exe、3 个 dll 动态库、5 个 pdb 调试符号、3 个配置文件及 2 个说明文本整体约 291KB结构紧凑既适合直接部署也方便二次修改。这套源码经过实际测试无需额外配置即可启动目前已有 3992 人学习/下载。通过继承其中的扫码枪通讯基类开发者可以快速扩展数据解析与业务逻辑整体可作为扫码枪 TCP 接入的基础框架。1. 扫码枪网口TCP通讯demo是什么一把枪、一个端口、一段源码产线上换网口扫码枪最头疼的不是枪本身而是上位机。USB枪插上就能用网口枪只有“支持TCP/IP”这半句说明书剩下的全靠自己猜。扫码枪网口TCP通讯demo就是解决这个“猜”的用一个最小可复现的socket服务端监听固定端口把扫码枪发来的条码数据通过TCP连接收下来拆包后打印或入库。适合正在给MES、WMS、出入库系统接网口扫码枪的工程师。有了这套demo和源码不用从tcp协议栈开始啃改改IP和端口就能跑通一条完整链路。但想不翻车得先弄明白枪到底是以客户端还是服务端的角色连过来。2. 扫码枪网口TCP通讯原理先搞清楚枪是客户端还是服务端2.1 两种工作模式扫码枪主动连还是等待上位机连网口扫码枪本质是一个网络终端常见的工业品牌都支持在配置界面里切换“TCP Client”和“TCP Server”两种模式。TCP Client模式是最推荐的扫码枪扫描到条码后主动去连接你预先填写好的目标IP和端口把条码数据以TCP数据包发出去发完可以保持连接也可以断开。上位机只需要起一个服务端监听不需要关心枪什么时候来、什么时候走。另一种TCP Server模式则相反扫码枪自己在一个端口上等待上位机需要主动连过去适合枪的位置固定、IP不变、需要连续读取的场景但代价是上位机要维护连接状态掉线以后扫码枪不会主动把数据推过来。我一般优先选TCP Client模式。原因很实际现场网络偶尔抖动枪自己会重连上位机不用写心跳保持。如果选TCP Server模式上位机掉线那几分钟里扫过的条码可能直接积压在枪的缓冲里等上位机恢复后再补发业务上很难解释。配置时要注意枪上填的“目标IP”是工控机的IP不是枪自己的“目标端口”必须和服务端监听端口一致。很多枪出厂默认是串口透传网口服务端根本没打开需要先用厂商配置软件或者扫说明书的配置码切换。这里还有一个常见误区把扫码枪的端口号设成和网上搜的“默认9100”一样但服务端监听的是另一个端口导致枪一直在尝试连接却被拒绝。在实际项目中端口号的选择不止是躲开冲突。常见做法是用某个固定端口比如9200、9210之类然后在上位机防火墙里单独放行。切忌用telnet去试一连串端口那只能验证网络通不通验证不了扫码枪的帧格式。2.2 条码数据的帧格式结束符、中文编码和一个容易踩的坑扫码枪通过TCP发送的数据并不是裸的条码字符串。绝大多数扫码枪会在条码末尾追加一个结束符常见的是CR-LF回车换行也有的默认只有LF或者被配置成无结束符。所以服务端收到的内容可能是“ABC-12345\r\n”也可能是“ABC-12345\n”。如果代码把整个recv结果直接打印会把换行符也打出来如果按“收到一个包就是一条码”处理一旦网络栈把两条条码合并成一个包就会把一整行当成一条码。正确做法是先按字节累积到缓冲区遇到换行符再切开切出来的行去掉末尾的回车符这才是干净的条码。中文编码是这个链路里最容易出乱码的地方。英文数字条码用ASCII解析没错但如果标签里包含汉字、中文生僻字扫码枪会按配置的字符集发送常见是GBK或UTF-8。如果上位机一律用UTF-8解码遇到GBK字节流就会得到一堆“”。处理办法是分两步先按字节累积直到拿到完整一帧再尝试解码UTF-8失败之后回退到GBK再试一次。注意不要先decode再按行切因为多字节字符可能恰好在recv边界被截断提前解码会直接把半个中文字符变成错误字节后面补再多逻辑都救不回来。还有一个容易被忽略的坑扫码枪在读到空码或识别失败时会发送固定字符串比如“NO READ”或“READ NG”。这些不是条码是状态码。如果业务系统把这些字符当条码入库后期对账会非常痛苦。demo阶段可以先把这类字符串过滤掉或者单独在日志里标记但绝不能当正常条码处理。2.3 粘包、半包、断线重连为什么串口经验在TCP里会翻车写过串口通讯的工程师容易有一个惯性每次收到一包数据就认为这是一条完整消息。但TCP和串口完全不是一回事TCP是流协议底层tcp协议栈只保证字节顺序和可达性不保证一次recv恰好拿到一整条业务消息。一个条码可能被拆成两个TCP段这叫半包两条条码可能合并成一个段这叫粘包。扫码枪的发送频率不高但网络缓冲、调度器、防火墙都会影响包的边界。所以任何正经的TCP解析代码都必须维护一个字节缓冲区把每次recv到的数据放进去再按约定的结束符循环切分切剩下的残留在缓冲里等下一次数据到达。连接生命周期也要提前设计好。有的扫码枪是每次扫码建一条TCP连接发完立即断开有的是连接建立后长期保持每次只发一行数据。前一种模式服务端在recv返回0时必须把缓冲区里剩余的数据也处理掉因为最后一条条码可能还没带换行符就被枪断开了。后一种模式服务端要处理“连接一直挂着但长时间没数据”的情况设置读超时或空闲告警防止枪掉到网上某个角落却不自知。断线重连在扫码枪场景里被很多人挂在嘴边但真正写起来要分清是哪一端去重连。如果是TCP Client模式的枪重连逻辑在枪里上位机服务端只需要保证端口一直监听如果是TCP Server模式的枪重连逻辑在上位机必须把重连做成带退避的循环不能每秒钟拼命去连同一把没开机的枪。从工程角度说TCP Client模式省掉了这整套重连复杂度所以我很少让上位机主动去连枪。3. 跑通一个扫码枪网口TCP服务端demoPython socket完整代码3.1 最小可用的服务端代码接收、拆包、打印条码先用Python标准库写一个不出幺蛾子的服务端demo不需要安装任何第三方包。核心逻辑是监听端口、接收字节流、按换行符切分条码、处理编码后打印。代码直接可运行但请一定看后面的参数说明再改。import socket import threading from datetime import datetime LISTEN_IP 0.0.0.0 # 监听所有网卡工控机可能有多块网卡 LISTEN_PORT 9100 # 端口号要和扫码枪目标端口一致 BUFSIZE 1024 def process_barcode(line): # 去掉行尾可能的 \r只保留条码主体 line line.strip(b\r) try: text line.decode(utf-8) except UnicodeDecodeError: # 遇到 GBK 编码的中文条码退一步再解一次 text line.decode(gbk, errorsreplace) print(f[条码] {text}) def handle_conn(conn, addr): print(f[连接] {addr[0]}:{addr[1]} - {datetime.now():%H:%M:%S}) buffer b try: while True: data conn.recv(BUFSIZE) if not data: # 对端关闭把缓冲里最后一条没有换行符的数据也吐出来 if buffer: process_barcode(buffer) break buffer data # 按行切分b\n是大多数扫码枪默认结束符 while b\n in buffer: line, buffer buffer.split(b\n, 1) if line: process_barcode(line) finally: conn.close() print(f[断开] {addr[0]}:{addr[1]}) def start(): srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SO_REUSEADDR 解决重启时端口被 TIME_WAIT 占用的问题 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((LISTEN_IP, LISTEN_PORT)) srv.listen(5) print(f[监听] {LISTEN_IP}:{LISTEN_PORT}) while True: conn, addr srv.accept() threading.Thread(targethandle_conn, args(conn, addr), daemonTrue).start() if __name__ __main__: start()这段代码的关键在于对TCP流的处理每次recv到的数据先放进buffer再检查里面有没有换行符。如果有就切到换行符为止剩余部分继续留在buffer里等下一批数据再处理。这样不管扫码枪怎么发、底层网络怎么分片最终都能还原出完整条码。buffer的残留逻辑在连接关闭时相当重要因为最后一条数据很可能没带换行符就被枪关闭了不处理会丢码。参数方面值得展开说几个。LISTEN_IP用“0.0.0.0”是监听所有网卡地址这样扫码枪走哪个网段都能连如果只希望允许某一网段可以改成工控机实际的IP例如“192.168.1.10”。LISTEN_PORT要和扫码枪配置的“服务器端口”完全一致这是最常见的返工原因。BUFSIZE1024对条码足够因为普通条码最多几百字节但不要因为够用就假定一次recv能收完整条码TCP协议栈不受BUFSIZE限制该拆还是拆。listen(5)是控制未accept的握手队列长度单把枪足够了多枪并发可以适当调大但真正瓶颈在于应用层处理速度而不是这个数字。3.2 模拟扫码枪发送测试telnet与Python客户端两种方式写好的服务端不能干等实体扫码枪得先自己验证。最简单的方式是用telnet连到服务端手动输入一串字符后回车。如果服务端打印出条码说明监听和拆包逻辑基本正常。但telnet有个问题它发送的是终端输入的字节协议栈可能还会加一些控制字符而且不好模拟“扫码后立刻断开”的场景。所以我更推荐用一个Python客户端脚本当“假扫码枪”精确控制发送内容、结束符和连接断开时机。import socket import time # 模拟扫码枪主动连接服务端发送一条条码后断开 HOST 127.0.0.1 PORT 9100 def send_barcode(barcode: str, end\n): # 拼上换行符模拟扫码枪的帧结束标记 payload barcode.encode(utf-8) end.encode(utf-8) with socket.create_connection((HOST, PORT), timeout3) as s: s.sendall(payload) time.sleep(0.05) # 给对端一点处理时间避免立刻关闭导致数据丢失 if __name__ __main__: send_barcode(ABC-12345) send_barcode(中文条码测试)这个脚本看似简单但把几个关键细节都覆盖了sendall保证全部字节发出不会因为网络缓冲只发一半0.05秒的sleep模拟扫码枪发完包后稍作停顿的现象with语句确保发送后连接正常关闭从而触发服务端的断开处理逻辑。测试时可以故意把end参数改成空字符串“”验证服务端在连接断开时能不能把缓冲里最后一段没有换行符的条码处理掉。这个测试用例非常值得保留它专门用来防止“丢最后一条”的回归。3.3 扫码枪端参数配置IP、端口、协议类型怎么填代码跑通以后真正接枪时要在扫码枪配置界面或配置码里填一堆参数。不同品牌菜单叫法不一样但核心参数是下面这张表照着捋一遍就不会乱。参数项常见配置说明通讯模式TCP Client让枪主动连上位机是最稳的组合服务器IP192.168.1.10工控机IP填枪自己的IP就错了服务器端口9100必须与服务端监听端口一致连接方式发送后断开 / 保持连接每扫码建连更稳但断开频繁结束符CRLF 或 LF必须与服务端按\r\n或\n切分匹配字符集UTF-8 或 GBK中文条码必查否则乱码输出格式条码 结束符不要勾选“前缀/后缀”额外内容配置方式通常有两种用厂商配置软件通过USB或串口连接枪生成配置码后扫描或者直接扫描说明书打印好的配置码条码。这里要特别提醒一旦把枪切成网口TCP Client模式它的串口和USB口可能不再输出数据配置软件就够不着它了。所以我一般会先把枪的IP、端口、结束符都写成一条配置码备份在工控机目录里万一需要恢复配置还能再扫回默认值。不要等到枪变砖才想起没有后悔药。4. 源码改造清单从demo到能上线的扫码枪通讯模块4.1 多枪并发与队列解耦demo里的服务端是每来一个连接就开一个线程拆包逻辑直接在当前线程里跑。这在单枪验证时没问题但一上线就会发现如果扫码枪密集扫码而业务线程在写数据库、调MES接口socket线程会卡住recv的缓冲区被积压枪端看到的是“连上了但没人读”。真正上线的通讯模块要把“接收条码”和“处理条码”解耦。import queue import threading import time barcode_queue queue.Queue(maxsize200) # 背压队列满了就阻塞socket线程 def process_barcode(line): line line.strip(b\r) try: text line.decode(utf-8) except UnicodeDecodeError: text line.decode(gbk, errorsreplace) # 不再直接处理而是放进队列由业务线程干重活 if text: barcode_queue.put(text) def business_worker(): # 独立线程消费队列写库或推MQ都放在这里 while True: text barcode_queue.get() print(f[入库] {text}, flushTrue) # 这里可以接数据库、API、日志文件 def start(): # socket监听、accept逻辑同demo threading.Thread(targetbusiness_worker, daemonTrue).start()队列的maxsize是背压的关键参数。设为200表示当业务线程卡住时最多缓存200条条码再往队列写就会被阻塞从而反向压住socket线程避免内存无限上涨。这种积压比丢数据更容易排查至少队列长度能反映业务处理能力是否够用。注意Python的queue.Queue本身是线程安全的不需要额外加锁。多枪场景下每个连接的线程只负责拆包写入队列消费端只有一个业务线程也不会乱序因为同一把枪的条码顺序由队列先进先出保证。4.2 断线重连与掉线告警如果坚持用了TCP Server模式的扫码枪上位机就必须实现客户端重连。常见做法是用一个独立线程循环connect失败后按1秒、2秒、4秒的指数退避重试连接成功后再进入“拉数据”循环。这个重连逻辑要放在独立线程里不能阻塞主流程。下面是重连骨架。def connect_with_retry(host, port): retry_delay 1 while True: try: s socket.create_connection((host, port), timeout3) print(f[重连成功] {host}:{port}) return s except OSError as e: print(f[重连失败] {e}, {retry_delay}s 后重试) time.sleep(retry_delay) # 退避上限设为30秒避免现场网络抖动后恢复太慢 retry_delay min(retry_delay * 2, 30)这段代码在网络抖动恢复后能自动接回来但要注意异常处理里不能只捕获ConnectionRefusedErrorDNS解析失败、超时都属于OSError要一起兜住。对于TCP Client模式的枪上位机不需要重连但要在服务端加“空闲超时”。比如一把枪配置为保持连接但现场把枪线拔了服务端TCP连接可能还挂着。解法是在handle_conn里设置conn.settimeout(300)超过300秒没收到数据就主动关闭并告警。注意这个超时是“完全无数据”的时长不是“无条码”的时长。还有一个容易误判的坑TCP Client模式的枪每次扫码都新建连接发完就断开同一把枪可能一小时内建立几十次连接。如果把“连接断开”当成掉线告警日志会被刷爆。正确的告警维度应该是“连续N分钟没有任何条码数据”而不是“连接断开”。所以掉线检测要放在业务层统计最后一次条码到达时间超过阈值才通知。4.3 日志落盘与异常数据过滤demo里的print只适合开发期上线后必须有落盘日志最好按天分文件并同时记录来源IP和原始字节。来源IP可以区分不同的工位枪原始字节能帮你复盘乱码到底出在编码还是出在发送端。日志格式不搞复杂用标准logging模块加上时间和来源IP就够了。对于异常数据建议在process_barcode里先做一层白名单过滤再进队列。def clean_barcode(raw_text: str) - str: # 去掉扫码枪返回的错误状态 if raw_text.upper() in {NO READ, READ NG, FAIL}: return # 去掉首尾不可见字符和全角空格 return raw_text.strip().replace(\u3000, ).upper()过滤规则要克制别做太多业务判断。有的条码本身区分大小写、包含空格你不能因为“看起来不太像条码”就给过滤掉那会让产线数据少一条。我见过最迷惑的现场是条码被打印成含小写字母的QR码上位机因为统一做了upper()而丢失了原始信息。所以白名单过滤只处理“扫码失败”的固定状态码数据清洗放到业务系统里去决定。5. 扫码枪TCP通讯避坑指南5个常见问题与排查方法5.1 服务端重启时报Address already in use现象程序停止后马上重启socket bind报错端口被占用。原因TCP连接断开后由主动关闭方产生TIME_WAIT状态的连接占用端口一段时间。如果服务端先关闭且端口没设SO_REUSEADDR就会撞上这个问题。解决在socket创建后立刻加一行srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)让TIME_WAIT端口能被重新绑定。如果加了仍然报错用netstat -ano | findstr 9100查看端口被哪个PID占着确定是不是还活着的老进程别盲目kill。5.2 条码首字母偶尔丢失现象实枪扫码收到的条码身份证或批次号经常少第一位但日志里同一把枪有时正常。原因很多扫码枪在“按字符发送”模式下会在第一个字符前多塞一个触发前缀或者上位机把TCP数据里的ASCII控制字符当成了条码一部分后又被strip掉。解决先用抓包或模拟客户端确认枪发出的原始字节序列看第一个字符是不是真正的条码字符然后尽量把扫码枪配置成“按数据块发送”不要在帧头加前缀。我遇到的案例是枪配置了“起始符”为STX上位机没过滤导致条码被截断。5.3 中文条码出现半个字符乱码现象标签上的汉字在日志里变成“”或者两个半截字符。原因字符集不匹配或者服务端在字节边界上提前解码。解决第一优先保证按换行符切出完整帧后再尝试decode。具体到实现先decode(utf-8)失败后回退decode(gbk)。如果枪可以配置字符集统一设为UTF-8并让条码标签的中文也按UTF-8生成能最大程度避免现场差异。注意不要试图在decode失败后“修复”半字符半字符一旦出现已经丢失了编码边界必须回到原始字节重新切。5.4 枪连上服务端但始终不发数据现象服务端看到连接建立但收不到任何条码。原因扫码枪被配成了TCP Server模式正坐在端口上等上位机来连而你的服务端也在等枪来连两边都在等。解决把枪切换成TCP Client模式或者让上位机主动去连枪。判断方法很简单看连接是哪一方建立的。在服务端打印“[连接]”说明枪主动连过来如果枪没有建连动作基本可以确定枪在Server模式。另外检查枪的“目标端口”是不是真的指向服务端监听端口扫码枪配置软件里填写的端口号和服务端LISTEN_PORT不一致也会出现“连接被拒绝”的日志。5.5 频繁“连接-断开”刷屏现象日志里同一把枪每几秒就记录一次连接和断开业务上却没有新的条码。原因部分扫码枪配置了“发送后断开连接”每次扫码都会建连发数据然后断开这本身是正常行为但如果你把每次断开都当成异常告警就会刷屏。解决把告警从“连接断开”改成“超过N分钟无业务条码”。还有一个可能是条码内容带心跳字符某些枪在上电后会周期性发送测试数据服务端没过滤误当成正常业务处理。遇到这种情况先把日志里的原始字节打出来确认发送方到底发了什么。6. 进阶验证技巧用一门新语言重写demo前先做的三件事如果你准备把demo移植到Java、C#或Go收到源码后别急着翻译先做三件事。第一件用抓包工具或直接改造模拟客户端把真实扫码枪发出的原始字节记下来确认结束符到底是LF、CRLF还是无结束符确认字符集是UTF-8还是GBK。这一步决定重写时按行切分和decode的边界条件也是整个程序最容易被抄错的地方。第二件写一个多线程模拟扫码枪脚本用同一个脚本连续发几千条条码统计接收端丢码率和顺序错乱率。TCP本身不会丢数据但你的重连逻辑、队列背压、超时处理任何一个有问题都会在高频下露出马脚。第三件在上线清单里加上一句“检查TCP全局参数”用netsh int tcp show global看一眼当前系统的TCP时间戳、窗口缩放等设置确认没有被人为改到异常值。这个命令只是排查手段不要凭感觉乱改真正需要动它的时候是网络工程师拿着抓包证据来的。我第一次接网口扫码枪时就是没抓包凭经验按Windows的\r\n切行结果枪实际发的是\n导致每条条码尾部多一个空行当时还以为是条码本身的问题白折腾了一下午。后来养成习惯任何新设备上位机对接第一件事永远是先看原始字节再写解析逻辑。这套demo和源码也一样跑通只是起点真正值钱的是调那一两个边界参数的经验。希望帮到你。本文还有配套的精品资源点击获取
返回列表