Browse Source

jvnjfhj

feature/MultiClientLog
root 1 month ago
parent
commit
c6db21a672
  1. 173
      LTEMvcApp/Controllers/ClientController.cs
  2. 83
      LTEMvcApp/Controllers/ConfigController.cs
  3. 7
      LTEMvcApp/Controllers/HomeController.cs
  4. 487
      LTEMvcApp/Controllers/LogController.cs
  5. 368
      LTEMvcApp/Controllers/MessageController.cs
  6. 175
      LTEMvcApp/Controllers/TestConfigController.cs
  7. 1165
      LTEMvcApp/Controllers/WebSocketController.cs
  8. 50
      LTEMvcApp/README_ClientMessageLogs.md
  9. 174
      LTEMvcApp/README_MultipleTestConfigs.md
  10. 26
      LTEMvcApp/README_StreamLogs_Optimization.md
  11. 212
      LTEMvcApp/Services/WebSocketManagerService.cs
  12. 10
      LTEMvcApp/Views/Home/ClientMessages.cshtml
  13. 12
      LTEMvcApp/Views/Home/Index.cshtml
  14. 66
      LTEMvcApp/Views/Home/Logs.cshtml
  15. 155
      LTEMvcApp/Views/Home/TestClientConfig.cshtml
  16. 153
      LTEMvcApp/test_client_config.json
  17. 310
      LTEMvcApp/test_client_configs.json

173
LTEMvcApp/Controllers/ClientController.cs

@ -0,0 +1,173 @@
using Microsoft.AspNetCore.Mvc;
using LTEMvcApp.Models;
using LTEMvcApp.Services;
using Microsoft.Extensions.Logging;
namespace LTEMvcApp.Controllers
{
/// <summary>
/// 客户端管理控制器 - 负责客户端的基本管理功能
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ClientController : ControllerBase
{
private readonly WebSocketManagerService _webSocketManager;
private readonly ILogger<ClientController> _logger;
public ClientController(WebSocketManagerService webSocketManager, ILogger<ClientController> logger)
{
_webSocketManager = webSocketManager;
_logger = logger;
}
/// <summary>
/// 获取所有客户端状态
/// </summary>
/// <returns>客户端状态列表</returns>
[HttpGet("states")]
public ActionResult<Dictionary<string, ClientState>> GetClientStates()
{
_logger.LogInformation("获取所有客户端状态");
var states = _webSocketManager.GetAllClientStates();
return Ok(states);
}
/// <summary>
/// 启动客户端
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>操作结果</returns>
[HttpPost("{clientName}/start")]
public ActionResult StartClient(string clientName)
{
_logger.LogInformation($"API请求: 启动客户端 {clientName}");
var success = _webSocketManager.StartClient(clientName);
if (success)
{
_logger.LogInformation($"客户端 {clientName} 启动成功");
return Ok(new { message = $"客户端 '{clientName}' 已启动" });
}
else
{
_logger.LogWarning($"客户端 {clientName} 启动失败");
return BadRequest($"启动客户端 '{clientName}' 失败");
}
}
/// <summary>
/// 停止客户端
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>操作结果</returns>
[HttpPost("{clientName}/stop")]
public ActionResult StopClient(string clientName)
{
var success = _webSocketManager.StopClient(clientName);
if (success)
return Ok(new { message = $"客户端 '{clientName}' 已停止" });
else
return BadRequest($"停止客户端 '{clientName}' 失败");
}
/// <summary>
/// 播放/暂停客户端
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>操作结果</returns>
[HttpPost("{clientName}/playpause")]
public ActionResult PlayPauseClient(string clientName)
{
var success = _webSocketManager.PlayPauseClient(clientName);
if (success)
return Ok(new { message = $"客户端 '{clientName}' 播放/暂停状态已切换" });
else
return BadRequest($"切换客户端 '{clientName}' 播放/暂停状态失败");
}
/// <summary>
/// 重置客户端日志
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>操作结果</returns>
[HttpPost("{clientName}/reset-logs")]
public ActionResult ResetClientLogs(string clientName)
{
var success = _webSocketManager.ResetClientLogs(clientName);
if (success)
return Ok(new { message = $"客户端 '{clientName}' 日志已重置" });
else
return BadRequest($"重置客户端 '{clientName}' 日志失败");
}
/// <summary>
/// 获取客户端日志
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <param name="limit">日志数量限制</param>
/// <returns>日志列表</returns>
[HttpGet("{clientName}/logs")]
public ActionResult<List<LTELog>?> GetClientLogs(string clientName, [FromQuery] int limit = 100)
{
var logs = _webSocketManager.GetClientLogs(clientName);
if (logs == null)
return NotFound($"客户端 '{clientName}' 不存在或未连接");
// 限制返回的日志数量
var limitedLogs = logs.TakeLast(limit).ToList();
return Ok(limitedLogs);
}
/// <summary>
/// 设置客户端日志配置
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <param name="request">请求体</param>
[HttpPost("{clientName}/logs-config")]
public ActionResult SetClientLogsConfig(string clientName, [FromBody] ClientLogsConfig request)
{
var success = _webSocketManager.SetClientLogsConfig(clientName, request);
if (success)
{
return Ok(new { message = "日志配置已更新" });
}
else
{
return NotFound(new { message = $"客户端 '{clientName}' 未找到或更新失败" });
}
}
/// <summary>
/// 获取连接统计信息
/// </summary>
/// <returns>统计信息</returns>
[HttpGet("statistics")]
public ActionResult<ConnectionStatistics> GetStatistics()
{
var stats = _webSocketManager.GetConnectionStatistics();
return Ok(stats);
}
/// <summary>
/// 启动所有已配置的客户端
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("start-all")]
public ActionResult StartAllClients()
{
_webSocketManager.StartAllConfiguredClients();
return Ok(new { message = "所有已配置的客户端已启动" });
}
/// <summary>
/// 停止所有客户端
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("stop-all")]
public ActionResult StopAllClients()
{
_webSocketManager.StopAllClients();
return Ok(new { message = "所有客户端已停止" });
}
}
}

83
LTEMvcApp/Controllers/ConfigController.cs

@ -0,0 +1,83 @@
using Microsoft.AspNetCore.Mvc;
using LTEMvcApp.Models;
using LTEMvcApp.Services;
using Microsoft.Extensions.Logging;
namespace LTEMvcApp.Controllers
{
/// <summary>
/// 配置管理控制器 - 负责普通客户端配置管理
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ConfigController : ControllerBase
{
private readonly WebSocketManagerService _webSocketManager;
private readonly ILogger<ConfigController> _logger;
public ConfigController(WebSocketManagerService webSocketManager, ILogger<ConfigController> logger)
{
_webSocketManager = webSocketManager;
_logger = logger;
}
/// <summary>
/// 获取客户端配置
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>客户端配置</returns>
[HttpGet("{clientName}")]
public ActionResult<ClientConfig?> GetClientConfig(string clientName)
{
var config = _webSocketManager.GetClientConfig(clientName);
if (config == null)
return NotFound($"客户端 '{clientName}' 不存在");
return Ok(config);
}
/// <summary>
/// 获取所有客户端配置
/// </summary>
/// <returns>客户端配置列表</returns>
[HttpGet]
public ActionResult<List<ClientConfig>> GetAllConfigs()
{
var configs = _webSocketManager.GetAllClientConfigs();
return Ok(configs);
}
/// <summary>
/// 添加客户端配置
/// </summary>
/// <param name="config">客户端配置</param>
/// <returns>操作结果</returns>
[HttpPost]
public ActionResult AddClientConfig([FromBody] ClientConfig config)
{
if (string.IsNullOrEmpty(config.Name))
return BadRequest("客户端名称不能为空");
var success = _webSocketManager.AddClientConfig(config);
if (success)
return Ok(new { message = $"客户端 '{config.Name}' 配置已添加" });
else
return BadRequest("添加客户端配置失败");
}
/// <summary>
/// 移除客户端配置
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>操作结果</returns>
[HttpDelete("{clientName}")]
public ActionResult RemoveClientConfig(string clientName)
{
var success = _webSocketManager.RemoveClientConfig(clientName);
if (success)
return Ok(new { message = $"客户端 '{clientName}' 配置已移除" });
else
return BadRequest($"移除客户端 '{clientName}' 配置失败");
}
}
}

7
LTEMvcApp/Controllers/HomeController.cs

@ -28,7 +28,7 @@ public class HomeController : Controller
ViewBag.ClientConfigs = configs;
// 获取测试客户端配置
var testConfig = _webSocketManager.GetTestClientConfig();
var testConfig = _webSocketManager.GetDefaultTestClientConfig();
var testClient = _webSocketManager.GetTestClient();
var clientState = testClient?.State ?? LTEMvcApp.Models.ClientState.Stop;
@ -147,8 +147,11 @@ public class HomeController : Controller
/// </summary>
public IActionResult TestClientConfig()
{
var testConfig = _webSocketManager.GetTestClientConfig();
var testConfig = _webSocketManager.GetDefaultTestClientConfig();
var allTestConfigs = _webSocketManager.GetAllTestClientConfigs();
ViewBag.TestConfig = testConfig;
ViewBag.AllTestConfigs = allTestConfigs;
return View();
}

487
LTEMvcApp/Controllers/LogController.cs

@ -0,0 +1,487 @@
using Microsoft.AspNetCore.Mvc;
using LTEMvcApp.Models;
using LTEMvcApp.Services;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using System.Threading;
using System.IO;
using System.Text;
namespace LTEMvcApp.Controllers
{
/// <summary>
/// 日志管理控制器 - 负责日志相关的管理功能
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class LogController : ControllerBase
{
private readonly WebSocketManagerService _webSocketManager;
private readonly ILogger<LogController> _logger;
private readonly string _logsDirectory = "ClientMessageLogs";
public LogController(WebSocketManagerService webSocketManager, ILogger<LogController> logger)
{
_webSocketManager = webSocketManager;
_logger = logger;
// 确保日志目录存在
if (!Directory.Exists(_logsDirectory))
{
Directory.CreateDirectory(_logsDirectory);
}
}
/// <summary>
/// 获取日志缓存统计信息
/// </summary>
/// <returns>统计信息</returns>
[HttpGet("stats")]
public ActionResult<object> GetLogCacheStats()
{
try
{
var stats = new
{
totalLogs = _webSocketManager.GetLogCacheCount(),
cacheSize = 10000, // LogCacheSize
timestamp = DateTime.UtcNow
};
return Ok(stats);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取日志缓存统计信息时发生错误");
return StatusCode(500, new { message = "获取统计信息失败", error = ex.Message });
}
}
/// <summary>
/// 清空全局日志缓存
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("clear")]
public ActionResult ClearLogCache()
{
try
{
_webSocketManager.ClearLogCache();
return Ok(new { message = "日志缓存已清空" });
}
catch (Exception ex)
{
_logger.LogError(ex, "清空日志缓存时发生错误");
return StatusCode(500, new { message = "清空日志缓存失败", error = ex.Message });
}
}
/// <summary>
/// 重置全局日志缓存
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("reset")]
public ActionResult ResetLogCache()
{
try
{
_webSocketManager.ResetLogCache();
return Ok(new { message = "日志缓存已重置" });
}
catch (Exception ex)
{
_logger.LogError(ex, "重置日志缓存时发生错误");
return StatusCode(500, new { message = "重置日志缓存失败", error = ex.Message });
}
}
/// <summary>
/// 添加测试日志数据
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("add-test-data")]
public ActionResult AddTestLogData()
{
try
{
var testLogs = new List<LTELog>();
_webSocketManager.AddLogsToCache(testLogs);
return Ok(new { message = $"已添加 {testLogs.Count} 条测试日志" });
}
catch (Exception ex)
{
_logger.LogError(ex, "添加测试日志数据时发生错误");
return StatusCode(500, new { message = "添加测试日志失败", error = ex.Message });
}
}
/// <summary>
/// 使用 Server-Sent Events (SSE) 实时推送全局日志
/// </summary>
[HttpGet("stream")]
public async Task StreamLogs(CancellationToken cancellationToken)
{
try
{
Response.ContentType = "text/event-stream";
Response.Headers.Append("Cache-Control", "no-cache");
Response.Headers.Append("Connection", "keep-alive");
Response.Headers.Append("Access-Control-Allow-Origin", "*");
// 发送连接成功事件
await SendSseEvent("connected", new { message = "日志流连接已建立", timestamp = DateTime.UtcNow });
await Response.Body.FlushAsync(cancellationToken);
int lastLogCount = 0;
var lastLogs = new List<LTELog>();
var lastLogHash = string.Empty;
// 首先,一次性推送所有已缓存的日志
try
{
var initialLogs = _webSocketManager.GetLogCache()?.ToList() ?? new List<LTELog>();
_logger.LogInformation("StreamLogs: 获取到初始日志 {Count} 条", initialLogs.Count);
if (initialLogs.Any())
{
_logger.LogInformation("StreamLogs: 发送历史日志事件,日志数量: {Count}", initialLogs.Count);
await SendSseEvent("history", new { logs = initialLogs, totalCount = initialLogs.Count });
await Response.Body.FlushAsync(cancellationToken);
lastLogCount = initialLogs.Count;
lastLogs = initialLogs.ToList();
lastLogHash = CalculateLogsHash(initialLogs);
}
else
{
_logger.LogInformation("StreamLogs: 没有历史日志数据");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "获取初始日志时发生错误");
await SendSseEvent("error", new { message = "获取初始日志失败", error = ex.Message });
await Response.Body.FlushAsync(cancellationToken);
}
while (!cancellationToken.IsCancellationRequested)
{
try
{
var currentLogs = _webSocketManager.GetLogCache()?.ToList() ?? new List<LTELog>();
var currentLogHash = CalculateLogsHash(currentLogs);
bool hasNewLogs = false;
List<LTELog> newLogs = new List<LTELog>();
if (currentLogs.Count > lastLogCount)
{
newLogs = currentLogs.Skip(lastLogCount).ToList();
hasNewLogs = newLogs.Any();
}
else if (currentLogs.Count == lastLogCount && currentLogHash != lastLogHash)
{
newLogs = GetChangedLogs(lastLogs, currentLogs);
hasNewLogs = newLogs.Any();
}
else if (currentLogs.Count < lastLogCount)
{
_logger.LogInformation("检测到日志缓存被重置,重新同步");
await SendSseEvent("reset", new {
message = "日志缓存已重置",
totalCount = currentLogs.Count
});
await Response.Body.FlushAsync(cancellationToken);
lastLogCount = currentLogs.Count;
lastLogs = currentLogs.ToList();
lastLogHash = currentLogHash;
continue;
}
if (hasNewLogs && newLogs.Any())
{
_logger.LogInformation("StreamLogs: 发送新日志事件,新增日志数量: {NewCount}, 总日志数量: {TotalCount}", newLogs.Count, currentLogs.Count);
var eventData = new {
logs = newLogs,
totalCount = currentLogs.Count,
newCount = newLogs.Count
};
await SendSseEvent("new_logs", eventData);
await Response.Body.FlushAsync(cancellationToken);
lastLogCount = currentLogs.Count;
lastLogs = currentLogs.ToList();
lastLogHash = currentLogHash;
}
await Task.Delay(250, cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "StreamLogs 循环中发生错误");
await SendSseEvent("error", new {
message = "处理日志流时发生错误",
error = ex.Message,
timestamp = DateTime.UtcNow
});
await Response.Body.FlushAsync(cancellationToken);
await Task.Delay(1000, cancellationToken);
}
}
await SendSseEvent("disconnected", new {
message = "日志流连接已断开",
timestamp = DateTime.UtcNow
});
await Response.Body.FlushAsync(cancellationToken);
}
catch (OperationCanceledException)
{
_logger.LogInformation("StreamLogs 连接被客户端取消");
}
catch (Exception ex)
{
_logger.LogError(ex, "StreamLogs 方法执行时发生未处理的异常");
try
{
await SendSseEvent("fatal_error", new {
message = "服务器内部错误",
error = ex.Message,
timestamp = DateTime.UtcNow
});
await Response.Body.FlushAsync();
}
catch
{
// 忽略发送错误事件时的异常
}
}
}
/// <summary>
/// 获取日志缓存详细状态(调试用)
/// </summary>
/// <returns>日志缓存详细状态</returns>
[HttpGet("debug")]
public ActionResult<object> GetLogCacheDebugInfo()
{
try
{
var logs = _webSocketManager.GetLogCache()?.ToList() ?? new List<LTELog>();
var logCount = _webSocketManager.GetLogCacheCount();
var cacheStatus = _webSocketManager.GetLogCacheStatus();
var debugInfo = new
{
totalLogs = logCount,
actualLogsCount = logs.Count,
cacheSize = 10000,
timestamp = DateTime.UtcNow,
sampleLogs = logs.TakeLast(5).Select(log => new
{
timestamp = log.Timestamp,
layer = log.Layer,
message = log.Message?.Substring(0, Math.Min(100, log.Message?.Length ?? 0)) + "..."
}).ToList(),
logHash = CalculateLogsHash(logs),
cacheStatus = cacheStatus
};
return Ok(debugInfo);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取日志缓存调试信息时发生错误");
return StatusCode(500, new { message = "获取调试信息失败", error = ex.Message });
}
}
/// <summary>
/// 获取SSE连接状态(调试用)
/// </summary>
/// <returns>连接状态信息</returns>
[HttpGet("connection-status")]
public ActionResult<object> GetSseConnectionStatus()
{
try
{
var status = new
{
timestamp = DateTime.UtcNow,
requestHeaders = Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString()),
userAgent = Request.Headers["User-Agent"].ToString(),
accept = Request.Headers["Accept"].ToString(),
cacheControl = Request.Headers["Cache-Control"].ToString(),
connection = Request.Headers["Connection"].ToString(),
isHttps = Request.IsHttps,
host = Request.Host.ToString(),
path = Request.Path.ToString(),
queryString = Request.QueryString.ToString(),
method = Request.Method,
contentType = Request.ContentType,
contentLength = Request.ContentLength
};
return Ok(status);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取SSE连接状态时发生错误");
return StatusCode(500, new { message = "获取连接状态失败", error = ex.Message });
}
}
/// <summary>
/// 测试SSE连接(调试用)
/// </summary>
/// <returns>测试结果</returns>
[HttpGet("test-connection")]
public ActionResult<object> TestSseConnection()
{
try
{
var testResult = new
{
message = "SSE连接测试成功",
timestamp = DateTime.UtcNow,
serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
logCacheCount = _webSocketManager.GetLogCacheCount(),
testData = new { test = true, message = "这是一个测试消息" }
};
return Ok(testResult);
}
catch (Exception ex)
{
_logger.LogError(ex, "测试SSE连接时发生错误");
return StatusCode(500, new { message = "测试连接失败", error = ex.Message });
}
}
/// <summary>
/// 强制推送测试日志(调试用)
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("force-push-test")]
public ActionResult ForcePushTestLogs()
{
try
{
var testLogs = new List<LTELog>();
_webSocketManager.AddLogsToCache(testLogs);
_logger.LogInformation("强制推送测试日志: {Message}", testLogs[0].Message);
return Ok(new {
message = $"已强制推送测试日志: {testLogs[0].Message}",
timestamp = testLogs[0].Timestamp
});
}
catch (Exception ex)
{
_logger.LogError(ex, "强制推送测试日志时发生错误");
return StatusCode(500, new { message = "强制推送失败", error = ex.Message });
}
}
private async Task SendSseEvent(string eventName, object data)
{
try
{
if (string.IsNullOrEmpty(eventName))
{
_logger.LogWarning("尝试发送空事件名称的SSE事件");
return;
}
if (data == null)
{
_logger.LogWarning("尝试发送空数据的SSE事件: {EventName}", eventName);
return;
}
var json = Newtonsoft.Json.JsonConvert.SerializeObject(data);
var eventData = $"event: {eventName}\ndata: {json}\n\n";
await Response.WriteAsync(eventData);
}
catch (Exception ex)
{
_logger.LogError(ex, "发送SSE事件时发生错误: {EventName}", eventName);
}
}
/// <summary>
/// 计算日志列表的哈希值,用于检测内容变化
/// </summary>
/// <param name="logs">日志列表</param>
/// <returns>哈希值字符串</returns>
private string CalculateLogsHash(List<LTELog> logs)
{
if (logs == null || !logs.Any())
return string.Empty;
try
{
var hashInput = string.Join("|", logs.Select(log =>
$"{log.Timestamp}_{log.Layer}_{log.Message}"));
using (var sha256 = System.Security.Cryptography.SHA256.Create())
{
var bytes = System.Text.Encoding.UTF8.GetBytes(hashInput);
var hash = sha256.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "计算日志哈希值时发生错误");
return string.Empty;
}
}
/// <summary>
/// 获取变化的日志项
/// </summary>
/// <param name="oldLogs">旧日志列表</param>
/// <param name="newLogs">新日志列表</param>
/// <returns>变化的日志列表</returns>
private List<LTELog> GetChangedLogs(List<LTELog> oldLogs, List<LTELog> newLogs)
{
var changedLogs = new List<LTELog>();
if (oldLogs.Count != newLogs.Count)
{
return newLogs;
}
for (int i = 0; i < newLogs.Count; i++)
{
if (i < oldLogs.Count)
{
var oldLog = oldLogs[i];
var newLog = newLogs[i];
if (oldLog.Timestamp != newLog.Timestamp ||
oldLog.Layer != newLog.Layer ||
oldLog.Message != newLog.Message)
{
changedLogs.Add(newLog);
}
}
else
{
changedLogs.Add(newLogs[i]);
}
}
return changedLogs;
}
}
}

368
LTEMvcApp/Controllers/MessageController.cs

@ -0,0 +1,368 @@
using Microsoft.AspNetCore.Mvc;
using LTEMvcApp.Models;
using LTEMvcApp.Services;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using System.Threading.Tasks;
using System.Threading;
using System.IO;
using System.Text;
namespace LTEMvcApp.Controllers
{
/// <summary>
/// 客户端消息管理控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class MessageController : ControllerBase
{
private readonly WebSocketManagerService _webSocketManager;
private readonly ILogger<MessageController> _logger;
private readonly string _logsDirectory = "ClientMessageLogs";
public MessageController(WebSocketManagerService webSocketManager, ILogger<MessageController> logger)
{
_webSocketManager = webSocketManager;
_logger = logger;
if (!Directory.Exists(_logsDirectory))
{
Directory.CreateDirectory(_logsDirectory);
}
}
/// <summary>
/// SSE推送客户端消息流
/// </summary>
[HttpGet("{clientName}/stream")]
public async Task StreamClientMessages(string clientName)
{
try
{
Response.ContentType = "text/event-stream";
Response.Headers.Append("Cache-Control", "no-cache");
Response.Headers.Append("Connection", "keep-alive");
Response.Headers.Append("Access-Control-Allow-Origin", "*");
var client = _webSocketManager.GetClientInstance(clientName);
if (client == null)
{
await SendSseEvent("error", new { message = "客户端未连接或不存在", clientName });
return;
}
await SendSseEvent("open", new {
message = "成功连接到服务器事件流",
clientName,
timestamp = DateTime.UtcNow
});
await Response.Body.FlushAsync(HttpContext.RequestAborted);
int lastSentCount = 0;
int lastReceivedCount = 0;
var cancellationToken = HttpContext.RequestAborted;
var sentLogFilePath = Path.Combine(_logsDirectory, $"{clientName}_sent_messages.log");
var receivedLogFilePath = Path.Combine(_logsDirectory, $"{clientName}_received_messages.log");
while (!cancellationToken.IsCancellationRequested)
{
try
{
bool hasNewMessages = false;
var currentSentCount = client.SentMessagesCount;
if (currentSentCount > lastSentCount)
{
var sentMessages = client.SentMessages?.ToList() ?? new List<string>();
if (sentMessages.Count > lastSentCount)
{
var newMessages = sentMessages.Skip(lastSentCount).ToList();
if (newMessages.Any())
{
await LogMessagesToFile(sentLogFilePath, newMessages, "SENT", clientName);
await SendSseEvent("update", new {
type = "sent",
messages = newMessages,
totalCount = currentSentCount,
newCount = newMessages.Count
});
lastSentCount = currentSentCount;
hasNewMessages = true;
}
}
}
var currentReceivedCount = client.ReceivedMessagesCount;
if (currentReceivedCount > lastReceivedCount)
{
var receivedMessages = client.ReceivedMessages?.ToList() ?? new List<string>();
if (receivedMessages.Count > lastReceivedCount)
{
var newMessages = receivedMessages.Skip(lastReceivedCount).ToList();
if (newMessages.Any())
{
await LogMessagesToFile(receivedLogFilePath, newMessages, "RECEIVED", clientName);
await SendSseEvent("update", new {
type = "received",
messages = newMessages,
totalCount = currentReceivedCount,
newCount = newMessages.Count
});
lastReceivedCount = currentReceivedCount;
hasNewMessages = true;
}
}
}
if (hasNewMessages)
{
await Response.Body.FlushAsync(cancellationToken);
}
await Task.Delay(250, cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "StreamClientMessages 循环中发生错误,客户端: {ClientName}", clientName);
await SendSseEvent("error", new {
message = "处理消息流时发生错误",
error = ex.Message,
clientName,
timestamp = DateTime.UtcNow
});
await Response.Body.FlushAsync(cancellationToken);
await Task.Delay(1000, cancellationToken);
}
}
await SendSseEvent("disconnected", new {
message = "客户端消息流连接已断开",
clientName,
timestamp = DateTime.UtcNow
});
await Response.Body.FlushAsync(cancellationToken);
}
catch (OperationCanceledException)
{
_logger.LogInformation("StreamClientMessages 连接被客户端取消,客户端: {ClientName}", clientName);
}
catch (Exception ex)
{
_logger.LogError(ex, "StreamClientMessages 方法执行时发生未处理的异常,客户端: {ClientName}", clientName);
try
{
await SendSseEvent("fatal_error", new {
message = "服务器内部错误",
error = ex.Message,
clientName,
timestamp = DateTime.UtcNow
});
await Response.Body.FlushAsync();
}
catch { }
}
}
/// <summary>
/// 发送消息到客户端
/// </summary>
[HttpPost("{clientName}/send")]
public ActionResult SendMessage(string clientName, [FromBody] JObject message)
{
var messageId = _webSocketManager.SendMessageToClient(clientName, message);
if (messageId >= 0)
return Ok(new { messageId, message = $"消息已发送到客户端 '{clientName}'" });
else
return BadRequest($"发送消息到客户端 '{clientName}' 失败");
}
/// <summary>
/// 获取客户端消息日志文件列表
/// </summary>
[HttpGet("logs")]
public ActionResult<object> GetClientMessageLogFiles()
{
try
{
if (!Directory.Exists(_logsDirectory))
{
return Ok(new { files = new List<object>(), message = "日志目录不存在" });
}
var logFiles = Directory.GetFiles(_logsDirectory, "*.log")
.Select(filePath => new
{
fileName = Path.GetFileName(filePath),
filePath = filePath,
size = new FileInfo(filePath).Length,
lastModified = System.IO.File.GetLastWriteTime(filePath),
clientName = Path.GetFileNameWithoutExtension(filePath).Replace("_sent_messages", "").Replace("_received_messages", ""),
type = filePath.Contains("_sent_messages") ? "发送消息" : "接收消息"
})
.OrderByDescending(f => f.lastModified)
.ToList();
return Ok(new { files = logFiles, totalCount = logFiles.Count });
}
catch (Exception ex)
{
_logger.LogError(ex, "获取客户端消息日志文件列表时发生错误");
return StatusCode(500, new { message = "获取日志文件列表失败", error = ex.Message });
}
}
/// <summary>
/// 获取客户端消息日志文件内容
/// </summary>
[HttpGet("logs/{fileName}")]
public ActionResult<object> GetClientMessageLogContent(string fileName, [FromQuery] int lines = 100)
{
try
{
var filePath = Path.Combine(_logsDirectory, fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound(new { message = $"日志文件 '{fileName}' 不存在" });
}
var fileInfo = new System.IO.FileInfo(filePath);
var allLines = System.IO.File.ReadAllLines(filePath, Encoding.UTF8);
var lastLines = allLines.TakeLast(lines).ToList();
return Ok(new
{
fileName = fileName,
filePath = filePath,
totalLines = allLines.Length,
returnedLines = lastLines.Count,
fileSize = fileInfo.Length,
lastModified = fileInfo.LastWriteTime,
content = lastLines
});
}
catch (Exception ex)
{
_logger.LogError(ex, "获取客户端消息日志文件内容时发生错误: {FileName}", fileName);
return StatusCode(500, new { message = "获取日志文件内容失败", error = ex.Message });
}
}
/// <summary>
/// 清空客户端消息日志文件
/// </summary>
[HttpDelete("logs")]
public ActionResult ClearClientMessageLogs([FromQuery] string? fileName = null)
{
try
{
if (!Directory.Exists(_logsDirectory))
{
return Ok(new { message = "日志目录不存在,无需清空" });
}
int clearedCount = 0;
if (string.IsNullOrEmpty(fileName))
{
var logFiles = Directory.GetFiles(_logsDirectory, "*.log");
foreach (var filePath in logFiles)
{
System.IO.File.WriteAllText(filePath, string.Empty);
clearedCount++;
}
_logger.LogInformation("已清空 {Count} 个客户端消息日志文件", clearedCount);
return Ok(new { message = $"已清空 {clearedCount} 个客户端消息日志文件" });
}
else
{
var filePath = Path.Combine(_logsDirectory, fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound(new { message = $"日志文件 '{fileName}' 不存在" });
}
System.IO.File.WriteAllText(filePath, string.Empty);
_logger.LogInformation("已清空客户端消息日志文件: {FileName}", fileName);
return Ok(new { message = $"已清空客户端消息日志文件 '{fileName}'" });
}
}
catch (Exception ex)
{
_logger.LogError(ex, "清空客户端消息日志文件时发生错误");
return StatusCode(500, new { message = "清空日志文件失败", error = ex.Message });
}
}
/// <summary>
/// 删除客户端消息日志文件
/// </summary>
[HttpDelete("logs/delete")]
public ActionResult DeleteClientMessageLogs([FromQuery] string? fileName = null)
{
try
{
if (!Directory.Exists(_logsDirectory))
{
return Ok(new { message = "日志目录不存在,无需删除" });
}
int deletedCount = 0;
if (string.IsNullOrEmpty(fileName))
{
var logFiles = Directory.GetFiles(_logsDirectory, "*.log");
foreach (var filePath in logFiles)
{
System.IO.File.Delete(filePath);
deletedCount++;
}
_logger.LogInformation("已删除 {Count} 个客户端消息日志文件", deletedCount);
return Ok(new { message = $"已删除 {deletedCount} 个客户端消息日志文件" });
}
else
{
var filePath = Path.Combine(_logsDirectory, fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound(new { message = $"日志文件 '{fileName}' 不存在" });
}
System.IO.File.Delete(filePath);
_logger.LogInformation("已删除客户端消息日志文件: {FileName}", fileName);
return Ok(new { message = $"已删除客户端消息日志文件 '{fileName}'" });
}
}
catch (Exception ex)
{
_logger.LogError(ex, "删除客户端消息日志文件时发生错误");
return StatusCode(500, new { message = "删除日志文件失败", error = ex.Message });
}
}
private async Task LogMessagesToFile(string logFilePath, List<string> messages, string messageType, string clientName)
{
try
{
var logBuilder = new StringBuilder();
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
foreach (var message in messages)
{
logBuilder.AppendLine($"[{timestamp}] [{clientName}] [{messageType}] {message}");
logBuilder.AppendLine(new string('-', 80));
}
await System.IO.File.AppendAllTextAsync(logFilePath, logBuilder.ToString(), Encoding.UTF8);
_logger.LogDebug("已记录 {Count} 条 {MessageType} 消息到文件: {FilePath}", messages.Count, messageType, logFilePath);
}
catch (Exception ex)
{
_logger.LogError(ex, "记录消息到文件时发生错误: {FilePath}", logFilePath);
}
}
private async Task SendSseEvent(string eventName, object data)
{
try
{
if (string.IsNullOrEmpty(eventName) || data == null)
return;
var json = Newtonsoft.Json.JsonConvert.SerializeObject(data);
var eventData = $"event: {eventName}\ndata: {json}\n\n";
await Response.WriteAsync(eventData);
}
catch (Exception ex)
{
_logger.LogError(ex, "发送SSE事件时发生错误: {EventName}", eventName);
}
}
}
}

175
LTEMvcApp/Controllers/TestConfigController.cs

@ -0,0 +1,175 @@
using Microsoft.AspNetCore.Mvc;
using LTEMvcApp.Models;
using LTEMvcApp.Services;
using Microsoft.Extensions.Logging;
namespace LTEMvcApp.Controllers
{
/// <summary>
/// 测试配置管理控制器 - 负责测试客户端配置管理
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class TestConfigController : ControllerBase
{
private readonly WebSocketManagerService _webSocketManager;
private readonly ILogger<TestConfigController> _logger;
public TestConfigController(WebSocketManagerService webSocketManager, ILogger<TestConfigController> logger)
{
_webSocketManager = webSocketManager;
_logger = logger;
}
/// <summary>
/// 获取默认测试客户端配置
/// </summary>
/// <returns>测试客户端配置</returns>
[HttpGet("default")]
public ActionResult<ClientConfig> GetDefaultTestClientConfig()
{
var testConfig = _webSocketManager.GetDefaultTestClientConfig();
return Ok(testConfig);
}
/// <summary>
/// 设置测试客户端配置
/// </summary>
/// <param name="config">测试客户端配置</param>
/// <returns>操作结果</returns>
[HttpPost("default")]
public ActionResult SetTestClientConfig([FromBody] ClientConfig config)
{
if (string.IsNullOrEmpty(config.Name))
return BadRequest("客户端名称不能为空");
var success = _webSocketManager.SetTestClientConfig(config);
if (success)
return Ok(new { message = "测试客户端配置已更新" });
else
return BadRequest("更新测试客户端配置失败");
}
/// <summary>
/// 获取所有测试客户端配置
/// </summary>
/// <returns>测试客户端配置列表</returns>
[HttpGet]
public ActionResult<List<ClientConfig>> GetAllTestClientConfigs()
{
var configs = _webSocketManager.GetAllTestClientConfigs();
return Ok(configs);
}
/// <summary>
/// 根据地址获取测试客户端配置
/// </summary>
/// <param name="address">服务器地址</param>
/// <returns>测试客户端配置</returns>
[HttpGet("address/{address}")]
public ActionResult<ClientConfig?> GetTestClientConfigByAddress(string address)
{
var config = _webSocketManager.GetTestClientConfigByAddress(address);
if (config == null)
return NotFound($"测试客户端配置 (地址: {address}) 不存在");
return Ok(config);
}
/// <summary>
/// 删除测试客户端配置
/// </summary>
/// <param name="address">服务器地址</param>
/// <returns>操作结果</returns>
[HttpDelete("address/{address}")]
public ActionResult RemoveTestClientConfig(string address)
{
var success = _webSocketManager.RemoveTestClientConfig(address);
if (success)
return Ok(new { message = $"测试客户端配置 (地址: {address}) 已删除" });
else
return NotFound($"测试客户端配置 (地址: {address}) 不存在");
}
/// <summary>
/// 启动测试客户端
/// </summary>
/// <param name="request">启动请求</param>
/// <returns>操作结果</returns>
[HttpPost("start")]
public ActionResult StartTestClient([FromBody] StartStopRequest? request = null)
{
string? address = request?.Address;
if (string.IsNullOrEmpty(address))
{
// 使用默认配置启动
var success = _webSocketManager.StartTestClient();
if (success)
return Ok(new { message = "测试客户端已启动" });
else
return BadRequest("启动测试客户端失败");
}
else
{
// 根据地址查找配置并启动
var config = _webSocketManager.GetTestClientConfigByAddress(address);
if (config == null)
return NotFound($"未找到地址为 {address} 的测试客户端配置");
var success = _webSocketManager.StartClient(config.Name);
if (success)
return Ok(new { message = $"测试客户端 {config.Name} 已启动" });
else
return BadRequest($"启动测试客户端 {config.Name} 失败");
}
}
/// <summary>
/// 停止测试客户端
/// </summary>
/// <param name="request">停止请求</param>
/// <returns>操作结果</returns>
[HttpPost("stop")]
public ActionResult StopTestClient([FromBody] StartStopRequest? request = null)
{
string? address = request?.Address;
if (string.IsNullOrEmpty(address))
{
// 使用默认配置停止
_logger.LogInformation("API 请求: 停止测试客户端");
var success = _webSocketManager.StopTestClient();
if (success)
return Ok(new { message = "测试客户端停止成功" });
else
return BadRequest("停止测试客户端失败");
}
else
{
// 根据地址查找配置并停止
var config = _webSocketManager.GetTestClientConfigByAddress(address);
if (config == null)
return NotFound($"未找到地址为 {address} 的测试客户端配置");
_logger.LogInformation($"API 请求: 停止测试客户端 {config.Name}");
var success = _webSocketManager.StopClient(config.Name);
if (success)
return Ok(new { message = $"测试客户端 {config.Name} 停止成功" });
else
return BadRequest($"停止测试客户端 {config.Name} 失败");
}
}
}
/// <summary>
/// 启动/停止请求
/// </summary>
public class StartStopRequest
{
/// <summary>
/// 服务器地址
/// </summary>
public string? Address { get; set; }
}
}

1165
LTEMvcApp/Controllers/WebSocketController.cs

File diff suppressed because it is too large

50
LTEMvcApp/README_ClientMessageLogs.md

@ -20,35 +20,45 @@
--------------------------------------------------------------------------------
```
### 3. 日志文件管理 API
## API 接口
#### 获取日志文件列表
```http
GET /api/websocket/clients/message-logs
### 获取客户端消息日志文件列表
```
GET /api/message/logs
```
#### 获取日志文件内容
```http
GET /api/websocket/clients/message-logs/{fileName}?lines=100
### 获取客户端消息日志文件内容
```
GET /api/message/logs/{fileName}?lines=100
```
#### 清空日志文件
```http
DELETE /api/websocket/clients/message-logs?fileName={fileName}
### 清空客户端消息日志文件
```
DELETE /api/message/logs?fileName={fileName}
```
#### 删除日志文件
```http
DELETE /api/websocket/clients/message-logs/delete?fileName={fileName}
### 删除客户端消息日志文件
```
DELETE /api/message/logs/delete?fileName={fileName}
```
### 4. Web 界面管理
`ClientMessages.cshtml` 页面中添加了日志文件管理面板,提供:
- 日志文件列表显示
- 文件大小和修改时间信息
- 查看日志内容(支持不同行数显示)
- 清空和删除日志文件功能
- 实时刷新日志文件列表
### 客户端消息流(SSE)
```
GET /api/message/{clientName}/stream
```
### 发送消息到客户端
```
POST /api/message/{clientName}/send
Content-Type: application/json
{
"type": "command",
"data": {
"action": "test"
}
}
```
## 技术实现

174
LTEMvcApp/README_MultipleTestConfigs.md

@ -0,0 +1,174 @@
# 多个测试客户端配置功能
## 功能概述
本功能允许系统保存和管理多个测试客户端配置,使用服务器地址(Address)作为唯一标识符。系统已简化为只保留多个配置的管理功能。
## 主要特性
### 1. 基于Address的唯一性管理
- 使用服务器地址(Address)作为配置的唯一key
- 保存前自动检查Address是否存在
- 存在则更新,不存在则添加新配置
### 2. 自动配置加载
- 页面加载时显示默认配置(第一个配置)
- 输入Address时自动查询并加载对应配置
- 支持实时配置切换
### 3. 简化的配置文件管理
- 只使用 `test_client_configs.json` 存储所有配置
- 移除了单个配置文件的管理
- 自动创建默认配置(如果没有配置存在)
## API接口
### 获取默认测试客户端配置
```
GET /api/testconfig/default
```
### 获取所有测试客户端配置
```
GET /api/testconfig
```
### 根据地址获取测试客户端配置
```
GET /api/testconfig/address/{address}
```
### 设置默认测试客户端配置
```
POST /api/testconfig/default
```
### 删除测试客户端配置
```
DELETE /api/testconfig/address/{address}
```
### 启动测试客户端
```
POST /api/testconfig/start
Content-Type: application/json
{
"address": "192.168.13.12:9001" // 可选,如果不提供则使用默认配置
}
```
### 停止测试客户端
```
POST /api/testconfig/stop
Content-Type: application/json
{
"address": "192.168.13.12:9001" // 可选,如果不提供则使用默认配置
}
```
## 前端功能
### 自动配置查询
- 在地址输入框失去焦点时自动查询配置
- 如果找到配置则自动填充表单
- 如果未找到则准备创建新配置
### 智能保存
- 保存时自动检查Address是否存在
- 存在则更新现有配置
- 不存在则创建新配置
## 使用示例
### 1. 创建新配置
1. 在配置页面输入新的服务器地址
2. 填写其他配置信息
3. 点击保存,系统会自动创建新配置
### 2. 更新现有配置
1. 输入已存在的服务器地址
2. 系统会自动加载现有配置
3. 修改配置信息
4. 点击保存,系统会更新现有配置
### 3. 切换配置
1. 在地址输入框中输入不同的地址
2. 系统会自动加载对应的配置
3. 可以查看和编辑不同地址的配置
## 技术实现
### 后端实现
- `WebSocketManagerService` 简化为只管理多个配置
- 使用 `List<ClientConfig>` 存储所有配置
- 基于Address的唯一性检查
- 自动文件持久化
- 移除了单个配置的管理逻辑
### 前端实现
- JavaScript自动查询功能
- 表单自动填充
- 实时配置切换
## 文件结构
```
Services/
├── WebSocketManagerService.cs # 简化的多配置管理逻辑
Controllers/
├── WebSocketController.cs # API接口
Views/Home/
├── TestClientConfig.cshtml # 配置管理页面
```
## 配置文件格式
### test_client_configs.json
```json
[
{
"name": "TestClient1",
"address": "192.168.13.12:9001",
"enabled": true,
"password": "test123",
"reconnectDelay": 15000,
"ssl": false,
"readonly": false,
"mode": "ran",
"logs": {
"layers": {
"PHY": {
"level": "debug",
"filter": "debug",
"maxSize": 1000,
"payload": true
}
}
}
}
]
```
## 简化设计说明
### 移除的功能
- 单个测试配置文件 (`test_client_config.json`)
- 单个配置的独立管理方法
- 复杂的配置切换逻辑
### 保留的功能
- 多个配置的管理
- 基于Address的唯一性检查
- 自动配置加载和保存
- 默认配置的自动创建
## 注意事项
1. Address必须是唯一的,不能重复
2. 系统会自动创建默认配置(如果没有配置存在)
3. 删除配置时使用Address作为标识
4. 页面刷新时会加载默认配置(第一个配置)
5. 所有配置都会持久化到 `test_client_configs.json` 文件中
6. 系统已简化为只管理多个配置,不再维护单个配置文件

26
LTEMvcApp/README_StreamLogs_Optimization.md

@ -65,17 +65,35 @@
### 1. 获取日志缓存统计信息
```
GET /api/websocket/logs/stats
GET /api/log/stats
```
### 2. 清空日志缓存
```
POST /api/websocket/logs/clear
POST /api/log/clear
```
### 3. 重置日志缓存
```
POST /api/websocket/logs/reset
POST /api/log/reset
```
### 4. 添加测试日志数据
```
POST /api/log/add-test-data
```
### 5. 实时日志流(SSE)
```
GET /api/log/stream
```
### 6. 调试信息
```
GET /api/log/debug
GET /api/log/connection-status
GET /api/log/test-connection
POST /api/log/force-push-test
```
## 事件类型
@ -102,7 +120,7 @@ POST /api/websocket/logs/reset
```javascript
// 连接日志流
const eventSource = new EventSource('/api/websocket/logs/stream');
const eventSource = new EventSource('/api/log/stream');
eventSource.addEventListener('connected', (event) => {
console.log('日志流连接已建立');

212
LTEMvcApp/Services/WebSocketManagerService.cs

@ -23,10 +23,10 @@ namespace LTEMvcApp.Services
private readonly ConcurrentDictionary<string, ClientConfig> _configs;
private readonly ILogger<WebSocketManagerService> _logger;
private readonly IServiceProvider _serviceProvider;
private ClientConfig _testClientConfig;
private List<ClientConfig> _testClientConfigs; // 只保留多个测试配置
private const int LogCacheSize = 10000; // 服务器最多缓存10000条最新日志
private readonly ConcurrentQueue<LTELog> _logCache = new ConcurrentQueue<LTELog>();
private readonly string _configFilePath = "test_client_config.json";
private readonly string _configsFilePath = "test_client_configs.json"; // 只保留多个配置文件路径
#endregion
@ -65,88 +65,57 @@ namespace LTEMvcApp.Services
_configs = new ConcurrentDictionary<string, ClientConfig>();
_logger = logger;
_serviceProvider = serviceProvider;
_testClientConfigs = new List<ClientConfig>(); // 初始化测试配置列表
LoadTestClientConfig();
LoadTestClientConfigs(); // 加载多个测试配置
_logger.LogInformation("WebSocketManagerService 初始化");
}
/// <summary>
/// 加载测试客户端配置
/// 加载多个测试客户端配置
/// </summary>
private void LoadTestClientConfig()
private void LoadTestClientConfigs()
{
try
{
if (File.Exists(_configFilePath))
if (File.Exists(_configsFilePath))
{
var json = File.ReadAllText(_configFilePath);
_testClientConfig = JsonSerializer.Deserialize<ClientConfig>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
_logger.LogInformation("成功从 {FilePath} 加载测试客户端配置。", _configFilePath);
var json = File.ReadAllText(_configsFilePath);
_testClientConfigs = JsonSerializer.Deserialize<List<ClientConfig>>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new List<ClientConfig>();
_logger.LogInformation("成功从 {FilePath} 加载 {Count} 个测试客户端配置。", _configsFilePath, _testClientConfigs.Count);
}
else
{
_logger.LogWarning("配置文件 {FilePath} 未找到,将创建并使用默认配置。", _configFilePath);
_testClientConfig = GetDefaultTestConfig();
SaveTestClientConfig();
_logger.LogWarning("多个配置文件 {FilePath} 未找到,将创建空配置列表。", _configsFilePath);
_testClientConfigs = new List<ClientConfig>();
SaveTestClientConfigs();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "加载或创建测试客户端配置文件时出错。将使用默认配置。");
_testClientConfig = GetDefaultTestConfig();
_logger.LogError(ex, "加载多个测试客户端配置文件时出错。将使用空配置列表。");
_testClientConfigs = new List<ClientConfig>();
}
}
/// <summary>
/// 保存测试客户端配置到文件
/// 保存多个测试客户端配置到文件
/// </summary>
private void SaveTestClientConfig()
private void SaveTestClientConfigs()
{
try
{
var options = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var json = JsonSerializer.Serialize(_testClientConfig, options);
File.WriteAllText(_configFilePath, json);
_logger.LogInformation("测试客户端配置已成功保存到 {FilePath}。", _configFilePath);
var json = JsonSerializer.Serialize(_testClientConfigs, options);
File.WriteAllText(_configsFilePath, json);
_logger.LogInformation("多个测试客户端配置已成功保存到 {FilePath},共 {Count} 个配置。", _configsFilePath, _testClientConfigs.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "保存测试客户端配置文件失败。");
_logger.LogError(ex, "保存多个测试客户端配置文件失败。");
}
}
private ClientConfig GetDefaultTestConfig()
{
var layers = new Dictionary<string, LogLayerConfig>();
foreach(var layerName in LogLayerTypes.AllLayers.Where(l => l != "EVENT"))
{
layers[layerName] = new LogLayerConfig { Level = LogLayerTypes.GetDefaultLevel(layerName), Filter = "warn", MaxSize = 1000, Payload = false };
}
// Set some specific payloads to true
if(layers.ContainsKey("PHY")) layers["PHY"].Payload = true;
if(layers.ContainsKey("MAC")) layers["MAC"].Payload = true;
if(layers.ContainsKey("RRC")) layers["RRC"].Payload = true;
if(layers.ContainsKey("NAS")) layers["NAS"].Payload = true;
return new ClientConfig
{
Name = "TestClient",
Enabled = true,
Address = "192.168.13.12:9001",
Ssl = false,
ReconnectDelay = 15000,
Password = "test123",
Logs = new ClientLogsConfig
{
Layers = layers,
Signal = true,
Cch = true
}
};
}
#endregion
@ -200,10 +169,12 @@ namespace LTEMvcApp.Services
_logger.LogInformation($"启动客户端: {clientName}");
ClientConfig config;
if (clientName == _testClientConfig.Name)
// 检查是否是测试客户端
var testConfig = _testClientConfigs.FirstOrDefault(c => c.Name == clientName);
if (testConfig != null)
{
// 使用测试客户端配置
config = _testClientConfig;
config = testConfig;
_logger.LogInformation($"使用测试客户端配置: {config.Name}");
}
else if (!_configs.TryGetValue(clientName, out config))
@ -430,12 +401,30 @@ namespace LTEMvcApp.Services
}
/// <summary>
/// 获取测试客户端配置
/// 获取所有测试客户端配置
/// </summary>
/// <returns>测试客户端配置</returns>
public ClientConfig GetTestClientConfig()
/// <returns>测试客户端配置列表</returns>
public List<ClientConfig> GetAllTestClientConfigs()
{
return _testClientConfigs.ToList();
}
/// <summary>
/// 获取默认测试客户端配置(第一个配置或创建默认配置)
/// </summary>
/// <returns>默认测试客户端配置</returns>
public ClientConfig GetDefaultTestClientConfig()
{
return _testClientConfig;
if (_testClientConfigs.Any())
{
return _testClientConfigs.First();
}
// 如果没有配置,创建默认配置
var defaultConfig = CreateDefaultTestConfig();
_testClientConfigs.Add(defaultConfig);
SaveTestClientConfigs();
return defaultConfig;
}
/// <summary>
@ -451,9 +440,32 @@ namespace LTEMvcApp.Services
return false;
}
_logger.LogInformation($"更新测试客户端配置: {config.Name}");
_testClientConfig = config;
SaveTestClientConfig();
if (string.IsNullOrEmpty(config.Address))
{
_logger.LogWarning("尝试设置空地址的测试客户端配置");
return false;
}
_logger.LogInformation($"更新测试客户端配置: {config.Name} (地址: {config.Address})");
// 使用Address作为唯一key来检查是否存在
var existingConfigIndex = _testClientConfigs.FindIndex(c => c.Address == config.Address);
if (existingConfigIndex >= 0)
{
// 更新现有配置
_testClientConfigs[existingConfigIndex] = config;
_logger.LogInformation($"更新现有测试配置 (地址: {config.Address})");
}
else
{
// 添加新配置
_testClientConfigs.Add(config);
_logger.LogInformation($"添加新测试配置 (地址: {config.Address})");
}
// 保存到文件
SaveTestClientConfigs();
return true;
}
@ -463,8 +475,9 @@ namespace LTEMvcApp.Services
/// <returns>是否成功启动</returns>
public bool StartTestClient()
{
_logger.LogInformation("启动测试客户端");
return StartClient(_testClientConfig.Name);
var defaultConfig = GetDefaultTestClientConfig();
_logger.LogInformation("启动测试客户端: " + defaultConfig.Name);
return StartClient(defaultConfig.Name);
}
/// <summary>
@ -473,8 +486,9 @@ namespace LTEMvcApp.Services
/// <returns>是否成功停止</returns>
public bool StopTestClient()
{
_logger.LogInformation("停止测试客户端");
return StopClient(_testClientConfig.Name);
var defaultConfig = GetDefaultTestClientConfig();
_logger.LogInformation("停止测试客户端: " + defaultConfig.Name);
return StopClient(defaultConfig.Name);
}
/// <summary>
@ -483,7 +497,71 @@ namespace LTEMvcApp.Services
/// <returns>测试客户端的WebSocket实例</returns>
public LTEClientWebSocket? GetTestClient()
{
return GetClientInstance(_testClientConfig.Name);
var defaultConfig = GetDefaultTestClientConfig();
return GetClientInstance(defaultConfig.Name);
}
/// <summary>
/// 创建默认测试配置
/// </summary>
/// <returns>默认测试配置</returns>
private ClientConfig CreateDefaultTestConfig()
{
var layers = new Dictionary<string, LogLayerConfig>();
foreach(var layerName in LogLayerTypes.AllLayers.Where(l => l != "EVENT"))
{
layers[layerName] = new LogLayerConfig { Level = LogLayerTypes.GetDefaultLevel(layerName), Filter = "warn", MaxSize = 1000, Payload = false };
}
// Set some specific payloads to true
if(layers.ContainsKey("PHY")) layers["PHY"].Payload = true;
if(layers.ContainsKey("MAC")) layers["MAC"].Payload = true;
if(layers.ContainsKey("RRC")) layers["RRC"].Payload = true;
if(layers.ContainsKey("NAS")) layers["NAS"].Payload = true;
return new ClientConfig
{
Name = "TestClient",
Enabled = true,
Address = "192.168.13.12:9001",
Ssl = false,
ReconnectDelay = 15000,
Password = "test123",
Logs = new ClientLogsConfig
{
Layers = layers,
Signal = true,
Cch = true
}
};
}
/// <summary>
/// 根据地址获取测试客户端配置
/// </summary>
/// <param name="address">服务器地址</param>
/// <returns>测试客户端配置</returns>
public ClientConfig? GetTestClientConfigByAddress(string address)
{
return _testClientConfigs.FirstOrDefault(c => c.Address == address);
}
/// <summary>
/// 删除测试客户端配置
/// </summary>
/// <param name="address">服务器地址</param>
/// <returns>是否成功删除</returns>
public bool RemoveTestClientConfig(string address)
{
var config = _testClientConfigs.FirstOrDefault(c => c.Address == address);
if (config != null)
{
_testClientConfigs.Remove(config);
SaveTestClientConfigs();
_logger.LogInformation($"删除测试客户端配置 (地址: {address})");
return true;
}
return false;
}
/// <summary>

10
LTEMvcApp/Views/Home/ClientMessages.cshtml

@ -315,7 +315,7 @@
function loadLogFiles() {
$('#logFilesContainer').html('<div class="text-muted">正在加载日志文件列表...</div>');
$.get('/api/websocket/clients/message-logs')
$.get('/api/message/logs')
.done(function(response) {
if (response.files && response.files.length > 0) {
const filesHtml = response.files.map(file => createLogFileItemHtml(file)).join('');
@ -372,7 +372,7 @@
function loadLogContent(fileName, lines) {
$('#logContentContainer').html('<div class="text-muted">正在加载日志内容...</div>');
$.get(`/api/websocket/clients/message-logs/${encodeURIComponent(fileName)}?lines=${lines}`)
$.get(`/api/message/logs/${encodeURIComponent(fileName)}?lines=${lines}`)
.done(function(response) {
if (response.content && response.content.length > 0) {
const contentHtml = response.content.map(line =>
@ -402,7 +402,7 @@
}
$.ajax({
url: '/api/websocket/clients/message-logs',
url: '/api/message/logs',
method: 'DELETE',
data: { fileName: fileName }
})
@ -422,7 +422,7 @@
}
$.ajax({
url: '/api/websocket/clients/message-logs/delete',
url: '/api/message/logs/delete',
method: 'DELETE',
data: { fileName: fileName }
})
@ -436,7 +436,7 @@
}
function initializeEventSource() {
const source = new EventSource(`/api/websocket/clients/${encodeURIComponent(clientName)}/messages/stream`);
const source = new EventSource(`/api/message/${encodeURIComponent(clientName)}/stream`);
const statusBadge = $('#connection-status');
source.addEventListener('open', function(e) {

12
LTEMvcApp/Views/Home/Index.cshtml

@ -167,11 +167,11 @@
}
</td>
<td class="project-actions text-right">
<a class="btn btn-primary btn-sm @(state == LTEMvcApp.Models.ClientState.Connected ? "disabled" : "")" href="#" onclick="startTestClient()">
<a class="btn btn-primary btn-sm @(state == LTEMvcApp.Models.ClientState.Connected ? "disabled" : "")" href="#" onclick="startTestClient('@config.Address')">
<i class="fas fa-play"></i>
启动
</a>
<a class="btn btn-danger btn-sm @(state != LTEMvcApp.Models.ClientState.Connected ? "disabled" : "")" href="#" onclick="stopTestClient()">
<a class="btn btn-danger btn-sm @(state != LTEMvcApp.Models.ClientState.Connected ? "disabled" : "")" href="#" onclick="stopTestClient('@config.Address')">
<i class="fas fa-stop"></i>
停止
</a>
@ -198,8 +198,8 @@
@section Scripts {
<script>
function startTestClient() {
$.post('/api/websocket/test-client/start')
function startTestClient(address) {
$.post('/api/testconfig/start', { address: address })
.done(function() {
alert('启动请求已发送!页面将在2秒后刷新。');
setTimeout(() => location.reload(), 2000);
@ -209,8 +209,8 @@
});
}
function stopTestClient() {
$.post('/api/websocket/test-client/stop')
function stopTestClient(address) {
$.post('/api/testconfig/stop', { address: address })
.done(function() {
alert('停止请求已发送!页面将在2秒后刷新。');
setTimeout(() => location.reload(), 2000);

66
LTEMvcApp/Views/Home/Logs.cshtml

@ -1170,7 +1170,7 @@
updateConnectionStatus('connecting', '连接中...');
console.log('开始建立SSE连接...');
eventSource = new EventSource('/api/websocket/logs/stream');
eventSource = new EventSource('/api/log/stream');
// 添加连接打开事件监听
eventSource.onopen = function(event) {
@ -1324,7 +1324,7 @@
// 控制按钮事件
clearLogsBtn.addEventListener('click', function() {
if (confirm('确定要清空所有日志吗?')) {
fetch('/api/websocket/logs/clear', { method: 'POST' })
fetch('/api/log/clear', { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.message) {
@ -1340,7 +1340,7 @@
resetLogsBtn.addEventListener('click', function() {
if (confirm('确定要重置日志缓存吗?')) {
fetch('/api/websocket/logs/reset', { method: 'POST' })
fetch('/api/log/reset', { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.message) {
@ -1355,7 +1355,7 @@
});
addTestDataBtn.addEventListener('click', function() {
fetch('/api/websocket/logs/add-test-data', { method: 'POST' })
fetch('/api/log/add-test-data', { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.message) {
@ -1372,72 +1372,52 @@
connectSSE();
});
// 调试按钮事件
debugBtn.addEventListener('click', function() {
// 显示调试信息
const debugInfo = {
totalLogs: allLogsData.length,
availableLayers: Array.from(availableLayers),
selectedLayers: Array.from(selectedLayers),
connectionStatus: statusText.textContent,
eventSourceReadyState: eventSource ? eventSource.readyState : 'null',
sortField: sortField,
sortDirection: sortDirection,
columnVisibility: columnVisibility,
clusterizeRows: clusterize.rows.length,
url: window.location.href,
userAgent: navigator.userAgent
};
console.log('前端调试信息:', debugInfo);
// 检查服务器端日志缓存状态
fetch('/api/websocket/logs/debug')
fetch('/api/log/debug')
.then(response => response.json())
.then(data => {
console.log('服务器端日志缓存状态:', data);
showInfo(`调试信息已输出到控制台。服务器缓存: ${data.totalLogs} 条日志`);
console.log('日志缓存调试信息:', data);
showInfo('调试信息已输出到控制台');
})
.catch(error => {
console.error('获取服务器调试信息失败:', error);
showError('获取服务器调试信息失败: ' + error.message);
showError('获取调试信息失败: ' + error.message);
});
// 检查SSE连接状态
fetch('/api/websocket/logs/connection-status')
});
connectionStatusBtn.addEventListener('click', function() {
fetch('/api/log/connection-status')
.then(response => response.json())
.then(data => {
console.log('SSE连接状态:', data);
showInfo('连接状态信息已输出到控制台');
})
.catch(error => {
console.error('获取SSE连接状态失败:', error);
showError('获取连接状态失败: ' + error.message);
});
});
testConnectionBtn.addEventListener('click', function() {
// 测试连接
fetch('/api/websocket/logs/test-connection')
fetch('/api/log/test-connection')
.then(response => response.json())
.then(data => {
if (data.message) {
showInfo(data.message);
}
console.log('SSE连接测试结果:', data);
showInfo('连接测试完成,结果已输出到控制台');
})
.catch(error => {
showError('测试连接失败: ' + error.message);
showError('连接测试失败: ' + error.message);
});
});
forcePushBtn.addEventListener('click', function() {
// 强制推送
fetch('/api/websocket/logs/force-push-test', { method: 'POST' })
fetch('/api/log/force-push-test', { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.message) {
showInfo(data.message);
}
console.log('强制推送测试结果:', data);
showInfo('强制推送测试完成,结果已输出到控制台');
})
.catch(error => {
showError('强制推送失败: ' + error.message);
showError('强制推送测试失败: ' + error.message);
});
});

155
LTEMvcApp/Views/Home/TestClientConfig.cshtml

@ -237,21 +237,21 @@
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-check form-check-inline">
<div class="row mb-3">
<div class="col-md-12 d-flex align-items-center flex-wrap">
<div class="form-check form-check-inline mr-3">
<input type="checkbox" class="form-check-input" id="enabled" name="enabled" @(testConfig?.Enabled == true ? "checked" : "")>
<label class="form-check-label" for="enabled">启用</label>
</div>
<div class="form-check form-check-inline">
<div class="form-check form-check-inline mr-3">
<input type="checkbox" class="form-check-input" id="ssl" name="ssl" @(testConfig?.Ssl == true ? "checked" : "")>
<label class="form-check-label" for="ssl">使用SSL</label>
</div>
<div class="form-check form-check-inline">
<div class="form-check form-check-inline mr-3">
<input type="checkbox" class="form-check-input" id="readonly" name="readonly" @(testConfig?.Readonly == true ? "checked" : "")>
<label class="form-check-label" for="readonly">只读模式</label>
</div>
<div class="form-check form-check-inline">
<div class="form-check form-check-inline mr-3">
<input type="radio" class="form-check-input" id="mode_ran" name="mode" value="ran" @(testConfig?.Mode == "ran" ? "checked" : "checked")>
<label class="form-check-label" for="mode_ran">RAN</label>
</div>
@ -262,12 +262,17 @@
</div>
</div>
<!-- 日志层配置 -->
<div class="row mt-4 logs-config-section">
<!-- 日志层配置表格 -->
<div class="row logs-config-section">
<div class="col-md-12">
<h5 class="section-title">
<i class="fas fa-layer-group"></i> 日志层配置
</h5>
<div class="d-flex justify-content-between align-items-center mb-2 px-3 py-2" style="background: #f8f9fa; border-radius: 0.5rem; border: 1px solid #e9ecef;">
<h5 class="section-title mb-0" style="font-size: 1.1rem; color: #343a40;">
<i class="fas fa-layer-group"></i> 日志层配置
</h5>
<button type="submit" class="btn btn-primary" form="testClientConfigForm" style="min-width: 110px;">
<i class="fas fa-save"></i> 保存配置
</button>
</div>
<div class="logs-config-content">
<div class="table-responsive">
<table class="table table-bordered table-sm">
@ -282,7 +287,7 @@
</thead>
<tbody id="layersTableBody">
<!-- RAN 模式下的日志层 -->
<tbody id="ranLayers" class="mode-layers" style="display: block;">
<tbody id="ranLayers" class="mode-layers" style="">
@foreach (var layer in allLayers)
{
var config = layerConfigs.ContainsKey(layer) ? layerConfigs[layer] : new LTEMvcApp.Models.LogLayerConfig();
@ -385,25 +390,6 @@
</div>
</div>
</form>
<!-- 操作按钮 -->
<div class="button-area">
<button type="submit" class="btn btn-primary" form="testClientConfigForm">
<i class="fas fa-save"></i> 保存配置
</button>
<button type="button" class="btn btn-success" onclick="startTestClient()">
<i class="fas fa-play"></i> 启动测试客户端
</button>
<button type="button" class="btn btn-danger" onclick="stopTestClient()">
<i class="fas fa-stop"></i> 停止测试客户端
</button>
<a href="@Url.Action("ClientMessages", "Home", new { clientName = testConfig?.Name })" class="btn btn-info">
<i class="fas fa-list"></i> 查看消息队列
</a>
<button type="button" class="btn btn-secondary" onclick="resetToDefaults()">
<i class="fas fa-undo"></i> 重置为默认值
</button>
</div>
</div>
</div>
</div>
@ -425,8 +411,60 @@
$('.mode-layers').hide();
$('#' + selectedMode + 'Layers').show();
});
// 监听地址输入框变化,自动查询配置
$('#address').on('blur', function() {
var address = $(this).val().trim();
if (address) {
loadConfigByAddress(address);
}
});
});
// 根据地址加载配置
function loadConfigByAddress(address) {
$.ajax({
url: '/api/testconfig/address/' + encodeURIComponent(address),
type: 'GET',
success: function(config) {
if (config) {
// 填充表单
$('#name').val(config.name);
$('#password').val(config.password);
$('#reconnectDelay').val(config.reconnectDelay);
$('#enabled').prop('checked', config.enabled);
$('#ssl').prop('checked', config.ssl);
$('#readonly').prop('checked', config.readonly);
// 设置模式
if (config.mode) {
$('input[name="mode"][value="' + config.mode + '"]').prop('checked', true).trigger('change');
}
// 填充日志层配置
if (config.logs && config.logs.layers) {
Object.keys(config.logs.layers).forEach(function(layer) {
var layerConfig = config.logs.layers[layer];
$(`select[name="layers[${layer}][level]"]`).val(layerConfig.level);
$(`select[name="layers[${layer}][filter]"]`).val(layerConfig.filter);
$(`input[name="layers[${layer}][max_size]"]`).val(layerConfig.maxSize);
$(`input[name="layers[${layer}][payload]"]`).prop('checked', layerConfig.payload);
});
}
console.log('已加载配置:', config.name);
}
},
error: function(xhr) {
if (xhr.status === 404) {
console.log('未找到该地址的配置,将创建新配置');
} else {
console.error('查询配置失败:', xhr.responseText);
}
}
});
}
function saveTestClientConfig() {
var formData = {
name: $('#name').val(),
@ -460,11 +498,8 @@
};
});
// 添加其他日志配置
// formData.logs.signal = ...; // 如果需要的话
$.ajax({
url: '/api/websocket/test-client-config',
url: '/api/testconfig/default',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(formData),
@ -478,55 +513,5 @@
});
}
function startTestClient() {
$.ajax({
url: '/api/websocket/test-client/start',
type: 'POST',
success: function(response) {
alert('测试客户端启动成功!');
},
error: function(xhr) {
alert('启动测试客户端失败:' + xhr.responseText);
}
});
}
function stopTestClient() {
$.ajax({
url: '/api/websocket/test-client/stop',
type: 'POST',
success: function(response) {
alert('测试客户端停止成功!');
},
error: function(xhr) {
alert('停止测试客户端失败:' + xhr.responseText);
}
});
}
function resetToDefaults() {
if (confirm('确定要重置为默认配置吗?')) {
// 重置为默认值
var defaultLevels = {
'PHY': 'info', 'MAC': 'info', 'RLC': 'info', 'PDCP': 'warn',
'RRC': 'debug', 'NAS': 'debug', 'S1AP': 'debug', 'NGAP': 'debug',
'GTPU': 'info', 'X2AP': 'debug', 'XnAP': 'info', 'M2AP': 'info'
};
Object.keys(defaultLevels).forEach(function(layer) {
$(`select[name="layers[${layer}][level]"]`).val(defaultLevels[layer]);
$(`select[name="layers[${layer}][filter]"]`).val('warn');
$(`input[name="layers[${layer}][max_size]"]`).val(1);
$(`input[name="layers[${layer}][payload]"]`).prop('checked', false);
});
// 设置一些层的 payload 为 true
['PHY', 'MAC', 'RRC', 'NAS'].forEach(function(layer) {
$(`input[name="layers[${layer}][payload]"]`).prop('checked', true);
});
alert('已重置为默认配置!');
}
}
</script>
}

153
LTEMvcApp/test_client_config.json

@ -1,153 +0,0 @@
{
"name": "TestClient",
"address": "192.168.13.12:9001",
"enabled": true,
"password": "test123",
"reconnectDelay": 15000,
"ssl": false,
"logs": {
"layers": {
"PHY": {
"level": "info",
"maxSize": 1000,
"payload": true,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"MAC": {
"level": "warn",
"maxSize": 1000,
"payload": true,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"RLC": {
"level": "warn",
"maxSize": 1000,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"PDCP": {
"level": "warn",
"maxSize": 1000,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"RRC": {
"level": "debug",
"maxSize": 1000,
"payload": true,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"NAS": {
"level": "debug",
"maxSize": 1000,
"payload": true,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"S1AP": {
"level": "debug",
"maxSize": 1000,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"NGAP": {
"level": "debug",
"maxSize": 1000,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"GTPU": {
"level": "warn",
"maxSize": 1000,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"X2AP": {
"level": "debug",
"maxSize": 1000,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"XnAP": {
"level": "warn",
"maxSize": 1000,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"M2AP": {
"level": "info",
"maxSize": 1000,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
}
},
"signal": null,
"cch": null,
"extensionData": null
},
"pause": false,
"readonly": false,
"skipLogMenu": false,
"locked": false,
"active": false,
"model": null
}

310
LTEMvcApp/test_client_configs.json

@ -0,0 +1,310 @@
[
{
"name": "TestClient",
"address": "192.168.13.12:9001",
"enabled": true,
"password": "test123",
"reconnectDelay": 15000,
"ssl": false,
"logs": {
"layers": {
"PHY": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"MAC": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"RLC": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"PDCP": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"RRC": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"NAS": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"S1AP": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"NGAP": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"GTPU": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"X2AP": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"XnAP": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"M2AP": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
}
},
"signal": null,
"cch": null,
"extensionData": null
},
"pause": false,
"readonly": false,
"skipLogMenu": false,
"locked": false,
"active": false,
"mode": "ran",
"model": null
},
{
"name": "TestClient",
"address": "192.168.13.12:9002",
"enabled": true,
"password": "test123",
"reconnectDelay": 15000,
"ssl": false,
"logs": {
"layers": {
"PHY": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"MAC": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"RLC": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"PDCP": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"RRC": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"NAS": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"S1AP": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"NGAP": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"GTPU": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"X2AP": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"XnAP": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
},
"M2AP": {
"level": "warn",
"maxSize": 1,
"payload": false,
"filter": "warn",
"color": "#000000",
"direction": {},
"epc": false,
"debug": null,
"max": null
}
},
"signal": null,
"cch": null,
"extensionData": null
},
"pause": false,
"readonly": false,
"skipLogMenu": false,
"locked": false,
"active": false,
"mode": "ran",
"model": null
}
]
Loading…
Cancel
Save