Browse Source

feat: 重构首页和测试客户端配置页面

- 将首页重构为客户端管理表格,取代原有的状态卡片,界面更清晰。
- 在表格中为每个客户端提供独立的启动、停止、配置和查看消息的操作。
- 增强了测试客户端配置页面,支持所有日志层的完整配置(级别、大小、负载)。
- 修正了服务层,添加了 GetTestClient 方法以正确获取客户端实例。
- 根据要求,在配置界面移除了 EVENT 日志层。
master
root 1 month ago
parent
commit
402dde4119
  1. 32
      LTEMvcApp/Controllers/HomeController.cs
  2. 80
      LTEMvcApp/Controllers/WebSocketController.cs
  3. 80
      LTEMvcApp/Models/LogLayerConfig.cs
  4. 19
      LTEMvcApp/Services/LTEClientWebSocket.cs
  5. 116
      LTEMvcApp/Services/WebSocketManagerService.cs
  6. 181
      LTEMvcApp/Views/Home/ClientMessages.cshtml
  7. 317
      LTEMvcApp/Views/Home/Index.cshtml
  8. 292
      LTEMvcApp/Views/Home/TestClientConfig.cshtml
  9. 189
      LTEMvcApp/Views/Home/WebSocketTest.cshtml
  10. 7
      LTEMvcApp/Views/Shared/_Layout.cshtml

32
LTEMvcApp/Controllers/HomeController.cs

@ -27,6 +27,19 @@ public class HomeController : Controller
var configs = _webSocketManager.GetAllClientConfigs();
ViewBag.ClientConfigs = configs;
// 获取测试客户端配置
var testConfig = _webSocketManager.GetTestClientConfig();
var testClient = _webSocketManager.GetTestClient();
var clientState = testClient?.State ?? LTEMvcApp.Models.ClientState.Stop;
ViewBag.TestConfig = testConfig;
ViewBag.ClientState = clientState;
// 以后可以扩展为客户端列表
ViewBag.Clients = new List<dynamic>
{
new { Config = testConfig, State = clientState }
};
return View();
}
@ -140,4 +153,23 @@ public class HomeController : Controller
return RedirectToAction(nameof(Index));
}
/// <summary>
/// 测试客户端配置页面
/// </summary>
public IActionResult TestClientConfig()
{
var testConfig = _webSocketManager.GetTestClientConfig();
ViewBag.TestConfig = testConfig;
return View();
}
/// <summary>
/// 客户端消息队列页面
/// </summary>
public IActionResult ClientMessages(string clientName = "TestClient")
{
ViewBag.ClientName = clientName;
return View();
}
}

80
LTEMvcApp/Controllers/WebSocketController.cs

@ -242,6 +242,86 @@ namespace LTEMvcApp.Controllers
else
return BadRequest($"移除客户端 '{clientName}' 配置失败");
}
/// <summary>
/// 获取客户端消息队列
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>发送和接收的消息队列</returns>
[HttpGet("clients/{clientName}/messages")]
public ActionResult<object> GetClientMessages(string clientName)
{
var client = _webSocketManager.GetClientInstance(clientName);
if (client == null)
return NotFound($"客户端 '{clientName}' 不存在或未连接");
var result = new
{
SentMessages = client.SentMessages.ToList(),
ReceivedMessages = client.ReceivedMessages.ToList(),
SentCount = client.SentMessages.Count(),
ReceivedCount = client.ReceivedMessages.Count()
};
return Ok(result);
}
/// <summary>
/// 获取测试客户端配置
/// </summary>
/// <returns>测试客户端配置</returns>
[HttpGet("test-client-config")]
public ActionResult<ClientConfig> GetTestClientConfig()
{
var testConfig = _webSocketManager.GetTestClientConfig();
return Ok(testConfig);
}
/// <summary>
/// 设置测试客户端配置
/// </summary>
/// <param name="config">测试客户端配置</param>
/// <returns>操作结果</returns>
[HttpPost("test-client-config")]
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>
[HttpPost("test-client/start")]
public ActionResult StartTestClient()
{
var success = _webSocketManager.StartTestClient();
if (success)
return Ok(new { message = "测试客户端已启动" });
else
return BadRequest("启动测试客户端失败");
}
/// <summary>
/// 停止测试客户端
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("test-client/stop")]
public ActionResult StopTestClient()
{
var success = _webSocketManager.StopTestClient();
if (success)
return Ok(new { message = "测试客户端已停止" });
else
return BadRequest("停止测试客户端失败");
}
}
/// <summary>

80
LTEMvcApp/Models/LogLayerConfig.cs

@ -0,0 +1,80 @@
using System.ComponentModel.DataAnnotations;
namespace LTEMvcApp.Models
{
/// <summary>
/// 日志层配置
/// </summary>
public class LogLayerConfig
{
/// <summary>
/// 日志级别
/// </summary>
[Required]
public string Level { get; set; } = "warn";
/// <summary>
/// 最大大小
/// </summary>
public int MaxSize { get; set; } = 1;
/// <summary>
/// 是否包含负载
/// </summary>
public bool Payload { get; set; } = false;
/// <summary>
/// 过滤器(用于兼容性)
/// </summary>
public string Filter { get; set; } = "warn";
}
/// <summary>
/// 日志层类型枚举
/// </summary>
public static class LogLayerTypes
{
public const string PHY = "PHY";
public const string MAC = "MAC";
public const string RLC = "RLC";
public const string PDCP = "PDCP";
public const string RRC = "RRC";
public const string NAS = "NAS";
public const string S1AP = "S1AP";
public const string NGAP = "NGAP";
public const string GTPU = "GTPU";
public const string X2AP = "X2AP";
public const string XnAP = "XnAP";
public const string M2AP = "M2AP";
public const string EVENT = "EVENT";
/// <summary>
/// 获取所有日志层类型
/// </summary>
public static readonly string[] AllLayers = new[]
{
PHY, MAC, RLC, PDCP, RRC, NAS, S1AP, NGAP, GTPU, X2AP, XnAP, M2AP, EVENT
};
/// <summary>
/// 获取日志级别选项
/// </summary>
public static readonly string[] LogLevels = new[]
{
"debug", "info", "warn", "error"
};
/// <summary>
/// 获取默认日志级别
/// </summary>
public static string GetDefaultLevel(string layer)
{
return layer switch
{
NAS or RRC or S1AP or NGAP or X2AP => "debug",
EVENT or M2AP => "info",
_ => "warn"
};
}
}
}

19
LTEMvcApp/Services/LTEClientWebSocket.cs

@ -36,6 +36,8 @@ namespace LTEMvcApp.Services
private bool _disposed;
private LogParserService logParser = new LogParserService();
private readonly ILogger<LTEClientWebSocket> _logger;
private readonly ConcurrentQueue<string> _sentMessages = new ConcurrentQueue<string>();
private readonly ConcurrentQueue<string> _receivedMessages = new ConcurrentQueue<string>();
#endregion
#region 事件
@ -114,6 +116,9 @@ namespace LTEMvcApp.Services
/// </summary>
public bool IsReadonly => _config.Readonly;
public IEnumerable<string> SentMessages => _sentMessages;
public IEnumerable<string> ReceivedMessages => _receivedMessages;
#endregion
#region 构造函数
@ -358,6 +363,9 @@ namespace LTEMvcApp.Services
_messageFifo.Enqueue(message);
// 记录发送的消息
_sentMessages.Enqueue(message.ToString(Formatting.Indented));
if (_messageFifo.Count < 100) // 批处理大小
{
_messageDeferTimer = new Timer(_ => SendMessageNow(), null, 1, Timeout.Infinite);
@ -490,6 +498,9 @@ namespace LTEMvcApp.Services
_logger.LogDebug($"[{_config.Name}] 收到初始消息: {e.Message}");
StopTimers();
// 记录接收的消息
_receivedMessages.Enqueue(JToken.Parse(e.Message).ToString(Formatting.Indented));
try
{
var data = e.Message;
@ -647,6 +658,8 @@ namespace LTEMvcApp.Services
private void OnSocketMessage(object? sender, MessageReceivedEventArgs e)
{
_logger.LogDebug($"[{_config.Name}] 收到消息: {e.Message}");
// 记录接收的消息
_receivedMessages.Enqueue(JToken.Parse(e.Message).ToString(Formatting.Indented));
try
{
var data = e.Message;
@ -839,11 +852,15 @@ namespace LTEMvcApp.Services
{
var json = JsonConvert.SerializeObject(messages[0]);
_webSocket.Send(json);
// 记录发送的消息
_sentMessages.Enqueue(JToken.Parse(json).ToString(Formatting.Indented));
}
else
else if (messages.Count > 1)
{
var json = JsonConvert.SerializeObject(messages);
_webSocket.Send(json);
// 记录发送的消息
_sentMessages.Enqueue(JToken.Parse(json).ToString(Formatting.Indented));
}
_messageDeferTimer?.Dispose();

116
LTEMvcApp/Services/WebSocketManagerService.cs

@ -22,6 +22,7 @@ namespace LTEMvcApp.Services
private readonly LogParserService _logParser;
private readonly ILogger<WebSocketManagerService> _logger;
private readonly IServiceProvider _serviceProvider;
private ClientConfig _testClientConfig;
#endregion
@ -61,6 +62,41 @@ namespace LTEMvcApp.Services
_logParser = logParser;
_logger = logger;
_serviceProvider = serviceProvider;
// 初始化测试客户端配置
_testClientConfig = new ClientConfig
{
Name = "TestClient",
Enabled = true,
Address = "192.168.13.12:9001",
Ssl = false,
ReconnectDelay = 15000,
Pause = false,
Readonly = false,
SkipLogMenu = false,
Locked = false,
Active = true,
Logs = new Dictionary<string, object>
{
["layers"] = new Dictionary<string, object>
{
["PHY"] = new Dictionary<string, object> { ["level"] = "info", ["max_size"] = 1, ["payload"] = true, ["filter"] = "info" },
["MAC"] = new Dictionary<string, object> { ["level"] = "info", ["max_size"] = 1, ["payload"] = true, ["filter"] = "info" },
["RLC"] = new Dictionary<string, object> { ["level"] = "info", ["max_size"] = 1, ["payload"] = false, ["filter"] = "info" },
["PDCP"] = new Dictionary<string, object> { ["level"] = "warn", ["max_size"] = 1, ["payload"] = false, ["filter"] = "warn" },
["RRC"] = new Dictionary<string, object> { ["level"] = "debug", ["max_size"] = 1, ["payload"] = true, ["filter"] = "debug" },
["NAS"] = new Dictionary<string, object> { ["level"] = "debug", ["max_size"] = 1, ["payload"] = true, ["filter"] = "debug" },
["S1AP"] = new Dictionary<string, object> { ["level"] = "debug", ["max_size"] = 1, ["payload"] = false, ["filter"] = "debug" },
["NGAP"] = new Dictionary<string, object> { ["level"] = "debug", ["max_size"] = 1, ["payload"] = false, ["filter"] = "debug" },
["GTPU"] = new Dictionary<string, object> { ["level"] = "info", ["max_size"] = 1, ["payload"] = false, ["filter"] = "info" },
["X2AP"] = new Dictionary<string, object> { ["level"] = "debug", ["max_size"] = 1, ["payload"] = false, ["filter"] = "debug" },
["XnAP"] = new Dictionary<string, object> { ["level"] = "info", ["max_size"] = 1, ["payload"] = false, ["filter"] = "info" },
["M2AP"] = new Dictionary<string, object> { ["level"] = "info", ["max_size"] = 1, ["payload"] = false, ["filter"] = "info" },
["EVENT"] = new Dictionary<string, object> { ["level"] = "info", ["max_size"] = 1, ["payload"] = false, ["filter"] = "info" }
}
}
};
_logger.LogInformation("WebSocketManagerService 初始化");
}
@ -114,8 +150,19 @@ namespace LTEMvcApp.Services
public bool StartClient(string clientName)
{
_logger.LogInformation($"启动客户端: {clientName}");
if (!_configs.TryGetValue(clientName, out var config))
ClientConfig config;
if (clientName == _testClientConfig.Name)
{
// 使用测试客户端配置
config = _testClientConfig;
_logger.LogInformation($"使用测试客户端配置: {config.Name}");
}
else if (!_configs.TryGetValue(clientName, out config))
{
_logger.LogWarning($"客户端配置不存在: {clientName}");
return false;
}
// 如果客户端已存在,先停止
if (_clients.TryGetValue(clientName, out var existingClient))
@ -324,6 +371,73 @@ namespace LTEMvcApp.Services
}
}
/// <summary>
/// 获取客户端实例
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>客户端实例</returns>
public LTEClientWebSocket? GetClientInstance(string clientName)
{
_clients.TryGetValue(clientName, out var client);
return client;
}
/// <summary>
/// 获取测试客户端配置
/// </summary>
/// <returns>测试客户端配置</returns>
public ClientConfig GetTestClientConfig()
{
return _testClientConfig;
}
/// <summary>
/// 设置测试客户端配置
/// </summary>
/// <param name="config">测试客户端配置</param>
/// <returns>是否成功设置</returns>
public bool SetTestClientConfig(ClientConfig config)
{
if (string.IsNullOrEmpty(config.Name))
{
_logger.LogWarning("尝试设置空名称的测试客户端配置");
return false;
}
_logger.LogInformation($"更新测试客户端配置: {config.Name}");
_testClientConfig = config;
return true;
}
/// <summary>
/// 启动测试客户端
/// </summary>
/// <returns>是否成功启动</returns>
public bool StartTestClient()
{
_logger.LogInformation("启动测试客户端");
return StartClient(_testClientConfig.Name);
}
/// <summary>
/// 停止测试客户端
/// </summary>
/// <returns>是否成功停止</returns>
public bool StopTestClient()
{
_logger.LogInformation("停止测试客户端");
return StopClient(_testClientConfig.Name);
}
/// <summary>
/// 获取测试客户端实例
/// </summary>
/// <returns>测试客户端的WebSocket实例</returns>
public LTEClientWebSocket? GetTestClient()
{
return GetClientInstance(_testClientConfig.Name);
}
#endregion
#region 私有方法

181
LTEMvcApp/Views/Home/ClientMessages.cshtml

@ -0,0 +1,181 @@
@{
ViewData["Title"] = "客户端消息队列";
var clientName = ViewBag.ClientName as string ?? "TestClient";
}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">客户端消息队列 - @clientName</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary btn-sm" onclick="refreshMessages()">
<i class="fas fa-sync-alt"></i> 刷新
</button>
<a href="@Url.Action("TestClientConfig", "Home")" class="btn btn-info btn-sm">
<i class="fas fa-cog"></i> 配置
</a>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-paper-plane text-success"></i> 发送消息
<span class="badge badge-primary" id="sentCount">0</span>
</h5>
</div>
<div class="card-body" style="max-height: 600px; overflow-y: auto;">
<div id="sentMessages">
<div class="text-muted text-center">
<i class="fas fa-info-circle"></i> 暂无发送消息
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-download text-info"></i> 接收消息
<span class="badge badge-info" id="receivedCount">0</span>
</h5>
</div>
<div class="card-body" style="max-height: 600px; overflow-y: auto;">
<div id="receivedMessages">
<div class="text-muted text-center">
<i class="fas fa-info-circle"></i> 暂无接收消息
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
var clientName = '@clientName';
var refreshInterval;
$(document).ready(function() {
loadMessages();
// 每5秒自动刷新一次
refreshInterval = setInterval(loadMessages, 5000);
});
function loadMessages() {
$.ajax({
url: '/api/websocket/clients/' + encodeURIComponent(clientName) + '/messages',
type: 'GET',
success: function(data) {
displayMessages(data);
},
error: function(xhr) {
if (xhr.status === 404) {
$('#sentMessages').html('<div class="text-warning text-center"><i class="fas fa-exclamation-triangle"></i> 客户端未连接</div>');
$('#receivedMessages').html('<div class="text-warning text-center"><i class="fas fa-exclamation-triangle"></i> 客户端未连接</div>');
} else {
$('#sentMessages').html('<div class="text-danger text-center"><i class="fas fa-times-circle"></i> 加载失败</div>');
$('#receivedMessages').html('<div class="text-danger text-center"><i class="fas fa-times-circle"></i> 加载失败</div>');
}
}
});
}
function displayMessages(data) {
// 更新计数
$('#sentCount').text(data.sentCount || 0);
$('#receivedCount').text(data.receivedCount || 0);
// 显示发送消息
var sentHtml = '';
if (data.sentMessages && data.sentMessages.length > 0) {
data.sentMessages.forEach(function(message, index) {
sentHtml += createMessageCard(message, index, 'sent');
});
} else {
sentHtml = '<div class="text-muted text-center"><i class="fas fa-info-circle"></i> 暂无发送消息</div>';
}
$('#sentMessages').html(sentHtml);
// 显示接收消息
var receivedHtml = '';
if (data.receivedMessages && data.receivedMessages.length > 0) {
data.receivedMessages.forEach(function(message, index) {
receivedHtml += createMessageCard(message, index, 'received');
});
} else {
receivedHtml = '<div class="text-muted text-center"><i class="fas fa-info-circle"></i> 暂无接收消息</div>';
}
$('#receivedMessages').html(receivedHtml);
// 高亮JSON语法
$('pre code').each(function() {
if (typeof hljs !== 'undefined') {
hljs.highlightElement(this);
}
});
}
function createMessageCard(message, index, type) {
var timestamp = new Date().toLocaleTimeString();
var messageType = type === 'sent' ? '发送' : '接收';
var bgClass = type === 'sent' ? 'border-success' : 'border-info';
var iconClass = type === 'sent' ? 'fas fa-paper-plane text-success' : 'fas fa-download text-info';
return `
<div class="card mb-2 ${bgClass}">
<div class="card-header py-2">
<small class="text-muted">
<i class="${iconClass}"></i> ${messageType} #${index + 1} - ${timestamp}
</small>
<button class="btn btn-sm btn-outline-secondary float-right" onclick="toggleMessage('${type}-${index}')">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div class="card-body py-2" id="${type}-${index}" style="display: none;">
<pre><code class="json">${formatJson(message)}</code></pre>
</div>
</div>
`;
}
function toggleMessage(id) {
$('#' + id).slideToggle();
}
function formatJson(jsonString) {
try {
var obj = JSON.parse(jsonString);
return JSON.stringify(obj, null, 2);
} catch (e) {
return jsonString;
}
}
function refreshMessages() {
loadMessages();
}
// 页面卸载时清除定时器
$(window).on('beforeunload', function() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
</script>
<!-- 添加代码高亮支持 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
}

317
LTEMvcApp/Views/Home/Index.cshtml

@ -1,195 +1,104 @@
@{
ViewData["Title"] = "LTE WebSocket 客户端管理";
ViewData["Title"] = "主页";
var clients = ViewBag.Clients as List<dynamic>;
}
<div class="text-center">
<h1 class="display-4">LTE WebSocket 客户端管理</h1>
<p>基于 .NET 8 的 LTE WebSocket 客户端实现</p>
</div>
<!-- 消息提示 -->
@if (TempData["Message"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@TempData["Message"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
@if (TempData["Error"] != null)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@TempData["Error"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
<!-- 连接统计信息 -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">连接统计信息</h5>
</div>
<div class="card-body">
@if (ViewBag.ConnectionStats != null)
{
var stats = ViewBag.ConnectionStats as LTEMvcApp.Services.ConnectionStatistics;
<div class="row">
<div class="col-md-3">
<div class="text-center">
<h4 class="text-primary">@(stats?.TotalClients ?? 0)</h4>
<p class="text-muted">总客户端数</p>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-success">@(stats?.ConnectedClients ?? 0)</h4>
<p class="text-muted">已连接</p>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-warning">@(stats?.DisconnectedClients ?? 0)</h4>
<p class="text-muted">未连接</p>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-info">@(stats?.TotalLogs ?? 0)</h4>
<p class="text-muted">总日志数</p>
</div>
</div>
</div>
}
else
{
<p class="text-muted">暂无统计信息</p>
}
</div>
</div>
</div>
</div>
<!-- 客户端管理 -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">客户端管理</h5>
</div>
<div class="card-body">
<!-- 快速操作按钮 -->
<div class="row mb-3">
<div class="col-md-12">
<form method="post" style="display: inline;">
<button type="submit" asp-action="AddTestClient" class="btn btn-primary me-2">
<i class="bi bi-plus-circle"></i> 添加测试客户端
</button>
</form>
<form method="post" style="display: inline;">
<button type="submit" asp-action="StartTestClient" class="btn btn-success me-2">
<i class="bi bi-play-circle"></i> 启动测试客户端
</button>
</form>
<form method="post" style="display: inline;">
<button type="submit" asp-action="StopTestClient" class="btn btn-danger me-2">
<i class="bi bi-stop-circle"></i> 停止测试客户端
</button>
</form>
<a href="@Url.Action("WebSocketTest")" class="btn btn-info">
<i class="bi bi-gear"></i> WebSocket 测试
</a>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-server mr-2"></i>客户端管理
</h3>
</div>
<!-- 客户端配置列表 -->
@if (ViewBag.ClientConfigs != null && ((List<LTEMvcApp.Models.ClientConfig>)ViewBag.ClientConfigs).Count > 0)
{
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped">
<table class="table table-striped projects">
<thead>
<tr>
<th>客户端名称</th>
<th>地址</th>
<th>状态</th>
<th>SSL</th>
<th>启用</th>
<th>操作</th>
<th style="width: 20%">客户端名称</th>
<th style="width: 25%">地址</th>
<th style="width: 10%">状态</th>
<th style="width: 8%" class="text-center">SSL</th>
<th style="width: 8%" class="text-center">启用</th>
<th style="width: 29%" class="text-center">操作</th>
</tr>
</thead>
<tbody>
@foreach (var config in ViewBag.ClientConfigs as List<LTEMvcApp.Models.ClientConfig>)
@if (clients != null)
{
var stats = ViewBag.ConnectionStats as LTEMvcApp.Services.ConnectionStatistics;
var clientStates = stats?.ClientStates ?? new Dictionary<string, LTEMvcApp.Models.ClientState>();
var state = clientStates.GetValueOrDefault(config.Name, LTEMvcApp.Models.ClientState.Stop);
<tr>
<td>@config.Name</td>
<td>@(config.Ssl ? "wss://" : "ws://")@config.Address</td>
<td>
<span class="badge @(state == LTEMvcApp.Models.ClientState.Connected ? "bg-success" :
state == LTEMvcApp.Models.ClientState.Connecting ? "bg-warning" :
state == LTEMvcApp.Models.ClientState.Error ? "bg-danger" : "bg-secondary")">
@state
</span>
</td>
<td>
<i class="bi @(config.Ssl ? "bi-check-circle-fill text-success" : "bi-x-circle-fill text-muted")"></i>
</td>
<td>
<i class="bi @(config.Enabled ? "bi-check-circle-fill text-success" : "bi-x-circle-fill text-muted")"></i>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" onclick="startClient('@config.Name')">
<i class="bi bi-play"></i>
</button>
<button type="button" class="btn btn-outline-danger" onclick="stopClient('@config.Name')">
<i class="bi bi-stop"></i>
</button>
<button type="button" class="btn btn-outline-info" onclick="viewLogs('@config.Name')">
<i class="bi bi-list-ul"></i>
</button>
</div>
</td>
</tr>
foreach (var client in clients)
{
var config = client.Config as LTEMvcApp.Models.ClientConfig;
var state = (LTEMvcApp.Models.ClientState)client.State;
<tr>
<td><a>@config.Name</a></td>
<td><small>@((config.Ssl ? "wss://" : "ws://") + config.Address)</small></td>
<td>
@if (state == LTEMvcApp.Models.ClientState.Connected)
{
<span class="badge badge-success">已连接</span>
}
else if (state == LTEMvcApp.Models.ClientState.Connecting)
{
<span class="badge badge-warning">连接中</span>
}
else if (state == LTEMvcApp.Models.ClientState.Error)
{
<span class="badge badge-danger">错误</span>
}
else
{
<span class="badge badge-secondary">已停止</span>
}
</td>
<td class="text-center">
@if (config.Ssl)
{
<i class="fas fa-check-circle text-success"></i>
}
else
{
<i class="fas fa-times-circle text-muted"></i>
}
</td>
<td class="text-center">
@if (config.Enabled)
{
<i class="fas fa-check-circle text-success"></i>
}
else
{
<i class="fas fa-times-circle text-muted"></i>
}
</td>
<td class="project-actions text-right">
<a class="btn btn-primary btn-sm @(state == LTEMvcApp.Models.ClientState.Connected ? "disabled" : "")" href="#" onclick="startTestClient()">
<i class="fas fa-play"></i>
启动
</a>
<a class="btn btn-danger btn-sm @(state != LTEMvcApp.Models.ClientState.Connected ? "disabled" : "")" href="#" onclick="stopTestClient()">
<i class="fas fa-stop"></i>
停止
</a>
<a class="btn btn-info btn-sm" href="@Url.Action("ClientMessages", "Home", new { clientName = config.Name })">
<i class="fas fa-list"></i>
消息
</a>
<a class="btn btn-secondary btn-sm" href="@Url.Action("TestClientConfig", "Home")">
<i class="fas fa-cogs"></i>
配置
</a>
</td>
</tr>
}
}
</tbody>
</table>
</div>
}
else
{
<div class="text-center text-muted">
<i class="bi bi-info-circle" style="font-size: 2rem;"></i>
<p class="mt-2">暂无客户端配置</p>
<p>点击"添加测试客户端"按钮来创建第一个客户端配置</p>
</div>
}
</div>
</div>
</div>
</div>
<!-- API 信息 -->
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">API 接口</h5>
</div>
<div class="card-body">
<p>以下API接口可用于程序化控制WebSocket客户端:</p>
<ul class="list-unstyled">
<li><code>GET /api/websocket/clients</code> - 获取所有客户端状态</li>
<li><code>GET /api/websocket/configs</code> - 获取所有客户端配置</li>
<li><code>POST /api/websocket/configs</code> - 添加客户端配置</li>
<li><code>POST /api/websocket/clients/{name}/start</code> - 启动客户端</li>
<li><code>POST /api/websocket/clients/{name}/stop</code> - 停止客户端</li>
<li><code>GET /api/websocket/clients/{name}/logs</code> - 获取客户端日志</li>
<li><code>GET /api/websocket/statistics</code> - 获取连接统计信息</li>
</ul>
</div>
</div>
</div>
</div>
@ -197,44 +106,26 @@
@section Scripts {
<script>
function startClient(clientName) {
fetch(`/api/websocket/clients/${encodeURIComponent(clientName)}/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
alert(data.message || '操作成功');
location.reload();
})
.catch(error => {
console.error('Error:', error);
alert('操作失败');
});
}
function stopClient(clientName) {
fetch(`/api/websocket/clients/${encodeURIComponent(clientName)}/stop`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
alert(data.message || '操作成功');
location.reload();
})
.catch(error => {
console.error('Error:', error);
alert('操作失败');
});
function startTestClient() {
$.post('/api/websocket/test-client/start')
.done(function() {
alert('启动请求已发送!页面将在2秒后刷新。');
setTimeout(() => location.reload(), 2000);
})
.fail(function(xhr) {
alert('启动失败:' + xhr.responseText);
});
}
function viewLogs(clientName) {
window.open(`/api/websocket/clients/${encodeURIComponent(clientName)}/logs?limit=100`, '_blank');
function stopTestClient() {
$.post('/api/websocket/test-client/stop')
.done(function() {
alert('停止请求已发送!页面将在2秒后刷新。');
setTimeout(() => location.reload(), 2000);
})
.fail(function(xhr) {
alert('停止失败:' + xhr.responseText);
});
}
</script>
}

292
LTEMvcApp/Views/Home/TestClientConfig.cshtml

@ -0,0 +1,292 @@
@{
ViewData["Title"] = "测试客户端配置";
var testConfig = ViewBag.TestConfig as LTEMvcApp.Models.ClientConfig;
// 只保留不含 EVENT 的日志层
var allLayers = LTEMvcApp.Models.LogLayerTypes.AllLayers.Where(l => l != "EVENT").ToArray();
var layerConfigs = new Dictionary<string, Dictionary<string, object>>();
if (testConfig?.Logs?.ContainsKey("layers") == true && testConfig.Logs["layers"] is Dictionary<string, object> layers)
{
foreach (var layer in allLayers)
{
var config = new Dictionary<string, object>();
if (layers.ContainsKey(layer) && layers[layer] is Dictionary<string, object> layerConfig)
{
config["level"] = layerConfig.ContainsKey("level") ? layerConfig["level"]?.ToString() : LTEMvcApp.Models.LogLayerTypes.GetDefaultLevel(layer);
config["maxSize"] = layerConfig.ContainsKey("max_size") && layerConfig["max_size"] != null ? Convert.ToInt32(layerConfig["max_size"]) : 1;
config["payload"] = layerConfig.ContainsKey("payload") && layerConfig["payload"] != null ? Convert.ToBoolean(layerConfig["payload"]) : false;
}
else
{
config["level"] = LTEMvcApp.Models.LogLayerTypes.GetDefaultLevel(layer);
config["maxSize"] = 1;
config["payload"] = false;
}
layerConfigs[layer] = config;
}
}
else
{
foreach (var layer in allLayers)
{
layerConfigs[layer] = new Dictionary<string, object>
{
["level"] = LTEMvcApp.Models.LogLayerTypes.GetDefaultLevel(layer),
["maxSize"] = 1,
["payload"] = false
};
}
}
}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">测试客户端配置</h3>
</div>
<div class="card-body">
<form id="testClientConfigForm">
<!-- 基本配置 -->
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="name">客户端名称</label>
<input type="text" class="form-control" id="name" name="name" value="@testConfig?.Name" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="address">服务器地址</label>
<input type="text" class="form-control" id="address" name="address" value="@testConfig?.Address" placeholder="192.168.13.12:9001">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="password">密码</label>
<input type="password" class="form-control" id="password" name="password" value="@testConfig?.Password">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="reconnectDelay">重连延迟 (毫秒)</label>
<input type="number" class="form-control" id="reconnectDelay" name="reconnectDelay" value="@testConfig?.ReconnectDelay">
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-check">
<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>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-check">
<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>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-check">
<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>
</div>
<!-- 日志层配置 -->
<div class="row mt-4">
<div class="col-md-12">
<h5>日志层配置</h5>
<div class="table-responsive">
<table class="table table-bordered table-sm">
<thead class="table-dark">
<tr>
<th>日志层</th>
<th>级别</th>
<th>最大大小</th>
<th>包含负载</th>
</tr>
</thead>
<tbody>
@foreach (var layer in allLayers)
{
var config = layerConfigs[layer];
var level = config["level"]?.ToString();
var maxSize = Convert.ToInt32(config["maxSize"]);
var payload = Convert.ToBoolean(config["payload"]);
<tr>
<td><strong>@layer</strong></td>
<td>
<select class="form-control form-control-sm" name="layers[@layer][level]">
@foreach (var logLevel in LTEMvcApp.Models.LogLayerTypes.LogLevels)
{
if (logLevel == level)
{
<option value="@logLevel" selected>@logLevel.ToUpper()</option>
}
else
{
<option value="@logLevel">@logLevel.ToUpper()</option>
}
}
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm" name="layers[@layer][max_size]" value="@maxSize" min="1" max="1000">
</td>
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="layers[@layer][payload]" @(payload ? "checked" : "")>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="row mt-4">
<div class="col-md-12">
<button type="submit" class="btn btn-primary">
<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>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
$(document).ready(function() {
$('#testClientConfigForm').on('submit', function(e) {
e.preventDefault();
saveTestClientConfig();
});
});
function saveTestClientConfig() {
var formData = {
name: $('#name').val(),
address: $('#address').val(),
password: $('#password').val(),
reconnectDelay: parseInt($('#reconnectDelay').val()) || 15000,
enabled: $('#enabled').is(':checked'),
ssl: $('#ssl').is(':checked'),
readonly: $('#readonly').is(':checked'),
logs: {
layers: {}
}
};
// 构建日志层配置
var layers = @Html.Raw(Json.Serialize(LTEMvcApp.Models.LogLayerTypes.AllLayers));
layers.forEach(function(layer) {
var level = $(`select[name="layers.${layer}.level"]`).val();
var maxSize = parseInt($(`input[name="layers.${layer}.max_size"]`).val()) || 1;
var payload = $(`input[name="layers.${layer}.payload"]`).is(':checked');
formData.logs.layers[layer] = {
level: level,
max_size: maxSize,
payload: payload,
filter: level
};
});
$.ajax({
url: '/api/websocket/test-client-config',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(formData),
success: function(response) {
alert('配置保存成功!');
location.reload();
},
error: function(xhr) {
alert('保存配置失败:' + xhr.responseText);
}
});
}
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]);
$(`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>
}

189
LTEMvcApp/Views/Home/WebSocketTest.cshtml

@ -0,0 +1,189 @@
@{
ViewData["Title"] = "WebSocket测试";
}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">WebSocket测试</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">连接状态</h5>
</div>
<div class="card-body">
<div class="form-group">
<label for="serverUrl">服务器地址</label>
<input type="text" class="form-control" id="serverUrl" value="ws://192.168.13.12:9001" placeholder="ws://localhost:9001">
</div>
<div class="form-group mt-3">
<button type="button" class="btn btn-success" id="connectBtn" onclick="connect()">连接</button>
<button type="button" class="btn btn-danger" id="disconnectBtn" onclick="disconnect()" style="display:none;">断开</button>
</div>
<div class="mt-3">
<span class="badge" id="statusBadge">未连接</span>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">消息发送</h5>
</div>
<div class="card-body">
<div class="form-group">
<label for="messageInput">消息内容</label>
<textarea class="form-control" id="messageInput" rows="3" placeholder="输入要发送的消息"></textarea>
</div>
<div class="form-group mt-3">
<button type="button" class="btn btn-primary" onclick="sendMessage()" id="sendBtn" disabled>发送</button>
<button type="button" class="btn btn-secondary" onclick="clearMessages()">清空消息</button>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title">消息日志</h5>
</div>
<div class="card-body">
<div id="messageLog" style="height: 400px; overflow-y: auto; background-color: #f8f9fa; padding: 10px; border-radius: 5px;">
<div class="text-muted">等待连接...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
var websocket = null;
var messageLog = document.getElementById('messageLog');
var statusBadge = document.getElementById('statusBadge');
var connectBtn = document.getElementById('connectBtn');
var disconnectBtn = document.getElementById('disconnectBtn');
var sendBtn = document.getElementById('sendBtn');
var messageInput = document.getElementById('messageInput');
function connect() {
var url = document.getElementById('serverUrl').value;
if (!url) {
alert('请输入服务器地址');
return;
}
try {
websocket = new WebSocket(url);
websocket.onopen = function(event) {
updateStatus('已连接', 'success');
addMessage('系统', '连接已建立', 'success');
connectBtn.style.display = 'none';
disconnectBtn.style.display = 'inline-block';
sendBtn.disabled = false;
};
websocket.onmessage = function(event) {
addMessage('接收', event.data, 'info');
};
websocket.onclose = function(event) {
updateStatus('已断开', 'secondary');
addMessage('系统', '连接已断开', 'warning');
connectBtn.style.display = 'inline-block';
disconnectBtn.style.display = 'none';
sendBtn.disabled = true;
};
websocket.onerror = function(error) {
updateStatus('连接错误', 'danger');
addMessage('系统', '连接错误: ' + error, 'danger');
};
} catch (error) {
alert('连接失败: ' + error.message);
}
}
function disconnect() {
if (websocket) {
websocket.close();
websocket = null;
}
}
function sendMessage() {
var message = messageInput.value.trim();
if (!message) {
alert('请输入要发送的消息');
return;
}
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.send(message);
addMessage('发送', message, 'primary');
messageInput.value = '';
} else {
alert('WebSocket未连接');
}
}
function addMessage(type, content, color) {
var timestamp = new Date().toLocaleTimeString();
var messageDiv = document.createElement('div');
messageDiv.className = 'mb-2';
messageDiv.innerHTML = `
<small class="text-muted">[${timestamp}]</small>
<span class="badge badge-${color}">${type}</span>
<span class="ml-2">${escapeHtml(content)}</span>
`;
messageLog.appendChild(messageDiv);
messageLog.scrollTop = messageLog.scrollHeight;
}
function updateStatus(status, color) {
statusBadge.textContent = status;
statusBadge.className = 'badge badge-' + color;
}
function clearMessages() {
messageLog.innerHTML = '<div class="text-muted">消息已清空</div>';
}
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 回车键发送消息
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// 页面卸载时断开连接
window.addEventListener('beforeunload', function() {
if (websocket) {
websocket.close();
}
});
</script>
}

7
LTEMvcApp/Views/Shared/_Layout.cshtml

@ -7,6 +7,7 @@
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/LTEMvcApp.styles.css" asp-append-version="true" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" />
</head>
<body>
<header>
@ -22,6 +23,12 @@
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="TestClientConfig">测试客户端配置</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="WebSocketTest">WebSocket测试</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>

Loading…
Cancel
Save