ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

串口与TCP相互转发工具:原理、实现与远程调试实践

串口与TCP相互转发工具:原理、实现与远程调试实践 上周在客户现场调试一台边缘网关设备只留了一个RS232串口我人却在几百公里外的办公室。串口调试助手、USB转串口线这种常规武器全派不上用场。后来我把思路改了在网关边上放一台小主机写了一个串口和TCP互相转发工具。本地打开串口另一头监听TCP端口网络侧任何一条收到的数据流都会被送到串口上串口收到的字节也原样塞进TCP连接。结果就是你在办公室开一个TCP客户端就能像插着串口线一样操作远处的设备。这类工具在嵌入式调试、工业自动化、物联网设备上云里太常见了。说穿了不复杂但要做到稳定运行、不丢数据、断线能重连里面有不少坑。这篇文章会把原理、工具选型、自己动手实现的完整代码以及我实跑过程中的故障排查记录全部趟一遍。不管是远程调试串口设备、把老旧串口仪表接入网络还是做Modbus RTU转TCP的网关应该都能用得上。1. 为什么串口设备非要搭上TCP这趟车1.1 串口是硬件调试的最后十厘米TCP是网络的最后一公里串口通信也就是UART是嵌入式世界最基础的调试手段。单片机打印日志、配置寄存器、烧写固件都靠它。但串口的物理特性决定了它的传输距离很短RS232电平通常也就十五米左右就算转成RS485能跑上千米也没法直接跨城跨网。TCP则不同它天然就是为网络通信设计的局域网、公网、云平台只要网络能通TCP就能通。一个在工厂车间里的设备如果只有串口接口你要远程读它的数据唯一的办法就是中间加一层转换把串口数据装进TCP的Payload里。这个转换层就是标题里说的串口和TCP互相转发工具。它可以是一个软件进程也可以是一块硬件模块本质都是双向数据通道串口到网络网络到串口。1.2 典型场景远程调试、设备上云、协议互转远程调试是最直接的刚需。设备在客户现场出了故障工程师在办公室打开调试工具通过TCP连接远程网关的某一个端口中间经转发工具把命令写到串口设备回应的数据再沿原路返回。整个过程看起来就像设备就在本地能省掉大量出差成本。设备上云是另一个大头。以前许多传感器、电表、PLC都只有串口想上云就要加一个DTU或者边缘网关。这类硬件内部跑的核心功能就是一个串口与TCP的双向转发程序。云端下发指令到TCP本地串口执行设备上报数据从串口进来打包成TCP流发到云端。还有协议互转的场景。工业现场老设备很多走Modbus RTU但新上位机、SCADA系统往往只认Modbus TCP。这时转发工具就不仅要透传还要拆包、去校验、重新组帧把RTU帧转换成TCP报文这也是一个非常典型的应用。1.3 做这种工具到底难在哪说实话透明转发本身一点都不难。难的是把边界情况处理好设备掉线了怎么办TCP客户端反复断连怎么办串口数据量远大于TCP发送能力时怎么办多个上位机同时要读写一个串口怎么办作为一个长期要跑的中间件稳定性比功能本身更值钱。这也是我在这篇文章里反复强调排查和坑位的原因。2. 转发工具背后的数据模型串口和TCP根本就不是一回事2.1 串口关心速率和电气参数TCP只关心连接状态串口的数据本质上是一串以指定波特率流动的二进制位。双方得约定好波特率、数据位、停止位、校验位数据才能被正确解析。比如9600、8、N、1就能简写为9600-N-8-1。这个参数改错一个收到的就是乱码或者干脆收不到。TCP则是字节流协议。它不关心你的数据内容是什么、边界在哪里只保证两个端点之间建立可靠的虚拟链路数据按序到达不丢失、不重复。所以做转发时两边要处理的问题完全不同串口那头要配置波特率、校验位处理物理层异常TCP那头要管理连接状态处理三次握手、四次挥手、超时、重传。这个差异直接决定了转发程序的结构。2.2 速率不匹配是丢数据的第一来源一个典型低速串口9600波特率每秒也就传960字节左右。TCP呢局域网里每秒传几MB都非常轻松。当TCP客户端一下子发来几十KB的数据要写入串口时串口这边只能按每秒960字节慢慢吐。如果程序不做缓冲直接把数据丢给串口驱动驱动缓冲区满了后面的数据就全丢了。反过来也一样。串口以115200波特率连续上报数据时如果程序正在处理网络事件没能及时读取串口缓冲区串口驱动的接收缓冲区溢出数据就丢了。所以转发工具必须有一个缓冲机制同时要有流控意识当TCP写入串口的速度太快时要么阻塞要么丢弃并通知对方。很多现成工具默认用的是尽力而为的丢数据策略这是我最不放心的一点。2.3 透明转发与协议转换的分界线透明转发是最朴素的实现字节进字节出。工具不管数据里是Modbus、自定义协议还是纯日志原样转发。好处是通用性强、代码简单坏处是接收方必须自己处理分包、粘包的问题。协议转换则是更高级的玩法。工具需要理解协议结构拆出帧把字段重新封装成目标协议的格式。例如Modbus RTU转TCP工具要识别地址、功能码、数据区去掉CRC16加上MBAP头。这种转发已经不只是搬运而是翻译。在动手做之前先搞清楚你要的是哪种。很多人在只需求透传的时候用了协议转换工具结果发现对方发来的字节被工具解析、重组后根本不是原来的样子反而踩坑。2.4 把握核心模型两条单向管道加一个连接管理不管用Python、C还是Go来写转发工具的核心逻辑都逃不开这个模型建立两个数据源串口对象、TCP socket。串口读线程从串口读到的字节直接写到TCP socket。TCP读线程从TCP socket读到的字节直接写到串口。TCP连接管理作为服务端时等待客户端接入作为客户端时负责重连。异常处理任意一侧断开都要干净地关闭另一侧释放资源。后面第4章代码就是按这个模型写的你不用改结构直接替换语言和库就能迁移到其他平台。3. 现成工具盘点ser2net、socat、硬件串口服务器怎么选3.1 直接拿Linux上的ser2net来做生产级透传如果跑在Linux上只想快速实现一个稳定的串口转TCP我会优先看ser2net。它是我见过的老牌工具里最省心的一个。装好之后在配置文件里写一行映射规则即可2001:raw:600:/dev/ttyUSB0:115200 8DATABITS NONE 1STOPBIT这一行的意思是监听TCP端口2001以raw模式透明透传关联/dev/ttyUSB0串口设备波特率115200。客户端只要telnet 192.168.1.100 2001就能连上串口。ser2net还有一个很实用的能力支持RFC 2217也就是通过Telnet扩展远程配置串口参数。这在某些上位机软件需要动态切换波特率的场景里非常有用而自研工具要实现这个协议工作量会多出不少。3.2 socat临时调试用的瑞士军刀socat更像是一个数据通道拼接器。一条命令就能把串口和TCP监听端口绑在一起socat /dev/ttyUSB0,raw,b115200 TCP-LISTEN:2001,reuseaddr运行之后TCP 2001端口收到的数据会写到串口串口收到的数据会发给TCP连接。和ser2net相比socat的优势是灵活可以自由组合Unix Socket、UDP、文件等各种通道调试时特别好用。可一旦涉及长时间稳定运行socat的管理方式过于简陋重连策略、日志记录、权限管理都不够方便。我个人的习惯是临时验证某个硬件行为时用socat正经部署服务时用ser2net或者干脆自己写。3.3 硬件串口服务器不用管驱动和进程上电就跑如果不想依赖电脑可以考虑硬件串口服务器模块。市面上的USR-TCP232、有人物联网、周立功这类产品很多都是内置了串口转以太网固件的。你只要接上串口设备和网线在网页配置界面里填一下TCP端口它就成了一个独立的转发器。硬件方案最大的优势是功耗低、稳定性高、不占CPU还能支持宽温环境。很多工业网关、DTU内部干的就是这个活。但缺点也明显协议转换灵活性差。如果只是透明转发问题不大如果要自定义协议重装、处理特殊帧间隔硬件模块的二次开发门槛比软件高得多。3.4 我的选型参考表方案平台透明转发协议转换建议场景ser2netLinux很好稳定不支持生产环境长期透传socatLinux/Unix方便灵活不支持临时调试、实验验证自研Python小工具跨平台可定制可扩展需要日志、协议转换、多年维护硬件串口服务器独立设备很好有限工业现场、低功耗部署从我踩坑的经验看选型最忌讳反复横跳。如果只是拉一个临时测试环境socat一行命令顶得上所有纠结。如果要稳定跑几个月ser2net或硬件设备。如果后期要加Modbus转换、数据日志、自动重连那别犹豫自己写一个你后面会感谢自己的。4. 自己写一个最小可用的双向转发器Python版4.1 为什么先拿Python做原型Python搭配pyserial库和内置的socket模块可以在一两百行里实现一个非常清晰的转发器。开发速度快、逻辑容易验证后期的可移植性也还行。我很多工具都是从Python原型开始的量级不大的生产环境直接拿Python跑也没问题。依赖安装很简单只需要pyserialpip install pyserial4.2 最小实现监听一个TCP端口把数据双向透传到串口先给一个能直接跑的最小版本。这是一个典型的TCP服务端模型程序启动后监听某个端口等待客户端连接一旦有TCP客户端接入就开两个线程分别处理两个方向的转发。import serial import socket import threading SERIAL_PORT /dev/ttyUSB0 BAUDRATE 115200 TCP_HOST 0.0.0.0 TCP_PORT 2001 def uart_to_tcp(ser, client): while True: try: data ser.read(ser.in_waiting or 1) if data: client.sendall(data) except Exception: break def tcp_to_uart(ser, client): while True: try: data client.recv(1024) if not data: break ser.write(data) except Exception: break def main(): ser serial.Serial(SERIAL_PORT, BAUDRATE, timeout0.1) srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((TCP_HOST, TCP_PORT)) srv.listen(1) print(flistening on {TCP_HOST}:{TCP_PORT}, serial{SERIAL_PORT}) while True: client, addr srv.accept() print(fclient connected: {addr}) t1 threading.Thread(targetuart_to_tcp, args(ser, client), daemonTrue) t2 threading.Thread(targettcp_to_uart, args(ser, client), daemonTrue) t1.start() t2.start() t2.join() client.close() print(client disconnected, waiting for next connection) if __name__ __main__: main()这段代码的核心就是两个while循环。一个不断从串口读字节发给TCP客户端另一个不断从TCP客户端读字节写入串口。看似简单但已经能解决基本的远程串口调试问题。我一般会用两个TCP调试工具验证一个工具连TCP端口另一个打开本地串口两边互发十六进制数据看是否一致。4.3 必须补充的细节加锁、超时与干净退出上面这个最小版本有个隐患串口对象被两个线程同时读写。pyserial通常在绝大多数平台能容忍并发读写但在Windows上偶尔会报权限异常。更稳妥的做法是给串口读写加一把锁ser_lock threading.Lock() def uart_to_tcp(ser, client): while True: try: with ser_lock: data ser.read(ser.in_waiting or 1) if data: client.sendall(data) except Exception: break def tcp_to_uart(ser, client): while True: try: data client.recv(1024) if not data: break with ser_lock: ser.write(data) except Exception: break串口读要设置timeout不能永久阻塞。上面的代码在打开串口时指定了timeout0.1这样读不到数据时最多等0.1秒就会返回空字节线程不会卡死。如果你把timeout设为NoneTCP断开后串口读线程就永远阻塞在read()上程序无法退出。还有一点很容易漏TCP客户端断开时recv会返回空字节或抛异常这时两个线程都应该退出。上面的代码用异常和空字节判断来跳出循环算是比较精简的写法。如果你在更复杂的场景里建议用threading.Event做退出通知线程内部定期检查事件标志。4.4 做成命令行工具别总改代码最小版本写出来以后我建议尽快把它参数化避免每次换串口设备都要改源码。用argparse解析参数把串口号、波特率、TCP端口、TCP服务端/客户端模式都变成命令行可配置项。再往后可以加入十六进制收发日志、按行显示、自动重连TCP、串口拨插重载等功能。这些加下来转发工具就从一个演示脚本变成了能伺候生产环境的小服务。5. 实跑中踩过的坑丢数据、粘包、断线重连一线排查5.1 数据丢得莫名其妙先别怀疑代码第一次实跑时我遇到数据时有时无的问题第一反应是线程锁没写好后来排查了一圈才发现是USB转串口模块的驱动有问题。我之前在Windows上换了根线之后设备管理器里看到的不是CH340而是某个不兼容的型号波特率设置也没同步过去。换回CH340或者装对驱动问题立刻消失。所以遇到丢数据先按这个顺序排查确认串口参数波特率、数据位、校验位和远端设备一致。确认USB转串口芯片驱动正确Windows用设备管理器看有没有感叹号。确认串口读缓冲区没有溢出可以先用串口调试助手单测。再用工具跑看日志里有没有连续收发记录。步骤越基础往往越是元凶。很多所谓转发工具丢数据的问题其实是串口驱动没装好。5.2 缓冲区溢出串口数据量大于TCP处理速度时的真实表现连接正常后高波特率下容易复现一个问题TCP客户端开启之后程序偶尔收到乱码或者某一段数据中间缺了几十个字节。用抓包工具看TCP层面数据是完整的那问题就出在串口读取侧。串口驱动接收缓冲区是有限的如果程序不能及时读走新的数据会覆盖旧数据。解决方法是给串口读取分配一个独立的线程并且把读到的数据先放入队列再异步发送到TCP。不能等网络发送完成后才去读串口。我实测115200波特率下如果只用单线程边读边发概率性丢数据改成双线程加队列后长时间跑再也没有丢过。5.3 粘包到底算不算bug很多刚接触串口转TCP的人最纠结的就是粘包。比如串口发来两个Modbus RTU帧TCP接收端一次全收到了他们认为是工具的问题。我在这要说清楚对于透明转发工具粘包不是bug。TCP本身就是面向字节流的没有任何消息边界。两个连续的串口帧完全可以放在同一个TCP段里送达只要接收端按协议解析时能正确分帧就没有任何影响。工具如果强行在帧之间插入分隔符或延迟反而会破坏原始数据流。真正需要处理粘包的场景是协议转换。比如Modbus RTU转TCP时你必须从串口流里识别出一帧完整的RTU报文再封装成TCP的APDU。识别帧结尾靠的是3.5个字符时间的静默间隔。实现时可以用定时器串口收到最后一个字节后超过间隔没新数据就认为一帧结束。5.4 TCP服务端与客户端两种模式的断线处理转发工具如果作TCP服务端TCP客户端断开后程序要能继续accept新连接。我上面代码里的t2.join()会阻塞到TCP读线程结束而TCP读线程在客户端断开后将退出所以逻辑是对的。但要注意客户端断开后不能立即开新线程等待下一连接因为串口读线程可能还在老连接里阻塞最好用事件通知让两个线程一起退出。如果工具作TCP客户端主动连接远端服务器比如把串口数据推送到云平台那断线重连就重要了。我建议使用指数退避策略第一次失败等1秒第二次等2秒然后4秒、8秒最大间隔不要超过30秒。否则设备端反复快速重连服务端容易被大量连接请求打满。5.5 端口占用和防火墙是常见拦路虎自研工具第一次跑经常莫名提示端口bind失败。Windows上报错类似Only one usage of each socket address。解决方法是设置SO_REUSEADDR也就是代码里那行setsockopt。同时可以用netstat -ano | findstr 2001看看端口是不是被残留进程占了。Linux上则要小心防火墙。外部机器连不上TCP端口但在本机用telnet 127.0.0.1 2001能连上基本就是防火墙没放行。CentOS等系统上可以用firewall-cmd --add-port2001/tcp --permanent放行指定端口改完记得reload。这个操作本身不难难的是别一上来就把所有端口全开放要从最小必要权限开始。5.6 长期运行时的句柄与线程管理转发工具一旦部署到现场可能连续跑好几个月。这时候最怕的不是功能bug而是资源泄漏。每来一个TCP连接如果不及时关闭socket文件句柄数就会涨每处理一次异常如果线程不退出线程数就会涨。我把连接处理循环写成一个独立函数内部用try/finally保证连接关闭。同时给线程设置daemonTrue并且用Event标志让两个线程都能在连接断开时退出。日志里定期打印线程数和打开文件描述符数一旦发现异常可以尽早介入而不是等到系统卡死。6. 更进一步Modbus协议互转、多客户端共享与硬件化6.1 把透明转发升级成Modbus RTU/TCP网关工业现场最常见的需求不是透明透传而是Modbus协议互转。串口设备走Modbus RTU网络侧上位机走Modbus TCP两者报文格式差异很大项目Modbus RTUModbus TCP报文头无MBAP头7字节单元标识从站地址单元标识符校验CRC16无TCP可靠传输帧结束3.5字符时间静默长度字段直接指明想做这个网关转发工具需要拆包、重新封装。串口侧收到完整RTU帧后去掉CRC加上事务ID、协议ID、长度、单元标识然后通过TCP发送TCP侧收到Modbus TCP请求后去掉MBAP头加上CRC16再发到串口。这个逻辑我从零写大概三百行关键点在RTU分帧。如果你不想自己造轮子也可以先用成熟库比如Python的pymodbus它内部已经实现了RTU和TCP两种模式的编解码你要做的只是搭建转发链路。6.2 多TCP客户端同时共享一个串口别让它们乱写有时候多个上位机软件想同时读一个串口仪表比如一个看实时数据一个做配置管理。如果两个客户端同时往串口写设备会收到交错指令轻则命令无效重则设备挂掉。我的处理方案是串口读线程只有一个收到的数据广播给所有在线客户端串口写操作加锁同一时刻只允许一个客户端的数据写入串口。如果你能确认上位机会发类似Modbus请求这样的事务可以做得更精细——把请求来源记录下来串口响应只发回给对应的那个客户端这样并发性会好很多。6.3 低功耗硬件化ESP32、STM32加W5500软件工具再稳定也依赖一台电脑或小主机功耗和体积摆在那里。野外或者配电柜里长期运行还是硬件方案靠谱。ESP32方案最简单自带UART、WiFi和以太网写个几十行的Arduino程序就能完成串口转TCP透传。我做过一个最小原型成本不到二十块钱延迟在几十毫秒级非常适合小流量传感器数据上报。STM32加W5500则是更工业化的路线。W5500内置硬件TCP/IP协议栈MCU不占用资源去处理TCP状态稳定性很高。缺点是固件开发周期比Python长一些需要处理串口缓冲区、网络状态机、异常恢复。6.4 部署到Linux别忘了用systemd管起来如果是部署在Linux服务器上我不建议直接用nohup python xxx.py 跑进程挂了没人管。写一个systemd service设置Restartalways开机自启比手动管理省心太多。需要注意如果服务使用USB转串口设备设备文件路径可能在每次插拔后变化。我踩过这个坑现场设备重启后ttyUSB0变成了ttyUSB1服务起不来。解决方案是在systemd服务里用udev规则固定设备别名或者让程序启动时扫描串口设备并自动匹配。6.5 扩展方向把串口数据变成WebSocket、MQTT掌握了串口和TCP互转的核心模型就可以把出口替换成任意网络协议。把TCP socket换成WebSocket浏览器里就能实时显示串口日志换成MQTT串口数据就能直接上报云平台实现远程可视化监控。我自己做的一个版本就是把转发工具加了条WebSocket通道方便同事在网页上调试现场设备省去了大家在办公室装一堆上位机软件的麻烦。核心架构还是串口读写的两线程模型只不过网络侧从TCP换成了WebSocket。这个方向我最近还在完善等稳定一点再单独写一篇展开。总之串口和TCP的转发并不复杂但把边界条件处理好、把长期运行的稳定性做上去才是真正考验细节的地方。希望这篇文章的踩坑记录能帮你少走点弯路。
返回列表