From 31b174bf13cf8447ae5441e7e1573dbc024ad147 Mon Sep 17 00:00:00 2001 From: hyh Date: Mon, 23 Jun 2025 11:48:08 +0800 Subject: [PATCH] 1 --- LTEMvcApp/Models/LTEClient.cs | 644 +++++++++++++++++- LTEMvcApp/Models/LogChannelConfig.cs | 121 ++++ LTEMvcApp/Models/LogConstants.cs | 254 +++++++ LTEMvcApp/Models/LogDirection.cs | 74 ++ LTEMvcApp/Models/LogEventManager.cs | 229 +++++++ LTEMvcApp/Models/LogFilterConfig.cs | 270 ++++++++ LTEMvcApp/Models/LogLayerConfig.cs | 164 ++++- LTEMvcApp/Models/LogModelConfig.cs | 121 ++++ LTEMvcApp/Models/LogState.cs | 314 +++++++++ LTEMvcApp/Models/LogUtils.cs | 331 +++++++++ LTEMvcApp/Program.cs | 2 - LTEMvcApp/Services/LTEClientWebSocket.cs | 3 +- LTEMvcApp/Services/LogParserService.cs | 638 ----------------- LTEMvcApp/Services/WebSocketManagerService.cs | 4 +- 14 files changed, 2518 insertions(+), 651 deletions(-) create mode 100644 LTEMvcApp/Models/LogChannelConfig.cs create mode 100644 LTEMvcApp/Models/LogConstants.cs create mode 100644 LTEMvcApp/Models/LogDirection.cs create mode 100644 LTEMvcApp/Models/LogEventManager.cs create mode 100644 LTEMvcApp/Models/LogFilterConfig.cs create mode 100644 LTEMvcApp/Models/LogModelConfig.cs create mode 100644 LTEMvcApp/Models/LogState.cs create mode 100644 LTEMvcApp/Models/LogUtils.cs delete mode 100644 LTEMvcApp/Services/LogParserService.cs diff --git a/LTEMvcApp/Models/LTEClient.cs b/LTEMvcApp/Models/LTEClient.cs index dc59e2e..c3a79bd 100644 --- a/LTEMvcApp/Models/LTEClient.cs +++ b/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; /// public class LTEClient { + #region 常量定义 + + /// + /// HFN回绕阈值 + /// + private const int HFN_WRAP_THRESHOLD = 512; + + /// + /// 最大日志数量 + /// + private const int LOGS_MAX = 2000000; + + #endregion + + #region 正则表达式 + + /// + /// PHY层日志正则表达式 + /// + private static readonly Regex RegExpPhy = new(@"^([a-f0-9\-]+)\s+([a-f0-9\-]+)\s+([\d\.\-]+) (\w+): (.+)", RegexOptions.Compiled); + + /// + /// 信息1正则表达式 + /// + private static readonly Regex RegExpInfo1 = new(@"^([\w\-]+): (.+)", RegexOptions.Compiled); + + /// + /// 信息2正则表达式 + /// + private static readonly Regex RegExpInfo2 = new(@"^([\w]+) (.+)", RegexOptions.Compiled); + + /// + /// IP日志正则表达式 + /// + private static readonly Regex RegExpIP = new(@"^(len=\d+)\s+(\S+)\s+(.*)", RegexOptions.Compiled); + + /// + /// IPsec日志正则表达式 + /// + private static readonly Regex RegExpIPsec = new(@"^len=(\d+)\s+(.*)", RegexOptions.Compiled); + + /// + /// SDU长度正则表达式 + /// + private static readonly Regex RegExpSDULen = new(@"SDU_len=(\d+)", RegexOptions.Compiled); + + /// + /// SIP日志正则表达式 + /// + private static readonly Regex RegExpSIP = new(@"^([:\.\[\]\da-f]+)\s+(\S+) (.+)", RegexOptions.Compiled); + + /// + /// 媒体请求正则表达式 + /// + private static readonly Regex RegExpMediaReq = new(@"^(\S+) (.+)", RegexOptions.Compiled); + + /// + /// 信号记录正则表达式 + /// + private static readonly Regex RegExpSignalRecord = new(@"Link:\s([\w\d]+)@(\d+)", RegexOptions.Compiled); + + /// + /// 小区ID正则表达式 + /// + private static readonly Regex RegExpCellID = new(@"^([a-f0-9\-]+) (.+)", RegexOptions.Compiled); + + /// + /// RRC UE ID正则表达式 + /// + private static readonly Regex RegExpRRC_UE_ID = new(@"Changing UE_ID to 0x(\d+)", RegexOptions.Compiled); + + /// + /// RRC TMSI正则表达式 + /// + private static readonly Regex RegExpRRC_TMSI = new(@"(5G|m)-TMSI '([\dA-F]+)'H", RegexOptions.Compiled); + + /// + /// RRC新ID正则表达式 + /// + private static readonly Regex RegExpRRC_NEW_ID = new(@"newUE-Identity (['\dA-FH]+)", RegexOptions.Compiled); + + /// + /// RRC CRNTI正则表达式 + /// + private static readonly Regex RegExpRRC_CRNTI = new(@"c-RNTI '([\dA-F]+)'H", RegexOptions.Compiled); + + /// + /// NAS TMSI正则表达式 + /// + private static readonly Regex RegExpNAS_TMSI = new(@"m-TMSI = 0x([\da-f]+)", RegexOptions.Compiled); + + /// + /// NAS 5G TMSI正则表达式 + /// + private static readonly Regex RegExpNAS_5GTMSI = new(@"5G-TMSI = 0x([\da-f]+)", RegexOptions.Compiled); + + /// + /// RRC频段组合正则表达式 + /// + private static readonly Regex RegExpRRC_BC = new(@"(EUTRA|MRDC|NR|NRDC) band combinations", RegexOptions.Compiled); + + /// + /// PDCCH正则表达式 + /// + private static readonly Regex RegExpPDCCH = new(@"^\s*(.+)=(\d+)$", RegexOptions.Compiled); + + /// + /// S1/NGAP正则表达式 + /// + private static readonly Regex RegExpS1NGAP = new(@"^([\da-f\-]+)\s+([\da-f\-]+) (([^\s]+) .+)", RegexOptions.Compiled); + + /// + /// 十六进制转储正则表达式 + /// + private static readonly Regex RegExpHexDump = new(@"^[\da-f]+:(\s+[\da-f]{2}){1,16}\s+.{1,16}$", RegexOptions.Compiled); + + #endregion + #region 基础属性 /// @@ -169,6 +287,25 @@ public class LTEClient #endregion + #region 解析器私有字段 + + /// + /// 字符串到ID的映射缓存 + /// + private readonly Dictionary _stringToIdCache = new(); + + /// + /// ID到字符串的映射缓存 + /// + private readonly Dictionary _idToStringCache = new(); + + /// + /// 字符串ID计数器 + /// + private int _stringIdCounter = 0; + + #endregion + #region 构造函数 public LTEClient(ClientConfig config) @@ -193,7 +330,7 @@ public class LTEClient #endregion - #region 公共方法 + #region 客户端控制方法 /// /// 启动客户端 @@ -219,6 +356,10 @@ public class LTEClient SetState(ClientState.Destroy); } + #endregion + + #region 日志管理方法 + /// /// 重置日志 /// @@ -257,6 +398,10 @@ public class LTEClient log.Id = GenerateLogId(); } + #endregion + + #region 工具方法 + /// /// 设置头信息 /// @@ -295,13 +440,497 @@ public class LTEClient /// 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; + } + + /// + /// ID转字符串 + /// + public string IdToString(int id) + { + return _idToStringCache.TryGetValue(id, out var str) ? str : ""; + } + + #endregion + + #region 日志解析方法 + + /// + /// 解析日志列表 - 对应JavaScript的_logListParse方法 + /// + /// 日志列表 + /// 是否为WebSocket消息 + public void ParseLogList(List 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(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层解析方法 + + /// + /// 解析PHY日志 + /// + 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; + } + + /// + /// 解析PHY参数 + /// + 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; + } + } + + /// + /// 处理PHY日志 + /// + 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; + } + } + } + + /// + /// 处理HARQ日志 + /// + 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(); + } + + LastHarq[cell][key] = log; + } + } + + #endregion + + #region 其他层解析方法 + + /// + /// 解析小区ID + /// + 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; + } + } + } + + /// + /// 设置日志信息 + /// + private bool SetLogInfo(LTELog log, string info) + { + if (string.IsNullOrEmpty(info) || info == "?") + return false; + + log.Info = StringToId(info); + return true; + } + + /// + /// 设置相同UE + /// + private void SetSameUe(LTELog log, int ueId) + { + log.UeId = ueId; + if (!UeList.ContainsKey(ueId)) + { + UeList[ueId] = new UEInfo { UeId = ueId }; + } + } + + /// + /// 处理RRC日志 + /// + 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); + } + } + + /// + /// 处理NAS日志 + /// + private void ProcessNasLog(LTELog log) + { + // NAS日志处理逻辑 + } + + /// + /// 解析MAC日志 + /// + private void ParseMacLog(LTELog log) + { + // MAC日志解析逻辑 + } + + /// + /// 处理提示信息 + /// + private void ProcessHints(LTELog log, Dictionary? config = null) + { + // 处理提示信息逻辑 + } + + /// + /// 设置TMSI + /// + 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; + } + } + + /// + /// 设置RNTI + /// + 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 私有辅助方法 /// /// 设置状态 @@ -339,6 +968,11 @@ public class LTEClient LastTimestamp = 0; TimestampOffset = 0; LastCell = null; + + // 重置解析器缓存 + _stringToIdCache.Clear(); + _idToStringCache.Clear(); + _stringIdCounter = 0; } /// diff --git a/LTEMvcApp/Models/LogChannelConfig.cs b/LTEMvcApp/Models/LogChannelConfig.cs new file mode 100644 index 0000000..4defd7c --- /dev/null +++ b/LTEMvcApp/Models/LogChannelConfig.cs @@ -0,0 +1,121 @@ +namespace LTEMvcApp.Models +{ + /// + /// 日志信道配置 + /// + public static class LogChannelConfig + { + /// + /// 信道定义列表 + /// + public static readonly List 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" } + }; + + /// + /// 信道定义字典(按名称索引) + /// + public static readonly Dictionary ChannelsByName = ChannelDefinitions + .ToDictionary(c => c.Name, c => c); + + /// + /// 信道定义字典(按ID索引) + /// + public static readonly Dictionary ChannelsById = ChannelDefinitions + .Select((c, i) => new { Channel = c, Index = i }) + .ToDictionary(x => x.Index, x => x.Channel); + + /// + /// 获取信道定义 + /// + /// 信道名称 + /// 信道定义 + public static ChannelDefinition? GetChannelByName(string name) + { + return ChannelsByName.TryGetValue(name, out var channel) ? channel : null; + } + + /// + /// 获取信道定义 + /// + /// 信道ID + /// 信道定义 + public static ChannelDefinition? GetChannelById(int id) + { + return ChannelsById.TryGetValue(id, out var channel) ? channel : null; + } + + /// + /// 获取所有信道名称 + /// + /// 信道名称列表 + public static List GetAllChannelNames() + { + return ChannelDefinitions.Select(c => c.Name).ToList(); + } + + /// + /// 验证信道名称是否有效 + /// + /// 信道名称 + /// 是否有效 + public static bool IsValidChannel(string name) + { + return ChannelsByName.ContainsKey(name); + } + + /// + /// 获取信道ID + /// + /// 信道名称 + /// 信道ID,如果不存在返回-1 + public static int GetChannelId(string name) + { + for (int i = 0; i < ChannelDefinitions.Count; i++) + { + if (ChannelDefinitions[i].Name == name) + return i; + } + return -1; + } + } + + /// + /// 信道定义 + /// + public class ChannelDefinition + { + /// + /// 信道名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 颜色代码 + /// + public string Color { get; set; } = "#000000"; + + /// + /// 信道ID + /// + public int Id { get; set; } + + /// + /// 信道索引 + /// + public int Index { get; set; } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Models/LogConstants.cs b/LTEMvcApp/Models/LogConstants.cs new file mode 100644 index 0000000..a7e3e08 --- /dev/null +++ b/LTEMvcApp/Models/LogConstants.cs @@ -0,0 +1,254 @@ +namespace LTEMvcApp.Models +{ + /// + /// 日志常量 + /// + public static class LogConstants + { + #region 全局常量 + + /// + /// 最大日志数量 + /// + public const int LOGS_MAX = 2000000; + + /// + /// 日志范围 + /// + public static readonly List LOGS_RANGE = new(); + + /// + /// 日志信息 + /// + public static readonly List LOGS_INFO = new(); + + /// + /// 日志层 + /// + public static readonly List LOGS_LAYERS = new(); + + /// + /// 导出时间格式 + /// + public static string EXPORT_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss.fff"; + + /// + /// 是否启用内联缓存 + /// + public static bool INLINE_CACHE_ENABLED = false; + + #endregion + + #region 应用模式 + + /// + /// 应用模式 + /// + public static class AppMode + { + public const string Web = "web"; + public const string App = "app"; + } + + /// + /// 导出模式 + /// + public static class ExportMode + { + public const string Zip = "zip"; + public const string Text = "text"; + } + + #endregion + + #region 字体和样式 + + /// + /// 默认字体 + /// + public const string DEFAULT_FONT = "13px helvetica, arial, verdana, sans-serif"; + + /// + /// 默认颜色 + /// + public const string DEFAULT_COLOR = "#000000"; + + #endregion + + #region 时间相关 + + /// + /// 时间戳基准(2000-01-01 00:00:00) + /// + public const long TIMESTAMP_BASE = 946681200000; + + /// + /// 默认时间原点 + /// + public const string DEFAULT_TIME_ORIGIN = "00:00:00.000"; + + #endregion + + #region 客户端相关 + + /// + /// 默认重连延迟(毫秒) + /// + public const int DEFAULT_RECONNECT_DELAY = 2500; + + /// + /// 默认重连延迟(毫秒) + /// + public const int DEFAULT_RECONNECT_DELAY_APP = 5000; + + /// + /// HFN回绕阈值 + /// + public const int HFN_WRAP_THRESHOLD = 512; + + #endregion + + #region 日志级别 + + /// + /// 日志级别名称 + /// + public static readonly string[] LOG_LEVEL_NAMES = { "none", "error", "warn", "info", "debug" }; + + /// + /// 日志方法名称 + /// + public static readonly string[] LOG_METHODS = { "error", "warn", "info", "debug", "prof" }; + + #endregion + + #region 性能相关 + + /// + /// 大日志列表阈值 + /// + public const int LARGE_LOG_THRESHOLD = 500000; + + /// + /// 小日志列表阈值 + /// + public const int SMALL_LOG_THRESHOLD = 50000; + + /// + /// 日志模块列表最大长度 + /// + public const int LOG_MODULE_LIST_MAX = 20; + + #endregion + + #region 文件相关 + + /// + /// 支持的文件类型 + /// + public static class FileTypes + { + public const string Binary = "bin"; + public const string Base64 = "base64"; + } + + /// + /// 文件大小单位 + /// + public static class FileSizeUnits + { + public const string Kilobyte = "K"; + public const string Megabyte = "M"; + public const string Gigabyte = "G"; + } + + #endregion + + #region 正则表达式 + + /// + /// 参数解析正则表达式 + /// + public const string PARAMS_REGEX = @"\s*([^=]+)=([^,\s]+)"; + + #endregion + + #region 事件相关 + + /// + /// 事件类型 + /// + public static class EventTypes + { + public const string NewLogs = "newLogs"; + public const string SelectLog = "selectLog"; + public const string All = "*"; + } + + #endregion + + #region 浏览器相关 + + /// + /// 鼠标滚轮事件 + /// + public static class MouseWheelEvents + { + public const string Firefox = "DOMMouseScroll"; + public const string Other = "mousewheel"; + } + + #endregion + + #region 工具方法 + + /// + /// 获取日志级别名称 + /// + /// 级别值 + /// 级别名称 + public static string GetLogLevelName(int level) + { + if (level >= 0 && level < LOG_LEVEL_NAMES.Length) + return LOG_LEVEL_NAMES[level]; + return "unknown"; + } + + /// + /// 获取日志级别值 + /// + /// 级别名称 + /// 级别值 + 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; + } + + /// + /// 检查是否为持续时间戳 + /// + /// 时间戳 + /// 是否为持续时间 + public static bool IsDurationTimestamp(long timestamp) + { + return timestamp < TIMESTAMP_BASE; + } + + /// + /// 获取默认重连延迟 + /// + /// 应用模式 + /// 重连延迟 + public static int GetDefaultReconnectDelay(string mode) + { + return mode == AppMode.App ? DEFAULT_RECONNECT_DELAY_APP : DEFAULT_RECONNECT_DELAY; + } + + #endregion + } +} \ No newline at end of file diff --git a/LTEMvcApp/Models/LogDirection.cs b/LTEMvcApp/Models/LogDirection.cs new file mode 100644 index 0000000..ebf4e3f --- /dev/null +++ b/LTEMvcApp/Models/LogDirection.cs @@ -0,0 +1,74 @@ +namespace LTEMvcApp.Models +{ + /// + /// 日志传输方向常量 + /// + public static class LogDirection + { + /// + /// 无方向 + /// + public const int None = 0; + + /// + /// 上行传输 + /// + public const int UL = 1; + + /// + /// 下行传输 + /// + public const int DL = 2; + + /// + /// 从某处传输 + /// + public const int From = 3; + + /// + /// 传输到某处 + /// + public const int To = 4; + + /// + /// 获取方向名称 + /// + /// 方向值 + /// 方向名称 + public static string GetDirectionName(int direction) + { + return direction switch + { + None => "-", + UL => "UL", + DL => "DL", + From => "FROM", + To => "TO", + _ => "UNKNOWN" + }; + } + + /// + /// 获取所有方向选项 + /// + /// 方向选项列表 + public static List GetDirectionOptions() + { + return new List + { + new() { Value = None, Text = "-" }, + new() { Value = DL, Text = "DL" }, + new() { Value = UL, Text = "UL" } + }; + } + } + + /// + /// 方向选项 + /// + public class DirectionOption + { + public int Value { get; set; } + public string Text { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/LTEMvcApp/Models/LogEventManager.cs b/LTEMvcApp/Models/LogEventManager.cs new file mode 100644 index 0000000..9370f41 --- /dev/null +++ b/LTEMvcApp/Models/LogEventManager.cs @@ -0,0 +1,229 @@ +namespace LTEMvcApp.Models +{ + /// + /// 日志事件管理器 + /// + public class LogEventManager + { + /// + /// 事件监听器字典 + /// + private readonly Dictionary _eventListeners = new(); + + /// + /// 事件监听器ID计数器 + /// + private int _eventListenerId = 0; + + /// + /// 注册事件监听器 + /// + /// 事件类型 + /// 回调函数 + /// 监听器ID + public int RegisterEventListener(string type, Action callback) + { + var id = _eventListenerId++; + _eventListeners[id] = new EventListener + { + Type = type, + Callback = callback + }; + return id; + } + + /// + /// 注销事件监听器 + /// + /// 监听器ID + public void UnregisterEventListener(int id) + { + _eventListeners.Remove(id); + } + + /// + /// 发送事件 + /// + /// 事件对象 + 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; + } + } + } + + /// + /// 发送新日志事件 + /// + /// 日志列表 + public void SendNewLogsEvent(List logs) + { + SendEvent(new LogEvent + { + Type = LogConstants.EventTypes.NewLogs, + Data = new { Logs = logs } + }); + } + + /// + /// 发送选择日志事件 + /// + /// 选中的日志 + public void SendSelectLogEvent(LTELog log) + { + SendEvent(new LogEvent + { + Type = LogConstants.EventTypes.SelectLog, + Data = new { Log = log } + }); + } + + /// + /// 清除所有事件监听器 + /// + public void ClearAllListeners() + { + _eventListeners.Clear(); + _eventListenerId = 0; + } + + /// + /// 获取监听器数量 + /// + /// 监听器数量 + public int GetListenerCount() + { + return _eventListeners.Count; + } + + /// + /// 获取指定类型的事件监听器数量 + /// + /// 事件类型 + /// 监听器数量 + public int GetListenerCountByType(string type) + { + return _eventListeners.Values.Count(l => l.Type == type || l.Type == LogConstants.EventTypes.All); + } + } + + /// + /// 事件监听器 + /// + public class EventListener + { + /// + /// 事件类型 + /// + public string Type { get; set; } = string.Empty; + + /// + /// 回调函数 + /// + public Action Callback { get; set; } = _ => { }; + } + + /// + /// 日志事件 + /// + public class LogEvent + { + /// + /// 事件类型 + /// + public string Type { get; set; } = string.Empty; + + /// + /// 事件数据 + /// + public object? Data { get; set; } + + /// + /// 时间戳 + /// + public DateTime Timestamp { get; set; } = DateTime.Now; + + /// + /// 事件源 + /// + public string? Source { get; set; } + } + + /// + /// 日志事件类型 + /// + public static class LogEventTypes + { + /// + /// 新日志事件 + /// + public const string NewLogs = "newLogs"; + + /// + /// 选择日志事件 + /// + public const string SelectLog = "selectLog"; + + /// + /// 过滤器更新事件 + /// + public const string FilterUpdate = "filterUpdate"; + + /// + /// 客户端状态变化事件 + /// + public const string ClientStateChange = "clientStateChange"; + + /// + /// 日志清除事件 + /// + public const string LogsCleared = "logsCleared"; + + /// + /// 错误事件 + /// + public const string Error = "error"; + + /// + /// 警告事件 + /// + public const string Warning = "warning"; + + /// + /// 信息事件 + /// + public const string Info = "info"; + + /// + /// 调试事件 + /// + public const string Debug = "debug"; + + /// + /// 所有事件 + /// + public const string All = "*"; + + /// + /// 获取所有事件类型 + /// + /// 事件类型列表 + public static List GetAllEventTypes() + { + return new List + { + NewLogs, SelectLog, FilterUpdate, ClientStateChange, LogsCleared, + Error, Warning, Info, Debug, All + }; + } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Models/LogFilterConfig.cs b/LTEMvcApp/Models/LogFilterConfig.cs new file mode 100644 index 0000000..d1ca598 --- /dev/null +++ b/LTEMvcApp/Models/LogFilterConfig.cs @@ -0,0 +1,270 @@ +namespace LTEMvcApp.Models +{ + /// + /// 日志过滤器配置 + /// + public class LogFilterConfig + { + /// + /// 全局UE ID列表 + /// + public List GlobalUeIds { get; set; } = new(); + + /// + /// 小区ID列表 + /// + public List CellIds { get; set; } = new(); + + /// + /// IMSI列表 + /// + public List ImsiList { get; set; } = new(); + + /// + /// IMEI列表 + /// + public List ImeiList { get; set; } = new(); + + /// + /// 信息类型列表 + /// + public List InfoTypes { get; set; } = new(); + + /// + /// 层类型列表 + /// + public List LayerTypes { get; set; } = new(); + + /// + /// 日志级别列表 + /// + public List LogLevels { get; set; } = new(); + + /// + /// 方向过滤器(false表示过滤掉,true表示保留) + /// + public bool[] DirectionFilter { get; set; } = { false, false, false }; + + /// + /// 开始时间戳 + /// + public long StartTime { get; set; } = 0; + + /// + /// 结束时间戳 + /// + public long EndTime { get; set; } = long.MaxValue; + + /// + /// 时间原点 + /// + public string TimeOrigin { get; set; } = "00:00:00.000"; + + /// + /// 是否分组UE ID + /// + public bool GroupUeId { get; set; } = true; + + /// + /// 搜索文本 + /// + public string? SearchText { get; set; } + + /// + /// 是否使用正则表达式搜索 + /// + public bool UseRegexSearch { get; set; } = false; + + /// + /// 是否忽略大小写 + /// + public bool IgnoreCase { get; set; } = true; + + /// + /// 是否只显示错误 + /// + public bool ShowErrorsOnly { get; set; } = false; + + /// + /// 是否只显示警告 + /// + public bool ShowWarningsOnly { get; set; } = false; + + /// + /// 客户端过滤器 + /// + public Dictionary ClientFilters { get; set; } = new(); + + /// + /// 是否启用过滤器 + /// + public bool IsEnabled { get; set; } = true; + + /// + /// 过滤器名称 + /// + public string? FilterName { get; set; } + + /// + /// 是否持久化 + /// + public bool IsPersistent { get; set; } = false; + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } = DateTime.Now; + + /// + /// 更新时间 + /// + public DateTime UpdatedAt { get; set; } = DateTime.Now; + + /// + /// 清除所有过滤器 + /// + 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; + } + + /// + /// 重置为默认值 + /// + public void ResetToDefault() + { + ClearAll(); + GroupUeId = true; + IsEnabled = true; + UseRegexSearch = false; + IgnoreCase = true; + ShowErrorsOnly = false; + ShowWarningsOnly = false; + } + + /// + /// 复制过滤器配置 + /// + /// 新的过滤器配置 + public LogFilterConfig Clone() + { + return new LogFilterConfig + { + GlobalUeIds = new List(GlobalUeIds), + CellIds = new List(CellIds), + ImsiList = new List(ImsiList), + ImeiList = new List(ImeiList), + InfoTypes = new List(InfoTypes), + LayerTypes = new List(LayerTypes), + LogLevels = new List(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(ClientFilters), + IsEnabled = IsEnabled, + FilterName = FilterName, + IsPersistent = IsPersistent, + CreatedAt = CreatedAt, + UpdatedAt = DateTime.Now + }; + } + + /// + /// 检查是否为空过滤器 + /// + /// 是否为空 + 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; + } + } + + /// + /// 过滤器选项 + /// + public class FilterOption + { + /// + /// 值 + /// + public object Value { get; set; } = string.Empty; + + /// + /// 显示文本 + /// + public string Text { get; set; } = string.Empty; + + /// + /// 样式 + /// + public string Style { get; set; } = string.Empty; + + /// + /// 是否被选中 + /// + public bool IsSelected { get; set; } = false; + } + + /// + /// 过滤器类型 + /// + 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"; + + /// + /// 获取所有过滤器类型 + /// + /// 过滤器类型列表 + public static List GetAllFilterTypes() + { + return new List + { + GlobalUeId, Cell, Imsi, Imei, Info, Layer, Level, Direction, Time, Search, Client + }; + } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Models/LogLayerConfig.cs b/LTEMvcApp/Models/LogLayerConfig.cs index 6786c37..9224c8d 100644 --- a/LTEMvcApp/Models/LogLayerConfig.cs +++ b/LTEMvcApp/Models/LogLayerConfig.cs @@ -27,6 +27,48 @@ namespace LTEMvcApp.Models /// 过滤器(用于兼容性) /// public string Filter { get; set; } = "warn"; + + /// + /// 颜色代码 + /// + public string Color { get; set; } = "#000000"; + + /// + /// 方向配置 + /// + public Dictionary Direction { get; set; } = new(); + + /// + /// 是否为EPC相关 + /// + public bool EPC { get; set; } = false; + + /// + /// 调试配置 + /// + public DebugConfig? Debug { get; set; } + + /// + /// 最大配置 + /// + public MaxConfig? Max { get; set; } + } + + /// + /// 调试配置 + /// + public class DebugConfig + { + public string Level { get; set; } = "debug"; + public int MaxSize { get; set; } = 1; + } + + /// + /// 最大配置 + /// + public class MaxConfig + { + public int MaxSize { get; set; } = 32; } /// @@ -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"; /// /// 获取所有日志层类型 /// 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 }; /// @@ -76,5 +157,86 @@ namespace LTEMvcApp.Models _ => "warn" }; } + + /// + /// 获取层配置 + /// + /// 层配置字典 + public static Dictionary GetLayerConfigs() + { + return new Dictionary + { + [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() { } } + }; + } + + /// + /// 获取层配置 + /// + /// 层名称 + /// 层配置 + public static LogLayerConfig? GetLayerConfig(string layerName) + { + var configs = GetLayerConfigs(); + return configs.TryGetValue(layerName, out var config) ? config : null; + } + + /// + /// 验证层名称是否有效 + /// + /// 层名称 + /// 是否有效 + public static bool IsValidLayer(string layerName) + { + return AllLayers.Contains(layerName); + } } } \ No newline at end of file diff --git a/LTEMvcApp/Models/LogModelConfig.cs b/LTEMvcApp/Models/LogModelConfig.cs new file mode 100644 index 0000000..8b79928 --- /dev/null +++ b/LTEMvcApp/Models/LogModelConfig.cs @@ -0,0 +1,121 @@ +using System.Text.RegularExpressions; + +namespace LTEMvcApp.Models +{ + /// + /// 日志模型配置 + /// + public static class LogModelConfig + { + /// + /// 模型列表(顺序重要) + /// + public static readonly string[] ModelList = + { + "RUE", "UE", "PROBE", "ENB", "N3IWF", "MBMSGW", "MME", "IMS", "LICENSE", "MONITOR" + }; + + /// + /// 模型配置字典 + /// + public static readonly Dictionary 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" } + }; + + /// + /// 获取模型配置 + /// + /// 模型名称 + /// 模型配置 + public static ModelConfig? GetModelConfig(string modelName) + { + return Models.TryGetValue(modelName, out var config) ? config : null; + } + + /// + /// 根据提示匹配模型 + /// + /// 提示文本 + /// 匹配的模型名称 + public static string? MatchModelByHint(string hint) + { + foreach (var kvp in Models) + { + if (kvp.Value.ModelHint?.IsMatch(hint) == true) + { + return kvp.Key; + } + } + return null; + } + + /// + /// 获取所有模型名称 + /// + /// 模型名称列表 + public static List GetAllModelNames() + { + return ModelList.ToList(); + } + + /// + /// 验证模型名称是否有效 + /// + /// 模型名称 + /// 是否有效 + public static bool IsValidModel(string modelName) + { + return Models.ContainsKey(modelName); + } + } + + /// + /// 模型配置 + /// + public class ModelConfig + { + /// + /// 图标类名 + /// + public string? Icon { get; set; } + + /// + /// 模型提示正则表达式 + /// + public Regex? ModelHint { get; set; } + + /// + /// 显示标题 + /// + public string? Title { get; set; } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Models/LogState.cs b/LTEMvcApp/Models/LogState.cs new file mode 100644 index 0000000..8c14fe3 --- /dev/null +++ b/LTEMvcApp/Models/LogState.cs @@ -0,0 +1,314 @@ +namespace LTEMvcApp.Models +{ + /// + /// 日志状态管理类 + /// + public class LogState + { + /// + /// 日志列表 + /// + public List Logs { get; set; } = new(); + + /// + /// 客户端列表 + /// + public List ClientList { get; set; } = new(); + + /// + /// 新日志列表 + /// + public List NewLogs { get; set; } = new(); + + /// + /// 是否重置元数据 + /// + public bool ResetMetadata { get; set; } = false; + + /// + /// 检测到的最小日志级别 + /// + public int MinLevelDetected { get; set; } = 10; + + /// + /// 是否启用过滤器 + /// + public bool Filter { get; set; } = false; + + /// + /// 是否更新网格 + /// + public bool UpdateGrid { get; set; } = false; + + /// + /// 是否跟随最新日志 + /// + public bool Follow { get; set; } = false; + + /// + /// 过滤器锁定状态 + /// + public string FilterLock { get; set; } = string.Empty; + + /// + /// 日志总数 + /// + public int LogCount => Logs.Count; + + /// + /// 客户端总数 + /// + public int ClientCount => ClientList.Count; + + /// + /// 新日志数量 + /// + public int NewLogCount => NewLogs.Count; + + /// + /// 启用的客户端数量 + /// + public int EnabledClientCount => ClientList.Count(c => c.State == ClientState.Connected || c.State == ClientState.Start); + + /// + /// 添加日志 + /// + /// 日志对象 + public void AddLog(LTELog log) + { + Logs.Add(log); + NewLogs.Add(log); + } + + /// + /// 添加客户端 + /// + /// 客户端对象 + public void AddClient(LTEClient client) + { + ClientList.Add(client); + } + + /// + /// 移除客户端 + /// + /// 客户端对象 + /// 是否成功移除 + 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; + } + + /// + /// 清除所有日志 + /// + public void ClearLogs() + { + Logs.Clear(); + NewLogs.Clear(); + ResetMetadata = true; + UpdateGrid = true; + } + + /// + /// 清除新日志 + /// + public void ClearNewLogs() + { + NewLogs.Clear(); + } + + /// + /// 重置状态 + /// + public void Reset() + { + Logs.Clear(); + NewLogs.Clear(); + ResetMetadata = true; + MinLevelDetected = 10; + Filter = false; + UpdateGrid = false; + Follow = false; + FilterLock = string.Empty; + } + + /// + /// 获取指定客户端的日志 + /// + /// 客户端对象 + /// 日志列表 + public List GetLogsByClient(LTEClient client) + { + return Logs.Where(log => log.Client == client).ToList(); + } + + /// + /// 获取启用的客户端 + /// + /// 启用的客户端列表 + public List GetEnabledClients() + { + return ClientList.Where(c => c.State == ClientState.Connected || c.State == ClientState.Start).ToList(); + } + + /// + /// 获取错误状态的客户端 + /// + /// 错误状态的客户端列表 + public List GetErrorClients() + { + return ClientList.Where(c => c.State == ClientState.Error).ToList(); + } + + /// + /// 获取连接状态的客户端 + /// + /// 连接状态的客户端列表 + public List GetConnectedClients() + { + return ClientList.Where(c => c.State == ClientState.Connected).ToList(); + } + + /// + /// 检查是否有实时客户端 + /// + /// 是否有实时客户端 + public bool HasRealTimeClients() + { + return ClientList.Any(c => c.State == ClientState.Connected); + } + + /// + /// 获取日志统计信息 + /// + /// 统计信息 + public LogStatistics GetStatistics() + { + return new LogStatistics + { + TotalLogs = LogCount, + NewLogs = NewLogCount, + TotalClients = ClientCount, + EnabledClients = EnabledClientCount, + ConnectedClients = GetConnectedClients().Count, + ErrorClients = GetErrorClients().Count, + MinLevel = MinLevelDetected, + HasRealTime = HasRealTimeClients() + }; + } + + /// + /// 更新最小日志级别 + /// + /// 日志级别 + public void UpdateMinLevel(int level) + { + if (level < MinLevelDetected) + { + MinLevelDetected = level; + } + } + + /// + /// 设置过滤器状态 + /// + /// 是否启用 + public void SetFilter(bool enabled) + { + Filter = enabled; + if (enabled) + { + UpdateGrid = true; + } + } + + /// + /// 锁定过滤器 + /// + /// 锁定名称 + public void LockFilter(string lockName) + { + FilterLock = lockName; + } + + /// + /// 解锁过滤器 + /// + public void UnlockFilter() + { + FilterLock = string.Empty; + } + + /// + /// 检查过滤器是否被锁定 + /// + /// 是否被锁定 + public bool IsFilterLocked() + { + return !string.IsNullOrEmpty(FilterLock); + } + } + + /// + /// 日志统计信息 + /// + public class LogStatistics + { + /// + /// 总日志数 + /// + public int TotalLogs { get; set; } + + /// + /// 新日志数 + /// + public int NewLogs { get; set; } + + /// + /// 总客户端数 + /// + public int TotalClients { get; set; } + + /// + /// 启用的客户端数 + /// + public int EnabledClients { get; set; } + + /// + /// 连接的客户端数 + /// + public int ConnectedClients { get; set; } + + /// + /// 错误的客户端数 + /// + public int ErrorClients { get; set; } + + /// + /// 最小日志级别 + /// + public int MinLevel { get; set; } + + /// + /// 是否有实时客户端 + /// + public bool HasRealTime { get; set; } + + /// + /// 获取统计信息字符串 + /// + /// 统计信息字符串 + public override string ToString() + { + return $"日志: {TotalLogs} (新: {NewLogs}), 客户端: {EnabledClients}/{TotalClients} (连接: {ConnectedClients}, 错误: {ErrorClients}), 最小级别: {MinLevel}"; + } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Models/LogUtils.cs b/LTEMvcApp/Models/LogUtils.cs new file mode 100644 index 0000000..04784a3 --- /dev/null +++ b/LTEMvcApp/Models/LogUtils.cs @@ -0,0 +1,331 @@ +using System.Text.RegularExpressions; + +namespace LTEMvcApp.Models +{ + /// + /// 日志工具类 + /// + public static class LogUtils + { + /// + /// 日志级别常量 + /// + 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; + + /// + /// 获取级别名称 + /// + /// 级别值 + /// 级别名称 + public static string GetLevelName(int level) + { + return level switch + { + None => "none", + Error => "error", + Warn => "warn", + Info => "info", + Debug => "debug", + _ => "unknown" + }; + } + + /// + /// 获取级别值 + /// + /// 级别名称 + /// 级别值 + public static int GetLevelValue(string levelName) + { + return levelName?.ToLower() switch + { + "none" => None, + "error" => Error, + "warn" => Warn, + "info" => Info, + "debug" => Debug, + _ => None + }; + } + + /// + /// 获取所有级别选项 + /// + /// 级别选项列表 + public static List GetLevelOptions() + { + return new List + { + new() { Value = Error, Text = "Error" }, + new() { Value = Warn, Text = "Warning" }, + new() { Value = Info, Text = "Info" }, + new() { Value = Debug, Text = "Debug" } + }; + } + } + + /// + /// 级别选项 + /// + public class LevelOption + { + public int Value { get; set; } + public string Text { get; set; } = string.Empty; + } + + /// + /// 时间戳转换为时间字符串 + /// + /// 时间戳(毫秒) + /// 是否显示完整日期 + /// 时间字符串 + 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"); + } + + /// + /// 时间戳转换为持续时间字符串 + /// + /// 时间戳(毫秒) + /// 持续时间字符串 + 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}"; + } + + /// + /// 时间字符串转换为时间戳 + /// + /// 时间字符串 + /// 时间戳,转换失败返回null + 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; + } + + /// + /// 获取日期偏移量 + /// + /// 时间戳 + /// 日期偏移量 + 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(); + } + + /// + /// 字符串转ID + /// + /// 字符串 + /// ID值 + 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; + } + + /// + /// 创建正则表达式 + /// + /// 模式 + /// 是否忽略大小写 + /// 正则表达式,创建失败返回null + 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; + } + } + + /// + /// 高亮搜索文本 + /// + /// 原文本 + /// 搜索模式 + /// 高亮后的文本 + 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, "$&"); + } + + /// + /// 格式化文件大小 + /// + /// 字节数 + /// 格式化后的字符串 + 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]}"; + } + + /// + /// 解析大小字符串 + /// + /// 大小字符串 + /// 字节数 + 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 + }; + } + + /// + /// 获取颜色亮度 + /// + /// 颜色代码 + /// 亮度值 (0-1) + 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); + } + + /// + /// 获取文本颜色 + /// + /// 背景颜色 + /// 文本颜色 + public static string GetTextColor(string backgroundColor) + { + var brightness = GetColorBrightness(backgroundColor); + return brightness >= 0.15 ? "black" : "white"; + } + + /// + /// 生成唯一ID + /// + /// 唯一ID + public static string GenerateUniqueId() + { + return Guid.NewGuid().ToString("N"); + } + + /// + /// 安全地执行操作 + /// + /// 要执行的操作 + /// 默认值 + /// 操作结果或默认值 + public static T SafeExecute(Func action, T defaultValue) + { + try + { + return action(); + } + catch + { + return defaultValue; + } + } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Program.cs b/LTEMvcApp/Program.cs index 09c9026..f511167 100644 --- a/LTEMvcApp/Program.cs +++ b/LTEMvcApp/Program.cs @@ -9,10 +9,8 @@ builder.Services.AddControllersWithViews(); builder.Services.AddSingleton(sp => new WebSocketManagerService( sp.GetRequiredService>(), - sp.GetRequiredService(), sp )); -builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/LTEMvcApp/Services/LTEClientWebSocket.cs b/LTEMvcApp/Services/LTEClientWebSocket.cs index d7a7052..036bc8d 100644 --- a/LTEMvcApp/Services/LTEClientWebSocket.cs +++ b/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 _logger; private const int ServerMessageCacheLimit = 1000; private readonly ConcurrentQueue _sentMessages = new ConcurrentQueue(); @@ -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); } diff --git a/LTEMvcApp/Services/LogParserService.cs b/LTEMvcApp/Services/LogParserService.cs deleted file mode 100644 index efd54ab..0000000 --- a/LTEMvcApp/Services/LogParserService.cs +++ /dev/null @@ -1,638 +0,0 @@ -using LTEMvcApp.Models; -using Newtonsoft.Json; -using System.Text.RegularExpressions; - -namespace LTEMvcApp.Services; - -/// -/// LTE日志解析服务 - 实现JavaScript _logListParse方法的核心功能 -/// -public class LogParserService -{ - #region 常量定义 - - /// - /// HFN回绕阈值 - /// - private const int HFN_WRAP_THRESHOLD = 512; - - /// - /// 最大日志数量 - /// - private const int LOGS_MAX = 2000000; - - #endregion - - #region 正则表达式 - - /// - /// PHY层日志正则表达式 - /// - private static readonly Regex RegExpPhy = new(@"^([a-f0-9\-]+)\s+([a-f0-9\-]+)\s+([\d\.\-]+) (\w+): (.+)", RegexOptions.Compiled); - - /// - /// 信息1正则表达式 - /// - private static readonly Regex RegExpInfo1 = new(@"^([\w\-]+): (.+)", RegexOptions.Compiled); - - /// - /// 信息2正则表达式 - /// - private static readonly Regex RegExpInfo2 = new(@"^([\w]+) (.+)", RegexOptions.Compiled); - - /// - /// IP日志正则表达式 - /// - private static readonly Regex RegExpIP = new(@"^(len=\d+)\s+(\S+)\s+(.*)", RegexOptions.Compiled); - - /// - /// IPsec日志正则表达式 - /// - private static readonly Regex RegExpIPsec = new(@"^len=(\d+)\s+(.*)", RegexOptions.Compiled); - - /// - /// SDU长度正则表达式 - /// - private static readonly Regex RegExpSDULen = new(@"SDU_len=(\d+)", RegexOptions.Compiled); - - /// - /// SIP日志正则表达式 - /// - private static readonly Regex RegExpSIP = new(@"^([:\.\[\]\da-f]+)\s+(\S+) (.+)", RegexOptions.Compiled); - - /// - /// 媒体请求正则表达式 - /// - private static readonly Regex RegExpMediaReq = new(@"^(\S+) (.+)", RegexOptions.Compiled); - - /// - /// 信号记录正则表达式 - /// - private static readonly Regex RegExpSignalRecord = new(@"Link:\s([\w\d]+)@(\d+)", RegexOptions.Compiled); - - /// - /// 小区ID正则表达式 - /// - private static readonly Regex RegExpCellID = new(@"^([a-f0-9\-]+) (.+)", RegexOptions.Compiled); - - /// - /// RRC UE ID正则表达式 - /// - private static readonly Regex RegExpRRC_UE_ID = new(@"Changing UE_ID to 0x(\d+)", RegexOptions.Compiled); - - /// - /// RRC TMSI正则表达式 - /// - private static readonly Regex RegExpRRC_TMSI = new(@"(5G|m)-TMSI '([\dA-F]+)'H", RegexOptions.Compiled); - - /// - /// RRC新ID正则表达式 - /// - private static readonly Regex RegExpRRC_NEW_ID = new(@"newUE-Identity (['\dA-FH]+)", RegexOptions.Compiled); - - /// - /// RRC CRNTI正则表达式 - /// - private static readonly Regex RegExpRRC_CRNTI = new(@"c-RNTI '([\dA-F]+)'H", RegexOptions.Compiled); - - /// - /// NAS TMSI正则表达式 - /// - private static readonly Regex RegExpNAS_TMSI = new(@"m-TMSI = 0x([\da-f]+)", RegexOptions.Compiled); - - /// - /// NAS 5G TMSI正则表达式 - /// - private static readonly Regex RegExpNAS_5GTMSI = new(@"5G-TMSI = 0x([\da-f]+)", RegexOptions.Compiled); - - /// - /// RRC频段组合正则表达式 - /// - private static readonly Regex RegExpRRC_BC = new(@"(EUTRA|MRDC|NR|NRDC) band combinations", RegexOptions.Compiled); - - /// - /// PDCCH正则表达式 - /// - private static readonly Regex RegExpPDCCH = new(@"^\s*(.+)=(\d+)$", RegexOptions.Compiled); - - /// - /// S1/NGAP正则表达式 - /// - private static readonly Regex RegExpS1NGAP = new(@"^([\da-f\-]+)\s+([\da-f\-]+) (([^\s]+) .+)", RegexOptions.Compiled); - - /// - /// 十六进制转储正则表达式 - /// - private static readonly Regex RegExpHexDump = new(@"^[\da-f]+:(\s+[\da-f]{2}){1,16}\s+.{1,16}$", RegexOptions.Compiled); - - #endregion - - #region 私有字段 - - /// - /// 字符串到ID的映射缓存 - /// - private readonly Dictionary _stringToIdCache = new(); - - /// - /// ID到字符串的映射缓存 - /// - private readonly Dictionary _idToStringCache = new(); - - /// - /// 字符串ID计数器 - /// - private int _stringIdCounter = 0; - - #endregion - - #region 公共方法 - - /// - /// 解析日志列表 - 对应JavaScript的_logListParse方法 - /// - /// LTE客户端 - /// 日志列表 - /// 是否为WebSocket消息 - public void ParseLogList(LTEClient client, List 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(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 私有方法 - - /// - /// 解析PHY日志 - /// - 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; - } - - /// - /// 解析PHY参数 - /// - 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; - } - } - - /// - /// 处理PHY日志 - /// - 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; - } - } - } - - /// - /// 处理HARQ日志 - /// - 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(); - } - - client.LastHarq[cell][key] = log; - } - } - - /// - /// 解析小区ID - /// - 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; - } - } - } - - /// - /// 设置日志信息 - /// - private bool SetLogInfo(LTEClient client, LTELog log, string info) - { - if (string.IsNullOrEmpty(info) || info == "?") - return false; - - log.Info = StringToId(info); - return true; - } - - /// - /// 字符串转ID - /// - private int StringToId(string str) - { - if (_stringToIdCache.TryGetValue(str, out var id)) - { - return id; - } - - id = ++_stringIdCounter; - _stringToIdCache[str] = id; - _idToStringCache[id] = str; - return id; - } - - /// - /// ID转字符串 - /// - private string IdToString(int id) - { - return _idToStringCache.TryGetValue(id, out var str) ? str : ""; - } - - /// - /// 设置相同UE - /// - private void SetSameUe(LTEClient client, LTELog log, int ueId) - { - log.UeId = ueId; - if (!client.UeList.ContainsKey(ueId)) - { - client.UeList[ueId] = new UEInfo { UeId = ueId }; - } - } - - /// - /// 处理RRC日志 - /// - 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); - } - } - - /// - /// 处理NAS日志 - /// - private void ProcessNasLog(LTEClient client, LTELog log) - { - // NAS日志处理逻辑 - } - - /// - /// 解析MAC日志 - /// - private void ParseMacLog(LTEClient client, LTELog log) - { - // MAC日志解析逻辑 - } - - /// - /// 处理提示信息 - /// - private void ProcessHints(LTEClient client, LTELog log, Dictionary? config = null) - { - // 处理提示信息逻辑 - } - - /// - /// 设置TMSI - /// - 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; - } - } - - /// - /// 设置RNTI - /// - 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 -} \ No newline at end of file diff --git a/LTEMvcApp/Services/WebSocketManagerService.cs b/LTEMvcApp/Services/WebSocketManagerService.cs index 1680dca..ad19a0e 100644 --- a/LTEMvcApp/Services/WebSocketManagerService.cs +++ b/LTEMvcApp/Services/WebSocketManagerService.cs @@ -21,7 +21,6 @@ namespace LTEMvcApp.Services private readonly ConcurrentDictionary _clients; private readonly ConcurrentDictionary _configs; - private readonly LogParserService _logParser; private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private ClientConfig _testClientConfig; @@ -60,11 +59,10 @@ namespace LTEMvcApp.Services /// /// 构造函数 /// - public WebSocketManagerService(ILogger logger, LogParserService logParser, IServiceProvider serviceProvider) + public WebSocketManagerService(ILogger logger, IServiceProvider serviceProvider) { _clients = new ConcurrentDictionary(); _configs = new ConcurrentDictionary(); - _logParser = logParser; _logger = logger; _serviceProvider = serviceProvider;