在上位机、工业控制、智能硬件等项目中,设备连接并不会一直保持稳定。
例如使用 TCP、串口或其他通信方式连接设备时,都可能遇到:
网络短暂中断
设备重启
设备掉电
网线松动
Wi-Fi切换
服务端主动断开
如果程序只是简单地提示“设备已断开”,然后等待用户手动重新连接,实际使用体验会比较差。
特别是一些需要长期运行的上位机软件,设备短暂掉线后,通常希望程序能够自动恢复连接。
因此,可以在通信模块外增加一个自动重连机制。
整体思路可以理解为:
正常连接
↓
检测设备断开
↓
进入重连状态
↓
等待一定时间
↓
尝试重新连接
↓
连接成功后恢复通信
下面以 TCP 设备为例,介绍一个比较简单的 C# 自动重连实现方式。
一、先把设备连接状态管理清楚
自动重连最容易出现的问题,并不是 ConnectAsync 怎么写,而是连接状态比较混乱。
例如程序可能同时出现:
正在连接
已经连接
设备掉线
正在重连
用户主动关闭
如果只是使用一个 bool:
bool isConnected;
后面很容易出现判断不清楚的情况。
因此可以先定义一个简单的连接状态:
public enum DeviceConnectionState
{
Disconnected,
Connecting,
Connected,
Reconnecting,
Stopped
}
其中:
Disconnected
表示当前没有连接设备。
Connecting
表示第一次连接设备。
Connected
表示设备已经正常连接。
Reconnecting
表示设备发生异常,目前正在尝试重新连接。
Stopped
表示用户主动停止连接,不再自动重连。
然后在设备客户端中保存:
private DeviceConnectionState connectionState
= DeviceConnectionState.Disconnected;
public DeviceConnectionState ConnectionState
{
get
{
return connectionState;
}
}
这样整个连接生命周期会比较清楚:
Disconnected
↓
Connecting
↓
Connected
↓
Reconnecting
↓
Connected
如果用户主动停止:
Connected
↓
Stopped
进入 Stopped 以后,就不应该继续触发自动重连。
二、封装一个基础连接方法
首先实现正常的设备连接。
例如:
public class DeviceClient
{
private TcpClient tcpClient;
private NetworkStream networkStream;
public async Task ConnectAsync(
string ip,
int port,
CancellationToken cancellationToken)
{
tcpClient = new TcpClient();
await tcpClient.ConnectAsync(
ip,
port,
cancellationToken
);
networkStream =
tcpClient.GetStream();
Console.WriteLine(
$"连接设备成功:{ip}:{port}"
);
}
}
第一版连接时:
connectionState =
DeviceConnectionState.Connecting;
try
{
await ConnectAsync(
ip,
port,
cancellationToken
);
connectionState =
DeviceConnectionState.Connected;
}
catch(Exception ex)
{
connectionState =
DeviceConnectionState.Disconnected;
Console.WriteLine(
$"连接失败:{ex.Message}"
);
}
这里建议把“连接设备”和“重连策略”分开。
也就是说:
ConnectAsync只负责:
连接一次设备
不要直接在 ConnectAsync 内部写:
连接失败
↓
继续连接
↓
再失败
↓
继续连接
否则后面很难管理取消、退出和重连次数。
三、实现自动重连循环
设备断开以后,可以启动一个独立的重连流程。
例如:
private async Task ReconnectAsync(
CancellationToken cancellationToken)
{
connectionState =
DeviceConnectionState.Reconnecting;
int retryCount = 0;
while(!cancellationToken.IsCancellationRequested)
{
retryCount++;
try
{
Console.WriteLine(
$"第 {retryCount} 次尝试重新连接设备"
);
await ConnectAsync(
deviceIp,
devicePort,
cancellationToken
);
connectionState =
DeviceConnectionState.Connected;
Console.WriteLine(
"设备重新连接成功"
);
return;
}
catch(OperationCanceledException)
{
return;
}
catch(Exception ex)
{
Console.WriteLine(
$"重新连接失败:{ex.Message}"
);
}
await Task.Delay(
3000,
cancellationToken
);
}
}
这段逻辑就是一个基础的自动重连机制。
例如设备掉线:
10:20:01
设备断开
10:20:04
第一次重连失败
10:20:07
第二次重连失败
10:20:10
第三次重连成功
对于很多上位机项目来说,这种简单策略已经可以满足基础需求。
不过需要特别注意:
不能每次检测到断线都重新启动一个 ReconnectAsync。
否则可能出现:
Reconnect Task 1
Reconnect Task 2
Reconnect Task 3
三个任务同时尝试连接同一个设备。
因此通常需要增加一个标记:
private bool isReconnecting;
例如:
private async Task StartReconnectAsync(
CancellationToken cancellationToken)
{
if(isReconnecting)
{
return;
}
isReconnecting = true;
try
{
await ReconnectAsync(
cancellationToken
);
}
finally
{
isReconnecting = false;
}
}
这样可以保证同一时间只有一个重连流程。
四、重连间隔不要固定得太激进
最简单的做法是:
每3秒尝试一次。
例如:
await Task.Delay(
3000,
cancellationToken
);
对于局域网设备来说,这种方式通常没有问题。
但是如果设备长期离线:
程序可能一直:
连接
失败
等待3秒
连接
失败
等待3秒
如果设备可能长时间断电,可以适当增加重连间隔。
例如:
第一次:
1秒
第二次:
2秒
第三次:
5秒
之后:
10秒
可以简单实现:
private TimeSpan GetRetryDelay(
int retryCount)
{
if(retryCount <= 1)
return TimeSpan.FromSeconds(1);
if(retryCount <= 3)
return TimeSpan.FromSeconds(3);
return TimeSpan.FromSeconds(10);
}
调用:
TimeSpan delay =
GetRetryDelay(retryCount);
await Task.Delay(
delay,
cancellationToken
);
这样在设备刚刚掉线时:
能够快速恢复。
如果设备已经长时间离线:
又不会频繁发起连接。
实际项目中还可以设置最大间隔,例如:
MaximumRetryDelay = 30秒
一般没有必要不断增加到几分钟甚至更长。
五、设备掉线以后需要做哪些处理
自动重连并不是检测到异常以后直接调用 ConnectAsync 就结束了。
原来的连接资源也需要先处理。
例如:
private void CloseConnection()
{
try
{
networkStream?.Dispose();
}
catch
{
}
try
{
tcpClient?.Dispose();
}
catch
{
}
networkStream = null;
tcpClient = null;
}
当接收任务发现:
ReadAsync 返回 0
或者出现:
IOException
SocketException
可以执行:
CloseConnection();
connectionState =
DeviceConnectionState.Disconnected;
await StartReconnectAsync(
cancellationToken
);
完整过程:
设备正常通信
↓
ReadAsync异常
↓
关闭旧连接
↓
修改连接状态
↓
启动重连
↓
建立新的TcpClient
↓
重新启动接收任务
这里还有一个容易忽略的问题:
重连成功以后,要重新启动和连接相关的任务。
例如:
ReceiveLoop
Heartbeat
DeviceStatusPolling
因为之前的任务通常已经随着旧连接结束。
因此重连成功后可能需要:
await ConnectAsync(...);
StartReceiveLoop();
StartHeartbeat();
StartDevicePolling();
而不是只重新创建 TcpClient。
六、实际项目中需要特别注意的几个问题
自动重连代码本身并不复杂,但真正使用时有几个地方比较容易出现问题。
用户主动断开时不要自动重连
例如用户点击:
断开设备
此时应该:
Stopped
而不是:
Disconnected
因为:
Disconnected
可能代表异常断开,需要重连。
Stopped
代表用户主动停止,不应该重连。
可以增加:
private bool manualStop;
用户主动停止:
manualStop = true;
if(!manualStop)
{
await StartReconnectAsync(
cancellationToken
);
}
重连之前释放旧Socket
不要在旧 TcpClient 还存在的情况下不断 new 新对象。
建议流程:
旧连接异常
↓
停止读写
↓
Dispose
↓
重新建立连接
否则时间长以后可能产生比较多无效资源。
CancellationToken要贯穿整个重连流程
例如程序退出:
cancellationToken.Cancel();
此时:
正在连接
重连等待
心跳任务
接收任务
都应该结束。
不要出现:
界面已经关闭
但后台仍然每3秒连接设备
这种情况。
重连成功不代表业务已经恢复
例如某些设备连接成功后还需要:
登录
身份验证
读取设备信息
同步参数
订阅状态
因此更完整的流程可能是:
TCP重新连接
↓
设备握手
↓
读取设备信息
↓
恢复订阅
↓
开始正常工作
只有这些步骤全部成功以后,才能真正认为设备恢复正常。
和心跳机制结合
有些设备网络异常以后:
Socket并不会立即报错。
因此通常结合心跳:
Connected
↓
定时Heartbeat
↓
连续多次无响应
↓
认为设备离线
↓
CloseConnection
↓
Reconnect
这样比单纯等待 Socket 报错更加主动。
记录重连过程
实际调试设备问题时,重连日志非常有用。
例如:
09:31:12 设备断开
09:31:15 第1次重连失败
09:31:18 第2次重连失败
09:31:21 重连成功
09:31:22 设备信息同步完成
这样以后出现现场问题时,可以比较容易判断:
到底是设备掉线
网络异常
还是重连逻辑本身出现问题。
一个比较完整的设备连接模块,最终可以形成:
DeviceService
↓
ConnectionManager
↓
TcpTransport
其中 ConnectionManager 负责:
连接状态
断线检测
自动重连
停止连接
TcpTransport 只负责:
建立Socket
收数据
发数据
业务层不需要关心设备掉线以后具体重连了多少次,只需要关注:
Connected
Disconnected
Reconnecting
这些状态即可。
对于需要长期运行的 C# 上位机软件来说,自动重连属于一个很基础但很重要的功能。真正需要注意的不是简单地重复调用 ConnectAsync,而是把连接状态、资源释放、取消、重连任务和业务恢复这几个环节处理清楚。