物联网 · 盛世宏博 · TCP长连接 · 断线排查 · 心跳保活 · 自动重连

关键词:网口温湿度记录仪、TCP长连接、连接中断、心跳保活、SO_KEEPALIVE、自动重连、socket超时、通信优化 标签:#物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控
某电力调度机房部署了16台网口温湿度记录仪,采集平台采用TCP长连接方式(每台设备保持一个持久连接,周期性发送Modbus功能码03读取寄存器)。上线首周运行平稳,但从第二周开始,运维陆续反馈:部分设备数据间歇性中断,持续30秒~5分钟不等后自动恢复,平台日志中反复出现 ConnectionResetError 和 BrokenPipeError。
这不是网络抖动——ping延迟稳定在1~2ms,丢包率0%。问题出在TCP长连接本身的生命周期管理上。
现象 | 对应错误 | 发生频率 |
|---|---|---|
读取数据时突然抛异常 | ConnectionResetError: [Errno 104] Connection reset by peer | 每天2~3次/台 |
写入时管道断裂 | BrokenPipeError: [Errno 32] Broken pipe | 伴随Reset出现 |
连接卡死无响应 | 无异常但recv()永远阻塞 | 偶尔(最隐蔽) |
恢复后数据正常 | 重连成功,读数正常 | 自动恢复 |
关键特征:不是"连不上",而是"连上了但中途断了",且断线后socket对象并未自动关闭,应用层浑然不知。
根因1:中间设备空闲断连
交换机/防火墙/NAT设备对空闲TCP连接有超时机制
典型值:15分钟~2小时(思科ASA默认1小时,华为防火墙默认30分钟)
连接空闲超过阈值 → 中间设备悄悄丢弃会话表项
结果:客户端发送数据时,对端无响应或RST
根因2:传感器端资源限制
网口记录仪MCU资源有限(RAM通常256KB~1MB)
最大TCP连接数限制(常见4~8个)
旧连接未正确关闭(FIN未发送或被丢弃)→ 连接数耗尽 → 新请求被RST
根因3:应用层未处理半关闭
对端已关闭写端(FIN),本地仍尝试写入
→ BrokenPipeError
或:对端已RST,本地仍尝试读取
→ ConnectionResetError
根因4:网络路径MTU/分片问题
某些交换机对大包进行分片或丢弃
Modbus TCP报文通常很小(<100字节),但某些记录仪固件bug会发送异常长度响应
→ TCP窗口卡死# 过滤条件:tcp and ip.addr == 192.168.10.50
# 正常通信序列:
# [SYN] → [SYN, ACK] → [ACK] (三次握手)
# [PSH, ACK] (请求) → [ACK]
# [PSH, ACK] (响应) → [ACK]
# ... 持续循环 ...
# 断线时的抓包:
# 场景A:中间设备丢弃
# [PSH, ACK] (请求) → 无响应 → 重传3次 → [RST] (来自中间设备)
# 场景B:传感器RST
# [PSH, ACK] (请求) → [RST, ACK] (传感器主动拒绝)
# 场景C:半关闭
# 传感器发送 [FIN, ACK] → 平台未处理 → 继续发送 [PSH, ACK] → [RST]# tcp_keepalive_client.py - 带心跳保活的长连接客户端
import socket
import struct
import time
import threading
from enum import Enum, auto
class ConnectionState(Enum):
DISCONNECTED = auto()
CONNECTING = auto()
CONNECTED = auto()
FAILED = auto()
class KeepAliveTCPClient:
def __init__(self, host, port, keepalive_interval=15, connect_timeout=5,
read_timeout=3, max_retries=3):
self.host = host
self.port = port
self.keepalive_interval = keepalive_interval
self.connect_timeout = connect_timeout
self.read_timeout = read_timeout
self.max_retries = max_retries
self.sock = None
self.state = ConnectionState.DISCONNECTED
self._running = False
self._last_recv_time = 0
self._send_lock = threading.Lock()
def connect(self):
"""建立连接并开启TCP KEEPALIVE"""
self.state = ConnectionState.CONNECTING
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.connect_timeout)
# 开启TCP KEEPALIVE
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Linux下设置保活参数
try:
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 30) # 30秒空闲后开始探测
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) # 每10秒探测一次
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) # 3次失败判定断线
except AttributeError:
pass # Windows/Mac不支持这些选项
self.sock.connect((self.host, self.port))
self.sock.settimeout(self.read_timeout)
self.state = ConnectionState.CONNECTED
self._last_recv_time = time.time()
return True
except Exception as e:
print(f"[TCP] 连接失败 {self.host}:{self.port} - {e}")
self.state = ConnectionState.FAILED
return False
def send_modbus_read(self, slave_id, reg_addr, reg_count):
"""发送Modbus TCP读请求(带自动重连)"""
req = self._build_modbus_request(slave_id, reg_addr, reg_count)
for attempt in range(self.max_retries):
try:
with self._send_lock:
if self.state != ConnectionState.CONNECTED:
self._reconnect()
self.sock.sendall(req)
resp = self.sock.recv(1024)
if len(resp) < 9:
raise ValueError("响应过短")
self._last_recv_time = time.time()
return self._parse_modbus_response(resp)
except (socket.timeout, socket.error, ConnectionResetError, BrokenPipeError) as e:
print(f"[TCP] 通信异常 (尝试{attempt+1}/{self.max_retries}): {e}")
self.state = ConnectionState.DISCONNECTED
if attempt < self.max_retries - 1:
time.sleep(2 ** attempt) # 指数退避
self._reconnect()
return None
def _reconnect(self):
"""断开重连"""
print(f"[TCP] 尝试重连 {self.host}:{self.port}...")
if self.sock:
try:
self.sock.close()
except:
pass
time.sleep(1)
self.connect()
def start_heartbeat(self):
"""启动应用层心跳线程"""
self._running = True
t = threading.Thread(target=self._heartbeat_loop, daemon=True)
t.start()
def _heartbeat_loop(self):
"""应用层心跳:发送Modbus空读请求保持连接活跃"""
while self._running:
time.sleep(self.keepalive_interval)
if self.state == ConnectionState.CONNECTED:
try:
# 发送一个轻量Modbus读请求作为心跳
self.send_modbus_read(slave_id=1, reg_addr=0x0001, reg_count=1)
except:
pass # 心跳失败由自动重连机制处理
def _build_modbus_request(self, slave_id, reg_addr, reg_count):
tid = int(time.time() * 1000) % 65535
return struct.pack('>HHHBH', tid, 0, 6, slave_id, 0x03, reg_addr, reg_count)
def _parse_modbus_response(self, resp):
byte_count = resp[8]
data = resp[9:9+byte_count]
values = struct.unpack(f'>{byte_count//2}H', data)
return [v / 10.0 if i == 0 else v for i, v in enumerate(values)]
def close(self):
self._running = False
if self.sock:
self.sock.close()
self.state = ConnectionState.DISCONNECTED对于不支持长连接保活的低端记录仪,改用短连接(每次读写后关闭连接):
# short_conn_client.py - 短连接模式(每次请求新建连接)
import socket
import struct
import time
class ShortConnClient:
def __init__(self, host, port, timeout=3):
self.host = host
self.port = port
self.timeout = timeout
def read_once(self, slave_id, reg_addr, reg_count):
"""每次读取新建连接,读完即关"""
sock = None
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(self.timeout)
sock.connect((self.host, self.port))
tid = int(time.time() * 1000) % 65535
req = struct.pack('>HHHBH', tid, 0, 6, slave_id, 0x03, reg_addr, reg_count)
sock.sendall(req)
resp = sock.recv(1024)
return resp
except Exception as e:
print(f"[ShortConn] 读取失败: {e}")
return None
finally:
if sock:
try:
sock.close()
except:
pass
# 使用:
# client = ShortConnClient("192.168.10.50", 502)
# resp = client.read_once(1, 0x0003, 2)长连接 vs 短连接对比:
维度 | 长连接+心跳 | 短连接 |
|---|---|---|
延迟 | 低(复用连接) | 高(每次三次握手) |
资源消耗 | 低(连接数固定) | 高(频繁创建/销毁) |
断线风险 | 需处理保活 | 无长连接断线问题 |
适用场景 | 传感器响应快、网络稳定 | 低端设备、网络不可靠 |
推荐指数 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
# connection_pool.py - TCP连接池
from queue import Queue, Empty
import threading
import time
class TCPConnectionPool:
def __init__(self, host, port, pool_size=3, max_idle=300):
self.host = host
self.port = port
self.pool_size = pool_size
self.max_idle = max_idle
self._pool = Queue(maxsize=pool_size)
self._lock = threading.Lock()
self._created = 0
def _create_connection(self):
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((self.host, self.port))
sock.settimeout(3)
self._created += 1
return sock
def get(self, timeout=5):
try:
sock, create_time = self._pool.get_nowait()
# 检查空闲超时
if time.time() - create_time > self.max_idle:
sock.close()
sock = self._create_connection()
return sock
except Empty:
if self._created < self.pool_size:
return self._create_connection()
# 等待可用连接
try:
sock, create_time = self._pool.get(timeout=timeout)
return sock
except Empty:
raise TimeoutError("连接池耗尽")
def put(self, sock):
try:
self._pool.put_nowait((sock, time.time()))
except:
sock.close()
self._created -= 1
def close_all(self):
while not self._pool.empty():
try:
sock, _ = self._pool.get_nowait()
sock.close()
except:
pass
self._created = 0参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
TCP超时断开 | 300秒 | 600秒 | 空闲多久后设备主动断开 |
最大连接数 | 4 | 8 | 允许并发连接数 |
心跳间隔 | 无 | 30秒 | 部分高端记录仪支持 |
重连等待 | 10秒 | 5秒 | 断开后多久允许重连 |
TCP KeepAlive | 关闭 | 开启 | 启用协议层保活 |
已知问题清单:
1. FIN_WAIT超时过短:设备发送FIN后等待ACK仅2秒,超时即RST
→ 规避:平台端收到FIN后立即close()
2. 半关闭不处理:平台发送FIN后设备仍尝试推送数据
→ 规避:平台不主动关闭写端,用全关闭
3. 连接数耗尽不释放:异常断开的连接不进入TIME_WAIT
→ 规避:平台端确保每次异常都调用close()
4. 大端/小端混淆:某些国产记录仪寄存器字节序反了
→ 规避:软件层做字节序转换# conn_monitor.py - 连接健康监控
import time
from collections import deque
class ConnectionMonitor:
def __init__(self, window_size=100):
self.window_size = window_size
self.latencies = deque(maxlen=window_size)
self.errors = deque(maxlen=window_size)
self.reconnects = 0
self.last_reconnect_time = 0
def record_success(self, latency_ms):
self.latencies.append(latency_ms)
self.errors.append(0)
def record_error(self, error_type):
self.errors.append(1)
if error_type == 'reconnect':
self.reconnects += 1
self.last_reconnect_time = time.time()
def health_score(self):
"""计算连接健康分(0~100)"""
if not self.errors:
return 100
error_rate = sum(self.errors) / len(self.errors)
if error_rate > 0.5:
return 0
score = int((1 - error_rate) * 100)
# 重连频率惩罚
if self.reconnects > 10:
score = max(0, score - 30)
return score
def should_alert(self):
"""判断是否需要告警"""
if self.reconnects >= 5:
return True
if self.health_score() < 50:
return True
return False指标 | 正常范围 | 告警阈值 |
|---|---|---|
连接存活时间 | >24小时 | <1小时 |
重连次数/小时 | 0~1 | >5 |
平均响应延迟 | <50ms | >200ms |
错误率 | <1% | >5% |
心跳成功率 | 100% | <95% |
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)。 SO_REUSEADDR或改用长连接。 signal(SIGPIPE, SIG_IGN)。 网口温湿度记录仪的TCP长连接断线,根因可以归纳为三类:中间设备空闲超时、传感器端资源限制、应用层未正确处理连接生命周期。解决方案的核心思路是"保活+检测+恢复"三位一体:协议层开启TCP KeepAlive、应用层实现心跳保活、异常时自动重连并带指数退避。对于低端设备,短连接模式是更稳妥的替代方案。大规模部署时引入连接池平衡资源消耗和响应延迟。最终目标不是"不断线",而是"断线后秒级自愈,用户无感知"。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。