Skip to content

Commit d255354

Browse files
committed
feat(server): 添加基于 Aspire 的服务拓扑监控
实现新的服务发现监控机制: - ServerComponentAgent 添加 WatchServiceTopology 方法 - 通过定时轮询构建服务拓扑快照 - 检测服务上线/下线事件并分发 - 添加 ServiceOnline/OfflineEventArgs 事件参数 - 新增 EventId.ServiceOnline/Offline 事件ID
1 parent 2eedd33 commit d255354

4 files changed

Lines changed: 185 additions & 4 deletions

File tree

GameFrameX.Apps/Common/Event/EventId.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ==========================================================================================
1+
// ==========================================================================================
22
// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
33
// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
44
// 均受中华人民共和国及相关国际法律法规保护。
@@ -84,6 +84,16 @@ public enum EventId
8484
/// 世界等级改变
8585
/// </summary>
8686
WorldLevelChange,
87+
88+
/// <summary>
89+
/// 服务上线
90+
/// </summary>
91+
ServiceOnline,
92+
93+
/// <summary>
94+
/// 服务下线
95+
/// </summary>
96+
ServiceOffline,
8797

8898
#endregion
89-
}
99+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using GameFrameX.Core.Abstractions.Events;
2+
3+
namespace GameFrameX.Apps.Common.EventData;
4+
5+
public sealed class ServiceOfflineEventArgs : GameEventArgs
6+
{
7+
public string ServiceName { get; }
8+
public long InstanceId { get; }
9+
public string Reason { get; }
10+
public DateTime Timestamp { get; }
11+
12+
public ServiceOfflineEventArgs(string serviceName, long instanceId, string reason, DateTime timestamp)
13+
{
14+
ServiceName = serviceName;
15+
InstanceId = instanceId;
16+
Reason = reason;
17+
Timestamp = timestamp;
18+
}
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using GameFrameX.Core.Abstractions.Events;
2+
3+
namespace GameFrameX.Apps.Common.EventData;
4+
5+
public sealed class ServiceOnlineEventArgs : GameEventArgs
6+
{
7+
public string ServiceName { get; }
8+
public long InstanceId { get; }
9+
public IReadOnlyList<string> Endpoints { get; }
10+
public DateTime Timestamp { get; }
11+
12+
public ServiceOnlineEventArgs(string serviceName, long instanceId, DateTime timestamp, IReadOnlyList<string> endpoints = null)
13+
{
14+
ServiceName = serviceName;
15+
InstanceId = instanceId;
16+
Timestamp = timestamp;
17+
Endpoints = endpoints ?? Array.Empty<string>();
18+
}
19+
}

GameFrameX.Hotfix/Logic/Server/ServerComponentAgent.cs

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ==========================================================================================
1+
// ==========================================================================================
22
// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
33
// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
44
// 均受中华人民共和国及相关国际法律法规保护。
@@ -31,23 +31,34 @@
3131

3232
using GameFrameX.Apps.Server.Component;
3333
using GameFrameX.Apps.Server.Entity;
34+
using GameFrameX.Apps.Common.Event;
35+
using GameFrameX.Apps.Common.EventData;
3436
using GameFrameX.Core.Abstractions.Attribute;
3537
using GameFrameX.Core.Abstractions.Events;
38+
using GameFrameX.Core.Events;
3639
using GameFrameX.Core.Timer.Handler;
3740
using GameFrameX.Foundation.Utility;
3841
using GameFrameX.Hotfix.Logic.Player.Login;
42+
using System.Collections;
43+
using System.Security.Cryptography;
44+
using System.Text;
3945

4046
namespace GameFrameX.Hotfix.Logic.Server;
4147

4248
public class ServerComponentAgent : StateComponentAgent<ServerComponent, ServerState>
4349
{
50+
private readonly Dictionary<string, HashSet<long>> _topologySnapshot = new(StringComparer.OrdinalIgnoreCase);
51+
private readonly Dictionary<string, int> _offlineConfirmCounter = new(StringComparer.OrdinalIgnoreCase);
52+
private static readonly string[] TopologyServiceNames = { GlobalConst.GameServiceName, GlobalConst.SocialServiceName, GlobalConst.GatewayServiceName };
53+
4454
public override async Task<bool> Active()
4555
{
4656
var isContinue = await base.Active();
4757
if (isContinue)
4858
{
4959
// 跨天定时器
5060
WithCronExpression<CrossDayTimeHandler>("0 0 0 * * ? *");
61+
Schedule<ServiceTopologyWatcherHandler>(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
5162
if (State.FirstStartTime == default)
5263
{
5364
State.FirstStartTime = TimerHelper.UnixTimeSeconds();
@@ -57,6 +68,120 @@ public override async Task<bool> Active()
5768

5869
return isContinue;
5970
}
71+
72+
private Task WatchServiceTopology()
73+
{
74+
var currentSnapshot = BuildCurrentTopologySnapshot();
75+
foreach (var currentPair in currentSnapshot)
76+
{
77+
var serviceName = currentPair.Key;
78+
var currentInstances = currentPair.Value;
79+
_topologySnapshot.TryGetValue(serviceName, out var previousInstances);
80+
previousInstances ??= new HashSet<long>();
81+
foreach (var instance in currentInstances)
82+
{
83+
if (!previousInstances.Contains(instance.Key))
84+
{
85+
EventDispatcher.Dispatch(ActorId, (int)EventId.ServiceOnline, new ServiceOnlineEventArgs(serviceName, instance.Key, DateTime.UtcNow, instance.Value));
86+
}
87+
88+
_offlineConfirmCounter.Remove(GetOfflineKey(serviceName, instance.Key));
89+
}
90+
91+
foreach (var instanceId in previousInstances)
92+
{
93+
if (currentInstances.ContainsKey(instanceId))
94+
{
95+
continue;
96+
}
97+
98+
var counterKey = GetOfflineKey(serviceName, instanceId);
99+
_offlineConfirmCounter.TryGetValue(counterKey, out var counter);
100+
counter++;
101+
if (counter >= 2)
102+
{
103+
EventDispatcher.Dispatch(ActorId, (int)EventId.ServiceOffline, new ServiceOfflineEventArgs(serviceName, instanceId, "Removed", DateTime.UtcNow));
104+
_offlineConfirmCounter.Remove(counterKey);
105+
continue;
106+
}
107+
108+
_offlineConfirmCounter[counterKey] = counter;
109+
}
110+
}
111+
112+
_topologySnapshot.Clear();
113+
foreach (var pair in currentSnapshot)
114+
{
115+
_topologySnapshot[pair.Key] = pair.Value.Keys.ToHashSet();
116+
}
117+
118+
return Task.CompletedTask;
119+
}
120+
121+
private static string GetOfflineKey(string serviceName, long instanceId)
122+
{
123+
return $"{serviceName}:{instanceId}";
124+
}
125+
126+
private static Dictionary<string, Dictionary<long, IReadOnlyList<string>>> BuildCurrentTopologySnapshot()
127+
{
128+
var snapshot = new Dictionary<string, Dictionary<long, IReadOnlyList<string>>>(StringComparer.OrdinalIgnoreCase);
129+
foreach (var serviceName in TopologyServiceNames)
130+
{
131+
snapshot[serviceName] = new Dictionary<long, IReadOnlyList<string>>();
132+
}
133+
134+
var variables = Environment.GetEnvironmentVariables();
135+
foreach (DictionaryEntry variable in variables)
136+
{
137+
if (variable.Key is not string rawKey || variable.Value is not string rawValue || string.IsNullOrWhiteSpace(rawValue))
138+
{
139+
continue;
140+
}
141+
142+
var key = rawKey.Replace("__", ":", StringComparison.Ordinal).Trim();
143+
if (!key.StartsWith("services:", StringComparison.OrdinalIgnoreCase))
144+
{
145+
continue;
146+
}
147+
148+
var segments = key.Split(':', StringSplitOptions.RemoveEmptyEntries);
149+
if (segments.Length < 3)
150+
{
151+
continue;
152+
}
153+
154+
var serviceToken = segments[1];
155+
var serviceName = TopologyServiceNames.FirstOrDefault(name => string.Equals(name, serviceToken, StringComparison.OrdinalIgnoreCase));
156+
if (serviceName == null)
157+
{
158+
continue;
159+
}
160+
161+
if (GlobalSettings.CurrentSetting != null && string.Equals(serviceName, GlobalSettings.CurrentSetting.ServerType, StringComparison.OrdinalIgnoreCase))
162+
{
163+
continue;
164+
}
165+
166+
var instanceId = BuildInstanceId(serviceName, rawValue);
167+
if (!snapshot[serviceName].TryGetValue(instanceId, out var endpoints))
168+
{
169+
endpoints = Array.Empty<string>();
170+
snapshot[serviceName][instanceId] = endpoints;
171+
}
172+
173+
snapshot[serviceName][instanceId] = endpoints.Concat(new[] { rawValue }).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
174+
}
175+
176+
return snapshot;
177+
}
178+
179+
private static long BuildInstanceId(string serviceName, string endpoint)
180+
{
181+
var payload = $"{serviceName}|{endpoint}";
182+
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(payload));
183+
return Math.Abs(BitConverter.ToInt64(hash, 0));
184+
}
60185

61186
[Service]
62187
[Discard]
@@ -207,5 +332,13 @@ protected override async Task HandleTimer(ServerComponentAgent agent, GameEventA
207332
await ActorManager.CrossDay(1, GlobalConst.ActorTypeServer);
208333
}
209334
}
335+
336+
private class ServiceTopologyWatcherHandler : TimerHandler<ServerComponentAgent>
337+
{
338+
protected override Task HandleTimer(ServerComponentAgent agent, GameEventArgs gameEventArgs)
339+
{
340+
return agent.WatchServiceTopology();
341+
}
342+
}
210343
/*******************演示代码**************************/
211-
}
344+
}

0 commit comments

Comments
 (0)