wanshenmean
2026-03-26 8e42d0c1b7ae36cff2e7c69999117911a4b6f300
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotClientManager.cs
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Net.Sockets;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_Tasks.SocketServer;
@@ -8,17 +9,56 @@
    /// <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;
        // 跟踪已经启动 HandleClientAsync 的客户端
        /// <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;
@@ -28,66 +68,88 @@
        /// <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 状态
                // 清理该客户端的 HandleClientAsync 启动标志
                // 以便下次重连时可以重新启动处理
                _handleClientStarted.TryRemove(ipAddress, out _);
                return false;
            }
            // 订阅一次 robot 事件(全局一次)- message事件由RobotJob订阅
            // 订阅一次 RobotReceived 事件(全局只订阅一次)
            // 使用 Interlocked.CompareExchange 实现原子操作,确保线程安全
            if (System.Threading.Interlocked.CompareExchange(ref _eventSubscribedFlag, 1, 0) == 0)
            {
                // 绑定客户端断开连接的事件处理
                _tcpSocket.RobotReceived += OnRobotReceived;
                Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 机器人TCP断开事件已订阅");
                // 记录日志(注意:日志内容为"客户端已断开连接",可能是遗留的占位文本)
                QuartzLogger.Error($"客户端已断开连接", robotCrane.DeviceName);
            }
            // 获取TcpClient
            // 从 TCP 服务器的客户端字典中获取 TcpClient 对象
            TcpClient? tcpClient = null;
            _tcpSocket._clients.TryGetValue(ipAddress, out tcpClient);
            // 如果获取失败(虽然 isClientConnected 为 true,但可能存在字典不同步的情况)
            if (tcpClient == null)
            {
                // isClientConnected为true但无法获取tcpClient,列表可能不同步
                // 移除启动标志,返回 false 表示客户端不可用
                _handleClientStarted.TryRemove(ipAddress, out _);
                return false;
            }
            // 检查是否已经为这个客户端启动过 HandleClientAsync
            // 检查是否已经为这个客户端启动过消息处理循环
            bool alreadyStarted = _handleClientStarted.TryGetValue(ipAddress, out _);
            // 如果尚未启动,则启动消息处理循环
            if (!alreadyStarted)
            {
                Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 启动客户端消息处理: {ipAddress}");
                // 记录日志
                QuartzLogger.Error($"启动客户端消息处理", robotCrane.DeviceName);
                // 重新获取最新的 state 对象
                // 获取最新的状态对象
                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);
                    // 更新 IsEventSubscribed 状态
                    // 安全更新状态,标记为已订阅消息事件
                    _stateManager.TryUpdateStateSafely(ipAddress, s =>
                    {
                        s.IsEventSubscribed = true;
@@ -96,54 +158,81 @@
                }
            }
            // 返回 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 启动标志
            // 移除该客户端的 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;
                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引用(用于RobotJob直接访问)
        /// 获取 TcpSocketServer 引用
        /// </summary>
        /// <remarks>
        /// RobotJob 可能需要直接访问 TcpSocketServer 进行配置
        /// 此属性提供只读访问
        /// </remarks>
        public TcpSocketServer TcpSocket => _tcpSocket;
    }
}