当前时间: 2026-05-20 19:41:55
分类:办公文件
评论(0)
PC与PLC重连机制详细说明目录
## 重连机制概述
### 为什么需要PLC需要重连机制?
在工业自动化场景中,PC与PLC之间的通信面临以下挑战:| **网络抖动** | 工业环境中电磁干扰导致短暂网络中断 || **设备重启** | PLC维护、升级或故障恢复 || **PLC断电** | 电源波动或计划性停机 || **服务端过载** | PLC处理能力不足导致超时 |### 重连机制的核心目标
1. **自动恢复** - 无需人工干预,自动重建连接## PLC通信架构
### 典型的PC-PLC通信层次结构
┌─────────────────────────────────────────────────────────┐│ ┌───────────────────────────────────────────────────┐ ││ └─────────────────────┬─────────────────────────────┘ ││ ┌─────────────────▼─────────────────────────────┐ ││ └─────────────────────┬─────────────────────────────┘ ││ ┌─────────────────▼─────────────────────────────┐ ││ │ ┌──────────────┐ ┌──────────────┐ ┌─────────┐ │ ││ │ │ 三菱 │ │ 欧姆龙 │ │ 西 │ │ ││ │ │ 协议 │ │ 协议 │ │ 门 │ │ ││ │ └──────────────┘ └──────────────┘ └─────┘ │ ││ └─────────────────────┬─────────────────────────────┘ │└────────────────────────┼─────────────────────────────┘┌─────────────────────────────────────────────────────────┐│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││ │ 三菱FX │ │ 欧姆龙CJ │ │ 西门子S7 │ ││ └──────────┘ └──────────┘ └──────────┘ │└─────────────────────────────────────────────────────────┘### 核心组件设计
/// PLC连接接口 - 定义所有PLC设备的统一接口public interface IPlcDevice : IDisposablestring DeviceName { get; }string IpAddress { get; }PlcConnectionState ConnectionState { get; }bool IsConnected { get; }event EventHandler ConnectionStateChanged;Task ConnectAsync(CancellationToken cancellationToken = default);Task ReconnectAsync(CancellationToken cancellationToken = default);Task> ReadAsync(string address);Task WriteAsync(string address, T value);public enum PlcConnectionStatepublic class PlcConnectionStateEventArgs : EventArgspublic PlcConnectionState OldState { get; set; }public PlcConnectionState NewState { get; set; }public DisconnectReason? Reason { get; set; }public string Message { get; set; }public Exception Exception { get; set; }public DateTime Timestamp { get; set; } = DateTime.Now;public enum DisconnectReasonpublic class OperateResultpublic bool IsSuccess { get; set; }public string Message { get; set; }public int ErrorCode { get; set; }public static OperateResult CreateSuccess() => new OperateResult { IsSuccess = true };public static OperateResult CreateFailed(string message) => new OperateResult { IsSuccess = false, Message = message };public class OperateResult : OperateResultpublic T Content { get; set; }public static new OperateResult CreateSuccess(T content) => new OperateResult { IsSuccess = true, Content = content };public static new OperateResult CreateFailed(string message) => new OperateResult { IsSuccess = false, Message = message };## 连接状态机
### 完整的状态转换图
┌──────────┐ ┌──────────┐│Disconnecting│ │ Failed │└──────────┘ └──────────┘### 状态机实现
public abstract class PlcDeviceBase : IPlcDeviceprotected readonly object _lockObject = new object();protected PlcConnectionState _connectionState;protected int _reconnectCount;protected DateTime _lastConnectedTime;protected DateTime _lastDisconnectedTime;protected Timer _heartbeatTimer;protected CancellationTokenSource _reconnectCts;protected Task _reconnectTask;protected readonly IPlcReconnectionStrategy _reconnectionStrategy;protected readonly ILogger _logger;public string DeviceName { get; }public string IpAddress { get; }public PlcConnectionState ConnectionStateif (_connectionState != value)var oldState = _connectionState;_connectionState = value;OnConnectionStateChanged(oldState, value);public bool IsConnected => ConnectionState == PlcConnectionState.Connected;public DateTime? LastConnectedTime => _lastConnectedTime;public DateTime? LastDisconnectedTime => _lastDisconnectedTime;public int ReconnectCount => _reconnectCount;public event EventHandler ConnectionStateChanged;IPlcReconnectionStrategy reconnectionStrategy,DeviceName = deviceName ?? throw new ArgumentNullException(nameof(deviceName));IpAddress = ipAddress ?? throw new ArgumentNullException(nameof(ipAddress));_reconnectionStrategy = reconnectionStrategy ?? new ExponentialBackoffStrategy();_logger = logger ?? NullLogger.Instance;_connectionState = PlcConnectionState.Disconnected;public async Task ConnectAsync(CancellationToken cancellationToken = default)if (ConnectionState == PlcConnectionState.Connected)_logger.LogInformation("[{DeviceName}] 已经处于连接状态", DeviceName);if (ConnectionState == PlcConnectionState.Connecting ||ConnectionState == PlcConnectionState.Reconnecting)_logger.LogInformation("[{DeviceName}] 连接操作正在进行中", DeviceName);ConnectionState = PlcConnectionState.Connecting;_logger.LogInformation("[{DeviceName}] 正在连接PLC: {Ip}:{Port}", DeviceName, IpAddress, Port);bool connected = await DoConnectAsync(cancellationToken);ConnectionState = PlcConnectionState.Connected;_lastConnectedTime = DateTime.Now;_reconnectionStrategy.Reset();_logger.LogInformation("[{DeviceName}] PLC连接成功", DeviceName);ConnectionState = PlcConnectionState.Disconnected;_lastDisconnectedTime = DateTime.Now;_logger.LogWarning("[{DeviceName}] PLC连接失败", DeviceName);ConnectionState = PlcConnectionState.Disconnected;_lastDisconnectedTime = DateTime.Now;_logger.LogError(ex, "[{DeviceName}] PLC连接异常", DeviceName);OnConnectionStateChanged(PlcConnectionState.Connecting, PlcConnectionState.Disconnected,DisconnectReason.NetworkTimeout, $"连接异常: {ex.Message}", ex);public async Task DisconnectAsync()if (ConnectionState == PlcConnectionState.Disconnected)ConnectionState = PlcConnectionState.Disconnecting;_logger.LogInformation("[{DeviceName}] 正在断开PLC连接", DeviceName);await DoDisconnectAsync();ConnectionState = PlcConnectionState.Disconnected;_lastDisconnectedTime = DateTime.Now;_logger.LogInformation("[{DeviceName}] PLC断开连接成功", DeviceName);OnConnectionStateChanged(PlcConnectionState.Disconnecting, PlcConnectionState.Disconnected,DisconnectReason.UserInitiated, "用户主动断开");public async Task ReconnectAsync(CancellationToken cancellationToken = default)_logger.LogInformation("[{DeviceName}] 开始重新连接...", DeviceName);await Task.Delay(500, cancellationToken);return await ConnectAsync(cancellationToken);protected void StartReconnection()if (_reconnectTask != null && !_reconnectTask.IsCompleted)_reconnectCts = new CancellationTokenSource();_reconnectTask = Task.Run(() => ReconnectionLoop(_reconnectCts.Token));protected void StopReconnection()if (_reconnectCts != null)private async Task ReconnectionLoop(CancellationToken cancellationToken)while (!cancellationToken.IsCancellationRequested)_reconnectCount = attempt;var interval = _reconnectionStrategy.GetNextRetryInterval(attempt);_logger.LogInformation("[{DeviceName}] 重连尝试 {Attempt}, 等待 {Delay:F1}秒...",DeviceName, attempt, interval.TotalSeconds);await Task.Delay(interval, cancellationToken);bool connected = await ConnectAsync(cancellationToken);if (!_reconnectionStrategy.ShouldRetry(attempt, null))ConnectionState = PlcConnectionState.Failed;_logger.LogError("[{DeviceName}] 重连失败,已达到最大重试次数 {Attempt}", DeviceName, attempt);catch (OperationCanceledException)_logger.LogError(ex, "[{DeviceName}] 重连异常", DeviceName);protected void StartHeartbeat()_heartbeatTimer = new Timer(TimeSpan.FromSeconds(5));protected void StopHeartbeat()_heartbeatTimer?.Dispose();private async void HeartbeatCallback(object state)bool alive = await HeartbeatAsync();_logger.LogWarning("[{DeviceName}] 心跳检测失败", DeviceName);await HandleConnectionErrorAsync(DisconnectReason.HeartbeatFailed, "心跳检测失败");_logger.LogError(ex, "[{DeviceName}] 心跳检测异常", DeviceName);await HandleConnectionErrorAsync(DisconnectReason.HeartbeatFailed, $"心跳异常: {ex.Message}");protected async Task HandleConnectionErrorAsync(DisconnectReason reason, string message)if (ConnectionState == PlcConnectionState.Disconnected)ConnectionState = PlcConnectionState.Disconnected;_lastDisconnectedTime = DateTime.Now;OnConnectionStateChanged(PlcConnectionState.Connected, PlcConnectionState.Disconnected, reason, message);protected virtual void OnConnectionStateChanged(PlcConnectionState oldState,PlcConnectionState newState,DisconnectReason? reason = null,Exception exception = null)ConnectionStateChanged?.Invoke(this, new PlcConnectionStateEventArgsprotected abstract Task DoConnectAsync(CancellationToken cancellationToken);protected abstract Task DoDisconnectAsync();public abstract Task> ReadAsync(string address);public abstract Task WriteAsync(string address, T value);public abstract Task HeartbeatAsync();public virtual void Dispose()DisconnectAsync().Wait(TimeSpan.FromSeconds(5));## 重连策略设计
### 1. 指数退避策略(推荐)
public interface IPlcReconnectionStrategyTimeSpan GetNextRetryInterval(int attemptCount);bool ShouldRetry(int attemptCount, Exception lastException);public class ExponentialBackoffConfigpublic int BaseRetryIntervalMs { get; set; } = 2000;public int MaxRetryIntervalMs { get; set; } = 60000;public double RetryMultiplier { get; set; } = 2.0;public int MaxRetryCount { get; set; } = int.MaxValue;public bool EnableJitter { get; set; } = true;public class ExponentialBackoffStrategy : IPlcReconnectionStrategyprivate readonly ExponentialBackoffConfig _config;private readonly Random _random;public ExponentialBackoffStrategy(ExponentialBackoffConfig config = null)_config = config ?? new ExponentialBackoffConfig();public TimeSpan GetNextRetryInterval(int attemptCount)double interval = _config.BaseRetryIntervalMs * Math.Pow(_config.RetryMultiplier, attemptCount - 1);interval = Math.Min(interval, _config.MaxRetryIntervalMs);if (_config.EnableJitter)double jitter = interval * 0.1 * (_random.NextDouble() * 2 - 1);return TimeSpan.FromMilliseconds(Math.Max(1000, interval));public bool ShouldRetry(int attemptCount, Exception lastException)if (attemptCount >= _config.MaxRetryCount)if (lastException != null)if (IsNonRetryableException(lastException))private bool IsNonRetryableException(Exception ex)if (ex.Message.Contains("认证失败") || ex.Message.Contains("Authentication failed"))if (ex.Message.Contains("设备已停用") || ex.Message.Contains("Device disabled"))public class FixedIntervalStrategy : IPlcReconnectionStrategyprivate readonly int _intervalMs;private readonly int _maxRetryCount;public FixedIntervalStrategy(int intervalMs = 5000, int maxRetryCount = int.MaxValue)_intervalMs = intervalMs;_maxRetryCount = maxRetryCount;public TimeSpan GetNextRetryInterval(int attemptCount)return TimeSpan.FromMilliseconds(_intervalMs);public bool ShouldRetry(int attemptCount, Exception lastException)return attemptCount < _maxRetryCount;public class LinearBackoffStrategy : IPlcReconnectionStrategyprivate readonly int _baseIntervalMs;private readonly int _incrementMs;private readonly int _maxIntervalMs;private readonly int _maxRetryCount;public LinearBackoffStrategy(int baseIntervalMs = 2000, int incrementMs = 2000, int maxIntervalMs = 30000, int maxRetryCount = int.MaxValue)_baseIntervalMs = baseIntervalMs;_incrementMs = incrementMs;_maxIntervalMs = maxIntervalMs;_maxRetryCount = maxRetryCount;public TimeSpan GetNextRetryInterval(int attemptCount)int interval = _baseIntervalMs + (attemptCount - 1) * _incrementMs;interval = Math.Min(interval, _maxIntervalMs);return TimeSpan.FromMilliseconds(interval));public bool ShouldRetry(int attemptCount, Exception lastException)return attemptCount < _maxRetryCount;### 重连策略对比
| 策略类型 | 优点 | 缺点 | 适用场景 ||---------|------|------|---------|| 固定间隔 | 简单易实现 | 可能造成网络风暴 | 网络稳定,设备少 || 指数退避 | 避免网络风暴,智能调整 | 长时间断线后恢复慢 | 大多数工业场景,推荐使用 || 线性退避 | 平衡简单和智能 | 间隔增长均匀 | 中等网络环境 |## 基于HslCommunication的完整实现
### 三菱PLC重连实现
using HslCommunication.Profinet.Melsec;using Microsoft.Extensions.Logging;namespace IndustrialPlc.PlcDevicespublic class MelsecPlcDevice : PlcDeviceBaseprivate MelsecMcNet _plc;private readonly string _plcType;IPlcReconnectionStrategy reconnectionStrategy = null,: base(deviceName, ipAddress, port, reconnectionStrategy, logger)protected override Task DoConnectAsync(CancellationToken cancellationToken)_plc = new MelsecMcNet(IpAddress, Port)_plc.Series = MelsecSeries.FX5U;else if (_plcType == "Q")_plc.Series = MelsecSeries.Q;OperateResult connectResult = _plc.ConnectServer();if (connectResult.IsSuccess)_logger.LogInformation("[{DeviceName}] 三菱PLC连接成功", DeviceName);_logger.LogWarning("[{DeviceName}] 三菱PLC连接失败: {Message}", DeviceName, connectResult.Message);_logger.LogError(ex, "[{DeviceName}] 三菱PLC连接异常", DeviceName);protected override Task DoDisconnectAsync()_logger.LogError(ex, "[{DeviceName}] 断开三菱PLC连接异常", DeviceName);return Task.CompletedTask;public override Task> ReadAsync(string address)if (!IsConnected || _plc == null)return OperateResult.CreateFailed("PLC未连接");if (type == typeof(short))result = _plc.ReadInt16(address);else if (type == typeof(ushort))result = _plc.ReadUInt16(address);else if (type == typeof(int))result = _plc.ReadInt32(address);else if (type == typeof(uint))result = _plc.ReadUInt32(address);else if (type == typeof(float))result = _plc.ReadFloat(address);else if (type == typeof(bool))result = _plc.ReadBool(address);return OperateResult.CreateFailed($"不支持的数据类型: {type.Name}");return OperateResult.CreateSuccess((T)result.Content);_ = HandleConnectionErrorAsync(DisconnectReason.ProtocolError, $"读取失败: {result.Message}");return OperateResult.CreateFailed(result.Message);_logger.LogError(ex, "[{DeviceName}] 读取数据异常", DeviceName);_ = HandleConnectionErrorAsync(DisconnectReason.ProtocolError, $"读取异常: {ex.Message}");return OperateResult.CreateFailed(ex.Message);public override Task WriteAsync(string address, T value)if (!IsConnected || _plc == null)return OperateResult.CreateFailed("PLC未连接");if (type == typeof(short))result = _plc.Write(address, (short)(object)value);else if (type == typeof(ushort))result = _plc.Write(address, (ushort)(object)value);else if (type == typeof(int))result = _plc.Write(address, (int)(object)value);else if (type == typeof(uint))result = _plc.Write(address, (uint)(object)value);else if (type == typeof(float))result = _plc.Write(address, (float)(object)value);else if (type == typeof(bool))result = _plc.Write(address, (bool)(object)value);return OperateResult.CreateFailed($"不支持的数据类型: {type.Name}");return OperateResult.CreateSuccess();_ = HandleConnectionErrorAsync(DisconnectReason.ProtocolError, $"写入失败: {result.Message}");return OperateResult.CreateFailed(result.Message);_logger.LogError(ex, "[{DeviceName}] 写入数据异常", DeviceName);_ = HandleConnectionErrorAsync(DisconnectReason.ProtocolError, $"写入异常: {ex.Message}");return OperateResult.CreateFailed(ex.Message);public override Task HeartbeatAsync()var result = _plc.ReadInt16("D0");### 欧姆龙PLC重连实现
using HslCommunication.Profinet.Omron;using Microsoft.Extensions.Logging;namespace IndustrialPlc.PlcDevicespublic class OmronPlcDevice : PlcDeviceBaseprivate OmronFinsNet _plc;private readonly byte _localNode;private readonly byte _plcNode;IPlcReconnectionStrategy reconnectionStrategy = null,: base(deviceName, ipAddress, port, reconnectionStrategy, logger)protected override Task DoConnectAsync(CancellationToken cancellationToken)_plc = new OmronFinsNet(IpAddress, Port)OperateResult connectResult = _plc.ConnectServer();if (connectResult.IsSuccess)
基本
文件
流程
错误
SQL
调试
- 请求信息 : 2026-05-22 14:25:38 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/652252.html
- 运行时间 : 0.191185s [ 吞吐率:5.23req/s ] 内存消耗:4,748.40kb 文件加载:145
- 缓存信息 : 0 reads,0 writes
- 会话信息 : SESSION_ID=33590e043a4801cdd17f5e963fd330b2
- CONNECT:[ UseTime:0.000917s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
- SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001381s ]
- SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.001141s ]
- SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000677s ]
- SHOW FULL COLUMNS FROM `set` [ RunTime:0.001299s ]
- SELECT * FROM `set` [ RunTime:0.000590s ]
- SHOW FULL COLUMNS FROM `article` [ RunTime:0.001428s ]
- SELECT * FROM `article` WHERE `id` = 652252 LIMIT 1 [ RunTime:0.001361s ]
- UPDATE `article` SET `lasttime` = 1779431138 WHERE `id` = 652252 [ RunTime:0.002570s ]
- SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000572s ]
- SELECT * FROM `article` WHERE `id` < 652252 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001096s ]
- SELECT * FROM `article` WHERE `id` > 652252 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001248s ]
- SELECT * FROM `article` WHERE `id` < 652252 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.001782s ]
- SELECT * FROM `article` WHERE `id` < 652252 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.001870s ]
- SELECT * FROM `article` WHERE `id` < 652252 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.002481s ]
0.195104s