wanshenmean
2026-03-26 8e42d0c1b7ae36cff2e7c69999117911a4b6f300
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Messaging.cs
@@ -8,16 +8,21 @@
    public partial class TcpSocketServer
    {
        /// <summary>
        /// 异步处理与已连接的TCP客户端的通信,处理机器人起重机会话中的传入消息和客户端状态更新。
        /// 处理客户端连接的消息循环
        /// </summary>
        /// <remarks>此方法管理客户端连接的生命周期,包括读取消息、更新客户端状态和调用相关事件。
        /// 当处理结束时,客户端和相关的网络资源将被释放。如果启用心跳或空闲超时选项,
        /// 将应用额外的取消逻辑。事件调用期间的异常将被捕获并抑制,以确保会话处理的鲁棒性。</remarks>
        /// <param name="client">表示要处理的远程连接的TCP客户端。方法完成后将释放此对象。</param>
        /// <param name="clientId">已连接客户端的唯一标识符。用于在整个会话中跟踪和更新客户端状态。</param>
        /// <param name="cancellationToken">可用于取消客户端处理操作的取消令牌。如果请求取消,方法将立即终止处理。</param>
        /// <param name="robotCrane">表示与客户端关联的机器人起重机的当前状态对象。用于为消息处理和事件调用提供上下文。</param>
        /// <returns>表示处理客户端连接的异步操作的任务。当客户端断开连接或请求取消时任务完成。</returns>
        /// <remarks>
        /// 持续接收客户端消息,直到连接断开或取消。
        /// 处理流程:
        /// 1. 接收消息(帧解析)
        /// 2. 更新客户端状态(活跃时间、编码)
        /// 3. 处理设备注册
        /// 4. 触发 MessageReceived 事件
        /// 连接断开时清理资源并触发 RobotReceived 事件。
        /// </remarks>
        /// <param name="client">TCP 客户端连接</param>
        /// <param name="clientId">客户端唯一标识</param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <param name="robotCrane">机器人状态</param>
        public async Task HandleClientAsync(TcpClient client, string clientId, CancellationToken cancellationToken, RobotSocketState robotCrane)
        {
            using (client)
@@ -28,19 +33,21 @@
                CancellationTokenSource? localCts = null;
                if (_options.EnableHeartbeat || _options.IdleTimeoutSeconds > 0)
                {
                    // 创建链接的取消令牌源
                    localCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
                }
                try
                {
                    // 消息接收循环
                    while (!cancellationToken.IsCancellationRequested && client.Connected)
                    {
                        string? message;
                        try
                        {
                            var ct = localCts?.Token ?? cancellationToken;
                            // 接收完整消息(帧解析)
                            message = await ReceiveFullMessageAsync(networkStream, _textEncoding, ct);
                            //message = await reader.ReadLineAsync().WaitAsync(ct);
                        }
                        catch (OperationCanceledException)
                        {
@@ -52,20 +59,23 @@
                            break;
                        }
                        // 更新客户端状态
                        UpdateClientStatus(clientId, message);
                        string messageLower = message.ToLowerInvariant();
                        // 处理注册消息
                        if (TryHandleRegister(messageLower, message, clientId, networkStream, cancellationToken))
                        {
                            continue;
                        }
                        // 触发消息接收事件
                        if (MessageReceived != null)
                        {
                            try
                            {
                                // 判断是否为 JSON 格式
                                // 判断是否为 JSON 格式
                                bool isJsonFormat = TryParseJsonSilent(message);
                                _ = MessageReceived.Invoke(message, isJsonFormat, client, robotCrane);
                            }
@@ -75,6 +85,7 @@
                }
                finally
                {
                    // 清理资源
                    try { localCts?.Cancel(); localCts?.Dispose(); } catch { }
                    RemoveClient(clientId);
                    try { _ = RobotReceived.Invoke(clientId); } catch { }
@@ -83,17 +94,18 @@
        }
        /// <summary>
        /// 尝试处理来自客户端的设备注册请求。返回一个值指示该消息是否被作为注册请求处理。
        /// 处理设备注册消息
        /// </summary>
        /// <remarks>如果消息是有效的注册请求且包含非空的设备标识符,
        /// 则将设备绑定到客户端并发送确认信息。此方法不会因无效消息而抛出异常;
        /// 它仅返回 false。</remarks>
        /// <param name="messageLower">客户端消息的小写版本,用于判断消息是否为注册请求。</param>
        /// <param name="message">包含注册命令和设备标识符的原始客户端消息。</param>
        /// <param name="clientId">发送注册请求的客户端的唯一标识符。</param>
        /// <param name="client">与客户端通信的TCP客户端连接。</param>
        /// <param name="cancellationToken">可用于取消注册操作的取消令牌。</param>
        /// <returns>如果消息被识别并作为注册请求处理,则返回 true;否则返回 false。</returns>
        /// <remarks>
        /// 注册消息格式:register,{deviceId}
        /// 将设备 ID 绑定到当前客户端 ID。
        /// </remarks>
        /// <param name="messageLower">消息小写版本</param>
        /// <param name="message">原始消息</param>
        /// <param name="clientId">客户端 ID</param>
        /// <param name="networkStream">网络流</param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns>是否处理了注册消息</returns>
        private bool TryHandleRegister(string messageLower, string message, string clientId, NetworkStream networkStream, CancellationToken cancellationToken)
        {
            if (!messageLower.StartsWith("register,"))
@@ -101,14 +113,17 @@
                return false;
            }
            // 提取设备 ID
            string deviceId = message.Substring("register,".Length).Trim();
            if (!string.IsNullOrEmpty(deviceId))
            {
                lock (_syncRoot)
                {
                    // 绑定设备到客户端
                    _deviceBindings[deviceId] = clientId;
                }
                // 回复注册成功
                _ = WriteToClientAsync(clientId, networkStream, $"Registered,{deviceId}", cancellationToken);
            }
@@ -116,20 +131,27 @@
        }
        /// <summary>
        /// 更新客户端状态
        /// 更新客户端状态
        /// </summary>
        /// <param name="clientId"></param>
        /// <param name="message"></param>
        /// <remarks>
        /// 更新最后活跃时间和字符编码。
        /// 如果开启了自动编码检测,根据消息内容判断是 UTF-8 还是 GBK。
        /// </remarks>
        /// <param name="clientId">客户端 ID</param>
        /// <param name="message">最新接收的消息</param>
        private void UpdateClientStatus(string clientId, string message)
        {
            lock (_syncRoot)
            {
                // 更新最后活跃时间
                _clientLastActive[clientId] = DateTime.Now;
                // 如果还没有记录编码
                if (!_clientEncodings.ContainsKey(clientId))
                {
                    if (_options.AutoDetectEncoding && _autoDetectedGb2312 != null)
                    {
                        // 自动检测编码:JSON 或 UTF-8 字节特征则用 UTF-8,否则用 GBK
                        bool isUtf8 = TryParseJsonSilent(message) || IsLikelyUtf8(_textEncoding.GetBytes(message));
                        _clientEncodings[clientId] = isUtf8 ? _textEncoding : _autoDetectedGb2312;
                    }
@@ -142,13 +164,11 @@
        }
        /// <summary>
        /// 写入消息到客户端
        /// 异步发送消息到客户端
        /// </summary>
        /// <param name="clientId"></param>
        /// <param name="networkStream"></param>
        /// <param name="message"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        /// <remarks>
        /// 内部方法,不使用帧格式,直接发送原始消息。
        /// </remarks>
        private async Task WriteToClientAsync(string clientId, NetworkStream networkStream, string message, CancellationToken cancellationToken)
        {
            SemaphoreSlim? sem = null;
@@ -175,10 +195,13 @@
        }
        /// <summary>
        /// 添加消息帧头尾
        /// 构建帧消息
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        /// <remarks>
        /// 在消息前后添加头尾标识。
        /// </remarks>
        /// <param name="message">原始消息</param>
        /// <returns>带帧标识的消息</returns>
        private string BuildFramedMessage(string message)
        {
            var header = _options.MessageHeader ?? string.Empty;
@@ -187,10 +210,14 @@
        }
        /// <summary>
        /// JSON格式尝试解析(静默失败)
        /// 静默尝试解析 JSON
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        /// <remarks>
        /// 判断消息是否以 { 或 [ 开头,如果是则尝试解析。
        /// 解析失败不抛异常。
        /// </remarks>
        /// <param name="message">消息内容</param>
        /// <returns>是否是有效的 JSON 格式</returns>
        private static bool TryParseJsonSilent(string message)
        {
            if (string.IsNullOrWhiteSpace(message)) return false;
@@ -200,30 +227,35 @@
        }
        /// <summary>
        /// utf-8 可能性检测
        /// 判断字节数组是否为 UTF-8 编码
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        /// <remarks>
        /// 通过检查字节序列是否符合 UTF-8 多字节字符的编码规则。
        /// </remarks>
        /// <param name="data">字节数组</param>
        /// <returns>是否可能是 UTF-8 编码</returns>
        private static bool IsLikelyUtf8(byte[] data)
        {
            int i = 0;
            while (i < data.Length)
            {
                byte b = data[i];
                if (b <= 0x7F) { i++; continue; }
                if (b >= 0xC2 && b <= 0xDF)
                if (b <= 0x7F) { i++; continue; }  // ASCII 字符
                // 检查多字节字符
                if (b >= 0xC2 && b <= 0xDF)  // 2字节字符
                {
                    if (i + 1 >= data.Length) return false;
                    if ((data[i + 1] & 0xC0) != 0x80) return false;
                    i += 2; continue;
                }
                if (b >= 0xE0 && b <= 0xEF)
                if (b >= 0xE0 && b <= 0xEF)  // 3字节字符
                {
                    if (i + 2 >= data.Length) return false;
                    if ((data[i + 1] & 0xC0) != 0x80 || (data[i + 2] & 0xC0) != 0x80) return false;
                    i += 3; continue;
                }
                if (b >= 0xF0 && b <= 0xF4)
                if (b >= 0xF0 && b <= 0xF4)  // 4字节字符
                {
                    if (i + 3 >= data.Length) return false;
                    if ((data[i + 1] & 0xC0) != 0x80 || (data[i + 2] & 0xC0) != 0x80 || (data[i + 3] & 0xC0) != 0x80) return false;
@@ -235,12 +267,16 @@
        }
        /// <summary>
        /// 读取完整消息
        /// 接收完整消息(帧解析)
        /// </summary>
        /// <param name="networkStream">字节流</param>
        /// <param name="encoding">编码格式</param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        /// <remarks>
        /// 根据配置的头尾标识解析消息。
        /// 如果未配置头尾,则一直读到数据不可用。
        /// </remarks>
        /// <param name="networkStream">网络流</param>
        /// <param name="encoding">字符编码</param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns>接收到的消息</returns>
        private async Task<string?> ReceiveFullMessageAsync(NetworkStream networkStream, Encoding encoding, CancellationToken cancellationToken)
        {
            var header = _options.MessageHeader ?? string.Empty;
@@ -251,15 +287,18 @@
            while (true)
            {
                // 读取数据
                int bytesRead = await networkStream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
                if (bytesRead <= 0)
                {
                    if (builder.Length == 0) return null;
                    // 无头尾配置时,返回已有数据
                    return string.IsNullOrEmpty(header) && string.IsNullOrEmpty(footer) ? builder.ToString() : null;
                }
                builder.Append(encoding.GetString(buffer, 0, bytesRead));
                // 如果没有配置头尾,且数据不可用,返回已有数据
                if (string.IsNullOrEmpty(header) && string.IsNullOrEmpty(footer))
                {
                    if (!networkStream.DataAvailable)
@@ -269,6 +308,7 @@
                    continue;
                }
                // 查找帧头
                var data = builder.ToString();
                var headerIndex = string.IsNullOrEmpty(header) ? 0 : data.IndexOf(header, StringComparison.Ordinal);
                if (headerIndex < 0)
@@ -276,6 +316,7 @@
                    continue;
                }
                // 提取帧内容
                var startIndex = headerIndex + header.Length;
                var footerIndex = string.IsNullOrEmpty(footer) ? data.Length : data.IndexOf(footer, startIndex, StringComparison.Ordinal);
                if (footerIndex >= 0)