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