From cde6ad77663a80d78d77568428a6287b53347716 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期四, 19 三月 2026 17:19:55 +0800
Subject: [PATCH] feat: 新增API路由缓存预热并完善机器人消息日志

---
 Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Services/RobotClientManager.cs |  588 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 588 insertions(+), 0 deletions(-)

diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Services/RobotClientManager.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Services/RobotClientManager.cs
new file mode 100644
index 0000000..1fd7887
--- /dev/null
+++ b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Services/RobotClientManager.cs
@@ -0,0 +1,588 @@
+锘縰sing System.Collections.Concurrent;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+namespace WIDESEAWCS_S7Simulator.Server.Services;
+
+public interface IRobotClientManager
+{
+    Task<RobotServerCollectionStatusResponse> StartAsync(RobotServerStartRequest request, CancellationToken cancellationToken = default);
+    Task StopAsync(string? serverId = null);
+    Task<RobotServerCollectionStatusResponse> GetStatusAsync();
+    Task SendToClientAsync(string serverId, int clientId, string message);
+    Task SendToAllAsync(string serverId, string message);
+    Task ClearReceivedMessagesAsync(string serverId);
+}
+
+/// <summary>
+/// 鏈烘鎵� TCP 瀹㈡埛绔瀹炰緥绠$悊鍣ㄣ��
+/// 涓�涓� ServerId 瀵瑰簲涓�涓� TcpClient锛屼富鍔ㄨ繛鎺ョ洰鏍囨湇鍔$銆�
+/// </summary>
+public sealed class RobotClientManager : IRobotClientManager, IDisposable
+{
+    private readonly ILogger<RobotClientManager> _logger;
+    private readonly ConcurrentDictionary<string, RobotClientRuntime> _clients = new(StringComparer.OrdinalIgnoreCase);
+    private bool _disposed;
+
+    public RobotClientManager(ILogger<RobotClientManager> logger)
+    {
+        _logger = logger;
+    }
+
+    public async Task<RobotServerCollectionStatusResponse> StartAsync(RobotServerStartRequest request, CancellationToken cancellationToken = default)
+    {
+        ValidateStartRequest(request);
+        EnsureLocalPortAvailable(request.LocalPort);
+
+        var key = request.ServerId.Trim();
+        if (_clients.ContainsKey(key))
+        {
+            throw new InvalidOperationException($"瀹㈡埛绔疄渚� '{key}' 宸插瓨鍦�");
+        }
+
+        var runtime = new RobotClientRuntime
+        {
+            ServerId = key,
+            RemoteIp = request.ListenIp,
+            RemotePort = request.ListenPort,
+            LocalPort = request.LocalPort,
+            Cancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken),
+            Connected = false
+        };
+
+        runtime.ConnectionLoopTask = Task.Run(() => ConnectionLoopAsync(runtime), runtime.Cancellation.Token);
+
+        if (!_clients.TryAdd(key, runtime))
+        {
+            await StopRuntimeAsync(runtime);
+            throw new InvalidOperationException($"瀹㈡埛绔疄渚� '{key}' 鍒涘缓澶辫触");
+        }
+
+        _logger.LogInformation("鏈烘鎵嬪鎴风宸插惎鍔�: {ServerId} 鏈湴:{LocalPort} -> 杩滅:{Ip}:{Port}",
+            key,
+            request.LocalPort,
+            request.ListenIp,
+            request.ListenPort);
+
+        return await GetStatusAsync();
+    }
+
+    public async Task StopAsync(string? serverId = null)
+    {
+        if (string.IsNullOrWhiteSpace(serverId))
+        {
+            var all = _clients.Values.ToArray();
+            _clients.Clear();
+            await Task.WhenAll(all.Select(StopRuntimeAsync));
+            return;
+        }
+
+        if (_clients.TryRemove(serverId.Trim(), out var runtime))
+        {
+            await StopRuntimeAsync(runtime);
+        }
+    }
+
+    public Task<RobotServerCollectionStatusResponse> GetStatusAsync()
+    {
+        var servers = _clients.Values
+            .OrderBy(x => x.ServerId, StringComparer.OrdinalIgnoreCase)
+            .Select(ToStatusItem)
+            .ToArray();
+
+        return Task.FromResult(new RobotServerCollectionStatusResponse
+        {
+            RunningServerCount = servers.Count(x => x.Running),
+            Servers = servers
+        });
+    }
+
+    public async Task SendToClientAsync(string serverId, int clientId, string message)
+    {
+        var runtime = GetClientOrThrow(serverId);
+        if (clientId != 1)
+        {
+            throw new InvalidOperationException($"瀹㈡埛绔疄渚� '{runtime.ServerId}' 浠呮敮鎸� ClientId=1");
+        }
+
+        await SendFrameAsync(runtime, message, CancellationToken.None);
+    }
+
+    public async Task SendToAllAsync(string serverId, string message)
+    {
+        var runtime = GetClientOrThrow(serverId);
+        await SendFrameAsync(runtime, message, CancellationToken.None);
+    }
+
+    /// <summary>
+    /// 娓呯┖鎸囧畾瀹㈡埛绔疄渚嬬殑鏀跺彂娑堟伅鏃ュ織銆�
+    /// </summary>
+    public Task ClearReceivedMessagesAsync(string serverId)
+    {
+        var runtime = GetClientOrThrow(serverId);
+
+        while (runtime.ReceivedMessages.TryDequeue(out _))
+        {
+        }
+
+        while (runtime.SentMessages.TryDequeue(out _))
+        {
+        }
+
+        return Task.CompletedTask;
+    }
+
+    private RobotClientRuntime GetClientOrThrow(string serverId)
+    {
+        if (string.IsNullOrWhiteSpace(serverId))
+        {
+            throw new ArgumentException("ServerId 涓嶈兘涓虹┖");
+        }
+
+        if (!_clients.TryGetValue(serverId.Trim(), out var runtime))
+        {
+            throw new InvalidOperationException($"瀹㈡埛绔疄渚� '{serverId}' 涓嶅瓨鍦�");
+        }
+
+        return runtime;
+    }
+
+    /// <summary>
+    /// 瀹㈡埛绔繛鎺ュ畧鎶ゅ惊鐜細鏂紑鍚庤嚜鍔ㄩ噸杩炪��
+    /// </summary>
+    private async Task ConnectionLoopAsync(RobotClientRuntime runtime)
+    {
+        var token = runtime.Cancellation.Token;
+
+        while (!token.IsCancellationRequested)
+        {
+            if (!runtime.Connected)
+            {
+                try
+                {
+                    await ConnectOnceAsync(runtime, token);
+                }
+                catch (OperationCanceledException)
+                {
+                    break;
+                }
+                catch (Exception ex)
+                {
+                    runtime.LastError = ex.Message;
+                    _logger.LogWarning(ex, "[{ServerId}] 杩炴帴澶辫触锛屽皢鍦� 2 绉掑悗閲嶈瘯", runtime.ServerId);
+                    await DelayForReconnect(token);
+                    continue;
+                }
+            }
+
+            var tcpClient = runtime.TcpClient;
+            if (tcpClient == null)
+            {
+                await DelayForReconnect(token);
+                continue;
+            }
+
+            await ReceiveLoopAsync(runtime, tcpClient);
+            await DelayForReconnect(token);
+        }
+    }
+
+    /// <summary>
+    /// 鍗曟杩炴帴鍔ㄤ綔銆傛垚鍔熷悗鏇存柊杩愯鏃惰繛鎺ョ姸鎬併��
+    /// </summary>
+    private async Task ConnectOnceAsync(RobotClientRuntime runtime, CancellationToken token)
+    {
+        var tcpClient = new TcpClient(AddressFamily.InterNetwork);
+        try
+        {
+            // 姣忔閲嶈繛閮藉浐瀹氱粦瀹氬悓涓�鏈湴绔彛銆�
+            tcpClient.Client.Bind(new IPEndPoint(IPAddress.Any, runtime.LocalPort));
+            await tcpClient.ConnectAsync(runtime.RemoteIp, runtime.RemotePort, token);
+        }
+        catch
+        {
+            tcpClient.Dispose();
+            throw;
+        }
+
+        runtime.TcpClient = tcpClient;
+        runtime.Connected = true;
+        runtime.ConnectedAt = DateTimeOffset.Now;
+        runtime.LastError = null;
+
+        _logger.LogInformation("[{ServerId}] 宸茶繛鎺� 鏈湴:{LocalPort} -> 杩滅:{Ip}:{Port}",
+            runtime.ServerId,
+            runtime.LocalPort,
+            runtime.RemoteIp,
+            runtime.RemotePort);
+    }
+
+    /// <summary>
+    /// 鍗曚釜瀹㈡埛绔敹鍖呭惊鐜紝鏀寔甯у崗璁拰鏅�氭枃鏈袱绉嶈緭鍏ャ��
+    /// </summary>
+    private async Task ReceiveLoopAsync(RobotClientRuntime runtime, TcpClient tcpClient)
+    {
+        var token = runtime.Cancellation.Token;
+        var buffer = new byte[2048];
+        var cache = new StringBuilder();
+
+        try
+        {
+            var stream = tcpClient.GetStream();
+            while (!token.IsCancellationRequested)
+            {
+                var length = await stream.ReadAsync(buffer, 0, buffer.Length, token);
+                if (length <= 0)
+                {
+                    break;
+                }
+
+                runtime.LastReceivedAt = DateTimeOffset.Now;
+                cache.Append(Encoding.UTF8.GetString(buffer, 0, length));
+                TryReadMessages(runtime, cache);
+            }
+        }
+        catch (OperationCanceledException)
+        {
+            // 姝e父鍋滄銆�
+        }
+        catch (Exception ex)
+        {
+            runtime.LastError = ex.Message;
+            _logger.LogWarning(ex, "[{ServerId}] 瀹㈡埛绔敹鍖呭紓甯�", runtime.ServerId);
+        }
+        finally
+        {
+            runtime.Connected = false;
+            await CloseRuntimeSocketAsync(runtime);
+            _logger.LogWarning("[{ServerId}] 瀹㈡埛绔繛鎺ュ凡鏂紑锛屽噯澶囪嚜鍔ㄩ噸杩�", runtime.ServerId);
+        }
+    }
+
+    private void TryReadMessages(RobotClientRuntime runtime, StringBuilder cache)
+    {
+        const string start = "<START>";
+        const string end = "<END>";
+
+        while (true)
+        {
+            var text = cache.ToString();
+            var startIndex = text.IndexOf(start, StringComparison.Ordinal);
+            var endIndex = text.IndexOf(end, StringComparison.Ordinal);
+
+            // 鏈懡涓抚鍗忚鏃讹紝鎸夋暣娈垫枃鏈褰曪紝閬垮厤娑堟伅涓㈠け銆�
+            if (startIndex < 0 || endIndex < 0 || endIndex <= startIndex)
+            {
+                if (cache.Length > 0 && cache.Length < 2048 && !text.Contains("<START>", StringComparison.Ordinal))
+                {
+                    AppendReceived(runtime, text.Trim());
+                    cache.Clear();
+                }
+
+                if (cache.Length > 10240)
+                {
+                    cache.Clear();
+                }
+
+                return;
+            }
+
+            var contentStart = startIndex + start.Length;
+            var contentLength = endIndex - contentStart;
+            var content = text.Substring(contentStart, contentLength);
+            cache.Remove(0, endIndex + end.Length);
+            AppendReceived(runtime, content);
+        }
+    }
+
+    private void AppendReceived(RobotClientRuntime runtime, string message)
+    {
+        if (string.IsNullOrWhiteSpace(message))
+        {
+            return;
+        }
+
+        runtime.LastReceivedMessage = message;
+        runtime.ReceivedMessages.Enqueue(new RobotServerReceivedMessageItem
+        {
+            ReceivedAt = DateTimeOffset.Now,
+            ClientId = 1,
+            RemoteEndPoint = $"{runtime.RemoteIp}:{runtime.RemotePort}",
+            Message = message
+        });
+
+        while (runtime.ReceivedMessages.Count > 500)
+        {
+            runtime.ReceivedMessages.TryDequeue(out _);
+        }
+
+        _logger.LogInformation("[{ServerId}] 鏀跺埌: {Message}", runtime.ServerId, message);
+    }
+
+    private async Task SendFrameAsync(RobotClientRuntime runtime, string message, CancellationToken token)
+    {
+        if (!runtime.Connected)
+        {
+            throw new InvalidOperationException($"瀹㈡埛绔疄渚� '{runtime.ServerId}' 鏈繛鎺�");
+        }
+
+        var frame = $"<START>{message}<END>";
+        var bytes = Encoding.UTF8.GetBytes(frame);
+
+        await runtime.SendLock.WaitAsync(token);
+        try
+        {
+            if (runtime.TcpClient == null)
+            {
+                throw new InvalidOperationException($"瀹㈡埛绔疄渚� '{runtime.ServerId}' 鏈繛鎺�");
+            }
+
+            var stream = runtime.TcpClient.GetStream();
+            await stream.WriteAsync(bytes, 0, bytes.Length, token);
+            await stream.FlushAsync(token);
+            runtime.LastSentAt = DateTimeOffset.Now;
+
+            runtime.SentMessages.Enqueue(new RobotServerSentMessageItem
+            {
+                SentAt = DateTimeOffset.Now,
+                ClientId = 1,
+                RemoteEndPoint = $"{runtime.RemoteIp}:{runtime.RemotePort}",
+                Message = message
+            });
+
+            while (runtime.SentMessages.Count > 500)
+            {
+                runtime.SentMessages.TryDequeue(out _);
+            }
+
+            _logger.LogInformation("[{ServerId}] 鍙戦��: {Frame}", runtime.ServerId, frame);
+        }
+        finally
+        {
+            runtime.SendLock.Release();
+        }
+    }
+
+    private static async Task CloseRuntimeSocketAsync(RobotClientRuntime runtime)
+    {
+        await runtime.SendLock.WaitAsync();
+        try
+        {
+            if (runtime.TcpClient != null)
+            {
+                try { runtime.TcpClient.Close(); } catch { }
+                runtime.TcpClient.Dispose();
+                runtime.TcpClient = null;
+            }
+            runtime.Connected = false;
+        }
+        finally
+        {
+            runtime.SendLock.Release();
+        }
+    }
+
+    private async Task StopRuntimeAsync(RobotClientRuntime runtime)
+    {
+        try { runtime.Cancellation.Cancel(); } catch { }
+
+        if (runtime.ConnectionLoopTask != null)
+        {
+            try { await runtime.ConnectionLoopTask; } catch { }
+        }
+
+        await CloseRuntimeSocketAsync(runtime);
+        runtime.Cancellation.Dispose();
+        _logger.LogInformation("鏈烘鎵嬪鎴风宸插仠姝�: {ServerId}", runtime.ServerId);
+    }
+
+    private static RobotServerStatusItem ToStatusItem(RobotClientRuntime runtime)
+    {
+        var clients = new[]
+        {
+            new RobotServerClientStatusItem
+            {
+                ClientId = 1,
+                RemoteEndPoint = $"{runtime.RemoteIp}:{runtime.RemotePort}",
+                Connected = runtime.Connected,
+                ConnectedAt = runtime.ConnectedAt,
+                LastReceivedAt = runtime.LastReceivedAt,
+                LastSentAt = runtime.LastSentAt,
+                LastReceivedMessage = runtime.LastReceivedMessage,
+                LastError = runtime.LastError
+            }
+        };
+
+        var receivedMessages = runtime.ReceivedMessages
+            .Reverse()
+            .Take(200)
+            .ToArray();
+
+        var sentMessages = runtime.SentMessages
+            .Reverse()
+            .Take(200)
+            .ToArray();
+
+        return new RobotServerStatusItem
+        {
+            ServerId = runtime.ServerId,
+            Running = !runtime.Cancellation.IsCancellationRequested,
+            ListenIp = runtime.RemoteIp,
+            ListenPort = runtime.RemotePort,
+            LocalPort = runtime.LocalPort,
+            ConnectedCount = runtime.Connected ? 1 : 0,
+            Clients = clients,
+            ReceivedMessages = receivedMessages,
+            SentMessages = sentMessages
+        };
+    }
+
+    private static void ValidateStartRequest(RobotServerStartRequest request)
+    {
+        if (string.IsNullOrWhiteSpace(request.ServerId))
+        {
+            throw new ArgumentException("ServerId 涓嶈兘涓虹┖");
+        }
+
+        if (string.IsNullOrWhiteSpace(request.ListenIp))
+        {
+            throw new ArgumentException("鐩爣鏈嶅姟绔湴鍧�涓嶈兘涓虹┖");
+        }
+
+        if (request.ListenPort <= 0 || request.ListenPort > 65535)
+        {
+            throw new ArgumentException("鐩爣鏈嶅姟绔鍙e繀椤诲湪 1-65535 鑼冨洿鍐�");
+        }
+
+        if (request.LocalPort <= 0 || request.LocalPort > 65535)
+        {
+            throw new ArgumentException("瀹㈡埛绔湰鍦扮鍙e繀椤诲湪 1-65535 鑼冨洿鍐�");
+        }
+    }
+
+    /// <summary>
+    /// 鍚姩鍓嶆鏌ユ湰鍦扮鍙f槸鍚﹀彲鐢紝閬垮厤瀹炰緥杩涘叆鏃犳晥閲嶈繛鐘舵�併��
+    /// </summary>
+    private static void EnsureLocalPortAvailable(int localPort)
+    {
+        Socket? probe = null;
+        try
+        {
+            probe = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+            probe.Bind(new IPEndPoint(IPAddress.Any, localPort));
+        }
+        catch (SocketException ex)
+        {
+            throw new InvalidOperationException($"鏈湴绔彛 {localPort} 宸茶鍗犵敤鎴栦笉鍙敤", ex);
+        }
+        finally
+        {
+            try { probe?.Dispose(); } catch { }
+        }
+    }
+
+    private static async Task DelayForReconnect(CancellationToken token)
+    {
+        try
+        {
+            await Task.Delay(TimeSpan.FromSeconds(2), token);
+        }
+        catch (OperationCanceledException)
+        {
+        }
+    }
+
+    public void Dispose()
+    {
+        if (_disposed)
+        {
+            return;
+        }
+
+        _disposed = true;
+        StopAsync().GetAwaiter().GetResult();
+    }
+
+    private sealed class RobotClientRuntime
+    {
+        public string ServerId { get; set; } = string.Empty;
+        public string RemoteIp { get; set; } = string.Empty;
+        public int RemotePort { get; set; }
+        public int LocalPort { get; set; }
+        public TcpClient? TcpClient { get; set; }
+        public CancellationTokenSource Cancellation { get; set; } = default!;
+        public Task? ConnectionLoopTask { get; set; }
+        public SemaphoreSlim SendLock { get; } = new(1, 1);
+        public bool Connected { get; set; }
+        public DateTimeOffset? ConnectedAt { get; set; }
+        public DateTimeOffset? LastReceivedAt { get; set; }
+        public DateTimeOffset? LastSentAt { get; set; }
+        public string? LastReceivedMessage { get; set; }
+        public string? LastError { get; set; }
+        public ConcurrentQueue<RobotServerReceivedMessageItem> ReceivedMessages { get; } = new();
+        public ConcurrentQueue<RobotServerSentMessageItem> SentMessages { get; } = new();
+    }
+}
+
+public sealed class RobotServerStartRequest
+{
+    public string ServerId { get; set; } = "default";
+    public string ListenIp { get; set; } = "127.0.0.1";
+    public int ListenPort { get; set; } = 2000;
+    public int LocalPort { get; set; } = 2001;
+}
+
+public sealed class RobotServerSendRequest
+{
+    public string ServerId { get; set; } = string.Empty;
+    public int? ClientId { get; set; }
+    public string Message { get; set; } = string.Empty;
+}
+
+public sealed class RobotServerCollectionStatusResponse
+{
+    public int RunningServerCount { get; set; }
+    public IReadOnlyList<RobotServerStatusItem> Servers { get; set; } = Array.Empty<RobotServerStatusItem>();
+}
+
+public sealed class RobotServerStatusItem
+{
+    public string ServerId { get; set; } = string.Empty;
+    public bool Running { get; set; }
+    public string ListenIp { get; set; } = string.Empty;
+    public int ListenPort { get; set; }
+    public int LocalPort { get; set; }
+    public int ConnectedCount { get; set; }
+    public IReadOnlyList<RobotServerClientStatusItem> Clients { get; set; } = Array.Empty<RobotServerClientStatusItem>();
+    public IReadOnlyList<RobotServerReceivedMessageItem> ReceivedMessages { get; set; } = Array.Empty<RobotServerReceivedMessageItem>();
+    public IReadOnlyList<RobotServerSentMessageItem> SentMessages { get; set; } = Array.Empty<RobotServerSentMessageItem>();
+}
+
+public sealed class RobotServerClientStatusItem
+{
+    public int ClientId { get; set; }
+    public string? RemoteEndPoint { get; set; }
+    public bool Connected { get; set; }
+    public DateTimeOffset? ConnectedAt { get; set; }
+    public DateTimeOffset? LastReceivedAt { get; set; }
+    public DateTimeOffset? LastSentAt { get; set; }
+    public string? LastReceivedMessage { get; set; }
+    public string? LastError { get; set; }
+}
+
+public sealed class RobotServerReceivedMessageItem
+{
+    public DateTimeOffset ReceivedAt { get; set; }
+    public int ClientId { get; set; }
+    public string? RemoteEndPoint { get; set; }
+    public string Message { get; set; } = string.Empty;
+}
+
+public sealed class RobotServerSentMessageItem
+{
+    public DateTimeOffset SentAt { get; set; }
+    public int ClientId { get; set; }
+    public string? RemoteEndPoint { get; set; }
+    public string Message { get; set; } = string.Empty;
+}

--
Gitblit v1.9.3