Browse Source

1

feature/LteClientLogFun
hyh 1 month ago
parent
commit
31b174bf13
  1. 644
      LTEMvcApp/Models/LTEClient.cs
  2. 121
      LTEMvcApp/Models/LogChannelConfig.cs
  3. 254
      LTEMvcApp/Models/LogConstants.cs
  4. 74
      LTEMvcApp/Models/LogDirection.cs
  5. 229
      LTEMvcApp/Models/LogEventManager.cs
  6. 270
      LTEMvcApp/Models/LogFilterConfig.cs
  7. 164
      LTEMvcApp/Models/LogLayerConfig.cs
  8. 121
      LTEMvcApp/Models/LogModelConfig.cs
  9. 314
      LTEMvcApp/Models/LogState.cs
  10. 331
      LTEMvcApp/Models/LogUtils.cs
  11. 2
      LTEMvcApp/Program.cs
  12. 3
      LTEMvcApp/Services/LTEClientWebSocket.cs
  13. 638
      LTEMvcApp/Services/LogParserService.cs
  14. 4
      LTEMvcApp/Services/WebSocketManagerService.cs

644
LTEMvcApp/Models/LTEClient.cs

@ -1,6 +1,6 @@
using System.Collections.Immutable;
using System.Text.RegularExpressions;
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace LTEMvcApp.Models;
@ -9,6 +9,124 @@ namespace LTEMvcApp.Models;
/// </summary>
public class LTEClient
{
#region 常量定义
/// <summary>
/// HFN回绕阈值
/// </summary>
private const int HFN_WRAP_THRESHOLD = 512;
/// <summary>
/// 最大日志数量
/// </summary>
private const int LOGS_MAX = 2000000;
#endregion
#region 正则表达式
/// <summary>
/// PHY层日志正则表达式
/// </summary>
private static readonly Regex RegExpPhy = new(@"^([a-f0-9\-]+)\s+([a-f0-9\-]+)\s+([\d\.\-]+) (\w+): (.+)", RegexOptions.Compiled);
/// <summary>
/// 信息1正则表达式
/// </summary>
private static readonly Regex RegExpInfo1 = new(@"^([\w\-]+): (.+)", RegexOptions.Compiled);
/// <summary>
/// 信息2正则表达式
/// </summary>
private static readonly Regex RegExpInfo2 = new(@"^([\w]+) (.+)", RegexOptions.Compiled);
/// <summary>
/// IP日志正则表达式
/// </summary>
private static readonly Regex RegExpIP = new(@"^(len=\d+)\s+(\S+)\s+(.*)", RegexOptions.Compiled);
/// <summary>
/// IPsec日志正则表达式
/// </summary>
private static readonly Regex RegExpIPsec = new(@"^len=(\d+)\s+(.*)", RegexOptions.Compiled);
/// <summary>
/// SDU长度正则表达式
/// </summary>
private static readonly Regex RegExpSDULen = new(@"SDU_len=(\d+)", RegexOptions.Compiled);
/// <summary>
/// SIP日志正则表达式
/// </summary>
private static readonly Regex RegExpSIP = new(@"^([:\.\[\]\da-f]+)\s+(\S+) (.+)", RegexOptions.Compiled);
/// <summary>
/// 媒体请求正则表达式
/// </summary>
private static readonly Regex RegExpMediaReq = new(@"^(\S+) (.+)", RegexOptions.Compiled);
/// <summary>
/// 信号记录正则表达式
/// </summary>
private static readonly Regex RegExpSignalRecord = new(@"Link:\s([\w\d]+)@(\d+)", RegexOptions.Compiled);
/// <summary>
/// 小区ID正则表达式
/// </summary>
private static readonly Regex RegExpCellID = new(@"^([a-f0-9\-]+) (.+)", RegexOptions.Compiled);
/// <summary>
/// RRC UE ID正则表达式
/// </summary>
private static readonly Regex RegExpRRC_UE_ID = new(@"Changing UE_ID to 0x(\d+)", RegexOptions.Compiled);
/// <summary>
/// RRC TMSI正则表达式
/// </summary>
private static readonly Regex RegExpRRC_TMSI = new(@"(5G|m)-TMSI '([\dA-F]+)'H", RegexOptions.Compiled);
/// <summary>
/// RRC新ID正则表达式
/// </summary>
private static readonly Regex RegExpRRC_NEW_ID = new(@"newUE-Identity (['\dA-FH]+)", RegexOptions.Compiled);
/// <summary>
/// RRC CRNTI正则表达式
/// </summary>
private static readonly Regex RegExpRRC_CRNTI = new(@"c-RNTI '([\dA-F]+)'H", RegexOptions.Compiled);
/// <summary>
/// NAS TMSI正则表达式
/// </summary>
private static readonly Regex RegExpNAS_TMSI = new(@"m-TMSI = 0x([\da-f]+)", RegexOptions.Compiled);
/// <summary>
/// NAS 5G TMSI正则表达式
/// </summary>
private static readonly Regex RegExpNAS_5GTMSI = new(@"5G-TMSI = 0x([\da-f]+)", RegexOptions.Compiled);
/// <summary>
/// RRC频段组合正则表达式
/// </summary>
private static readonly Regex RegExpRRC_BC = new(@"(EUTRA|MRDC|NR|NRDC) band combinations", RegexOptions.Compiled);
/// <summary>
/// PDCCH正则表达式
/// </summary>
private static readonly Regex RegExpPDCCH = new(@"^\s*(.+)=(\d+)$", RegexOptions.Compiled);
/// <summary>
/// S1/NGAP正则表达式
/// </summary>
private static readonly Regex RegExpS1NGAP = new(@"^([\da-f\-]+)\s+([\da-f\-]+) (([^\s]+) .+)", RegexOptions.Compiled);
/// <summary>
/// 十六进制转储正则表达式
/// </summary>
private static readonly Regex RegExpHexDump = new(@"^[\da-f]+:(\s+[\da-f]{2}){1,16}\s+.{1,16}$", RegexOptions.Compiled);
#endregion
#region 基础属性
/// <summary>
@ -169,6 +287,25 @@ public class LTEClient
#endregion
#region 解析器私有字段
/// <summary>
/// 字符串到ID的映射缓存
/// </summary>
private readonly Dictionary<string, int> _stringToIdCache = new();
/// <summary>
/// ID到字符串的映射缓存
/// </summary>
private readonly Dictionary<int, string> _idToStringCache = new();
/// <summary>
/// 字符串ID计数器
/// </summary>
private int _stringIdCounter = 0;
#endregion
#region 构造函数
public LTEClient(ClientConfig config)
@ -193,7 +330,7 @@ public class LTEClient
#endregion
#region 公共方法
#region 客户端控制方法
/// <summary>
/// 启动客户端
@ -219,6 +356,10 @@ public class LTEClient
SetState(ClientState.Destroy);
}
#endregion
#region 日志管理方法
/// <summary>
/// 重置日志
/// </summary>
@ -257,6 +398,10 @@ public class LTEClient
log.Id = GenerateLogId();
}
#endregion
#region 工具方法
/// <summary>
/// 设置头信息
/// </summary>
@ -295,13 +440,497 @@ public class LTEClient
/// </summary>
public int StringToId(string str)
{
// 实现字符串转ID逻辑
return 0;
if (_stringToIdCache.TryGetValue(str, out var id))
{
return id;
}
id = ++_stringIdCounter;
_stringToIdCache[str] = id;
_idToStringCache[id] = str;
return id;
}
/// <summary>
/// ID转字符串
/// </summary>
public string IdToString(int id)
{
return _idToStringCache.TryGetValue(id, out var str) ? str : "";
}
#endregion
#region 日志解析方法
/// <summary>
/// 解析日志列表 - 对应JavaScript的_logListParse方法
/// </summary>
/// <param name="logs">日志列表</param>
/// <param name="isWebSocket">是否为WebSocket消息</param>
public void ParseLogList(List<LTELog> logs, bool isWebSocket = false)
{
var length = logs.Count;
for (int i = 0; i < length; i++)
{
var log = logs[i];
log.Message = log.Message + "";
AddLog(log);
// 解析消息
switch (log.Layer)
{
case "PHY":
if (!ParsePhyLog(log, log.Message, isWebSocket)) continue;
ProcessPhyLog(log);
break;
case "RRC":
ParseCellId(log, isWebSocket);
var rrcUeIdMatch = RegExpRRC_UE_ID.Match(log.Message);
if (rrcUeIdMatch.Success)
{
SetSameUe(log, int.Parse(rrcUeIdMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber));
continue;
}
var infoMatch = RegExpInfo1.Match(log.Message);
if (infoMatch.Success)
{
if (!SetLogInfo(log, infoMatch.Groups[1].Value)) continue;
log.Message = infoMatch.Groups[2].Value;
ProcessRrcLog(log);
}
var bcMatch = RegExpRRC_BC.Match(log.Message);
if (bcMatch.Success)
{
try
{
var data = log.GetDataString();
var jsonData = JsonConvert.DeserializeObject<object>(data);
// 处理频段组合信息
}
catch
{
// 忽略JSON解析错误
}
}
break;
case "NAS":
ParseCellId(log, isWebSocket);
var nasTmsiMatch = RegExpNAS_TMSI.Match(log.Message);
if (nasTmsiMatch.Success)
{
SetTmsi(log, nasTmsiMatch.Groups[1].Value);
continue;
}
var nas5gTmsiMatch = RegExpNAS_5GTMSI.Match(log.Message);
if (nas5gTmsiMatch.Success)
{
SetTmsi(log, nas5gTmsiMatch.Groups[1].Value);
continue;
}
var nasInfoMatch = RegExpInfo2.Match(log.Message);
if (nasInfoMatch.Success)
{
if (!SetLogInfo(log, nasInfoMatch.Groups[1].Value)) continue;
log.Message = nasInfoMatch.Groups[2].Value;
ProcessNasLog(log);
}
break;
case "MAC":
ParseCellId(log, isWebSocket);
ParseMacLog(log);
break;
case "IP":
var ipMatch = RegExpIP.Match(log.Message);
if (ipMatch.Success)
{
var lenPart = ipMatch.Groups[1].Value;
log.IpLen = int.Parse(lenPart.Split('=')[1]);
if (!SetLogInfo(log, ipMatch.Groups[2].Value)) continue;
log.Message = ipMatch.Groups[3].Value;
HasData = true;
}
break;
case "GTPU":
var sduLenMatch = RegExpSDULen.Match(log.Message);
if (sduLenMatch.Success)
{
log.SduLen = int.Parse(sduLenMatch.Groups[1].Value);
HasData = true;
}
break;
case "S1AP":
case "NGAP":
var s1ngMatch = RegExpS1NGAP.Match(log.Message);
if (s1ngMatch.Success)
{
log.Message = s1ngMatch.Groups[3].Value;
var coreId = int.TryParse(s1ngMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber, null, out var core) ? core : (int?)null;
var ranId = int.TryParse(s1ngMatch.Groups[2].Value, System.Globalization.NumberStyles.HexNumber, null, out var ran) ? ran : (int?)null;
log.LinkIds = new LinkIds { Core = coreId, Ran = ranId };
}
break;
case "SIP":
var sipMatch = RegExpSIP.Match(log.Message);
if (sipMatch.Success)
{
if (!SetLogInfo(log, sipMatch.Groups[2].Value)) continue;
log.Message = sipMatch.Groups[3].Value;
}
break;
case "MEDIA":
var mediaMatch = RegExpMediaReq.Match(log.Message);
if (mediaMatch.Success)
{
if (!SetLogInfo(log, mediaMatch.Groups[1].Value)) continue;
log.Message = mediaMatch.Groups[2].Value;
}
break;
case "IPsec":
var ipsecMatch = RegExpIPsec.Match(log.Message);
if (ipsecMatch.Success)
{
log.IpLen = int.Parse(ipsecMatch.Groups[1].Value);
log.Message = ipsecMatch.Groups[2].Value;
}
break;
default:
break;
}
// 处理提示信息
ProcessHints(log);
}
// 限制日志数量
if (LogCount > LOGS_MAX)
{
var excess = LogCount - LOGS_MAX;
Logs.RemoveRange(0, excess);
}
}
#endregion
#region PHY层解析方法
/// <summary>
/// 解析PHY日志
/// </summary>
private bool ParsePhyLog(LTELog log, string message, bool isWebSocket)
{
if (!isWebSocket)
{
var phyMatch = RegExpPhy.Match(message);
if (!phyMatch.Success)
{
Console.WriteLine($"Bad PHY log: {log}");
return false;
}
log.Cell = int.Parse(phyMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
log.Rnti = int.Parse(phyMatch.Groups[2].Value, System.Globalization.NumberStyles.HexNumber);
log.Channel = phyMatch.Groups[4].Value;
log.Message = phyMatch.Groups[5].Value;
}
HasPhy = true;
HasCell = true;
HasRnti = true;
// 解析PHY参数
var lines = log.GetData();
foreach (var line in lines)
{
var parts = line.Split('=');
if (parts.Length == 2)
{
ParsePhyParameter(log, parts[0].Trim(), parts[1].Trim());
}
}
return true;
}
/// <summary>
/// 解析PHY参数
/// </summary>
private void ParsePhyParameter(LTELog log, string param, string value)
{
switch (param.ToLower())
{
case "frame":
if (int.TryParse(value, out var frame))
{
log.Frame = frame;
HasPhy = true;
}
break;
case "subframe":
if (int.TryParse(value, out var subframe))
{
log.Subframe = subframe;
}
break;
case "slot":
if (int.TryParse(value, out var slot))
{
log.Slot = slot;
}
break;
case "symbol":
if (int.TryParse(value, out var symbol))
{
log.Symbol = symbol;
}
break;
case "ant":
if (int.TryParse(value, out var ant))
{
log.AntennaPort = ant;
}
break;
case "rb_start":
if (int.TryParse(value, out var rbStart))
{
log.RbStart = rbStart;
HasRb = true;
}
break;
case "rb_count":
if (int.TryParse(value, out var rbCount))
{
log.RbCount = rbCount;
HasRb = true;
}
break;
case "mcs":
if (int.TryParse(value, out var mcs))
{
log.Mcs = mcs;
}
break;
case "tbs":
if (int.TryParse(value, out var tbs))
{
log.Tbs = tbs;
}
break;
case "harq_id":
if (int.TryParse(value, out var harqId))
{
log.HarqId = harqId;
}
break;
case "harq_ndi":
if (bool.TryParse(value, out var harqNdi))
{
log.HarqNdi = harqNdi;
}
break;
case "harq_rv":
if (int.TryParse(value, out var harqRv))
{
log.HarqRedundancyVersion = harqRv;
}
break;
}
}
/// <summary>
/// 处理PHY日志
/// </summary>
private void ProcessPhyLog(LTELog log)
{
// 处理HARQ信息
if (log.HarqId.HasValue)
{
ProcessHarqLog(log, log.HarqId.Value);
}
// 处理帧信息
if (log.Frame.HasValue)
{
var frame = log.Frame.Value;
var hfn = frame >> 10;
var sfn = frame & 0x3FF;
if (Frame.Timestamp == -1)
{
Frame.Timestamp = log.Timestamp;
Frame.Hfn = hfn;
Frame.Last = sfn;
}
else
{
var diff = sfn - Frame.Last;
if (diff < -HFN_WRAP_THRESHOLD)
{
Frame.Hfn++;
}
else if (diff > HFN_WRAP_THRESHOLD)
{
Frame.Hfn--;
}
Frame.Last = sfn;
}
}
}
/// <summary>
/// 处理HARQ日志
/// </summary>
private void ProcessHarqLog(LTELog log, int harq)
{
if (log.Cell.HasValue && log.UeId.HasValue)
{
var cell = log.Cell.Value;
var ueId = log.UeId.Value;
var key = ueId * 32 + harq;
if (!LastHarq.ContainsKey(cell))
{
LastHarq[cell] = new Dictionary<int, LTELog>();
}
LastHarq[cell][key] = log;
}
}
#endregion
#region 其他层解析方法
/// <summary>
/// 解析小区ID
/// </summary>
private void ParseCellId(LTELog log, bool isWebSocket)
{
if (!isWebSocket)
{
var cellMatch = RegExpCellID.Match(log.Message);
if (cellMatch.Success)
{
log.Cell = int.Parse(cellMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
log.Message = cellMatch.Groups[2].Value;
HasCell = true;
}
}
}
/// <summary>
/// 设置日志信息
/// </summary>
private bool SetLogInfo(LTELog log, string info)
{
if (string.IsNullOrEmpty(info) || info == "?")
return false;
log.Info = StringToId(info);
return true;
}
/// <summary>
/// 设置相同UE
/// </summary>
private void SetSameUe(LTELog log, int ueId)
{
log.UeId = ueId;
if (!UeList.ContainsKey(ueId))
{
UeList[ueId] = new UEInfo { UeId = ueId };
}
}
/// <summary>
/// 处理RRC日志
/// </summary>
private void ProcessRrcLog(LTELog log)
{
// 处理RRC TMSI
var tmsiMatch = RegExpRRC_TMSI.Match(log.Message);
if (tmsiMatch.Success)
{
SetTmsi(log, tmsiMatch.Groups[2].Value);
}
// 处理RRC CRNTI
var crntiMatch = RegExpRRC_CRNTI.Match(log.Message);
if (crntiMatch.Success)
{
SetRnti(log, crntiMatch.Groups[1].Value);
}
}
/// <summary>
/// 处理NAS日志
/// </summary>
private void ProcessNasLog(LTELog log)
{
// NAS日志处理逻辑
}
/// <summary>
/// 解析MAC日志
/// </summary>
private void ParseMacLog(LTELog log)
{
// MAC日志解析逻辑
}
/// <summary>
/// 处理提示信息
/// </summary>
private void ProcessHints(LTELog log, Dictionary<string, object>? config = null)
{
// 处理提示信息逻辑
}
/// <summary>
/// 设置TMSI
/// </summary>
private void SetTmsi(LTELog log, string tmsi)
{
var tmsiId = int.Parse(tmsi, System.Globalization.NumberStyles.HexNumber);
if (log.UeId.HasValue)
{
TmsiToUeId[tmsiId] = log.UeId.Value;
}
}
/// <summary>
/// 设置RNTI
/// </summary>
private void SetRnti(LTELog log, string rnti)
{
var rntiId = int.Parse(rnti, System.Globalization.NumberStyles.HexNumber);
if (log.UeId.HasValue)
{
RntiToUeId[rntiId] = log.UeId.Value;
}
}
#endregion
#region 私有方法
#region 私有辅助方法
/// <summary>
/// 设置状态
@ -339,6 +968,11 @@ public class LTEClient
LastTimestamp = 0;
TimestampOffset = 0;
LastCell = null;
// 重置解析器缓存
_stringToIdCache.Clear();
_idToStringCache.Clear();
_stringIdCounter = 0;
}
/// <summary>

121
LTEMvcApp/Models/LogChannelConfig.cs

@ -0,0 +1,121 @@
namespace LTEMvcApp.Models
{
/// <summary>
/// 日志信道配置
/// </summary>
public static class LogChannelConfig
{
/// <summary>
/// 信道定义列表
/// </summary>
public static readonly List<ChannelDefinition> ChannelDefinitions = new()
{
new() { Name = "PRACH", Color = "#ffff00" },
new() { Name = "NPRACH", Color = "#ffff00" },
new() { Name = "SRS", Color = "#ffff80" },
new() { Name = "PUCCH", Color = "#00ff00" },
new() { Name = "PUSCH", Color = "#ff0000" },
new() { Name = "NPUSCH", Color = "#ff0000" },
new() { Name = "PDSCH", Color = "#0000ff" },
new() { Name = "NPDSCH", Color = "#0000ff" },
new() { Name = "PDCCH", Color = "#00ffff" },
new() { Name = "EPDCCH", Color = "#00ffff" },
new() { Name = "NPDCCH", Color = "#00ffff" },
new() { Name = "PMCH", Color = "#ff80ff" },
new() { Name = "INV", Color = "#D0D0D0" }
};
/// <summary>
/// 信道定义字典(按名称索引)
/// </summary>
public static readonly Dictionary<string, ChannelDefinition> ChannelsByName = ChannelDefinitions
.ToDictionary(c => c.Name, c => c);
/// <summary>
/// 信道定义字典(按ID索引)
/// </summary>
public static readonly Dictionary<int, ChannelDefinition> ChannelsById = ChannelDefinitions
.Select((c, i) => new { Channel = c, Index = i })
.ToDictionary(x => x.Index, x => x.Channel);
/// <summary>
/// 获取信道定义
/// </summary>
/// <param name="name">信道名称</param>
/// <returns>信道定义</returns>
public static ChannelDefinition? GetChannelByName(string name)
{
return ChannelsByName.TryGetValue(name, out var channel) ? channel : null;
}
/// <summary>
/// 获取信道定义
/// </summary>
/// <param name="id">信道ID</param>
/// <returns>信道定义</returns>
public static ChannelDefinition? GetChannelById(int id)
{
return ChannelsById.TryGetValue(id, out var channel) ? channel : null;
}
/// <summary>
/// 获取所有信道名称
/// </summary>
/// <returns>信道名称列表</returns>
public static List<string> GetAllChannelNames()
{
return ChannelDefinitions.Select(c => c.Name).ToList();
}
/// <summary>
/// 验证信道名称是否有效
/// </summary>
/// <param name="name">信道名称</param>
/// <returns>是否有效</returns>
public static bool IsValidChannel(string name)
{
return ChannelsByName.ContainsKey(name);
}
/// <summary>
/// 获取信道ID
/// </summary>
/// <param name="name">信道名称</param>
/// <returns>信道ID,如果不存在返回-1</returns>
public static int GetChannelId(string name)
{
for (int i = 0; i < ChannelDefinitions.Count; i++)
{
if (ChannelDefinitions[i].Name == name)
return i;
}
return -1;
}
}
/// <summary>
/// 信道定义
/// </summary>
public class ChannelDefinition
{
/// <summary>
/// 信道名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 颜色代码
/// </summary>
public string Color { get; set; } = "#000000";
/// <summary>
/// 信道ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 信道索引
/// </summary>
public int Index { get; set; }
}
}

254
LTEMvcApp/Models/LogConstants.cs

@ -0,0 +1,254 @@
namespace LTEMvcApp.Models
{
/// <summary>
/// 日志常量
/// </summary>
public static class LogConstants
{
#region 全局常量
/// <summary>
/// 最大日志数量
/// </summary>
public const int LOGS_MAX = 2000000;
/// <summary>
/// 日志范围
/// </summary>
public static readonly List<long[]> LOGS_RANGE = new();
/// <summary>
/// 日志信息
/// </summary>
public static readonly List<int> LOGS_INFO = new();
/// <summary>
/// 日志层
/// </summary>
public static readonly List<string> LOGS_LAYERS = new();
/// <summary>
/// 导出时间格式
/// </summary>
public static string EXPORT_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss.fff";
/// <summary>
/// 是否启用内联缓存
/// </summary>
public static bool INLINE_CACHE_ENABLED = false;
#endregion
#region 应用模式
/// <summary>
/// 应用模式
/// </summary>
public static class AppMode
{
public const string Web = "web";
public const string App = "app";
}
/// <summary>
/// 导出模式
/// </summary>
public static class ExportMode
{
public const string Zip = "zip";
public const string Text = "text";
}
#endregion
#region 字体和样式
/// <summary>
/// 默认字体
/// </summary>
public const string DEFAULT_FONT = "13px helvetica, arial, verdana, sans-serif";
/// <summary>
/// 默认颜色
/// </summary>
public const string DEFAULT_COLOR = "#000000";
#endregion
#region 时间相关
/// <summary>
/// 时间戳基准(2000-01-01 00:00:00)
/// </summary>
public const long TIMESTAMP_BASE = 946681200000;
/// <summary>
/// 默认时间原点
/// </summary>
public const string DEFAULT_TIME_ORIGIN = "00:00:00.000";
#endregion
#region 客户端相关
/// <summary>
/// 默认重连延迟(毫秒)
/// </summary>
public const int DEFAULT_RECONNECT_DELAY = 2500;
/// <summary>
/// 默认重连延迟(毫秒)
/// </summary>
public const int DEFAULT_RECONNECT_DELAY_APP = 5000;
/// <summary>
/// HFN回绕阈值
/// </summary>
public const int HFN_WRAP_THRESHOLD = 512;
#endregion
#region 日志级别
/// <summary>
/// 日志级别名称
/// </summary>
public static readonly string[] LOG_LEVEL_NAMES = { "none", "error", "warn", "info", "debug" };
/// <summary>
/// 日志方法名称
/// </summary>
public static readonly string[] LOG_METHODS = { "error", "warn", "info", "debug", "prof" };
#endregion
#region 性能相关
/// <summary>
/// 大日志列表阈值
/// </summary>
public const int LARGE_LOG_THRESHOLD = 500000;
/// <summary>
/// 小日志列表阈值
/// </summary>
public const int SMALL_LOG_THRESHOLD = 50000;
/// <summary>
/// 日志模块列表最大长度
/// </summary>
public const int LOG_MODULE_LIST_MAX = 20;
#endregion
#region 文件相关
/// <summary>
/// 支持的文件类型
/// </summary>
public static class FileTypes
{
public const string Binary = "bin";
public const string Base64 = "base64";
}
/// <summary>
/// 文件大小单位
/// </summary>
public static class FileSizeUnits
{
public const string Kilobyte = "K";
public const string Megabyte = "M";
public const string Gigabyte = "G";
}
#endregion
#region 正则表达式
/// <summary>
/// 参数解析正则表达式
/// </summary>
public const string PARAMS_REGEX = @"\s*([^=]+)=([^,\s]+)";
#endregion
#region 事件相关
/// <summary>
/// 事件类型
/// </summary>
public static class EventTypes
{
public const string NewLogs = "newLogs";
public const string SelectLog = "selectLog";
public const string All = "*";
}
#endregion
#region 浏览器相关
/// <summary>
/// 鼠标滚轮事件
/// </summary>
public static class MouseWheelEvents
{
public const string Firefox = "DOMMouseScroll";
public const string Other = "mousewheel";
}
#endregion
#region 工具方法
/// <summary>
/// 获取日志级别名称
/// </summary>
/// <param name="level">级别值</param>
/// <returns>级别名称</returns>
public static string GetLogLevelName(int level)
{
if (level >= 0 && level < LOG_LEVEL_NAMES.Length)
return LOG_LEVEL_NAMES[level];
return "unknown";
}
/// <summary>
/// 获取日志级别值
/// </summary>
/// <param name="levelName">级别名称</param>
/// <returns>级别值</returns>
public static int GetLogLevelValue(string levelName)
{
for (int i = 0; i < LOG_LEVEL_NAMES.Length; i++)
{
if (LOG_LEVEL_NAMES[i] == levelName)
return i;
}
return -1;
}
/// <summary>
/// 检查是否为持续时间戳
/// </summary>
/// <param name="timestamp">时间戳</param>
/// <returns>是否为持续时间</returns>
public static bool IsDurationTimestamp(long timestamp)
{
return timestamp < TIMESTAMP_BASE;
}
/// <summary>
/// 获取默认重连延迟
/// </summary>
/// <param name="mode">应用模式</param>
/// <returns>重连延迟</returns>
public static int GetDefaultReconnectDelay(string mode)
{
return mode == AppMode.App ? DEFAULT_RECONNECT_DELAY_APP : DEFAULT_RECONNECT_DELAY;
}
#endregion
}
}

74
LTEMvcApp/Models/LogDirection.cs

@ -0,0 +1,74 @@
namespace LTEMvcApp.Models
{
/// <summary>
/// 日志传输方向常量
/// </summary>
public static class LogDirection
{
/// <summary>
/// 无方向
/// </summary>
public const int None = 0;
/// <summary>
/// 上行传输
/// </summary>
public const int UL = 1;
/// <summary>
/// 下行传输
/// </summary>
public const int DL = 2;
/// <summary>
/// 从某处传输
/// </summary>
public const int From = 3;
/// <summary>
/// 传输到某处
/// </summary>
public const int To = 4;
/// <summary>
/// 获取方向名称
/// </summary>
/// <param name="direction">方向值</param>
/// <returns>方向名称</returns>
public static string GetDirectionName(int direction)
{
return direction switch
{
None => "-",
UL => "UL",
DL => "DL",
From => "FROM",
To => "TO",
_ => "UNKNOWN"
};
}
/// <summary>
/// 获取所有方向选项
/// </summary>
/// <returns>方向选项列表</returns>
public static List<DirectionOption> GetDirectionOptions()
{
return new List<DirectionOption>
{
new() { Value = None, Text = "-" },
new() { Value = DL, Text = "DL" },
new() { Value = UL, Text = "UL" }
};
}
}
/// <summary>
/// 方向选项
/// </summary>
public class DirectionOption
{
public int Value { get; set; }
public string Text { get; set; } = string.Empty;
}
}

229
LTEMvcApp/Models/LogEventManager.cs

@ -0,0 +1,229 @@
namespace LTEMvcApp.Models
{
/// <summary>
/// 日志事件管理器
/// </summary>
public class LogEventManager
{
/// <summary>
/// 事件监听器字典
/// </summary>
private readonly Dictionary<int, EventListener> _eventListeners = new();
/// <summary>
/// 事件监听器ID计数器
/// </summary>
private int _eventListenerId = 0;
/// <summary>
/// 注册事件监听器
/// </summary>
/// <param name="type">事件类型</param>
/// <param name="callback">回调函数</param>
/// <returns>监听器ID</returns>
public int RegisterEventListener(string type, Action<LogEvent> callback)
{
var id = _eventListenerId++;
_eventListeners[id] = new EventListener
{
Type = type,
Callback = callback
};
return id;
}
/// <summary>
/// 注销事件监听器
/// </summary>
/// <param name="id">监听器ID</param>
public void UnregisterEventListener(int id)
{
_eventListeners.Remove(id);
}
/// <summary>
/// 发送事件
/// </summary>
/// <param name="event">事件对象</param>
public void SendEvent(LogEvent @event)
{
var type = @event.Type;
foreach (var kvp in _eventListeners)
{
var listener = kvp.Value;
switch (listener.Type)
{
case var t when t == type:
case LogConstants.EventTypes.All:
listener.Callback(@event);
break;
}
}
}
/// <summary>
/// 发送新日志事件
/// </summary>
/// <param name="logs">日志列表</param>
public void SendNewLogsEvent(List<LTELog> logs)
{
SendEvent(new LogEvent
{
Type = LogConstants.EventTypes.NewLogs,
Data = new { Logs = logs }
});
}
/// <summary>
/// 发送选择日志事件
/// </summary>
/// <param name="log">选中的日志</param>
public void SendSelectLogEvent(LTELog log)
{
SendEvent(new LogEvent
{
Type = LogConstants.EventTypes.SelectLog,
Data = new { Log = log }
});
}
/// <summary>
/// 清除所有事件监听器
/// </summary>
public void ClearAllListeners()
{
_eventListeners.Clear();
_eventListenerId = 0;
}
/// <summary>
/// 获取监听器数量
/// </summary>
/// <returns>监听器数量</returns>
public int GetListenerCount()
{
return _eventListeners.Count;
}
/// <summary>
/// 获取指定类型的事件监听器数量
/// </summary>
/// <param name="type">事件类型</param>
/// <returns>监听器数量</returns>
public int GetListenerCountByType(string type)
{
return _eventListeners.Values.Count(l => l.Type == type || l.Type == LogConstants.EventTypes.All);
}
}
/// <summary>
/// 事件监听器
/// </summary>
public class EventListener
{
/// <summary>
/// 事件类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 回调函数
/// </summary>
public Action<LogEvent> Callback { get; set; } = _ => { };
}
/// <summary>
/// 日志事件
/// </summary>
public class LogEvent
{
/// <summary>
/// 事件类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 事件数据
/// </summary>
public object? Data { get; set; }
/// <summary>
/// 时间戳
/// </summary>
public DateTime Timestamp { get; set; } = DateTime.Now;
/// <summary>
/// 事件源
/// </summary>
public string? Source { get; set; }
}
/// <summary>
/// 日志事件类型
/// </summary>
public static class LogEventTypes
{
/// <summary>
/// 新日志事件
/// </summary>
public const string NewLogs = "newLogs";
/// <summary>
/// 选择日志事件
/// </summary>
public const string SelectLog = "selectLog";
/// <summary>
/// 过滤器更新事件
/// </summary>
public const string FilterUpdate = "filterUpdate";
/// <summary>
/// 客户端状态变化事件
/// </summary>
public const string ClientStateChange = "clientStateChange";
/// <summary>
/// 日志清除事件
/// </summary>
public const string LogsCleared = "logsCleared";
/// <summary>
/// 错误事件
/// </summary>
public const string Error = "error";
/// <summary>
/// 警告事件
/// </summary>
public const string Warning = "warning";
/// <summary>
/// 信息事件
/// </summary>
public const string Info = "info";
/// <summary>
/// 调试事件
/// </summary>
public const string Debug = "debug";
/// <summary>
/// 所有事件
/// </summary>
public const string All = "*";
/// <summary>
/// 获取所有事件类型
/// </summary>
/// <returns>事件类型列表</returns>
public static List<string> GetAllEventTypes()
{
return new List<string>
{
NewLogs, SelectLog, FilterUpdate, ClientStateChange, LogsCleared,
Error, Warning, Info, Debug, All
};
}
}
}

270
LTEMvcApp/Models/LogFilterConfig.cs

@ -0,0 +1,270 @@
namespace LTEMvcApp.Models
{
/// <summary>
/// 日志过滤器配置
/// </summary>
public class LogFilterConfig
{
/// <summary>
/// 全局UE ID列表
/// </summary>
public List<int> GlobalUeIds { get; set; } = new();
/// <summary>
/// 小区ID列表
/// </summary>
public List<int> CellIds { get; set; } = new();
/// <summary>
/// IMSI列表
/// </summary>
public List<string> ImsiList { get; set; } = new();
/// <summary>
/// IMEI列表
/// </summary>
public List<string> ImeiList { get; set; } = new();
/// <summary>
/// 信息类型列表
/// </summary>
public List<int> InfoTypes { get; set; } = new();
/// <summary>
/// 层类型列表
/// </summary>
public List<string> LayerTypes { get; set; } = new();
/// <summary>
/// 日志级别列表
/// </summary>
public List<int> LogLevels { get; set; } = new();
/// <summary>
/// 方向过滤器(false表示过滤掉,true表示保留)
/// </summary>
public bool[] DirectionFilter { get; set; } = { false, false, false };
/// <summary>
/// 开始时间戳
/// </summary>
public long StartTime { get; set; } = 0;
/// <summary>
/// 结束时间戳
/// </summary>
public long EndTime { get; set; } = long.MaxValue;
/// <summary>
/// 时间原点
/// </summary>
public string TimeOrigin { get; set; } = "00:00:00.000";
/// <summary>
/// 是否分组UE ID
/// </summary>
public bool GroupUeId { get; set; } = true;
/// <summary>
/// 搜索文本
/// </summary>
public string? SearchText { get; set; }
/// <summary>
/// 是否使用正则表达式搜索
/// </summary>
public bool UseRegexSearch { get; set; } = false;
/// <summary>
/// 是否忽略大小写
/// </summary>
public bool IgnoreCase { get; set; } = true;
/// <summary>
/// 是否只显示错误
/// </summary>
public bool ShowErrorsOnly { get; set; } = false;
/// <summary>
/// 是否只显示警告
/// </summary>
public bool ShowWarningsOnly { get; set; } = false;
/// <summary>
/// 客户端过滤器
/// </summary>
public Dictionary<string, bool> ClientFilters { get; set; } = new();
/// <summary>
/// 是否启用过滤器
/// </summary>
public bool IsEnabled { get; set; } = true;
/// <summary>
/// 过滤器名称
/// </summary>
public string? FilterName { get; set; }
/// <summary>
/// 是否持久化
/// </summary>
public bool IsPersistent { get; set; } = false;
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.Now;
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; set; } = DateTime.Now;
/// <summary>
/// 清除所有过滤器
/// </summary>
public void ClearAll()
{
GlobalUeIds.Clear();
CellIds.Clear();
ImsiList.Clear();
ImeiList.Clear();
InfoTypes.Clear();
LayerTypes.Clear();
LogLevels.Clear();
DirectionFilter = new bool[] { false, false, false };
StartTime = 0;
EndTime = long.MaxValue;
TimeOrigin = "00:00:00.000";
SearchText = null;
ClientFilters.Clear();
UpdatedAt = DateTime.Now;
}
/// <summary>
/// 重置为默认值
/// </summary>
public void ResetToDefault()
{
ClearAll();
GroupUeId = true;
IsEnabled = true;
UseRegexSearch = false;
IgnoreCase = true;
ShowErrorsOnly = false;
ShowWarningsOnly = false;
}
/// <summary>
/// 复制过滤器配置
/// </summary>
/// <returns>新的过滤器配置</returns>
public LogFilterConfig Clone()
{
return new LogFilterConfig
{
GlobalUeIds = new List<int>(GlobalUeIds),
CellIds = new List<int>(CellIds),
ImsiList = new List<string>(ImsiList),
ImeiList = new List<string>(ImeiList),
InfoTypes = new List<int>(InfoTypes),
LayerTypes = new List<string>(LayerTypes),
LogLevels = new List<int>(LogLevels),
DirectionFilter = (bool[])DirectionFilter.Clone(),
StartTime = StartTime,
EndTime = EndTime,
TimeOrigin = TimeOrigin,
GroupUeId = GroupUeId,
SearchText = SearchText,
UseRegexSearch = UseRegexSearch,
IgnoreCase = IgnoreCase,
ShowErrorsOnly = ShowErrorsOnly,
ShowWarningsOnly = ShowWarningsOnly,
ClientFilters = new Dictionary<string, bool>(ClientFilters),
IsEnabled = IsEnabled,
FilterName = FilterName,
IsPersistent = IsPersistent,
CreatedAt = CreatedAt,
UpdatedAt = DateTime.Now
};
}
/// <summary>
/// 检查是否为空过滤器
/// </summary>
/// <returns>是否为空</returns>
public bool IsEmpty()
{
return GlobalUeIds.Count == 0 &&
CellIds.Count == 0 &&
ImsiList.Count == 0 &&
ImeiList.Count == 0 &&
InfoTypes.Count == 0 &&
LayerTypes.Count == 0 &&
LogLevels.Count == 0 &&
DirectionFilter.All(x => !x) &&
StartTime == 0 &&
EndTime == long.MaxValue &&
string.IsNullOrEmpty(SearchText) &&
ClientFilters.Count == 0 &&
!ShowErrorsOnly &&
!ShowWarningsOnly;
}
}
/// <summary>
/// 过滤器选项
/// </summary>
public class FilterOption
{
/// <summary>
/// 值
/// </summary>
public object Value { get; set; } = string.Empty;
/// <summary>
/// 显示文本
/// </summary>
public string Text { get; set; } = string.Empty;
/// <summary>
/// 样式
/// </summary>
public string Style { get; set; } = string.Empty;
/// <summary>
/// 是否被选中
/// </summary>
public bool IsSelected { get; set; } = false;
}
/// <summary>
/// 过滤器类型
/// </summary>
public static class FilterTypes
{
public const string GlobalUeId = "global_ue_id";
public const string Cell = "cell";
public const string Imsi = "imsi";
public const string Imei = "imei";
public const string Info = "info";
public const string Layer = "layer";
public const string Level = "level";
public const string Direction = "dir";
public const string Time = "time";
public const string Search = "search";
public const string Client = "client";
/// <summary>
/// 获取所有过滤器类型
/// </summary>
/// <returns>过滤器类型列表</returns>
public static List<string> GetAllFilterTypes()
{
return new List<string>
{
GlobalUeId, Cell, Imsi, Imei, Info, Layer, Level, Direction, Time, Search, Client
};
}
}
}

164
LTEMvcApp/Models/LogLayerConfig.cs

@ -27,6 +27,48 @@ namespace LTEMvcApp.Models
/// 过滤器(用于兼容性)
/// </summary>
public string Filter { get; set; } = "warn";
/// <summary>
/// 颜色代码
/// </summary>
public string Color { get; set; } = "#000000";
/// <summary>
/// 方向配置
/// </summary>
public Dictionary<string, int> Direction { get; set; } = new();
/// <summary>
/// 是否为EPC相关
/// </summary>
public bool EPC { get; set; } = false;
/// <summary>
/// 调试配置
/// </summary>
public DebugConfig? Debug { get; set; }
/// <summary>
/// 最大配置
/// </summary>
public MaxConfig? Max { get; set; }
}
/// <summary>
/// 调试配置
/// </summary>
public class DebugConfig
{
public string Level { get; set; } = "debug";
public int MaxSize { get; set; } = 1;
}
/// <summary>
/// 最大配置
/// </summary>
public class MaxConfig
{
public int MaxSize { get; set; } = 32;
}
/// <summary>
@ -43,17 +85,56 @@ namespace LTEMvcApp.Models
public const string S1AP = "S1AP";
public const string NGAP = "NGAP";
public const string GTPU = "GTPU";
public const string GTPC = "GTPC";
public const string IP = "IP";
public const string X2AP = "X2AP";
public const string XnAP = "XnAP";
public const string M2AP = "M2AP";
public const string IMS = "IMS";
public const string CX = "CX";
public const string RX = "RX";
public const string COM = "COM";
public const string SIP = "SIP";
public const string MEDIA = "MEDIA";
public const string RTP = "RTP";
public const string PROD = "PROD";
public const string S6 = "S6";
public const string S13 = "S13";
public const string SGsAP = "SGsAP";
public const string SBcAP = "SBcAP";
public const string N8 = "N8";
public const string N12 = "N12";
public const string N13 = "N13";
public const string N17 = "N17";
public const string N50 = "N50";
public const string MMS = "MMS";
public const string HTTP2 = "HTTP2";
public const string LCSAP = "LCSAP";
public const string LPPa = "LPPa";
public const string NL1 = "NL1";
public const string NRPPa = "NRPPa";
public const string IKEV2 = "IKEV2";
public const string SWU = "SWU";
public const string NWU = "NWU";
public const string IPSEC = "IPSEC";
public const string N3IWF = "N3IWF";
public const string TRX = "TRX";
public const string MON = "MON";
public const string EVENT = "EVENT";
public const string ALARM = "ALARM";
public const string LIC = "LIC";
public const string OTS = "OTS";
public const string ERROR = "ERROR";
/// <summary>
/// 获取所有日志层类型
/// </summary>
public static readonly string[] AllLayers = new[]
{
PHY, MAC, RLC, PDCP, RRC, NAS, S1AP, NGAP, GTPU, X2AP, XnAP, M2AP, EVENT
PHY, MAC, RLC, PDCP, RRC, NAS, S1AP, NGAP, GTPU, GTPC, IP, X2AP, XnAP, M2AP,
IMS, CX, RX, COM, SIP, MEDIA, RTP, PROD, S6, S13, SGsAP, SBcAP, N8, N12, N13,
N17, N50, MMS, HTTP2, LCSAP, LPPa, NL1, NRPPa, IKEV2, SWU, NWU, IPSEC, N3IWF,
TRX, MON, EVENT, ALARM, LIC, OTS, ERROR
};
/// <summary>
@ -76,5 +157,86 @@ namespace LTEMvcApp.Models
_ => "warn"
};
}
/// <summary>
/// 获取层配置
/// </summary>
/// <returns>层配置字典</returns>
public static Dictionary<string, LogLayerConfig> GetLayerConfigs()
{
return new Dictionary<string, LogLayerConfig>
{
[PHY] = new() { Color = "#606070", Direction = new() { ["UE"] = 2, ["PROBE"] = 3, ["ENB"] = 1 }, Debug = new() { Level = "debug", MaxSize = 1 } },
[MAC] = new() { Color = "#10A0FF", Direction = new() { ["UE"] = 2, ["PROBE"] = 3, ["ENB"] = 1 }, Debug = new() { Level = "debug", MaxSize = 1 } },
[RLC] = new() { Color = "#FFFF80", Direction = new() { ["UE"] = 2, ["PROBE"] = 3, ["ENB"] = 1 } },
[PDCP] = new() { Color = "#B0D0B0", Direction = new() { ["UE"] = 2, ["PROBE"] = 3, ["ENB"] = 1 } },
[RRC] = new() { Color = "#00FF60", Direction = new() { ["UE"] = 2, ["PROBE"] = 3, ["ENB"] = 1 }, Debug = new() { Level = "debug", MaxSize = 1 } },
[NAS] = new() { Color = "#90FFC0", Direction = new() { ["UE"] = 2, ["PROBE"] = 3, ["ENB"] = 3, ["N3IWF"] = 3, ["MME"] = 1 }, Debug = new() { Level = "debug", MaxSize = 1 } },
[GTPU] = new() { Color = "#FF80FF", Direction = new() { ["ENB"] = 2, ["N3IWF"] = 2, ["MME"] = 1, ["MBMSGW"] = 1, ["UE"] = 1, ["RUE"] = 2 }, EPC = true, Max = new() { MaxSize = 32 } },
[GTPC] = new() { Color = "#FFC0FF", Direction = new() { ["MME"] = 2 }, EPC = true },
[IP] = new() { Color = "#E0E0E0", Direction = new() { ["RUE"] = 2, ["UE"] = 2, ["N3IWF"] = 3, ["PROBE"] = 3, ["MME"] = 3 }, Max = new() { MaxSize = 32 } },
[S1AP] = new() { Color = "#80FF00", Direction = new() { ["ENB"] = 2, ["MME"] = 1 }, EPC = true, Debug = new() { Level = "debug", MaxSize = 1 } },
[NGAP] = new() { Color = "#5DD122", Direction = new() { ["ENB"] = 2, ["MME"] = 1, ["N3IWF"] = 2 }, EPC = true, Debug = new() { Level = "debug", MaxSize = 1 } },
[X2AP] = new() { Color = "#FF8000", Direction = new() { ["ENB"] = 2 } },
[XnAP] = new() { Color = "#FFB020", Direction = new() { ["ENB"] = 2 } },
[M2AP] = new() { Color = "#7F675B", Direction = new() { ["ENB"] = 2, ["MBMSGW"] = 1 }, EPC = true },
[IMS] = new() { Color = "#C0FF80", Direction = new() { ["MME"] = 2, ["IMS"] = 1 }, Debug = new() { Level = "debug", MaxSize = 1 }, EPC = true },
[CX] = new() { Color = "#F49542", Direction = new() { ["MME"] = 2, ["IMS"] = 1 }, EPC = true },
[RX] = new() { Color = "#D4A190", Direction = new() { ["MME"] = 2, ["IMS"] = 1 }, EPC = true },
[COM] = new() { Color = "#C000FF", Direction = new() { ["*"] = 2 } },
[SIP] = new() { Color = "#C080FF", Direction = new() { ["IMS"] = 1 }, EPC = true, Debug = new() { Level = "debug", MaxSize = 1 } },
[MEDIA] = new() { Color = "#800040", Direction = new() { ["IMS"] = 1 }, EPC = true },
[RTP] = new() { Color = "#FF00C0", Direction = new() { ["IMS"] = 1 }, EPC = true },
[PROD] = new() { Color = "#80C0FF", Direction = new() { } },
[S6] = new() { Color = "#F44B42", Direction = new() { ["MME"] = 2 }, EPC = true },
[S13] = new() { Color = "#D6F953", Direction = new() { ["MME"] = 2 }, EPC = true },
[SGsAP] = new() { Color = "#FF7DB8", Direction = new() { ["MME"] = 2 }, EPC = true },
[SBcAP] = new() { Color = "#8FA00F", Direction = new() { ["MME"] = 2 }, EPC = true },
[N8] = new() { Color = "#106020", Direction = new() { ["MME"] = 2 }, EPC = true },
[N12] = new() { Color = "#602020", Direction = new() { ["MME"] = 2 }, EPC = true },
[N13] = new() { Color = "#202060", Direction = new() { ["MME"] = 2 }, EPC = true },
[N17] = new() { Color = "#D6F953", Direction = new() { ["MME"] = 2 }, EPC = true },
[N50] = new() { Color = "#8FA00F", Direction = new() { ["MME"] = 2 }, EPC = true },
[MMS] = new() { Color = "#B4D98F", Direction = new() { ["MME"] = 2 }, EPC = true },
[HTTP2] = new() { Color = "#644824", Direction = new() { ["MME"] = 2 }, EPC = true },
[LCSAP] = new() { Color = "#cfd50d", Direction = new() { ["MME"] = 2 }, EPC = true },
[LPPa] = new() { Color = "#0dcfd5", Direction = new() { ["ENB"] = 2, ["MME"] = 3 }, EPC = true },
[NL1] = new() { Color = "#d040cf", Direction = new() { ["MME"] = 2 }, EPC = true },
[NRPPa] = new() { Color = "#0dd5cf", Direction = new() { ["ENB"] = 2, ["MME"] = 3 }, EPC = true },
[IKEV2] = new() { Color = "#C0B732", Direction = new() { ["UE"] = 2, ["MME"] = 1, ["N3IWF"] = 1 } },
[SWU] = new() { Color = "#101080", Direction = new() { ["UE"] = 2, ["MME"] = 1 } },
[NWU] = new() { Color = "#2080B8", Direction = new() { ["UE"] = 2, ["MME"] = 1 } },
[IPSEC] = new() { Color = "#F04010", Direction = new() { ["UE"] = 2, ["IMS"] = 1, ["N3IWF"] = 1, ["MME"] = 1 }, EPC = true, Max = new() { MaxSize = 32 } },
[N3IWF] = new() { Color = "#C080C0", Direction = new() { ["UE"] = 2, ["N3IWF"] = 1 } },
[TRX] = new() { Color = "#42C0a0", Direction = new() { ["UE"] = 2, ["ENB"] = 1 }, Debug = new() { Level = "debug" } },
[MON] = new() { Color = "#C0C080", Direction = new() { } },
[EVENT] = new() { Color = "#80C0FF", Direction = new() { } },
[ALARM] = new() { Color = "#FF8040", Direction = new() { } },
[LIC] = new() { Color = "#ff80c0", Direction = new() { ["LICENSE"] = 1 } },
[OTS] = new() { Color = "#8080FF", Direction = new() { } },
[ERROR] = new() { Color = "#ff0000", Direction = new() { } }
};
}
/// <summary>
/// 获取层配置
/// </summary>
/// <param name="layerName">层名称</param>
/// <returns>层配置</returns>
public static LogLayerConfig? GetLayerConfig(string layerName)
{
var configs = GetLayerConfigs();
return configs.TryGetValue(layerName, out var config) ? config : null;
}
/// <summary>
/// 验证层名称是否有效
/// </summary>
/// <param name="layerName">层名称</param>
/// <returns>是否有效</returns>
public static bool IsValidLayer(string layerName)
{
return AllLayers.Contains(layerName);
}
}
}

121
LTEMvcApp/Models/LogModelConfig.cs

@ -0,0 +1,121 @@
using System.Text.RegularExpressions;
namespace LTEMvcApp.Models
{
/// <summary>
/// 日志模型配置
/// </summary>
public static class LogModelConfig
{
/// <summary>
/// 模型列表(顺序重要)
/// </summary>
public static readonly string[] ModelList =
{
"RUE", "UE", "PROBE", "ENB", "N3IWF", "MBMSGW", "MME", "IMS", "LICENSE", "MONITOR"
};
/// <summary>
/// 模型配置字典
/// </summary>
public static readonly Dictionary<string, ModelConfig> Models = new()
{
["RUE"] = new() { Icon = "icon-ue2" },
["UE"] = new() { Icon = "icon-ue" },
["PROBE"] = new() { },
["ENB"] = new()
{
Icon = "icon-air",
ModelHint = new Regex(@"enb|gnb|bbu", RegexOptions.IgnoreCase),
Title = "RAN"
},
["N3IWF"] = new()
{
Icon = "icon-air",
ModelHint = new Regex(@"n3iwf", RegexOptions.IgnoreCase),
Title = "N3IWF"
},
["MME"] = new()
{
Icon = "icon-server",
ModelHint = new Regex(@"epc|mme|amf", RegexOptions.IgnoreCase),
Title = "CN"
},
["MBMSGW"] = new()
{
Icon = "icon-download",
ModelHint = new Regex(@"mbms", RegexOptions.IgnoreCase)
},
["IMS"] = new() { Icon = "icon-dial" },
["MONITOR"] = new() { Icon = "icon-monitor" },
["LICENSE"] = new() { Icon = "icon-file" }
};
/// <summary>
/// 获取模型配置
/// </summary>
/// <param name="modelName">模型名称</param>
/// <returns>模型配置</returns>
public static ModelConfig? GetModelConfig(string modelName)
{
return Models.TryGetValue(modelName, out var config) ? config : null;
}
/// <summary>
/// 根据提示匹配模型
/// </summary>
/// <param name="hint">提示文本</param>
/// <returns>匹配的模型名称</returns>
public static string? MatchModelByHint(string hint)
{
foreach (var kvp in Models)
{
if (kvp.Value.ModelHint?.IsMatch(hint) == true)
{
return kvp.Key;
}
}
return null;
}
/// <summary>
/// 获取所有模型名称
/// </summary>
/// <returns>模型名称列表</returns>
public static List<string> GetAllModelNames()
{
return ModelList.ToList();
}
/// <summary>
/// 验证模型名称是否有效
/// </summary>
/// <param name="modelName">模型名称</param>
/// <returns>是否有效</returns>
public static bool IsValidModel(string modelName)
{
return Models.ContainsKey(modelName);
}
}
/// <summary>
/// 模型配置
/// </summary>
public class ModelConfig
{
/// <summary>
/// 图标类名
/// </summary>
public string? Icon { get; set; }
/// <summary>
/// 模型提示正则表达式
/// </summary>
public Regex? ModelHint { get; set; }
/// <summary>
/// 显示标题
/// </summary>
public string? Title { get; set; }
}
}

314
LTEMvcApp/Models/LogState.cs

@ -0,0 +1,314 @@
namespace LTEMvcApp.Models
{
/// <summary>
/// 日志状态管理类
/// </summary>
public class LogState
{
/// <summary>
/// 日志列表
/// </summary>
public List<LTELog> Logs { get; set; } = new();
/// <summary>
/// 客户端列表
/// </summary>
public List<LTEClient> ClientList { get; set; } = new();
/// <summary>
/// 新日志列表
/// </summary>
public List<LTELog> NewLogs { get; set; } = new();
/// <summary>
/// 是否重置元数据
/// </summary>
public bool ResetMetadata { get; set; } = false;
/// <summary>
/// 检测到的最小日志级别
/// </summary>
public int MinLevelDetected { get; set; } = 10;
/// <summary>
/// 是否启用过滤器
/// </summary>
public bool Filter { get; set; } = false;
/// <summary>
/// 是否更新网格
/// </summary>
public bool UpdateGrid { get; set; } = false;
/// <summary>
/// 是否跟随最新日志
/// </summary>
public bool Follow { get; set; } = false;
/// <summary>
/// 过滤器锁定状态
/// </summary>
public string FilterLock { get; set; } = string.Empty;
/// <summary>
/// 日志总数
/// </summary>
public int LogCount => Logs.Count;
/// <summary>
/// 客户端总数
/// </summary>
public int ClientCount => ClientList.Count;
/// <summary>
/// 新日志数量
/// </summary>
public int NewLogCount => NewLogs.Count;
/// <summary>
/// 启用的客户端数量
/// </summary>
public int EnabledClientCount => ClientList.Count(c => c.State == ClientState.Connected || c.State == ClientState.Start);
/// <summary>
/// 添加日志
/// </summary>
/// <param name="log">日志对象</param>
public void AddLog(LTELog log)
{
Logs.Add(log);
NewLogs.Add(log);
}
/// <summary>
/// 添加客户端
/// </summary>
/// <param name="client">客户端对象</param>
public void AddClient(LTEClient client)
{
ClientList.Add(client);
}
/// <summary>
/// 移除客户端
/// </summary>
/// <param name="client">客户端对象</param>
/// <returns>是否成功移除</returns>
public bool RemoveClient(LTEClient client)
{
var removed = ClientList.Remove(client);
if (removed)
{
// 移除该客户端的日志
Logs.RemoveAll(log => log.Client == client);
NewLogs.RemoveAll(log => log.Client == client);
}
return removed;
}
/// <summary>
/// 清除所有日志
/// </summary>
public void ClearLogs()
{
Logs.Clear();
NewLogs.Clear();
ResetMetadata = true;
UpdateGrid = true;
}
/// <summary>
/// 清除新日志
/// </summary>
public void ClearNewLogs()
{
NewLogs.Clear();
}
/// <summary>
/// 重置状态
/// </summary>
public void Reset()
{
Logs.Clear();
NewLogs.Clear();
ResetMetadata = true;
MinLevelDetected = 10;
Filter = false;
UpdateGrid = false;
Follow = false;
FilterLock = string.Empty;
}
/// <summary>
/// 获取指定客户端的日志
/// </summary>
/// <param name="client">客户端对象</param>
/// <returns>日志列表</returns>
public List<LTELog> GetLogsByClient(LTEClient client)
{
return Logs.Where(log => log.Client == client).ToList();
}
/// <summary>
/// 获取启用的客户端
/// </summary>
/// <returns>启用的客户端列表</returns>
public List<LTEClient> GetEnabledClients()
{
return ClientList.Where(c => c.State == ClientState.Connected || c.State == ClientState.Start).ToList();
}
/// <summary>
/// 获取错误状态的客户端
/// </summary>
/// <returns>错误状态的客户端列表</returns>
public List<LTEClient> GetErrorClients()
{
return ClientList.Where(c => c.State == ClientState.Error).ToList();
}
/// <summary>
/// 获取连接状态的客户端
/// </summary>
/// <returns>连接状态的客户端列表</returns>
public List<LTEClient> GetConnectedClients()
{
return ClientList.Where(c => c.State == ClientState.Connected).ToList();
}
/// <summary>
/// 检查是否有实时客户端
/// </summary>
/// <returns>是否有实时客户端</returns>
public bool HasRealTimeClients()
{
return ClientList.Any(c => c.State == ClientState.Connected);
}
/// <summary>
/// 获取日志统计信息
/// </summary>
/// <returns>统计信息</returns>
public LogStatistics GetStatistics()
{
return new LogStatistics
{
TotalLogs = LogCount,
NewLogs = NewLogCount,
TotalClients = ClientCount,
EnabledClients = EnabledClientCount,
ConnectedClients = GetConnectedClients().Count,
ErrorClients = GetErrorClients().Count,
MinLevel = MinLevelDetected,
HasRealTime = HasRealTimeClients()
};
}
/// <summary>
/// 更新最小日志级别
/// </summary>
/// <param name="level">日志级别</param>
public void UpdateMinLevel(int level)
{
if (level < MinLevelDetected)
{
MinLevelDetected = level;
}
}
/// <summary>
/// 设置过滤器状态
/// </summary>
/// <param name="enabled">是否启用</param>
public void SetFilter(bool enabled)
{
Filter = enabled;
if (enabled)
{
UpdateGrid = true;
}
}
/// <summary>
/// 锁定过滤器
/// </summary>
/// <param name="lockName">锁定名称</param>
public void LockFilter(string lockName)
{
FilterLock = lockName;
}
/// <summary>
/// 解锁过滤器
/// </summary>
public void UnlockFilter()
{
FilterLock = string.Empty;
}
/// <summary>
/// 检查过滤器是否被锁定
/// </summary>
/// <returns>是否被锁定</returns>
public bool IsFilterLocked()
{
return !string.IsNullOrEmpty(FilterLock);
}
}
/// <summary>
/// 日志统计信息
/// </summary>
public class LogStatistics
{
/// <summary>
/// 总日志数
/// </summary>
public int TotalLogs { get; set; }
/// <summary>
/// 新日志数
/// </summary>
public int NewLogs { get; set; }
/// <summary>
/// 总客户端数
/// </summary>
public int TotalClients { get; set; }
/// <summary>
/// 启用的客户端数
/// </summary>
public int EnabledClients { get; set; }
/// <summary>
/// 连接的客户端数
/// </summary>
public int ConnectedClients { get; set; }
/// <summary>
/// 错误的客户端数
/// </summary>
public int ErrorClients { get; set; }
/// <summary>
/// 最小日志级别
/// </summary>
public int MinLevel { get; set; }
/// <summary>
/// 是否有实时客户端
/// </summary>
public bool HasRealTime { get; set; }
/// <summary>
/// 获取统计信息字符串
/// </summary>
/// <returns>统计信息字符串</returns>
public override string ToString()
{
return $"日志: {TotalLogs} (新: {NewLogs}), 客户端: {EnabledClients}/{TotalClients} (连接: {ConnectedClients}, 错误: {ErrorClients}), 最小级别: {MinLevel}";
}
}
}

331
LTEMvcApp/Models/LogUtils.cs

@ -0,0 +1,331 @@
using System.Text.RegularExpressions;
namespace LTEMvcApp.Models
{
/// <summary>
/// 日志工具类
/// </summary>
public static class LogUtils
{
/// <summary>
/// 日志级别常量
/// </summary>
public static class LogLevels
{
public const int None = 0;
public const int Error = 1;
public const int Warn = 2;
public const int Info = 3;
public const int Debug = 4;
/// <summary>
/// 获取级别名称
/// </summary>
/// <param name="level">级别值</param>
/// <returns>级别名称</returns>
public static string GetLevelName(int level)
{
return level switch
{
None => "none",
Error => "error",
Warn => "warn",
Info => "info",
Debug => "debug",
_ => "unknown"
};
}
/// <summary>
/// 获取级别值
/// </summary>
/// <param name="levelName">级别名称</param>
/// <returns>级别值</returns>
public static int GetLevelValue(string levelName)
{
return levelName?.ToLower() switch
{
"none" => None,
"error" => Error,
"warn" => Warn,
"info" => Info,
"debug" => Debug,
_ => None
};
}
/// <summary>
/// 获取所有级别选项
/// </summary>
/// <returns>级别选项列表</returns>
public static List<LevelOption> GetLevelOptions()
{
return new List<LevelOption>
{
new() { Value = Error, Text = "Error" },
new() { Value = Warn, Text = "Warning" },
new() { Value = Info, Text = "Info" },
new() { Value = Debug, Text = "Debug" }
};
}
}
/// <summary>
/// 级别选项
/// </summary>
public class LevelOption
{
public int Value { get; set; }
public string Text { get; set; } = string.Empty;
}
/// <summary>
/// 时间戳转换为时间字符串
/// </summary>
/// <param name="timestamp">时间戳(毫秒)</param>
/// <param name="full">是否显示完整日期</param>
/// <returns>时间字符串</returns>
public static string TimestampToTime(long timestamp, bool full = false)
{
// 持续时间 (< 2000-01-01 00:00:00)
if (timestamp < 946681200000)
{
return TimestampToDuration(timestamp);
}
var dateTime = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime;
if (full)
{
return dateTime.ToString("yyyy-MM-dd HH:mm:ss.fff");
}
return dateTime.ToString("HH:mm:ss.fff");
}
/// <summary>
/// 时间戳转换为持续时间字符串
/// </summary>
/// <param name="timestamp">时间戳(毫秒)</param>
/// <returns>持续时间字符串</returns>
public static string TimestampToDuration(long timestamp)
{
var prefix = timestamp < 0 ? "-" : "";
var absTimestamp = Math.Abs(timestamp);
var timeSpan = TimeSpan.FromMilliseconds(absTimestamp);
return $"{prefix}{timeSpan.Hours:D2}:{timeSpan.Minutes:D2}:{timeSpan.Seconds:D2}.{timeSpan.Milliseconds:D3}";
}
/// <summary>
/// 时间字符串转换为时间戳
/// </summary>
/// <param name="timeString">时间字符串</param>
/// <returns>时间戳,转换失败返回null</returns>
public static long? TimeStringToTimestamp(string timeString)
{
if (string.IsNullOrEmpty(timeString))
return null;
// 匹配格式: HH:mm:ss.fff 或 mm:ss.fff 或 ss.fff
var match = Regex.Match(timeString, @"^(((\d+):)?(\d+):)?(\d+)(\.(\d+))?$");
if (!match.Success)
return null;
var hours = int.Parse(match.Groups[3].Success ? match.Groups[3].Value : "0");
var minutes = int.Parse(match.Groups[4].Success ? match.Groups[4].Value : "0");
var seconds = int.Parse(match.Groups[5].Value);
var milliseconds = match.Groups[7].Success ? int.Parse(match.Groups[7].Value.PadRight(3, '0')) : 0;
return (hours * 3600000L) + (minutes * 60000L) + (seconds * 1000L) + milliseconds;
}
/// <summary>
/// 获取日期偏移量
/// </summary>
/// <param name="timestamp">时间戳</param>
/// <returns>日期偏移量</returns>
public static long GetDayOffset(long timestamp)
{
if (timestamp < 946681200000)
return 0;
var dateTime = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime;
var dayStart = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, 0, 0, 0, DateTimeKind.Utc);
return new DateTimeOffset(dayStart).ToUnixTimeMilliseconds();
}
/// <summary>
/// 字符串转ID
/// </summary>
/// <param name="str">字符串</param>
/// <returns>ID值</returns>
public static int StringToId(string str)
{
if (string.IsNullOrEmpty(str))
return 0;
var hash = 5381;
foreach (var c in str)
{
hash = ((hash << 5) + hash) ^ c;
}
return hash;
}
/// <summary>
/// 创建正则表达式
/// </summary>
/// <param name="pattern">模式</param>
/// <param name="ignoreCase">是否忽略大小写</param>
/// <returns>正则表达式,创建失败返回null</returns>
public static Regex? CreateRegex(string pattern, bool ignoreCase = true)
{
if (string.IsNullOrEmpty(pattern))
return null;
try
{
var options = ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None;
// 检查是否是正则表达式格式 /pattern/flags
var regexMatch = Regex.Match(pattern, @"^/(.+)/([gi]*)$");
if (regexMatch.Success)
{
var regexPattern = regexMatch.Groups[1].Value;
var flags = regexMatch.Groups[2].Value;
if (flags.Contains('i'))
options |= RegexOptions.IgnoreCase;
if (flags.Contains('g'))
options |= RegexOptions.Multiline;
return new Regex(regexPattern, options);
}
// 普通文本搜索,添加转义
var escapedPattern = Regex.Escape(pattern);
return new Regex(escapedPattern, options);
}
catch
{
return null;
}
}
/// <summary>
/// 高亮搜索文本
/// </summary>
/// <param name="text">原文本</param>
/// <param name="searchPattern">搜索模式</param>
/// <returns>高亮后的文本</returns>
public static string HighlightSearchText(string text, string searchPattern)
{
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(searchPattern))
return text;
var regex = CreateRegex(searchPattern);
if (regex == null)
return text;
return regex.Replace(text, "<highlight>$&</highlight>");
}
/// <summary>
/// 格式化文件大小
/// </summary>
/// <param name="bytes">字节数</param>
/// <returns>格式化后的字符串</returns>
public static string FormatFileSize(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
/// <summary>
/// 解析大小字符串
/// </summary>
/// <param name="sizeString">大小字符串</param>
/// <returns>字节数</returns>
public static long ParseSizeString(string sizeString)
{
if (string.IsNullOrEmpty(sizeString))
return 0;
var match = Regex.Match(sizeString, @"^(\d+)([KMG])?$", RegexOptions.IgnoreCase);
if (!match.Success)
return 0;
var size = long.Parse(match.Groups[1].Value);
var unit = match.Groups[2].Value.ToUpper();
return unit switch
{
"K" => size * 1024,
"M" => size * 1024 * 1024,
"G" => size * 1024 * 1024 * 1024,
_ => size
};
}
/// <summary>
/// 获取颜色亮度
/// </summary>
/// <param name="color">颜色代码</param>
/// <returns>亮度值 (0-1)</returns>
public static double GetColorBrightness(string color)
{
var match = Regex.Match(color, @"#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})");
if (!match.Success)
return 1;
var r = int.Parse(match.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
var g = int.Parse(match.Groups[2].Value, System.Globalization.NumberStyles.HexNumber);
var b = int.Parse(match.Groups[3].Value, System.Globalization.NumberStyles.HexNumber);
return (r * r * 0.299 + g * g * 0.587 + b * b * 0.114) / (255.0 * 255.0);
}
/// <summary>
/// 获取文本颜色
/// </summary>
/// <param name="backgroundColor">背景颜色</param>
/// <returns>文本颜色</returns>
public static string GetTextColor(string backgroundColor)
{
var brightness = GetColorBrightness(backgroundColor);
return brightness >= 0.15 ? "black" : "white";
}
/// <summary>
/// 生成唯一ID
/// </summary>
/// <returns>唯一ID</returns>
public static string GenerateUniqueId()
{
return Guid.NewGuid().ToString("N");
}
/// <summary>
/// 安全地执行操作
/// </summary>
/// <param name="action">要执行的操作</param>
/// <param name="defaultValue">默认值</param>
/// <returns>操作结果或默认值</returns>
public static T SafeExecute<T>(Func<T> action, T defaultValue)
{
try
{
return action();
}
catch
{
return defaultValue;
}
}
}
}

2
LTEMvcApp/Program.cs

@ -9,10 +9,8 @@ builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<WebSocketManagerService>(sp =>
new WebSocketManagerService(
sp.GetRequiredService<ILogger<WebSocketManagerService>>(),
sp.GetRequiredService<LogParserService>(),
sp
));
builder.Services.AddSingleton<LogParserService>();
var app = builder.Build();

3
LTEMvcApp/Services/LTEClientWebSocket.cs

@ -35,7 +35,6 @@ namespace LTEMvcApp.Services
private int _messageId;
private int _logGetId;
private bool _disposed;
private LogParserService logParser = new LogParserService();
private readonly ILogger<LTEClientWebSocket> _logger;
private const int ServerMessageCacheLimit = 1000;
private readonly ConcurrentQueue<string> _sentMessages = new ConcurrentQueue<string>();
@ -743,7 +742,7 @@ namespace LTEMvcApp.Services
logList.Add(log);
}
}
logParser.ParseLogList(_client, logList, true);
_client.ParseLogList(logList, true);
//_client.ParseLogList(logList, true);
LogsReceived?.Invoke(this, logList);
}

638
LTEMvcApp/Services/LogParserService.cs

@ -1,638 +0,0 @@
using LTEMvcApp.Models;
using Newtonsoft.Json;
using System.Text.RegularExpressions;
namespace LTEMvcApp.Services;
/// <summary>
/// LTE日志解析服务 - 实现JavaScript _logListParse方法的核心功能
/// </summary>
public class LogParserService
{
#region 常量定义
/// <summary>
/// HFN回绕阈值
/// </summary>
private const int HFN_WRAP_THRESHOLD = 512;
/// <summary>
/// 最大日志数量
/// </summary>
private const int LOGS_MAX = 2000000;
#endregion
#region 正则表达式
/// <summary>
/// PHY层日志正则表达式
/// </summary>
private static readonly Regex RegExpPhy = new(@"^([a-f0-9\-]+)\s+([a-f0-9\-]+)\s+([\d\.\-]+) (\w+): (.+)", RegexOptions.Compiled);
/// <summary>
/// 信息1正则表达式
/// </summary>
private static readonly Regex RegExpInfo1 = new(@"^([\w\-]+): (.+)", RegexOptions.Compiled);
/// <summary>
/// 信息2正则表达式
/// </summary>
private static readonly Regex RegExpInfo2 = new(@"^([\w]+) (.+)", RegexOptions.Compiled);
/// <summary>
/// IP日志正则表达式
/// </summary>
private static readonly Regex RegExpIP = new(@"^(len=\d+)\s+(\S+)\s+(.*)", RegexOptions.Compiled);
/// <summary>
/// IPsec日志正则表达式
/// </summary>
private static readonly Regex RegExpIPsec = new(@"^len=(\d+)\s+(.*)", RegexOptions.Compiled);
/// <summary>
/// SDU长度正则表达式
/// </summary>
private static readonly Regex RegExpSDULen = new(@"SDU_len=(\d+)", RegexOptions.Compiled);
/// <summary>
/// SIP日志正则表达式
/// </summary>
private static readonly Regex RegExpSIP = new(@"^([:\.\[\]\da-f]+)\s+(\S+) (.+)", RegexOptions.Compiled);
/// <summary>
/// 媒体请求正则表达式
/// </summary>
private static readonly Regex RegExpMediaReq = new(@"^(\S+) (.+)", RegexOptions.Compiled);
/// <summary>
/// 信号记录正则表达式
/// </summary>
private static readonly Regex RegExpSignalRecord = new(@"Link:\s([\w\d]+)@(\d+)", RegexOptions.Compiled);
/// <summary>
/// 小区ID正则表达式
/// </summary>
private static readonly Regex RegExpCellID = new(@"^([a-f0-9\-]+) (.+)", RegexOptions.Compiled);
/// <summary>
/// RRC UE ID正则表达式
/// </summary>
private static readonly Regex RegExpRRC_UE_ID = new(@"Changing UE_ID to 0x(\d+)", RegexOptions.Compiled);
/// <summary>
/// RRC TMSI正则表达式
/// </summary>
private static readonly Regex RegExpRRC_TMSI = new(@"(5G|m)-TMSI '([\dA-F]+)'H", RegexOptions.Compiled);
/// <summary>
/// RRC新ID正则表达式
/// </summary>
private static readonly Regex RegExpRRC_NEW_ID = new(@"newUE-Identity (['\dA-FH]+)", RegexOptions.Compiled);
/// <summary>
/// RRC CRNTI正则表达式
/// </summary>
private static readonly Regex RegExpRRC_CRNTI = new(@"c-RNTI '([\dA-F]+)'H", RegexOptions.Compiled);
/// <summary>
/// NAS TMSI正则表达式
/// </summary>
private static readonly Regex RegExpNAS_TMSI = new(@"m-TMSI = 0x([\da-f]+)", RegexOptions.Compiled);
/// <summary>
/// NAS 5G TMSI正则表达式
/// </summary>
private static readonly Regex RegExpNAS_5GTMSI = new(@"5G-TMSI = 0x([\da-f]+)", RegexOptions.Compiled);
/// <summary>
/// RRC频段组合正则表达式
/// </summary>
private static readonly Regex RegExpRRC_BC = new(@"(EUTRA|MRDC|NR|NRDC) band combinations", RegexOptions.Compiled);
/// <summary>
/// PDCCH正则表达式
/// </summary>
private static readonly Regex RegExpPDCCH = new(@"^\s*(.+)=(\d+)$", RegexOptions.Compiled);
/// <summary>
/// S1/NGAP正则表达式
/// </summary>
private static readonly Regex RegExpS1NGAP = new(@"^([\da-f\-]+)\s+([\da-f\-]+) (([^\s]+) .+)", RegexOptions.Compiled);
/// <summary>
/// 十六进制转储正则表达式
/// </summary>
private static readonly Regex RegExpHexDump = new(@"^[\da-f]+:(\s+[\da-f]{2}){1,16}\s+.{1,16}$", RegexOptions.Compiled);
#endregion
#region 私有字段
/// <summary>
/// 字符串到ID的映射缓存
/// </summary>
private readonly Dictionary<string, int> _stringToIdCache = new();
/// <summary>
/// ID到字符串的映射缓存
/// </summary>
private readonly Dictionary<int, string> _idToStringCache = new();
/// <summary>
/// 字符串ID计数器
/// </summary>
private int _stringIdCounter = 0;
#endregion
#region 公共方法
/// <summary>
/// 解析日志列表 - 对应JavaScript的_logListParse方法
/// </summary>
/// <param name="client">LTE客户端</param>
/// <param name="logs">日志列表</param>
/// <param name="isWebSocket">是否为WebSocket消息</param>
public void ParseLogList(LTEClient client, List<LTELog> logs, bool isWebSocket = false)
{
var length = logs.Count;
for (int i = 0; i < length; i++)
{
var log = logs[i];
log.Message = log.Message + "";
client.AddLog(log);
// 解析消息
switch (log.Layer)
{
case "PHY":
if (!ParsePhyLog(client, log, log.Message, isWebSocket)) continue;
ProcessPhyLog(client, log);
break;
case "RRC":
ParseCellId(client, log, isWebSocket);
var rrcUeIdMatch = RegExpRRC_UE_ID.Match(log.Message);
if (rrcUeIdMatch.Success)
{
SetSameUe(client, log, int.Parse(rrcUeIdMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber));
continue;
}
var infoMatch = RegExpInfo1.Match(log.Message);
if (infoMatch.Success)
{
if (!SetLogInfo(client, log, infoMatch.Groups[1].Value)) continue;
log.Message = infoMatch.Groups[2].Value;
ProcessRrcLog(client, log);
}
var bcMatch = RegExpRRC_BC.Match(log.Message);
if (bcMatch.Success)
{
try
{
var data = log.GetDataString();
var jsonData = JsonConvert.DeserializeObject<object>(data);
// 处理频段组合信息
}
catch
{
// 忽略JSON解析错误
}
}
break;
case "NAS":
ParseCellId(client, log, isWebSocket);
var nasTmsiMatch = RegExpNAS_TMSI.Match(log.Message);
if (nasTmsiMatch.Success)
{
SetTmsi(client, log, nasTmsiMatch.Groups[1].Value);
continue;
}
var nas5gTmsiMatch = RegExpNAS_5GTMSI.Match(log.Message);
if (nas5gTmsiMatch.Success)
{
SetTmsi(client, log, nas5gTmsiMatch.Groups[1].Value);
continue;
}
var nasInfoMatch = RegExpInfo2.Match(log.Message);
if (nasInfoMatch.Success)
{
if (!SetLogInfo(client, log, nasInfoMatch.Groups[1].Value)) continue;
log.Message = nasInfoMatch.Groups[2].Value;
ProcessNasLog(client, log);
}
break;
case "MAC":
ParseCellId(client, log, isWebSocket);
ParseMacLog(client, log);
break;
case "IP":
var ipMatch = RegExpIP.Match(log.Message);
if (ipMatch.Success)
{
var lenPart = ipMatch.Groups[1].Value;
log.IpLen = int.Parse(lenPart.Split('=')[1]);
if (!SetLogInfo(client, log, ipMatch.Groups[2].Value)) continue;
log.Message = ipMatch.Groups[3].Value;
client.HasData = true;
}
break;
case "GTPU":
var sduLenMatch = RegExpSDULen.Match(log.Message);
if (sduLenMatch.Success)
{
log.SduLen = int.Parse(sduLenMatch.Groups[1].Value);
client.HasData = true;
}
break;
case "S1AP":
case "NGAP":
var s1ngMatch = RegExpS1NGAP.Match(log.Message);
if (s1ngMatch.Success)
{
log.Message = s1ngMatch.Groups[3].Value;
var coreId = int.TryParse(s1ngMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber, null, out var core) ? core : (int?)null;
var ranId = int.TryParse(s1ngMatch.Groups[2].Value, System.Globalization.NumberStyles.HexNumber, null, out var ran) ? ran : (int?)null;
log.LinkIds = new LinkIds { Core = coreId, Ran = ranId };
}
break;
case "SIP":
var sipMatch = RegExpSIP.Match(log.Message);
if (sipMatch.Success)
{
if (!SetLogInfo(client, log, sipMatch.Groups[2].Value)) continue;
log.Message = sipMatch.Groups[3].Value;
}
break;
case "MEDIA":
var mediaMatch = RegExpMediaReq.Match(log.Message);
if (mediaMatch.Success)
{
if (!SetLogInfo(client, log, mediaMatch.Groups[1].Value)) continue;
log.Message = mediaMatch.Groups[2].Value;
}
break;
case "IPsec":
var ipsecMatch = RegExpIPsec.Match(log.Message);
if (ipsecMatch.Success)
{
log.IpLen = int.Parse(ipsecMatch.Groups[1].Value);
log.Message = ipsecMatch.Groups[2].Value;
}
break;
default:
break;
}
// 处理提示信息
ProcessHints(client, log);
}
// 限制日志数量
if (client.LogCount > LOGS_MAX)
{
var excess = client.LogCount - LOGS_MAX;
client.Logs.RemoveRange(0, excess);
}
}
#endregion
#region 私有方法
/// <summary>
/// 解析PHY日志
/// </summary>
private bool ParsePhyLog(LTEClient client, LTELog log, string message, bool isWebSocket)
{
if (!isWebSocket)
{
var phyMatch = RegExpPhy.Match(message);
if (!phyMatch.Success)
{
Console.WriteLine($"Bad PHY log: {log}");
return false;
}
log.Cell = int.Parse(phyMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
log.Rnti = int.Parse(phyMatch.Groups[2].Value, System.Globalization.NumberStyles.HexNumber);
log.Channel = phyMatch.Groups[4].Value;
log.Message = phyMatch.Groups[5].Value;
}
client.HasPhy = true;
client.HasCell = true;
client.HasRnti = true;
// 解析PHY参数
var lines = log.GetData();
foreach (var line in lines)
{
var parts = line.Split('=');
if (parts.Length == 2)
{
ParsePhyParameter(client, log, parts[0].Trim(), parts[1].Trim());
}
}
return true;
}
/// <summary>
/// 解析PHY参数
/// </summary>
private void ParsePhyParameter(LTEClient client, LTELog log, string param, string value)
{
switch (param.ToLower())
{
case "frame":
if (int.TryParse(value, out var frame))
{
log.Frame = frame;
client.HasPhy = true;
}
break;
case "subframe":
if (int.TryParse(value, out var subframe))
{
log.Subframe = subframe;
}
break;
case "slot":
if (int.TryParse(value, out var slot))
{
log.Slot = slot;
}
break;
case "symbol":
if (int.TryParse(value, out var symbol))
{
log.Symbol = symbol;
}
break;
case "ant":
if (int.TryParse(value, out var ant))
{
log.AntennaPort = ant;
}
break;
case "rb_start":
if (int.TryParse(value, out var rbStart))
{
log.RbStart = rbStart;
client.HasRb = true;
}
break;
case "rb_count":
if (int.TryParse(value, out var rbCount))
{
log.RbCount = rbCount;
client.HasRb = true;
}
break;
case "mcs":
if (int.TryParse(value, out var mcs))
{
log.Mcs = mcs;
}
break;
case "tbs":
if (int.TryParse(value, out var tbs))
{
log.Tbs = tbs;
}
break;
case "harq_id":
if (int.TryParse(value, out var harqId))
{
log.HarqId = harqId;
}
break;
case "harq_ndi":
if (bool.TryParse(value, out var harqNdi))
{
log.HarqNdi = harqNdi;
}
break;
case "harq_rv":
if (int.TryParse(value, out var harqRv))
{
log.HarqRedundancyVersion = harqRv;
}
break;
}
}
/// <summary>
/// 处理PHY日志
/// </summary>
private void ProcessPhyLog(LTEClient client, LTELog log)
{
// 处理HARQ信息
if (log.HarqId.HasValue)
{
ProcessHarqLog(client, log, log.HarqId.Value);
}
// 处理帧信息
if (log.Frame.HasValue)
{
var frame = log.Frame.Value;
var hfn = frame >> 10;
var sfn = frame & 0x3FF;
if (client.Frame.Timestamp == -1)
{
client.Frame.Timestamp = log.Timestamp;
client.Frame.Hfn = hfn;
client.Frame.Last = sfn;
}
else
{
var diff = sfn - client.Frame.Last;
if (diff < -HFN_WRAP_THRESHOLD)
{
client.Frame.Hfn++;
}
else if (diff > HFN_WRAP_THRESHOLD)
{
client.Frame.Hfn--;
}
client.Frame.Last = sfn;
}
}
}
/// <summary>
/// 处理HARQ日志
/// </summary>
private void ProcessHarqLog(LTEClient client, LTELog log, int harq)
{
if (log.Cell.HasValue && log.UeId.HasValue)
{
var cell = log.Cell.Value;
var ueId = log.UeId.Value;
var key = ueId * 32 + harq;
if (!client.LastHarq.ContainsKey(cell))
{
client.LastHarq[cell] = new Dictionary<int, LTELog>();
}
client.LastHarq[cell][key] = log;
}
}
/// <summary>
/// 解析小区ID
/// </summary>
private void ParseCellId(LTEClient client, LTELog log, bool isWebSocket)
{
if (!isWebSocket)
{
var cellMatch = RegExpCellID.Match(log.Message);
if (cellMatch.Success)
{
log.Cell = int.Parse(cellMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
log.Message = cellMatch.Groups[2].Value;
client.HasCell = true;
}
}
}
/// <summary>
/// 设置日志信息
/// </summary>
private bool SetLogInfo(LTEClient client, LTELog log, string info)
{
if (string.IsNullOrEmpty(info) || info == "?")
return false;
log.Info = StringToId(info);
return true;
}
/// <summary>
/// 字符串转ID
/// </summary>
private int StringToId(string str)
{
if (_stringToIdCache.TryGetValue(str, out var id))
{
return id;
}
id = ++_stringIdCounter;
_stringToIdCache[str] = id;
_idToStringCache[id] = str;
return id;
}
/// <summary>
/// ID转字符串
/// </summary>
private string IdToString(int id)
{
return _idToStringCache.TryGetValue(id, out var str) ? str : "";
}
/// <summary>
/// 设置相同UE
/// </summary>
private void SetSameUe(LTEClient client, LTELog log, int ueId)
{
log.UeId = ueId;
if (!client.UeList.ContainsKey(ueId))
{
client.UeList[ueId] = new UEInfo { UeId = ueId };
}
}
/// <summary>
/// 处理RRC日志
/// </summary>
private void ProcessRrcLog(LTEClient client, LTELog log)
{
// 处理RRC TMSI
var tmsiMatch = RegExpRRC_TMSI.Match(log.Message);
if (tmsiMatch.Success)
{
SetTmsi(client, log, tmsiMatch.Groups[2].Value);
}
// 处理RRC CRNTI
var crntiMatch = RegExpRRC_CRNTI.Match(log.Message);
if (crntiMatch.Success)
{
SetRnti(client, log, crntiMatch.Groups[1].Value);
}
}
/// <summary>
/// 处理NAS日志
/// </summary>
private void ProcessNasLog(LTEClient client, LTELog log)
{
// NAS日志处理逻辑
}
/// <summary>
/// 解析MAC日志
/// </summary>
private void ParseMacLog(LTEClient client, LTELog log)
{
// MAC日志解析逻辑
}
/// <summary>
/// 处理提示信息
/// </summary>
private void ProcessHints(LTEClient client, LTELog log, Dictionary<string, object>? config = null)
{
// 处理提示信息逻辑
}
/// <summary>
/// 设置TMSI
/// </summary>
private void SetTmsi(LTEClient client, LTELog log, string tmsi)
{
var tmsiId = int.Parse(tmsi, System.Globalization.NumberStyles.HexNumber);
if (log.UeId.HasValue)
{
client.TmsiToUeId[tmsiId] = log.UeId.Value;
}
}
/// <summary>
/// 设置RNTI
/// </summary>
private void SetRnti(LTEClient client, LTELog log, string rnti)
{
var rntiId = int.Parse(rnti, System.Globalization.NumberStyles.HexNumber);
if (log.UeId.HasValue)
{
client.RntiToUeId[rntiId] = log.UeId.Value;
}
}
#endregion
}

4
LTEMvcApp/Services/WebSocketManagerService.cs

@ -21,7 +21,6 @@ namespace LTEMvcApp.Services
private readonly ConcurrentDictionary<string, LTEClientWebSocket> _clients;
private readonly ConcurrentDictionary<string, ClientConfig> _configs;
private readonly LogParserService _logParser;
private readonly ILogger<WebSocketManagerService> _logger;
private readonly IServiceProvider _serviceProvider;
private ClientConfig _testClientConfig;
@ -60,11 +59,10 @@ namespace LTEMvcApp.Services
/// <summary>
/// 构造函数
/// </summary>
public WebSocketManagerService(ILogger<WebSocketManagerService> logger, LogParserService logParser, IServiceProvider serviceProvider)
public WebSocketManagerService(ILogger<WebSocketManagerService> logger, IServiceProvider serviceProvider)
{
_clients = new ConcurrentDictionary<string, LTEClientWebSocket>();
_configs = new ConcurrentDictionary<string, ClientConfig>();
_logParser = logParser;
_logger = logger;
_serviceProvider = serviceProvider;

Loading…
Cancel
Save