作为TCP服务器,如何维持与PLC客户端的永久连接?
问题与解决方案
问题背景
作为TCP服务器,需连接一台TCP客户端角色的PLC:
- PLC不会主动发送消息,仅接收服务器下发的控制指令
- 要求服务器每0.5秒发送一次数据,PLC需在0.5秒内接收后进入下一循环
- 当前核心问题:PLC主动断开连接时,如何自动重新建立并维持稳定的TCP连接
- 现有实现思路:主线程运行图像分类模型(仅拥有0.5秒图像预测时间),负责发送业务重要数据;子线程专门承担连接维持的职责
现有代码
主线程代码
def send_data(self, data, tcpCilSock): self.setting.send_data = data.encode('utf-8') respMsg = self.setting.send_data tcpCilSock.sendall((bytes(respMsg))) log.info(data) def run_thread(self): while True: try: if setting.is_tcp_client: tcpSvr.send_data(send_data, setting.tcpCilSock) break except: log.info('main thread is failed')
子线程代码
def run(self): while True: try: self.tcpSvr.send_data(content, setting.tcpcilsock) except: log.info('sub thread is failed')
子线程维持连接的实现方案
子线程需承担监听新连接、实时检测连接状态、断开后自动触发重连的核心职责,同时要保证与主线程共享socket资源的线程安全性:
子线程核心逻辑
- 初始化时启动TCP服务器监听,等待PLC客户端发起连接
- 连接建立后,通过定期发送心跳包(工业场景建议使用PLC可识别的专用心跳指令)检测连接存活状态
- 一旦捕获到发送/接收异常(如
ConnectionResetError、BrokenPipeError),立即标记当前socket失效,重新进入监听等待PLC重连 - 用线程锁保护全局socket对象和连接状态标记,避免主线程使用失效连接
优化后的子线程代码示例
import threading import socket import time import log class ConnectionMaintainThread(threading.Thread): def __init__(self, setting): super().__init__() self.setting = setting # 初始化服务器socket,设置端口复用避免断开后端口占用 self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server_socket.bind(('你的服务器IP', 目标端口)) # 替换为实际IP和端口 self.server_socket.listen(1) self.conn_lock = threading.Lock() # 线程安全锁 self.daemon = True # 设置为守护线程,随主线程退出 def run(self): while True: log.info("等待PLC客户端连接...") try: # 等待PLC发起连接 client_socket, addr = self.server_socket.accept() log.info(f"PLC已连接,设备地址:{addr}") # 线程安全更新全局连接状态 with self.conn_lock: self.setting.tcpCilSock = client_socket self.setting.is_tcp_client = True # 进入连接维持循环,定期发送心跳检测 while True: try: # 发送专用心跳指令(可根据PLC协议调整) client_socket.sendall("PLC_HEARTBEAT".encode('utf-8')) time.sleep(0.2) # 心跳间隔小于0.5秒,保证及时检测断开 except (socket.error, BrokenPipeError, ConnectionResetError): log.info("PLC连接已断开,重新等待连接...") # 标记连接失效并关闭旧socket with self.conn_lock: self.setting.is_tcp_client = False self.setting.tcpCilSock.close() break except Exception as e: log.info(f"连接监听异常:{str(e)}") time.sleep(1) # 异常后延迟重试,避免资源占用
- 主线程配合调整
主线程发送数据前需通过线程锁确认连接有效性,避免使用失效socket:
def send_data(self, data, tcpCilSock): try: with self.setting.conn_lock: if not self.setting.is_tcp_client: log.info("无有效PLC连接,跳过本次数据发送") return send_bytes = data.encode('utf-8') tcpCilSock.sendall(send_bytes) log.info(f"已下发控制指令:{data}") except Exception as e: log.info(f"指令发送失败:{str(e)}") # 主动标记连接失效,触发子线程重连 with self.setting.conn_lock: self.setting.is_tcp_client = False
- 工业场景注意事项
- 心跳包需使用PLC可识别的格式,避免干扰业务指令执行
- 线程锁需严格控制临界区(socket操作、连接状态更新),防止线程冲突
- 服务器socket必须设置
SO_REUSEADDR,避免断开后端口被占用无法重启监听 - 异常捕获需精准区分网络超时与真实连接断开,避免误判
内容的提问来源于stack exchange,提问作者TT Aano
相关产品推荐
相关产品推荐

