using System.Collections.Concurrent;
|
using System.Net.Sockets;
|
using WIDESEAWCS_Core.LogHelper;
|
using WIDESEAWCS_QuartzJob;
|
using WIDESEAWCS_Tasks.SocketServer;
|
|
namespace WIDESEAWCS_Tasks
|
{
|
/// <summary>
|
/// 机械手客户端连接管理器 - 负责 TCP 客户端连接管理和事件订阅
|
/// </summary>
|
/// <remarks>
|
/// 核心职责:
|
/// 1. 维护与机械手设备的 TCP 连接状态
|
/// 2. 确保每个客户端只启动一次消息处理循环
|
/// 3. 管理客户端连接/断开的生命周期事件
|
/// 4. 提供发送消息到客户端的接口
|
/// </remarks>
|
public class RobotClientManager
|
{
|
/// <summary>
|
/// TCP Socket 服务器实例,用于管理所有客户端连接
|
/// </summary>
|
private readonly TcpSocketServer _tcpSocket;
|
|
/// <summary>
|
/// 机械手状态管理器,用于读写设备状态
|
/// </summary>
|
private readonly RobotStateManager _stateManager;
|
|
/// <summary>
|
/// 跟踪已启动消息处理的客户端,避免重复启动
|
/// </summary>
|
/// <remarks>
|
/// Key: 客户端 IP 地址
|
/// Value: 是否已启动(true 表示已启动)
|
/// 使用 ConcurrentDictionary 保证线程安全
|
/// </remarks>
|
private static readonly ConcurrentDictionary<string, bool> _handleClientStarted = new();
|
|
/// <summary>
|
/// 事件订阅标志,确保 RobotReceived 事件只订阅一次
|
/// </summary>
|
/// <remarks>
|
/// 使用原子操作 Interlocked.CompareExchange 保证全局只订阅一次
|
/// </remarks>
|
private static int _eventSubscribedFlag;
|
|
/// <summary>
|
/// 客户端断开连接时触发的事件
|
/// </summary>
|
/// <remarks>
|
/// 事件参数包含断开连接的机械手状态信息
|
/// </remarks>
|
public event EventHandler<RobotSocketState>? OnClientDisconnected;
|
|
/// <summary>
|
/// 构造函数
|
/// </summary>
|
/// <param name="tcpSocket">TCP Socket 服务器实例</param>
|
/// <param name="stateManager">状态管理器实例</param>
|
public RobotClientManager(TcpSocketServer tcpSocket, RobotStateManager stateManager)
|
{
|
_tcpSocket = tcpSocket;
|
_stateManager = stateManager;
|
}
|
|
/// <summary>
|
/// 确保客户端已连接并订阅消息事件
|
/// </summary>
|
/// <remarks>
|
/// 这是 RobotJob Execute 方法中的核心检查逻辑:
|
/// 1. 验证客户端是否在线
|
/// 2. 订阅断开事件(全局只执行一次)
|
/// 3. 确保消息处理循环已启动
|
/// 4. 防止重复启动 HandleClientAsync
|
/// </remarks>
|
/// <param name="ipAddress">设备 IP 地址</param>
|
/// <param name="robotCrane">机器人设备信息</param>
|
/// <returns>客户端是否可用(已连接且消息处理已启动)</returns>
|
public bool EnsureClientSubscribed(string ipAddress, RobotCraneDevice robotCrane)
|
{
|
// 从 TCP 服务器获取所有已连接客户端的 ID 列表
|
var clientIds = _tcpSocket.GetClientIds();
|
|
// 检查该 IP 地址的客户端是否已连接
|
bool isClientConnected = clientIds.Contains(ipAddress);
|
|
// 如果客户端未连接
|
if (!isClientConnected)
|
{
|
// 清理该客户端的 HandleClientAsync 启动标志
|
// 以便下次重连时可以重新启动处理
|
_handleClientStarted.TryRemove(ipAddress, out _);
|
return false;
|
}
|
|
// 订阅一次 RobotReceived 事件(全局只订阅一次)
|
// 使用 Interlocked.CompareExchange 实现原子操作,确保线程安全
|
if (System.Threading.Interlocked.CompareExchange(ref _eventSubscribedFlag, 1, 0) == 0)
|
{
|
// 绑定客户端断开连接的事件处理
|
_tcpSocket.RobotReceived += OnRobotReceived;
|
// 记录日志(注意:日志内容为"客户端已断开连接",可能是遗留的占位文本)
|
QuartzLogger.Error($"客户端已断开连接", robotCrane.DeviceName);
|
}
|
|
// 从 TCP 服务器的客户端字典中获取 TcpClient 对象
|
TcpClient? tcpClient = null;
|
_tcpSocket._clients.TryGetValue(ipAddress, out tcpClient);
|
|
// 如果获取失败(虽然 isClientConnected 为 true,但可能存在字典不同步的情况)
|
if (tcpClient == null)
|
{
|
// 移除启动标志,返回 false 表示客户端不可用
|
_handleClientStarted.TryRemove(ipAddress, out _);
|
return false;
|
}
|
|
// 检查是否已经为这个客户端启动过消息处理循环
|
bool alreadyStarted = _handleClientStarted.TryGetValue(ipAddress, out _);
|
|
// 如果尚未启动,则启动消息处理循环
|
if (!alreadyStarted)
|
{
|
// 记录日志
|
QuartzLogger.Error($"启动客户端消息处理", robotCrane.DeviceName);
|
|
// 获取最新的状态对象
|
var latestStateForSubscribe = _stateManager.GetState(ipAddress);
|
if (latestStateForSubscribe != null)
|
{
|
// 标记为已启动,防止重复启动
|
_handleClientStarted[ipAddress] = true;
|
|
// 异步启动客户端消息处理循环
|
// 使用 TaskContinuationOptions.OnlyOnFaulted 捕获异常情况
|
_ = _tcpSocket.HandleClientAsync(tcpClient, robotCrane.IPAddress, _tcpSocket._cts.Token, latestStateForSubscribe)
|
.ContinueWith(t =>
|
{
|
// 如果处理出现异常
|
if (t.IsFaulted)
|
{
|
// 记录错误日志
|
QuartzLogger.Error($"监听客户端消息事件异常", robotCrane.DeviceName);
|
Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] HandleClientAsync error: {t.Exception?.GetBaseException().Message}");
|
// 发生错误时,移除启动标志,允许下次重试
|
_handleClientStarted.TryRemove(ipAddress, out _);
|
}
|
}, TaskContinuationOptions.OnlyOnFaulted);
|
|
// 安全更新状态,标记为已订阅消息事件
|
_stateManager.TryUpdateStateSafely(ipAddress, s =>
|
{
|
s.IsEventSubscribed = true;
|
return s;
|
});
|
}
|
}
|
|
// 返回 true 表示客户端可用
|
return true;
|
}
|
|
/// <summary>
|
/// 事件处理:客户端断开连接时调用
|
/// </summary>
|
/// <remarks>
|
/// 触发时机:当 TCP 服务器检测到客户端断开连接时
|
/// 处理逻辑:
|
/// 1. 清理 HandleClientAsync 启动标志
|
/// 2. 重置设备状态(取消订阅、清除动作和状态)
|
/// 3. 触发 OnClientDisconnected 事件通知上层
|
/// </remarks>
|
/// <param name="clientId">断开连接的客户端 IP 地址</param>
|
/// <returns>固定返回 null,因为是事件处理器而非真正的消息处理器</returns>
|
private Task<string?> OnRobotReceived(string clientId)
|
{
|
// 移除该客户端的 HandleClientAsync 启动标志
|
_handleClientStarted.TryRemove(clientId, out _);
|
|
// 重置该客户端的状态信息
|
_stateManager.TryUpdateStateSafely(clientId, state =>
|
{
|
state.IsEventSubscribed = false; // 取消订阅标志
|
state.CurrentAction = ""; // 清除当前动作
|
state.OperStatus = ""; // 清除运行状态
|
state.RobotArmObject = 0; // 重置手臂对象状态
|
state.RobotControlMode = 0; // 重置控制模式
|
state.RobotRunMode = 0; // 重置运行模式
|
return state;
|
});
|
|
// 触发客户端断开连接事件,通知上层(如 RobotJob)
|
// 使用空的状态对象作为后备(如果获取不到)
|
OnClientDisconnected?.Invoke(this, _stateManager.GetState(clientId) ?? new RobotSocketState { IPAddress = clientId });
|
|
// 返回 null,因为这是事件处理而非真正的消息路由
|
return Task.FromResult<string?>(null);
|
}
|
|
/// <summary>
|
/// 检查客户端是否已连接
|
/// </summary>
|
/// <param name="ipAddress">设备 IP 地址</param>
|
/// <returns>如果已连接则返回 true</returns>
|
public bool IsClientConnected(string ipAddress)
|
{
|
// 获取所有已连接客户端的 ID 列表
|
var clientIds = _tcpSocket.GetClientIds();
|
// 检查列表中是否包含指定的 IP 地址
|
return clientIds.Contains(ipAddress);
|
}
|
|
/// <summary>
|
/// 发送消息到客户端
|
/// </summary>
|
/// <remarks>
|
/// 封装 TcpSocketServer 的发送方法,提供更简洁的接口给业务层
|
/// </remarks>
|
/// <param name="ipAddress">目标客户端 IP 地址</param>
|
/// <param name="message">要发送的消息内容</param>
|
/// <returns>发送是否成功</returns>
|
public async Task<bool> SendToClientAsync(string ipAddress, string message)
|
{
|
return await _tcpSocket.SendToClientAsync(ipAddress, message);
|
}
|
|
/// <summary>
|
/// 获取 TcpSocketServer 引用
|
/// </summary>
|
/// <remarks>
|
/// RobotJob 可能需要直接访问 TcpSocketServer 进行配置
|
/// 此属性提供只读访问
|
/// </remarks>
|
public TcpSocketServer TcpSocket => _tcpSocket;
|
}
|
}
|