Network 层的迁移与强化已完成,TODO.md 与 MobaSyncMVP.md 给出后续方向

This commit is contained in:
SepComet
2026-03-27 22:14:33 +08:00
parent e361510100
commit 156d72bf4a
57 changed files with 2933 additions and 102 deletions
@@ -10,5 +10,5 @@
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
"noEngineReferences": true
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Network.Defines;
namespace Network.NetworkApplication
{
public sealed class ClientPredictionBuffer
{
private readonly List<PlayerInput> pendingInputs = new();
public long? LastAuthoritativeTick { get; private set; }
public IReadOnlyList<PlayerInput> PendingInputs => pendingInputs;
public void Record(PlayerInput input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
if (pendingInputs.Count > 0 && pendingInputs[^1].Tick >= input.Tick)
{
return;
}
pendingInputs.Add(input);
}
public bool TryApplyAuthoritativeState(PlayerState state, out IReadOnlyList<PlayerInput> replayInputs)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
if (LastAuthoritativeTick.HasValue && state.Tick <= LastAuthoritativeTick.Value)
{
replayInputs = Array.Empty<PlayerInput>();
return false;
}
LastAuthoritativeTick = state.Tick;
pendingInputs.RemoveAll(input => input.Tick <= state.Tick);
replayInputs = pendingInputs.ToArray();
return true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0835bc8e8b5c4f14eab1d4c429ec6238
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,35 @@
using System;
namespace Network.NetworkApplication
{
public sealed class ClockSyncState
{
private readonly Func<DateTimeOffset> utcNowProvider;
public ClockSyncState(Func<DateTimeOffset> utcNowProvider = null)
{
this.utcNowProvider = utcNowProvider ?? (() => DateTimeOffset.UtcNow);
}
public long? CurrentServerTick { get; private set; }
public DateTimeOffset? LastSampleReceivedAtUtc { get; private set; }
public bool ObserveSample(long? serverTick)
{
if (!serverTick.HasValue)
{
return false;
}
if (CurrentServerTick.HasValue && serverTick.Value < CurrentServerTick.Value)
{
return false;
}
CurrentServerTick = serverTick.Value;
LastSampleReceivedAtUtc = utcNowProvider();
return true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 95a64b5509806e94aac0803ca2a9823f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using Network.Defines;
namespace Network.NetworkApplication
{
public sealed class DefaultMessageDeliveryPolicyResolver : IMessageDeliveryPolicyResolver
{
private static readonly IReadOnlyDictionary<MessageType, DeliveryPolicy> DefaultPolicies =
new Dictionary<MessageType, DeliveryPolicy>
{
{ MessageType.PlayerInput, DeliveryPolicy.HighFrequencySync },
{ MessageType.PlayerState, DeliveryPolicy.HighFrequencySync }
};
public DeliveryPolicy Resolve(MessageType messageType)
{
return DefaultPolicies.TryGetValue(messageType, out var policy)
? policy
: DeliveryPolicy.ReliableOrdered;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b39164ada6225c4db48ec637634ba86
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
namespace Network.NetworkApplication
{
public enum DeliveryPolicy
{
ReliableOrdered = 0,
HighFrequencySync = 1
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2e26e6d78859ced41a9e1241414e5953
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
using Network.Defines;
namespace Network.NetworkApplication
{
public interface IMessageDeliveryPolicyResolver
{
DeliveryPolicy Resolve(MessageType messageType);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7732b6dea776deb43b39d6cb42874efc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
using System;
using System.Net;
namespace Network.NetworkApplication
{
public interface INetworkMessageLane
{
event Action<byte[], IPEndPoint> Received;
void Send(byte[] data);
void SendTo(byte[] data, IPEndPoint target);
void SendToAll(byte[] data);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 910fad1550952a748bcbded789d8ae87
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -5,14 +5,20 @@ namespace Network.NetworkApplication
{
public sealed class ManagedNetworkSession
{
public ManagedNetworkSession(IPEndPoint remoteEndPoint, SessionManager sessionManager)
public ManagedNetworkSession(
IPEndPoint remoteEndPoint,
SessionManager sessionManager,
ClockSyncState clockSync)
{
RemoteEndPoint = remoteEndPoint ?? throw new ArgumentNullException(nameof(remoteEndPoint));
SessionManager = sessionManager ?? throw new ArgumentNullException(nameof(sessionManager));
ClockSync = clockSync ?? throw new ArgumentNullException(nameof(clockSync));
}
public IPEndPoint RemoteEndPoint { get; }
public SessionManager SessionManager { get; }
public ClockSyncState ClockSync { get; }
}
}
@@ -10,17 +10,58 @@ namespace Network.NetworkApplication
{
public class MessageManager
{
private readonly ITransport transport;
private readonly INetworkMessageLane reliableLane;
private readonly INetworkMessageLane syncLane;
private readonly INetworkMessageDispatcher dispatcher;
private readonly IMessageDeliveryPolicyResolver deliveryPolicyResolver;
private readonly SyncSequenceTracker syncSequenceTracker;
private readonly Dictionary<MessageType, Func<byte[], IPEndPoint, Task>> handlers =
new();
public MessageManager(ITransport transport, INetworkMessageDispatcher dispatcher)
: this(
CreateLane(transport),
dispatcher,
new DefaultMessageDeliveryPolicyResolver(),
null,
new SyncSequenceTracker())
{
this.transport = transport ?? throw new ArgumentNullException(nameof(transport));
}
public MessageManager(
ITransport reliableTransport,
INetworkMessageDispatcher dispatcher,
IMessageDeliveryPolicyResolver deliveryPolicyResolver,
ITransport syncTransport = null,
SyncSequenceTracker syncSequenceTracker = null)
: this(
CreateLane(reliableTransport),
dispatcher,
deliveryPolicyResolver,
CreateLaneIfDistinct(reliableTransport, syncTransport),
syncSequenceTracker)
{
}
public MessageManager(
INetworkMessageLane reliableLane,
INetworkMessageDispatcher dispatcher,
IMessageDeliveryPolicyResolver deliveryPolicyResolver = null,
INetworkMessageLane syncLane = null,
SyncSequenceTracker syncSequenceTracker = null)
{
this.reliableLane = reliableLane ?? throw new ArgumentNullException(nameof(reliableLane));
this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
this.transport.OnReceive += OnTransportReceive;
this.deliveryPolicyResolver = deliveryPolicyResolver ?? new DefaultMessageDeliveryPolicyResolver();
this.syncLane = syncLane;
this.syncSequenceTracker = syncSequenceTracker ?? new SyncSequenceTracker();
this.reliableLane.Received += OnTransportReceive;
if (this.syncLane != null && !ReferenceEquals(this.syncLane, this.reliableLane))
{
this.syncLane.Received += OnTransportReceive;
}
}
public INetworkMessageDispatcher Dispatcher => dispatcher;
@@ -71,14 +112,15 @@ namespace Network.NetworkApplication
Type = (int)type,
Payload = message.ToByteString()
};
var lane = ResolveLane(type);
if (target != null)
{
transport.SendTo(envelope.ToByteArray(), target);
lane.SendTo(envelope.ToByteArray(), target);
}
else
{
transport.Send(envelope.ToByteArray());
lane.Send(envelope.ToByteArray());
}
Console.WriteLine($"[MessageManager] 发送消息:{type} -> {target?.ToString() ?? "default"}");
@@ -97,7 +139,7 @@ namespace Network.NetworkApplication
Type = (int)type,
Payload = message.ToByteString()
};
transport.SendToAll(envelope.ToByteArray());
ResolveLane(type).SendToAll(envelope.ToByteArray());
}
public Task<int> DrainPendingMessagesAsync(int maxMessages = int.MaxValue)
@@ -112,10 +154,16 @@ namespace Network.NetworkApplication
var envelope = Envelope.Parser.ParseFrom(data);
var type = (MessageType)envelope.Type;
Console.WriteLine($"[MessageManager] 收到消息:{type} 来自 {sender}");
var payload = envelope.Payload.ToByteArray();
if (!syncSequenceTracker.ShouldAccept(type, payload, sender))
{
Console.WriteLine($"[MessageManager] 丢弃过期同步消息:{type} 来自 {sender}");
return;
}
if (handlers.TryGetValue(type, out var handler))
{
var payload = envelope.Payload.ToByteArray();
dispatcher.Enqueue(() => DispatchAsync(handler, payload, sender, type));
}
else
@@ -144,5 +192,30 @@ namespace Network.NetworkApplication
Console.WriteLine($"[MessageManager] Handler 执行错误:{type} -> {ex.Message}");
}
}
private INetworkMessageLane ResolveLane(MessageType type)
{
var policy = deliveryPolicyResolver.Resolve(type);
return policy == DeliveryPolicy.HighFrequencySync && syncLane != null
? syncLane
: reliableLane;
}
private static INetworkMessageLane CreateLane(ITransport transport)
{
return new TransportMessageLane(transport ?? throw new ArgumentNullException(nameof(transport)));
}
private static INetworkMessageLane CreateLaneIfDistinct(ITransport reliableTransport, ITransport syncTransport)
{
if (syncTransport == null)
{
return null;
}
return ReferenceEquals(reliableTransport, syncTransport)
? null
: CreateLane(syncTransport);
}
}
}
@@ -119,7 +119,9 @@ namespace Network.NetworkApplication
public void NotifyHeartbeatReceived(IPEndPoint remoteEndPoint, long? serverTick = null)
{
GetOrCreateSession(remoteEndPoint).SessionManager.NotifyHeartbeatReceived(serverTick);
var session = GetOrCreateSession(remoteEndPoint);
session.SessionManager.NotifyHeartbeatReceived();
session.ClockSync.ObserveSample(serverTick);
}
public void NotifyInboundActivity(IPEndPoint remoteEndPoint)
@@ -127,6 +129,11 @@ namespace Network.NetworkApplication
GetOrCreateSession(remoteEndPoint).SessionManager.NotifyInboundActivity();
}
public void ObserveAuthoritativeState(IPEndPoint remoteEndPoint, long? serverTick)
{
GetOrCreateSession(remoteEndPoint).ClockSync.ObserveSample(serverTick);
}
public bool RemoveSession(IPEndPoint remoteEndPoint, string reason = null)
{
SessionRegistration registration;
@@ -194,7 +201,8 @@ namespace Network.NetworkApplication
}
var sessionManager = new SessionManager(reconnectPolicy, utcNowProvider);
var session = new ManagedNetworkSession(normalizedEndPoint, sessionManager);
var clockSync = new ClockSyncState(utcNowProvider);
var session = new ManagedNetworkSession(normalizedEndPoint, sessionManager, clockSync);
Action<SessionLifecycleEvent> handler = lifecycleEvent =>
LifecycleChanged?.Invoke(new MultiSessionLifecycleEvent(session.RemoteEndPoint, session.SessionManager, lifecycleEvent));
@@ -235,4 +243,3 @@ namespace Network.NetworkApplication
}
}
}
@@ -32,8 +32,6 @@ namespace Network.NetworkApplication
public TimeSpan? LastRoundTripTime { get; private set; }
public long? LastServerTick { get; private set; }
public string LastFailureReason { get; private set; }
public bool CanSendHeartbeat => State == ConnectionState.LoggedIn;
@@ -104,11 +102,10 @@ namespace Network.NetworkApplication
RaiseEvent(SessionEventKind.HeartbeatSent, State, State, lastHeartbeatSentUtc.Value);
}
public void NotifyHeartbeatReceived(long? serverTick = null)
public void NotifyHeartbeatReceived()
{
var now = utcNowProvider();
lastLivenessUtc = now;
LastServerTick = serverTick;
if (lastHeartbeatSentUtc.HasValue)
{
LastRoundTripTime = now - lastHeartbeatSentUtc.Value;
@@ -10,19 +10,34 @@ namespace Network.NetworkApplication
ITransport transport,
INetworkMessageDispatcher dispatcher,
SessionReconnectPolicy reconnectPolicy = null,
Func<DateTimeOffset> utcNowProvider = null)
Func<DateTimeOffset> utcNowProvider = null,
ITransport syncTransport = null,
IMessageDeliveryPolicyResolver deliveryPolicyResolver = null,
SyncSequenceTracker syncSequenceTracker = null,
ClockSyncState clockSync = null)
{
Transport = transport ?? throw new ArgumentNullException(nameof(transport));
SyncTransport = syncTransport;
SessionManager = new SessionManager(reconnectPolicy, utcNowProvider);
MessageManager = new MessageManager(transport, dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)));
ClockSync = clockSync ?? new ClockSyncState(utcNowProvider);
MessageManager = new MessageManager(
transport,
dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)),
deliveryPolicyResolver ?? new DefaultMessageDeliveryPolicyResolver(),
syncTransport,
syncSequenceTracker ?? new SyncSequenceTracker());
}
public ITransport Transport { get; }
public ITransport SyncTransport { get; }
public MessageManager MessageManager { get; }
public SessionManager SessionManager { get; }
public ClockSyncState ClockSync { get; }
public event Action<SessionLifecycleEvent> LifecycleChanged
{
add => SessionManager.LifecycleChanged += value;
@@ -32,13 +47,26 @@ namespace Network.NetworkApplication
public async Task StartAsync()
{
await Transport.StartAsync();
if (SyncTransport != null && !ReferenceEquals(SyncTransport, Transport))
{
await SyncTransport.StartAsync();
}
SessionManager.NotifyTransportConnected();
PublishMetricsSessionSnapshot();
}
public void Stop()
{
Transport.Stop();
if (SyncTransport != null && !ReferenceEquals(SyncTransport, Transport))
{
SyncTransport.Stop();
}
SessionManager.NotifyTransportDisconnected("Transport stopped");
PublishMetricsSessionSnapshot();
}
public Task<int> DrainPendingMessagesAsync(int maxMessages = int.MaxValue)
@@ -49,36 +77,90 @@ namespace Network.NetworkApplication
public void NotifyLoginStarted()
{
SessionManager.NotifyLoginStarted();
PublishMetricsSessionSnapshot();
}
public void NotifyLoginSucceeded()
{
SessionManager.NotifyLoginSucceeded();
PublishMetricsSessionSnapshot();
}
public void NotifyLoginFailed(string reason = null)
{
SessionManager.NotifyLoginFailed(reason);
PublishMetricsSessionSnapshot();
}
public void NotifyHeartbeatSent()
{
SessionManager.NotifyHeartbeatSent();
PublishMetricsSessionSnapshot();
}
public void NotifyHeartbeatReceived(long? serverTick = null)
{
SessionManager.NotifyHeartbeatReceived(serverTick);
SessionManager.NotifyHeartbeatReceived();
ClockSync.ObserveSample(serverTick);
PublishMetricsSessionSnapshot();
}
public void NotifyInboundActivity()
{
SessionManager.NotifyInboundActivity();
PublishMetricsSessionSnapshot();
}
public void UpdateLifecycle()
{
SessionManager.Evaluate();
PublishMetricsSessionSnapshot();
}
public void ObserveAuthoritativeState(long? serverTick)
{
ClockSync.ObserveSample(serverTick);
PublishMetricsSessionSnapshot();
}
private void PublishMetricsSessionSnapshot()
{
RecordMetricsSessionSnapshot(Transport, "shared-runtime", SessionManager, ClockSync, remoteEndPoint: null);
if (SyncTransport != null && !ReferenceEquals(SyncTransport, Transport))
{
RecordMetricsSessionSnapshot(SyncTransport, "shared-runtime-sync", SessionManager, ClockSync, remoteEndPoint: null);
}
}
private static void RecordMetricsSessionSnapshot(
ITransport transport,
string scope,
SessionManager sessionManager,
ClockSyncState clockSync,
System.Net.IPEndPoint remoteEndPoint)
{
if (transport is not ITransportMetricsSink metricsSink || sessionManager == null)
{
return;
}
metricsSink.RecordApplicationSessionSnapshot(new TransportApplicationSessionSnapshot
{
Scope = scope,
RemoteEndPoint = remoteEndPoint?.ToString(),
ConnectionState = sessionManager.State.ToString(),
CanSendHeartbeat = sessionManager.CanSendHeartbeat,
LastRoundTripTimeMs = sessionManager.LastRoundTripTime.HasValue
? (long?)System.Math.Max(0d, sessionManager.LastRoundTripTime.Value.TotalMilliseconds)
: null,
LastFailureReason = sessionManager.LastFailureReason,
LastLivenessUtc = sessionManager.LastLivenessUtc,
LastHeartbeatSentUtc = sessionManager.LastHeartbeatSentUtc,
NextReconnectAtUtc = sessionManager.NextReconnectAtUtc,
CurrentServerTick = clockSync?.CurrentServerTick,
ObservedAtUtc = DateTimeOffset.UtcNow
});
}
}
}
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Net;
using Network.Defines;
namespace Network.NetworkApplication
{
public sealed class SyncSequenceTracker
{
private readonly object gate = new();
private readonly Dictionary<string, long> latestSequenceByStream = new();
public bool ShouldAccept(MessageType messageType, byte[] payload, IPEndPoint sender)
{
if (!TryResolveSequence(messageType, payload, sender, out var streamKey, out var sequence))
{
return true;
}
lock (gate)
{
if (latestSequenceByStream.TryGetValue(streamKey, out var latestSequence) &&
sequence < latestSequence)
{
return false;
}
latestSequenceByStream[streamKey] = sequence;
return true;
}
}
private static bool TryResolveSequence(
MessageType messageType,
byte[] payload,
IPEndPoint sender,
out string streamKey,
out long sequence)
{
switch (messageType)
{
case MessageType.PlayerInput:
{
var input = PlayerInput.Parser.ParseFrom(payload);
streamKey = $"input:{Normalize(sender)}:{input.PlayerId}";
sequence = input.Tick;
return true;
}
case MessageType.PlayerState:
{
var state = PlayerState.Parser.ParseFrom(payload);
streamKey = $"state:{state.PlayerId}";
sequence = state.Tick;
return true;
}
default:
streamKey = null;
sequence = 0;
return false;
}
}
private static string Normalize(IPEndPoint sender)
{
return sender == null ? "unknown" : sender.ToString();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 377bd3719da11b346926ee82dd56bc45
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
using System;
using System.Net;
using Network.NetworkTransport;
namespace Network.NetworkApplication
{
public sealed class TransportMessageLane : INetworkMessageLane
{
private readonly ITransport transport;
public TransportMessageLane(ITransport transport)
{
this.transport = transport ?? throw new ArgumentNullException(nameof(transport));
this.transport.OnReceive += HandleReceive;
}
public event Action<byte[], IPEndPoint> Received;
public void Send(byte[] data)
{
transport.Send(data);
}
public void SendTo(byte[] data, IPEndPoint target)
{
transport.SendTo(data, target);
}
public void SendToAll(byte[] data)
{
transport.SendToAll(data);
}
private void HandleReceive(byte[] data, IPEndPoint sender)
{
Received?.Invoke(data, sender);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4c7a1935b45cd2e418814d4b6a5c0b45
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -10,24 +10,41 @@ namespace Network.NetworkHost
public sealed class ServerNetworkHost
{
private readonly ITransport transport;
private readonly ITransport syncTransport;
private readonly MessageManager messageManager;
public ServerNetworkHost(
ITransport transport,
INetworkMessageDispatcher dispatcher = null,
SessionReconnectPolicy reconnectPolicy = null,
Func<DateTimeOffset> utcNowProvider = null)
Func<DateTimeOffset> utcNowProvider = null,
ITransport syncTransport = null,
IMessageDeliveryPolicyResolver deliveryPolicyResolver = null,
SyncSequenceTracker syncSequenceTracker = null)
{
this.transport = transport ?? throw new ArgumentNullException(nameof(transport));
this.syncTransport = syncTransport;
SessionCoordinator = new MultiSessionManager(reconnectPolicy, utcNowProvider);
this.transport.OnReceive += HandleTransportReceive;
messageManager = new MessageManager(this.transport, dispatcher ?? new ImmediateNetworkMessageDispatcher());
if (this.syncTransport != null && !ReferenceEquals(this.syncTransport, this.transport))
{
this.syncTransport.OnReceive += HandleTransportReceive;
}
messageManager = new MessageManager(
this.transport,
dispatcher ?? new ImmediateNetworkMessageDispatcher(),
deliveryPolicyResolver ?? new DefaultMessageDeliveryPolicyResolver(),
this.syncTransport,
syncSequenceTracker ?? new SyncSequenceTracker());
}
public MessageManager MessageManager => messageManager;
public ITransport Transport => transport;
public ITransport SyncTransport => syncTransport;
// Server-side lifecycle entry point: inspect and control per-peer session state here.
public MultiSessionManager SessionCoordinator { get; }
@@ -41,13 +58,26 @@ namespace Network.NetworkHost
public Task StartAsync()
{
return transport.StartAsync();
var startTask = transport.StartAsync();
if (syncTransport == null || ReferenceEquals(syncTransport, transport))
{
return startTask;
}
return StartWithSyncAsync(startTask);
}
public void Stop()
{
PublishMetricsSessionSnapshots();
transport.Stop();
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
{
syncTransport.Stop();
}
SessionCoordinator.RemoveAllSessions("Transport stopped");
PublishMetricsSessionSnapshots();
}
public Task<int> DrainPendingMessagesAsync(int maxMessages = int.MaxValue)
@@ -58,6 +88,7 @@ namespace Network.NetworkHost
public void UpdateLifecycle()
{
SessionCoordinator.UpdateLifecycle();
PublishMetricsSessionSnapshots();
}
public bool TryGetSession(IPEndPoint remoteEndPoint, out ManagedNetworkSession session)
@@ -68,42 +99,132 @@ namespace Network.NetworkHost
public void NotifyLoginStarted(IPEndPoint remoteEndPoint)
{
SessionCoordinator.NotifyLoginStarted(remoteEndPoint);
PublishMetricsSessionSnapshot(remoteEndPoint);
}
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint)
{
SessionCoordinator.NotifyLoginSucceeded(remoteEndPoint);
PublishMetricsSessionSnapshot(remoteEndPoint);
}
public void NotifyLoginFailed(IPEndPoint remoteEndPoint, string reason = null)
{
SessionCoordinator.NotifyLoginFailed(remoteEndPoint, reason);
PublishMetricsSessionSnapshot(remoteEndPoint);
}
public void NotifyHeartbeatSent(IPEndPoint remoteEndPoint)
{
SessionCoordinator.NotifyHeartbeatSent(remoteEndPoint);
PublishMetricsSessionSnapshot(remoteEndPoint);
}
public void NotifyHeartbeatReceived(IPEndPoint remoteEndPoint, long? serverTick = null)
{
SessionCoordinator.NotifyHeartbeatReceived(remoteEndPoint, serverTick);
PublishMetricsSessionSnapshot(remoteEndPoint);
}
public void ObserveAuthoritativeState(IPEndPoint remoteEndPoint, long? serverTick)
{
SessionCoordinator.ObserveAuthoritativeState(remoteEndPoint, serverTick);
PublishMetricsSessionSnapshot(remoteEndPoint);
}
public void NotifyInboundActivity(IPEndPoint remoteEndPoint)
{
SessionCoordinator.NotifyInboundActivity(remoteEndPoint);
PublishMetricsSessionSnapshot(remoteEndPoint);
}
public bool RemoveSession(IPEndPoint remoteEndPoint, string reason = null)
{
return SessionCoordinator.RemoveSession(remoteEndPoint, reason);
if (!SessionCoordinator.TryGetSession(remoteEndPoint, out var session))
{
return false;
}
var removed = SessionCoordinator.RemoveSession(remoteEndPoint, reason);
if (!removed)
{
return false;
}
RecordMetricsSessionSnapshot(transport, "server-host", session, ConnectionState.Disconnected);
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
{
RecordMetricsSessionSnapshot(syncTransport, "server-host-sync", session, ConnectionState.Disconnected);
}
return true;
}
private void HandleTransportReceive(byte[] _, IPEndPoint sender)
{
SessionCoordinator.ObserveTransportActivity(sender);
PublishMetricsSessionSnapshot(sender);
}
private void PublishMetricsSessionSnapshots()
{
foreach (var session in ManagedSessions)
{
RecordMetricsSessionSnapshot(transport, "server-host", session);
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
{
RecordMetricsSessionSnapshot(syncTransport, "server-host-sync", session);
}
}
}
private void PublishMetricsSessionSnapshot(IPEndPoint remoteEndPoint)
{
if (!TryGetSession(remoteEndPoint, out var session))
{
return;
}
RecordMetricsSessionSnapshot(transport, "server-host", session);
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
{
RecordMetricsSessionSnapshot(syncTransport, "server-host-sync", session);
}
}
private static void RecordMetricsSessionSnapshot(
ITransport targetTransport,
string scope,
ManagedNetworkSession session,
ConnectionState? overrideState = null)
{
if (targetTransport is not ITransportMetricsSink metricsSink || session == null)
{
return;
}
metricsSink.RecordApplicationSessionSnapshot(new TransportApplicationSessionSnapshot
{
Scope = scope,
RemoteEndPoint = session.RemoteEndPoint.ToString(),
ConnectionState = (overrideState ?? session.SessionManager.State).ToString(),
CanSendHeartbeat = overrideState.HasValue ? overrideState.Value == ConnectionState.LoggedIn : session.SessionManager.CanSendHeartbeat,
LastRoundTripTimeMs = session.SessionManager.LastRoundTripTime.HasValue
? (long?)Math.Max(0d, session.SessionManager.LastRoundTripTime.Value.TotalMilliseconds)
: null,
LastFailureReason = session.SessionManager.LastFailureReason,
LastLivenessUtc = session.SessionManager.LastLivenessUtc,
LastHeartbeatSentUtc = session.SessionManager.LastHeartbeatSentUtc,
NextReconnectAtUtc = session.SessionManager.NextReconnectAtUtc,
CurrentServerTick = session.ClockSync.CurrentServerTick,
ObservedAtUtc = DateTimeOffset.UtcNow
});
}
private async Task StartWithSyncAsync(Task transportStartTask)
{
await transportStartTask;
await syncTransport.StartAsync();
}
}
}
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Runtime.InteropServices;
using kcp;
@@ -16,6 +17,9 @@ namespace Network.NetworkTransport
private IKCPCB* _kcp;
private bool _disposed;
private uint _nextUpdateAt;
private readonly Dictionary<uint, uint> _observedSegmentXmitBySequence = new();
private long _observedRetransmissionSends;
private long _observedLossSignals;
public KcpSession(KcpTransport owner, IPEndPoint remoteEndPoint, uint conv)
{
@@ -32,6 +36,7 @@ namespace Network.NetworkTransport
KCP.ikcp_setmtu(_kcp, DefaultMtu);
_nextUpdateAt = GetCurrentTimeMilliseconds();
_owner.RecordSessionDiagnostics(this, "active");
}
public uint Conv { get; }
@@ -68,6 +73,7 @@ namespace Network.NetworkTransport
LastActivityUtc = DateTime.UtcNow;
UpdateNoLock(GetCurrentTimeMilliseconds());
_owner.RecordSessionDiagnostics(this, "active");
}
}
@@ -100,6 +106,7 @@ namespace Network.NetworkTransport
LastActivityUtc = DateTime.UtcNow;
UpdateNoLock(GetCurrentTimeMilliseconds());
_owner.RecordSessionDiagnostics(this, "active");
}
}
@@ -139,6 +146,7 @@ namespace Network.NetworkTransport
}
LastActivityUtc = DateTime.UtcNow;
_owner.RecordSessionDiagnostics(this, "active");
return true;
}
}
@@ -158,6 +166,81 @@ namespace Network.NetworkTransport
}
UpdateNoLock(current);
_owner.RecordSessionDiagnostics(this, "active");
}
}
public TransportSessionDiagnosticsSnapshot CaptureDiagnostics(string lifecycleState)
{
lock (_gate)
{
var now = DateTimeOffset.UtcNow;
if (_disposed || _kcp == null)
{
return new TransportSessionDiagnosticsSnapshot
{
LifecycleState = lifecycleState,
ObservedAtUtc = now,
IdleMs = Math.Max(0L, (long)(now - LastActivityUtc).TotalMilliseconds)
};
}
var retransmittedSegmentsInFlight = 0;
var observedRetransmissionDelta = 0L;
var head = &_kcp->snd_buf;
for (var node = head->next; node != head; node = node->next)
{
var segment = (IKCPSEG*)node;
var currentXmit = Math.Max(0u, segment->xmit);
_observedSegmentXmitBySequence.TryGetValue(segment->sn, out var observedXmit);
var baselineXmit = observedXmit == 0 ? 1u : observedXmit;
if (currentXmit > baselineXmit)
{
observedRetransmissionDelta += currentXmit - baselineXmit;
}
if (currentXmit > observedXmit)
{
_observedSegmentXmitBySequence[segment->sn] = currentXmit;
}
if (currentXmit > 1)
{
retransmittedSegmentsInFlight++;
}
}
if (observedRetransmissionDelta > 0)
{
_observedRetransmissionSends += observedRetransmissionDelta;
_observedLossSignals += observedRetransmissionDelta;
}
return new TransportSessionDiagnosticsSnapshot
{
LifecycleState = lifecycleState,
ObservedAtUtc = now,
IdleMs = Math.Max(0L, (long)(now - LastActivityUtc).TotalMilliseconds),
KcpStateCode = unchecked((int)_kcp->state),
SmoothedRttMs = Math.Max(0, _kcp->rx_srtt),
RttVarianceMs = Math.Max(0, _kcp->rx_rttval),
RetransmissionTimeoutMs = Math.Max(0, _kcp->rx_rto),
LocalSendWindow = checked((int)_kcp->snd_wnd),
LocalReceiveWindow = checked((int)_kcp->rcv_wnd),
RemoteWindow = checked((int)_kcp->rmt_wnd),
CongestionWindow = checked((int)_kcp->cwnd),
WaitSendCount = Math.Max(0, KCP.ikcp_waitsnd(_kcp)),
SendQueueCount = checked((int)_kcp->nsnd_que),
SendBufferCount = checked((int)_kcp->nsnd_buf),
ReceiveQueueCount = checked((int)_kcp->nrcv_que),
ReceiveBufferCount = checked((int)_kcp->nrcv_buf),
DeadLinkThreshold = checked((int)_kcp->dead_link),
SegmentTransmitCount = _kcp->xmit,
RetransmittedSegmentsInFlight = retransmittedSegmentsInFlight,
ObservedRetransmissionSends = _observedRetransmissionSends,
ObservedLossSignals = _observedLossSignals
};
}
}
@@ -221,4 +304,4 @@ namespace Network.NetworkTransport
}
}
}
}
}
@@ -10,7 +10,7 @@ using kcp;
namespace Network.NetworkTransport
{
public partial class KcpTransport : ITransport
public partial class KcpTransport : ITransport, ITransportMetricsSink
{
private const uint DefaultConv = 1;
private const int DefaultNoDelay = 1;
@@ -69,6 +69,11 @@ namespace Network.NetworkTransport
return _metricsModule.GetCurrentSnapshot();
}
public void RecordApplicationSessionSnapshot(TransportApplicationSessionSnapshot snapshot)
{
_metricsModule.RecordApplicationSessionSnapshot(snapshot);
}
public Task StartAsync()
{
if (_isRunning)
@@ -318,6 +323,7 @@ namespace Network.NetworkTransport
foreach (var session in sessions)
{
_metricsModule.RecordSessionDiagnostics(session.RemoteEndPoint, session.CaptureDiagnostics("closed"));
session.Dispose();
_metricsModule.RecordSessionClosed(session.RemoteEndPoint);
}
@@ -358,5 +364,15 @@ namespace Network.NetworkTransport
{
_metricsModule.RecordError(stage, remoteEndPoint, detail);
}
private void RecordSessionDiagnostics(KcpSession session, string lifecycleState)
{
if (session == null)
{
return;
}
_metricsModule.RecordSessionDiagnostics(session.RemoteEndPoint, session.CaptureDiagnostics(lifecycleState));
}
}
}
}
@@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Network.NetworkTransport
{
public static class TransportMetricsDiagnosisFormatter
{
public static string BuildChineseDiagnosis(TransportMetricsSnapshot snapshot)
{
if (snapshot == null)
{
throw new ArgumentNullException(nameof(snapshot));
}
var builder = new StringBuilder();
builder.AppendLine("Transport Metrics Diagnosis / 传输诊断结论");
builder.AppendLine();
builder.AppendLine("结论摘要");
builder.AppendLine($"- 传输实现:{snapshot.TransportName}");
builder.AppendLine($"- 运行模式:{TranslateMode(snapshot.Mode)}");
builder.AppendLine($"- 运行时长:{snapshot.DurationMs} 毫秒");
builder.AppendLine($"- 总体判断:{BuildOverallAssessment(snapshot)}");
builder.AppendLine();
builder.AppendLine("关键观察");
foreach (var finding in BuildFindings(snapshot))
{
builder.AppendLine($"- {finding}");
}
builder.AppendLine();
builder.AppendLine("会话状态");
foreach (var line in BuildSessionLines(snapshot))
{
builder.AppendLine($"- {line}");
}
builder.AppendLine();
builder.AppendLine("热点对端");
foreach (var peerLine in BuildPeerLines(snapshot))
{
builder.AppendLine($"- {peerLine}");
}
return builder.ToString();
}
private static IEnumerable<string> BuildFindings(TransportMetricsSnapshot snapshot)
{
yield return $"业务消息发送/接收={snapshot.PayloadMessagesSent}/{snapshot.PayloadMessagesReceived},数据报发送/接收={snapshot.DatagramsSent}/{snapshot.DatagramsReceived}。";
if (snapshot.ApplicationSessionsTracked > 0)
{
yield return $"共享会话已跟踪 {snapshot.ApplicationSessionsTracked} 个,状态分布:{FormatCounts(snapshot.ApplicationSessionStateCounts)}。";
}
if (snapshot.SessionsWithDiagnostics == 0)
{
yield return "没有采集到 KCP 会话诊断数据,当前无法判断 RTT、队列堆积和重传情况。";
yield break;
}
yield return $"平均 RTT={snapshot.AverageSmoothedRttMs:F1} 毫秒,峰值 RTT={snapshot.PeakSmoothedRttMs} 毫秒。";
yield return $"WaitSnd 总量={snapshot.TotalWaitSendCount},发送队列={snapshot.TotalSendQueueCount},发送缓冲={snapshot.TotalSendBufferCount}。";
yield return $"在途重传段={snapshot.TotalRetransmittedSegmentsInFlight},累计观察到的重传次数={snapshot.TotalObservedRetransmissionSends}。";
if (snapshot.TotalWaitSendCount > 0 || snapshot.TotalSendQueueCount > 0 || snapshot.TotalSendBufferCount > 0)
{
yield return "存在发送侧堆积迹象,建议优先排查发送频率、消息体积和远端处理能力。";
}
if (snapshot.TotalObservedRetransmissionSends > 0 || snapshot.TotalRetransmittedSegmentsInFlight > 0)
{
yield return "存在重传迹象,建议结合网络抖动、丢包和窗口大小继续排查。";
}
if (snapshot.AverageSmoothedRttMs >= 150 || snapshot.PeakSmoothedRttMs >= 250)
{
yield return "延迟已偏高,若游戏表现出现卡顿或回滚,优先关注 RTT 抖动。";
}
if (snapshot.SendErrors + snapshot.ReceiveErrors + snapshot.OtherErrors > 0)
{
yield return $"记录到错误 {snapshot.SendErrors + snapshot.ReceiveErrors + snapshot.OtherErrors} 次,最高频阶段={FindBusiestStage(snapshot)}。";
}
}
private static IEnumerable<string> BuildSessionLines(TransportMetricsSnapshot snapshot)
{
if (snapshot.ApplicationSessionSummaries.Count == 0)
{
yield return "没有共享会话快照。";
yield break;
}
foreach (var session in snapshot.ApplicationSessionSummaries
.OrderBy(item => item.Scope, StringComparer.Ordinal)
.ThenBy(item => item.RemoteEndPoint, StringComparer.Ordinal))
{
var target = string.IsNullOrWhiteSpace(session.RemoteEndPoint) ? "default" : session.RemoteEndPoint;
var details = new List<string>
{
$"scope={session.Scope}",
$"target={target}",
$"state={session.ConnectionState}"
};
if (session.CanSendHeartbeat)
{
details.Add("heartbeat=ready");
}
if (session.LastRoundTripTimeMs.HasValue)
{
details.Add($"rtt={session.LastRoundTripTimeMs.Value}ms");
}
if (session.NextReconnectAtUtc.HasValue)
{
details.Add($"reconnectAt={session.NextReconnectAtUtc.Value:O}");
}
if (!string.IsNullOrWhiteSpace(session.LastFailureReason))
{
details.Add($"reason={session.LastFailureReason}");
}
if (session.CurrentServerTick.HasValue)
{
details.Add($"serverTick={session.CurrentServerTick.Value}");
}
yield return string.Join(", ", details);
}
}
private static IEnumerable<string> BuildPeerLines(TransportMetricsSnapshot snapshot)
{
var peers = snapshot.PeerSummaries
.OrderByDescending(peer => peer.PayloadBytesSent + peer.PayloadBytesReceived)
.ThenBy(peer => peer.RemoteEndPoint, StringComparer.Ordinal)
.Take(5)
.ToList();
if (peers.Count == 0)
{
yield return "没有热点对端数据。";
yield break;
}
foreach (var peer in peers)
{
yield return
$"{peer.RemoteEndPoint}: payload={peer.PayloadMessagesSent + peer.PayloadMessagesReceived} 条, datagram={peer.DatagramsSent + peer.DatagramsReceived} 个, " +
$"waitSnd={peer.SessionDiagnostics.WaitSendCount}, retrans={peer.ObservedRetransmissionSends}, state={peer.SessionLifecycleState}";
}
}
private static string BuildOverallAssessment(TransportMetricsSnapshot snapshot)
{
if (snapshot.SessionsWithDiagnostics == 0 && snapshot.ApplicationSessionsTracked == 0)
{
return "观测数据不足";
}
var hasErrors = snapshot.SendErrors + snapshot.ReceiveErrors + snapshot.OtherErrors > 0;
var hasRetransmissions = snapshot.TotalObservedRetransmissionSends > 0 || snapshot.TotalRetransmittedSegmentsInFlight > 0;
var hasBacklog = snapshot.TotalWaitSendCount > 0 || snapshot.TotalSendQueueCount > 0 || snapshot.TotalSendBufferCount > 0;
var hasReconnect = snapshot.ApplicationSessionStateCounts.TryGetValue("ReconnectPending", out var pending) && pending > 0;
var hasTimeout = snapshot.ApplicationSessionStateCounts.TryGetValue("TimedOut", out var timedOut) && timedOut > 0;
var highLatency = snapshot.AverageSmoothedRttMs >= 150 || snapshot.PeakSmoothedRttMs >= 250;
if (hasTimeout || hasReconnect)
{
return "已出现会话不稳定迹象";
}
if (hasErrors && (hasRetransmissions || hasBacklog || highLatency))
{
return "网络质量存在明显风险";
}
if (hasRetransmissions || hasBacklog || highLatency)
{
return "网络链路存在轻中度异常";
}
return "整体稳定";
}
private static string FindBusiestStage(TransportMetricsSnapshot snapshot)
{
return snapshot.ErrorCountsByStage.Count == 0
? "无"
: snapshot.ErrorCountsByStage
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.First()
.Key;
}
private static string FormatCounts(IReadOnlyDictionary<string, long> counts)
{
if (counts == null || counts.Count == 0)
{
return "无";
}
return string.Join(",", counts.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}={pair.Value}"));
}
private static string TranslateMode(string mode)
{
return string.Equals(mode, "server", StringComparison.OrdinalIgnoreCase)
? "server / 服务端"
: string.Equals(mode, "client", StringComparison.OrdinalIgnoreCase)
? "client / 客户端"
: mode ?? "unknown / 未知";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 71d7507d35817ed418a2e8194de6455e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -14,6 +14,8 @@ namespace Network.NetworkTransport
void BeginRun(TransportRunDescriptor descriptor);
void RecordSessionOpened(IPEndPoint remoteEndPoint);
void RecordSessionClosed(IPEndPoint remoteEndPoint);
void RecordSessionDiagnostics(IPEndPoint remoteEndPoint, TransportSessionDiagnosticsSnapshot diagnostics);
void RecordApplicationSessionSnapshot(TransportApplicationSessionSnapshot snapshot);
void RecordPayloadSent(IPEndPoint remoteEndPoint, int bytes);
void RecordPayloadReceived(IPEndPoint remoteEndPoint, int bytes);
void RecordDatagramSent(IPEndPoint remoteEndPoint, int bytes);
@@ -23,6 +25,11 @@ namespace Network.NetworkTransport
TransportMetricsSnapshot CompleteRun();
}
public interface ITransportMetricsSink
{
void RecordApplicationSessionSnapshot(TransportApplicationSessionSnapshot snapshot);
}
public sealed class TransportRunDescriptor
{
public TransportRunDescriptor(string transportName, bool isServer, IPEndPoint defaultRemoteEndPoint = null)
@@ -55,23 +62,98 @@ namespace Network.NetworkTransport
[DataMember(Order = 6)] public DateTimeOffset? CompletedAtUtc { get; set; }
[DataMember(Order = 7)] public long DurationMs { get; set; }
[DataMember(Order = 8)] public string ReportPath { get; set; }
[DataMember(Order = 9)] public int ActiveSessions { get; set; }
[DataMember(Order = 10)] public int PeakActiveSessions { get; set; }
[DataMember(Order = 11)] public long SessionsCreated { get; set; }
[DataMember(Order = 12)] public long SessionsClosed { get; set; }
[DataMember(Order = 13)] public long PayloadMessagesSent { get; set; }
[DataMember(Order = 14)] public long PayloadBytesSent { get; set; }
[DataMember(Order = 15)] public long PayloadMessagesReceived { get; set; }
[DataMember(Order = 16)] public long PayloadBytesReceived { get; set; }
[DataMember(Order = 17)] public long DatagramsSent { get; set; }
[DataMember(Order = 18)] public long DatagramBytesSent { get; set; }
[DataMember(Order = 19)] public long DatagramsReceived { get; set; }
[DataMember(Order = 20)] public long DatagramBytesReceived { get; set; }
[DataMember(Order = 21)] public long SendErrors { get; set; }
[DataMember(Order = 22)] public long ReceiveErrors { get; set; }
[DataMember(Order = 23)] public long OtherErrors { get; set; }
[DataMember(Order = 24)] public Dictionary<string, long> ErrorCountsByStage { get; set; } = new(StringComparer.Ordinal);
[DataMember(Order = 25)] public List<TransportPeerMetricsSnapshot> PeerSummaries { get; set; } = new();
[DataMember(Order = 9)] public string SummaryPath { get; set; }
[DataMember(Order = 10)] public TransportMetricsReadableSummary ReadableSummary { get; set; } = new();
[DataMember(Order = 11)] public int ActiveSessions { get; set; }
[DataMember(Order = 12)] public int PeakActiveSessions { get; set; }
[DataMember(Order = 13)] public long SessionsCreated { get; set; }
[DataMember(Order = 14)] public long SessionsClosed { get; set; }
[DataMember(Order = 15)] public long PayloadMessagesSent { get; set; }
[DataMember(Order = 16)] public long PayloadBytesSent { get; set; }
[DataMember(Order = 17)] public long PayloadMessagesReceived { get; set; }
[DataMember(Order = 18)] public long PayloadBytesReceived { get; set; }
[DataMember(Order = 19)] public long DatagramsSent { get; set; }
[DataMember(Order = 20)] public long DatagramBytesSent { get; set; }
[DataMember(Order = 21)] public long DatagramsReceived { get; set; }
[DataMember(Order = 22)] public long DatagramBytesReceived { get; set; }
[DataMember(Order = 23)] public long SendErrors { get; set; }
[DataMember(Order = 24)] public long ReceiveErrors { get; set; }
[DataMember(Order = 25)] public long OtherErrors { get; set; }
[DataMember(Order = 26)] public int SessionsWithDiagnostics { get; set; }
[DataMember(Order = 27)] public double AverageSmoothedRttMs { get; set; }
[DataMember(Order = 28)] public int PeakSmoothedRttMs { get; set; }
[DataMember(Order = 29)] public double AverageRetransmissionTimeoutMs { get; set; }
[DataMember(Order = 30)] public int PeakRetransmissionTimeoutMs { get; set; }
[DataMember(Order = 31)] public long TotalWaitSendCount { get; set; }
[DataMember(Order = 32)] public long PeakWaitSendCount { get; set; }
[DataMember(Order = 33)] public long TotalSendQueueCount { get; set; }
[DataMember(Order = 34)] public long TotalSendBufferCount { get; set; }
[DataMember(Order = 35)] public long TotalReceiveQueueCount { get; set; }
[DataMember(Order = 36)] public long TotalReceiveBufferCount { get; set; }
[DataMember(Order = 37)] public long TotalRetransmittedSegmentsInFlight { get; set; }
[DataMember(Order = 38)] public long PeakRetransmittedSegmentsInFlight { get; set; }
[DataMember(Order = 39)] public long TotalObservedRetransmissionSends { get; set; }
[DataMember(Order = 40)] public long TotalObservedLossSignals { get; set; }
[DataMember(Order = 41)] public Dictionary<string, long> SessionStateCounts { get; set; } = new(StringComparer.Ordinal);
[DataMember(Order = 42)] public int ApplicationSessionsTracked { get; set; }
[DataMember(Order = 43)] public Dictionary<string, long> ApplicationSessionStateCounts { get; set; } = new(StringComparer.Ordinal);
[DataMember(Order = 44)] public List<TransportApplicationSessionSnapshot> ApplicationSessionSummaries { get; set; } = new();
[DataMember(Order = 45)] public Dictionary<string, long> ErrorCountsByStage { get; set; } = new(StringComparer.Ordinal);
[DataMember(Order = 46)] public List<TransportPeerMetricsSnapshot> PeerSummaries { get; set; } = new();
}
[DataContract]
public sealed class TransportMetricsReadableSummary
{
[DataMember(Order = 1)] public string Headline { get; set; }
[DataMember(Order = 2)] public string SessionSummary { get; set; }
[DataMember(Order = 3)] public string TrafficSummary { get; set; }
[DataMember(Order = 4)] public string ErrorSummary { get; set; }
[DataMember(Order = 5)] public string LifecycleSummary { get; set; }
[DataMember(Order = 6)] public string HealthSummary { get; set; }
[DataMember(Order = 7)] public List<string> TopPeerHighlights { get; set; } = new();
}
[DataContract]
public sealed class TransportApplicationSessionSnapshot
{
[DataMember(Order = 1)] public string Scope { get; set; }
[DataMember(Order = 2)] public string RemoteEndPoint { get; set; }
[DataMember(Order = 3)] public string ConnectionState { get; set; }
[DataMember(Order = 4)] public bool CanSendHeartbeat { get; set; }
[DataMember(Order = 5)] public long? LastRoundTripTimeMs { get; set; }
[DataMember(Order = 6)] public string LastFailureReason { get; set; }
[DataMember(Order = 7)] public DateTimeOffset? LastLivenessUtc { get; set; }
[DataMember(Order = 8)] public DateTimeOffset? LastHeartbeatSentUtc { get; set; }
[DataMember(Order = 9)] public DateTimeOffset? NextReconnectAtUtc { get; set; }
[DataMember(Order = 10)] public long? CurrentServerTick { get; set; }
[DataMember(Order = 11)] public DateTimeOffset? ObservedAtUtc { get; set; }
}
[DataContract]
public sealed class TransportSessionDiagnosticsSnapshot
{
[DataMember(Order = 1)] public string LifecycleState { get; set; }
[DataMember(Order = 2)] public DateTimeOffset? ObservedAtUtc { get; set; }
[DataMember(Order = 3)] public long IdleMs { get; set; }
[DataMember(Order = 4)] public int KcpStateCode { get; set; }
[DataMember(Order = 5)] public int SmoothedRttMs { get; set; }
[DataMember(Order = 6)] public int RttVarianceMs { get; set; }
[DataMember(Order = 7)] public int RetransmissionTimeoutMs { get; set; }
[DataMember(Order = 8)] public int LocalSendWindow { get; set; }
[DataMember(Order = 9)] public int LocalReceiveWindow { get; set; }
[DataMember(Order = 10)] public int RemoteWindow { get; set; }
[DataMember(Order = 11)] public int CongestionWindow { get; set; }
[DataMember(Order = 12)] public int WaitSendCount { get; set; }
[DataMember(Order = 13)] public int SendQueueCount { get; set; }
[DataMember(Order = 14)] public int SendBufferCount { get; set; }
[DataMember(Order = 15)] public int ReceiveQueueCount { get; set; }
[DataMember(Order = 16)] public int ReceiveBufferCount { get; set; }
[DataMember(Order = 17)] public int DeadLinkThreshold { get; set; }
[DataMember(Order = 18)] public long SegmentTransmitCount { get; set; }
[DataMember(Order = 19)] public int RetransmittedSegmentsInFlight { get; set; }
[DataMember(Order = 20)] public long ObservedRetransmissionSends { get; set; }
[DataMember(Order = 21)] public long ObservedLossSignals { get; set; }
}
[DataContract]
@@ -90,7 +172,15 @@ namespace Network.NetworkTransport
[DataMember(Order = 11)] public long DatagramBytesSent { get; set; }
[DataMember(Order = 12)] public long DatagramsReceived { get; set; }
[DataMember(Order = 13)] public long DatagramBytesReceived { get; set; }
[DataMember(Order = 14)] public Dictionary<string, long> ErrorCountsByStage { get; set; } = new(StringComparer.Ordinal);
[DataMember(Order = 14)] public string SessionLifecycleState { get; set; }
[DataMember(Order = 15)] public TransportSessionDiagnosticsSnapshot SessionDiagnostics { get; set; } = new();
[DataMember(Order = 16)] public int PeakSmoothedRttMs { get; set; }
[DataMember(Order = 17)] public int PeakRetransmissionTimeoutMs { get; set; }
[DataMember(Order = 18)] public int PeakWaitSendCount { get; set; }
[DataMember(Order = 19)] public int PeakRetransmittedSegmentsInFlight { get; set; }
[DataMember(Order = 20)] public long ObservedRetransmissionSends { get; set; }
[DataMember(Order = 21)] public long ObservedLossSignals { get; set; }
[DataMember(Order = 22)] public Dictionary<string, long> ErrorCountsByStage { get; set; } = new(StringComparer.Ordinal);
}
public sealed class DefaultTransportMetricsModule : ITransportMetricsModule
@@ -99,7 +189,14 @@ namespace Network.NetworkTransport
private readonly Func<DateTimeOffset> utcNowProvider;
private readonly TextWriter consoleWriter;
private readonly string reportDirectory;
private readonly bool writeJsonReport;
private readonly bool writeTextSummaryReport;
private readonly bool writeDiagnosisReport;
private readonly bool emitConsoleSummary;
private readonly int maxPeerSummariesInTextReport;
private readonly int maxPeerSummariesInConsole;
private readonly Dictionary<string, PeerAccumulator> peers = new(StringComparer.Ordinal);
private readonly Dictionary<string, TransportApplicationSessionSnapshot> applicationSessions = new(StringComparer.Ordinal);
private readonly Dictionary<string, long> errorCountsByStage = new(StringComparer.Ordinal);
private string runId;
@@ -129,12 +226,32 @@ namespace Network.NetworkTransport
private TransportMetricsSnapshot completedSnapshot;
public DefaultTransportMetricsModule(string reportDirectory = null, Func<DateTimeOffset> utcNowProvider = null, TextWriter consoleWriter = null)
: this(
new TransportMetricsOptions
{
ReportDirectory = reportDirectory,
ConsoleWriter = consoleWriter
},
utcNowProvider)
{
}
public DefaultTransportMetricsModule(TransportMetricsOptions options, Func<DateTimeOffset> utcNowProvider = null)
{
options ??= TransportMetricsOptions.Default;
reportDirectory = options.ReportDirectory;
this.reportDirectory = string.IsNullOrWhiteSpace(reportDirectory)
? Path.Combine(Directory.GetCurrentDirectory(), "Logs", "transport-metrics")
: reportDirectory;
this.utcNowProvider = utcNowProvider ?? (() => DateTimeOffset.UtcNow);
consoleWriter = options.ConsoleWriter;
this.consoleWriter = consoleWriter ?? Console.Out;
writeJsonReport = options.WriteJsonReport;
writeTextSummaryReport = options.WriteTextSummaryReport;
writeDiagnosisReport = options.WriteDiagnosisReport;
emitConsoleSummary = options.EmitConsoleSummary;
maxPeerSummariesInTextReport = Math.Max(0, options.MaxPeerSummariesInTextReport);
maxPeerSummariesInConsole = Math.Max(0, options.MaxPeerSummariesInConsole);
}
public void BeginRun(TransportRunDescriptor descriptor)
@@ -147,6 +264,7 @@ namespace Network.NetworkTransport
lock (gate)
{
peers.Clear();
applicationSessions.Clear();
errorCountsByStage.Clear();
runId = Guid.NewGuid().ToString("N");
transportName = descriptor.TransportName;
@@ -182,6 +300,7 @@ namespace Network.NetworkTransport
activeSessions++;
peakActiveSessions = Math.Max(peakActiveSessions, activeSessions);
peer.SessionOpens++;
peer.SessionLifecycleState = "active";
});
public void RecordSessionClosed(IPEndPoint remoteEndPoint) => Update(remoteEndPoint, peer =>
@@ -189,8 +308,33 @@ namespace Network.NetworkTransport
sessionsClosed++;
activeSessions = Math.Max(0, activeSessions - 1);
peer.SessionCloses++;
peer.SessionLifecycleState = "closed";
});
public void RecordSessionDiagnostics(IPEndPoint remoteEndPoint, TransportSessionDiagnosticsSnapshot diagnostics) => Update(remoteEndPoint, peer =>
{
if (diagnostics == null)
{
return;
}
peer.RecordDiagnostics(diagnostics);
});
public void RecordApplicationSessionSnapshot(TransportApplicationSessionSnapshot snapshot)
{
lock (gate)
{
if (!hasRun || snapshot == null)
{
return;
}
var key = BuildApplicationSessionKey(snapshot.Scope, snapshot.RemoteEndPoint);
applicationSessions[key] = Clone(snapshot);
}
}
public void RecordPayloadSent(IPEndPoint remoteEndPoint, int bytes) => Update(remoteEndPoint, peer =>
{
payloadMessagesSent++;
@@ -286,8 +430,14 @@ namespace Network.NetworkTransport
completed = true;
completedSnapshot = BuildSnapshot();
completedSnapshot.ReportPath = WriteJsonReport(completedSnapshot);
completedSnapshot.SummaryPath = WriteTextSummaryReport(completedSnapshot);
WriteDiagnosisReport(completedSnapshot);
reportPath = completedSnapshot.ReportPath;
consoleWriter.WriteLine($"[TransportMetrics] {completedSnapshot.TransportName} mode={completedSnapshot.Mode} run={completedSnapshot.RunId} durationMs={completedSnapshot.DurationMs} peak={completedSnapshot.PeakActiveSessions} payloadTx={completedSnapshot.PayloadMessagesSent}/{completedSnapshot.PayloadBytesSent}B payloadRx={completedSnapshot.PayloadMessagesReceived}/{completedSnapshot.PayloadBytesReceived}B datagramTx={completedSnapshot.DatagramsSent}/{completedSnapshot.DatagramBytesSent}B datagramRx={completedSnapshot.DatagramsReceived}/{completedSnapshot.DatagramBytesReceived}B errors={completedSnapshot.SendErrors + completedSnapshot.ReceiveErrors + completedSnapshot.OtherErrors} report={completedSnapshot.ReportPath ?? "none"}");
if (emitConsoleSummary)
{
WriteConsoleSummary(completedSnapshot);
}
return completedSnapshot;
}
}
@@ -310,6 +460,26 @@ namespace Network.NetworkTransport
private TransportMetricsSnapshot BuildSnapshot()
{
var end = completedAtUtc ?? utcNowProvider();
var peerSnapshots = peers.Values.Select(peer => peer.ToSnapshot()).OrderBy(peer => peer.RemoteEndPoint, StringComparer.Ordinal).ToList();
var peersWithDiagnostics = peerSnapshots.Where(peer => peer.SessionDiagnostics?.ObservedAtUtc != null).ToList();
var averageSmoothedRttMs = peersWithDiagnostics.Count == 0
? 0d
: peersWithDiagnostics.Average(peer => peer.SessionDiagnostics.SmoothedRttMs);
var averageRetransmissionTimeoutMs = peersWithDiagnostics.Count == 0
? 0d
: peersWithDiagnostics.Average(peer => peer.SessionDiagnostics.RetransmissionTimeoutMs);
var sessionStateCounts = peerSnapshots
.GroupBy(peer => string.IsNullOrWhiteSpace(peer.SessionLifecycleState) ? "unknown" : peer.SessionLifecycleState, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => (long)group.Count(), StringComparer.Ordinal);
var applicationSessionSnapshots = applicationSessions.Values
.Select(Clone)
.OrderBy(snapshot => snapshot.Scope, StringComparer.Ordinal)
.ThenBy(snapshot => snapshot.RemoteEndPoint, StringComparer.Ordinal)
.ToList();
var applicationSessionStateCounts = applicationSessionSnapshots
.GroupBy(snapshot => string.IsNullOrWhiteSpace(snapshot.ConnectionState) ? "unknown" : snapshot.ConnectionState, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => (long)group.Count(), StringComparer.Ordinal);
return new TransportMetricsSnapshot
{
RunId = runId,
@@ -320,6 +490,8 @@ namespace Network.NetworkTransport
CompletedAtUtc = completedAtUtc,
DurationMs = startedAtUtc.HasValue ? Math.Max(0L, (long)(end - startedAtUtc.Value).TotalMilliseconds) : 0L,
ReportPath = reportPath,
SummaryPath = null,
ReadableSummary = BuildReadableSummary(),
ActiveSessions = activeSessions,
PeakActiveSessions = peakActiveSessions,
SessionsCreated = sessionsCreated,
@@ -335,13 +507,107 @@ namespace Network.NetworkTransport
SendErrors = sendErrors,
ReceiveErrors = receiveErrors,
OtherErrors = otherErrors,
SessionsWithDiagnostics = peersWithDiagnostics.Count,
AverageSmoothedRttMs = averageSmoothedRttMs,
PeakSmoothedRttMs = peerSnapshots.Count == 0 ? 0 : peerSnapshots.Max(peer => peer.PeakSmoothedRttMs),
AverageRetransmissionTimeoutMs = averageRetransmissionTimeoutMs,
PeakRetransmissionTimeoutMs = peerSnapshots.Count == 0 ? 0 : peerSnapshots.Max(peer => peer.PeakRetransmissionTimeoutMs),
TotalWaitSendCount = peerSnapshots.Sum(peer => (long)peer.SessionDiagnostics.WaitSendCount),
PeakWaitSendCount = peerSnapshots.Count == 0 ? 0 : peerSnapshots.Max(peer => (long)peer.PeakWaitSendCount),
TotalSendQueueCount = peerSnapshots.Sum(peer => (long)peer.SessionDiagnostics.SendQueueCount),
TotalSendBufferCount = peerSnapshots.Sum(peer => (long)peer.SessionDiagnostics.SendBufferCount),
TotalReceiveQueueCount = peerSnapshots.Sum(peer => (long)peer.SessionDiagnostics.ReceiveQueueCount),
TotalReceiveBufferCount = peerSnapshots.Sum(peer => (long)peer.SessionDiagnostics.ReceiveBufferCount),
TotalRetransmittedSegmentsInFlight = peerSnapshots.Sum(peer => (long)peer.SessionDiagnostics.RetransmittedSegmentsInFlight),
PeakRetransmittedSegmentsInFlight = peerSnapshots.Count == 0 ? 0 : peerSnapshots.Max(peer => (long)peer.PeakRetransmittedSegmentsInFlight),
TotalObservedRetransmissionSends = peerSnapshots.Sum(peer => peer.ObservedRetransmissionSends),
TotalObservedLossSignals = peerSnapshots.Sum(peer => peer.ObservedLossSignals),
SessionStateCounts = sessionStateCounts,
ApplicationSessionsTracked = applicationSessionSnapshots.Count,
ApplicationSessionStateCounts = applicationSessionStateCounts,
ApplicationSessionSummaries = applicationSessionSnapshots,
ErrorCountsByStage = errorCountsByStage.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal),
PeerSummaries = peers.Values.Select(peer => peer.ToSnapshot()).OrderBy(peer => peer.RemoteEndPoint, StringComparer.Ordinal).ToList()
PeerSummaries = peerSnapshots
};
}
private TransportMetricsReadableSummary BuildReadableSummary()
{
var topPeers = peers.Values
.OrderByDescending(peer => peer.PayloadBytesSent + peer.PayloadBytesReceived)
.ThenBy(peer => peer.RemoteEndPoint, StringComparer.Ordinal)
.Take(Math.Max(maxPeerSummariesInTextReport, maxPeerSummariesInConsole))
.Select(peer => $"{peer.RemoteEndPoint}: payload={peer.PayloadMessagesSent + peer.PayloadMessagesReceived} msgs, datagram={peer.DatagramsSent + peer.DatagramsReceived} packets, errors={peer.ErrorCountsByStage.Values.Sum()}")
.ToList();
var totalErrors = sendErrors + receiveErrors + otherErrors;
var busiestErrorStage = errorCountsByStage.Count == 0
? "none"
: errorCountsByStage.OrderByDescending(pair => pair.Value).ThenBy(pair => pair.Key, StringComparer.Ordinal).First().Key;
return new TransportMetricsReadableSummary
{
Headline = $"{transportName} {mode} run {runId} finished in {Math.Max(0L, startedAtUtc.HasValue ? (long)((completedAtUtc ?? utcNowProvider()) - startedAtUtc.Value).TotalMilliseconds : 0L)} ms.",
SessionSummary = $"Sessions active={activeSessions}, peak={peakActiveSessions}, opened={sessionsCreated}, closed={sessionsClosed}, peers={peers.Count}.",
TrafficSummary = $"Payload tx/rx={payloadMessagesSent}/{payloadMessagesReceived} msgs ({payloadBytesSent}/{payloadBytesReceived} B), datagram tx/rx={datagramsSent}/{datagramsReceived} ({datagramBytesSent}/{datagramBytesReceived} B).",
ErrorSummary = totalErrors == 0
? "No transport errors were recorded."
: $"Errors total={totalErrors}, send={sendErrors}, receive={receiveErrors}, other={otherErrors}, busiestStage={busiestErrorStage}.",
LifecycleSummary = BuildLifecycleSummary(),
HealthSummary = BuildHealthSummary(),
TopPeerHighlights = topPeers
};
}
private string BuildLifecycleSummary()
{
if (applicationSessions.Count == 0)
{
return "No shared session lifecycle snapshots were captured.";
}
var stateSummary = string.Join(
", ",
applicationSessions.Values
.GroupBy(snapshot => string.IsNullOrWhiteSpace(snapshot.ConnectionState) ? "unknown" : snapshot.ConnectionState, StringComparer.Ordinal)
.OrderBy(group => group.Key, StringComparer.Ordinal)
.Select(group => $"{group.Key}={group.Count()}"));
var heartbeatReady = applicationSessions.Values.Count(snapshot => snapshot.CanSendHeartbeat);
var pendingReconnects = applicationSessions.Values.Count(snapshot => snapshot.NextReconnectAtUtc.HasValue);
return $"Lifecycle tracked={applicationSessions.Count}, heartbeatReady={heartbeatReady}, reconnectPending={pendingReconnects}, states={stateSummary}.";
}
private string BuildHealthSummary()
{
var diagnosticsPeers = peers.Values.Where(peer => peer.SessionDiagnostics?.ObservedAtUtc != null).ToList();
if (diagnosticsPeers.Count == 0)
{
return "No KCP session diagnostics were captured.";
}
var averageRtt = diagnosticsPeers.Average(peer => peer.SessionDiagnostics.SmoothedRttMs);
var peakRtt = diagnosticsPeers.Max(peer => peer.PeakSmoothedRttMs);
var totalWaitSend = diagnosticsPeers.Sum(peer => peer.SessionDiagnostics.WaitSendCount);
var totalRetransmittedSegments = diagnosticsPeers.Sum(peer => peer.SessionDiagnostics.RetransmittedSegmentsInFlight);
var totalObservedRetransmissions = diagnosticsPeers.Sum(peer => peer.ObservedRetransmissionSends);
var stateSummary = string.Join(
", ",
diagnosticsPeers
.GroupBy(peer => string.IsNullOrWhiteSpace(peer.SessionLifecycleState) ? "unknown" : peer.SessionLifecycleState, StringComparer.Ordinal)
.OrderBy(group => group.Key, StringComparer.Ordinal)
.Select(group => $"{group.Key}={group.Count()}"));
return $"Health avgRtt={averageRtt:F1} ms, peakRtt={peakRtt} ms, waitSnd={totalWaitSend}, retransInFlight={totalRetransmittedSegments}, observedRetransmissions={totalObservedRetransmissions}, states={stateSummary}.";
}
private string WriteJsonReport(TransportMetricsSnapshot snapshot)
{
if (!writeJsonReport)
{
return null;
}
try
{
Directory.CreateDirectory(reportDirectory);
@@ -361,6 +627,282 @@ namespace Network.NetworkTransport
}
}
private string WriteTextSummaryReport(TransportMetricsSnapshot snapshot)
{
if (!writeTextSummaryReport)
{
return null;
}
try
{
Directory.CreateDirectory(reportDirectory);
var timestamp = (snapshot.CompletedAtUtc ?? utcNowProvider()).ToString("yyyyMMdd-HHmmssfff");
var filePath = Path.Combine(reportDirectory, $"{snapshot.Mode}-{snapshot.TransportName}-{timestamp}-{snapshot.RunId}.summary.txt");
File.WriteAllText(filePath, BuildReadableSummaryText(snapshot), new UTF8Encoding(false));
return filePath;
}
catch (Exception exception)
{
consoleWriter.WriteLine($"[TransportMetrics] Failed to write summary: {exception.Message}");
return null;
}
}
private string WriteDiagnosisReport(TransportMetricsSnapshot snapshot)
{
if (!writeDiagnosisReport)
{
return null;
}
try
{
Directory.CreateDirectory(reportDirectory);
var timestamp = (snapshot.CompletedAtUtc ?? utcNowProvider()).ToString("yyyyMMdd-HHmmssfff");
var filePath = Path.Combine(reportDirectory, $"{snapshot.Mode}-{snapshot.TransportName}-{timestamp}-{snapshot.RunId}.diagnosis.txt");
File.WriteAllText(filePath, TransportMetricsDiagnosisFormatter.BuildChineseDiagnosis(snapshot), new UTF8Encoding(false));
return filePath;
}
catch (Exception exception)
{
consoleWriter.WriteLine($"[TransportMetrics] Failed to write diagnosis: {exception.Message}");
return null;
}
}
private void WriteConsoleSummary(TransportMetricsSnapshot snapshot)
{
consoleWriter.WriteLine("[TransportMetrics] English Summary");
consoleWriter.WriteLine($"[TransportMetrics] Run: {snapshot.RunId}");
consoleWriter.WriteLine($"[TransportMetrics] Transport: {snapshot.TransportName}");
consoleWriter.WriteLine($"[TransportMetrics] Mode: {snapshot.Mode}");
consoleWriter.WriteLine($"[TransportMetrics] StartedAtUtc: {snapshot.StartedAtUtc}");
consoleWriter.WriteLine($"[TransportMetrics] CompletedAtUtc: {snapshot.CompletedAtUtc}");
consoleWriter.WriteLine($"[TransportMetrics] DurationMs: {snapshot.DurationMs}");
consoleWriter.WriteLine($"[TransportMetrics] JsonReport: {snapshot.ReportPath ?? "none"}");
consoleWriter.WriteLine($"[TransportMetrics] SummaryReport: {snapshot.SummaryPath ?? "none"}");
consoleWriter.WriteLine($"[TransportMetrics] {snapshot.ReadableSummary.Headline}");
consoleWriter.WriteLine($"[TransportMetrics] {snapshot.ReadableSummary.SessionSummary}");
consoleWriter.WriteLine($"[TransportMetrics] {snapshot.ReadableSummary.TrafficSummary}");
consoleWriter.WriteLine($"[TransportMetrics] {snapshot.ReadableSummary.ErrorSummary}");
consoleWriter.WriteLine($"[TransportMetrics] {snapshot.ReadableSummary.LifecycleSummary}");
consoleWriter.WriteLine($"[TransportMetrics] {snapshot.ReadableSummary.HealthSummary}");
consoleWriter.WriteLine("[TransportMetrics] Top Peers:");
foreach (var line in snapshot.ReadableSummary.TopPeerHighlights.Take(maxPeerSummariesInConsole))
{
consoleWriter.WriteLine($"[TransportMetrics] Peer: {line}");
}
if (snapshot.ReadableSummary.TopPeerHighlights.Count == 0)
{
consoleWriter.WriteLine("[TransportMetrics] Peer: none");
}
consoleWriter.WriteLine("[TransportMetrics] Chinese Summary");
consoleWriter.WriteLine($"[TransportMetrics] 运行ID: {snapshot.RunId}");
consoleWriter.WriteLine($"[TransportMetrics] 传输实现: {snapshot.TransportName}");
consoleWriter.WriteLine($"[TransportMetrics] 运行模式: {TranslateMode(snapshot.Mode)}");
consoleWriter.WriteLine($"[TransportMetrics] 开始时间(UTC): {snapshot.StartedAtUtc}");
consoleWriter.WriteLine($"[TransportMetrics] 结束时间(UTC): {snapshot.CompletedAtUtc}");
consoleWriter.WriteLine($"[TransportMetrics] 总耗时(毫秒): {snapshot.DurationMs}");
consoleWriter.WriteLine($"[TransportMetrics] Json报告: {snapshot.ReportPath ?? "无"}");
consoleWriter.WriteLine($"[TransportMetrics] 摘要报告: {snapshot.SummaryPath ?? "无"}");
consoleWriter.WriteLine($"[TransportMetrics] {BuildChineseHeadline(snapshot)}");
consoleWriter.WriteLine($"[TransportMetrics] {BuildChineseSessionSummary(snapshot)}");
consoleWriter.WriteLine($"[TransportMetrics] {BuildChineseTrafficSummary(snapshot)}");
consoleWriter.WriteLine($"[TransportMetrics] {BuildChineseErrorSummary(snapshot)}");
consoleWriter.WriteLine($"[TransportMetrics] {BuildChineseLifecycleSummary(snapshot)}");
consoleWriter.WriteLine($"[TransportMetrics] {BuildChineseHealthSummary(snapshot)}");
consoleWriter.WriteLine("[TransportMetrics] 重点对端:");
foreach (var peer in snapshot.PeerSummaries
.OrderByDescending(item => item.PayloadBytesSent + item.PayloadBytesReceived)
.ThenBy(item => item.RemoteEndPoint, StringComparer.Ordinal)
.Take(maxPeerSummariesInConsole))
{
consoleWriter.WriteLine($"[TransportMetrics] 对端: {peer.RemoteEndPoint}: 业务消息={peer.PayloadMessagesSent + peer.PayloadMessagesReceived} 条, 数据报={peer.DatagramsSent + peer.DatagramsReceived} 个, 错误={peer.ErrorCountsByStage.Values.Sum()}");
}
if (snapshot.PeerSummaries.Count == 0)
{
consoleWriter.WriteLine("[TransportMetrics] 对端: 无");
}
}
private string BuildReadableSummaryText(TransportMetricsSnapshot snapshot)
{
var builder = new StringBuilder();
builder.AppendLine("Transport Metrics Summary");
builder.AppendLine();
builder.AppendLine("English Summary");
builder.AppendLine();
builder.AppendLine($"Run: {snapshot.RunId}");
builder.AppendLine($"Transport: {snapshot.TransportName}");
builder.AppendLine($"Mode: {snapshot.Mode}");
builder.AppendLine($"StartedAtUtc: {snapshot.StartedAtUtc}");
builder.AppendLine($"CompletedAtUtc: {snapshot.CompletedAtUtc}");
builder.AppendLine($"DurationMs: {snapshot.DurationMs}");
builder.AppendLine();
builder.AppendLine(snapshot.ReadableSummary.Headline);
builder.AppendLine(snapshot.ReadableSummary.SessionSummary);
builder.AppendLine(snapshot.ReadableSummary.TrafficSummary);
builder.AppendLine(snapshot.ReadableSummary.ErrorSummary);
builder.AppendLine(snapshot.ReadableSummary.LifecycleSummary);
builder.AppendLine(snapshot.ReadableSummary.HealthSummary);
builder.AppendLine();
builder.AppendLine("Top Peers:");
foreach (var line in snapshot.ReadableSummary.TopPeerHighlights.Take(maxPeerSummariesInTextReport))
{
builder.AppendLine($"- {line}");
}
if (snapshot.ReadableSummary.TopPeerHighlights.Count == 0)
{
builder.AppendLine("- none");
}
builder.AppendLine();
builder.AppendLine("Paths:");
builder.AppendLine($"- JsonReport: {snapshot.ReportPath ?? "disabled"}");
builder.AppendLine($"- SummaryReport: {snapshot.SummaryPath ?? "pending"}");
builder.AppendLine();
builder.AppendLine("Chinese Summary");
builder.AppendLine();
builder.AppendLine($"运行ID: {snapshot.RunId}");
builder.AppendLine($"传输实现: {snapshot.TransportName}");
builder.AppendLine($"运行模式: {TranslateMode(snapshot.Mode)}");
builder.AppendLine($"开始时间(UTC): {snapshot.StartedAtUtc}");
builder.AppendLine($"结束时间(UTC): {snapshot.CompletedAtUtc}");
builder.AppendLine($"总耗时(毫秒): {snapshot.DurationMs}");
builder.AppendLine();
builder.AppendLine(BuildChineseHeadline(snapshot));
builder.AppendLine(BuildChineseSessionSummary(snapshot));
builder.AppendLine(BuildChineseTrafficSummary(snapshot));
builder.AppendLine(BuildChineseErrorSummary(snapshot));
builder.AppendLine(BuildChineseLifecycleSummary(snapshot));
builder.AppendLine(BuildChineseHealthSummary(snapshot));
builder.AppendLine();
builder.AppendLine("重点对端:");
foreach (var peer in snapshot.PeerSummaries
.OrderByDescending(item => item.PayloadBytesSent + item.PayloadBytesReceived)
.ThenBy(item => item.RemoteEndPoint, StringComparer.Ordinal)
.Take(maxPeerSummariesInTextReport))
{
builder.AppendLine($"- {peer.RemoteEndPoint}: 业务消息={peer.PayloadMessagesSent + peer.PayloadMessagesReceived} 条, 数据报={peer.DatagramsSent + peer.DatagramsReceived} 个, 错误={peer.ErrorCountsByStage.Values.Sum()}");
}
if (snapshot.PeerSummaries.Count == 0)
{
builder.AppendLine("- 无");
}
builder.AppendLine();
builder.AppendLine("文件路径:");
builder.AppendLine($"- Json报告: {snapshot.ReportPath ?? "已禁用"}");
builder.AppendLine($"- 摘要报告: {snapshot.SummaryPath ?? "待写入"}");
return builder.ToString();
}
private static string TranslateMode(string mode)
{
return string.Equals(mode, "server", StringComparison.OrdinalIgnoreCase)
? "server / 服务端"
: string.Equals(mode, "client", StringComparison.OrdinalIgnoreCase)
? "client / 客户端"
: mode ?? "unknown / 未知";
}
private static string BuildChineseHeadline(TransportMetricsSnapshot snapshot)
{
return $"{snapshot.TransportName} {TranslateMode(snapshot.Mode)} 运行已完成,总耗时 {snapshot.DurationMs} 毫秒。";
}
private static string BuildChineseSessionSummary(TransportMetricsSnapshot snapshot)
{
return $"会话统计:当前活跃={snapshot.ActiveSessions},峰值={snapshot.PeakActiveSessions},建立={snapshot.SessionsCreated},关闭={snapshot.SessionsClosed},对端数={snapshot.PeerSummaries.Count}。";
}
private static string BuildChineseTrafficSummary(TransportMetricsSnapshot snapshot)
{
return $"流量统计:业务消息发送/接收={snapshot.PayloadMessagesSent}/{snapshot.PayloadMessagesReceived} 条({snapshot.PayloadBytesSent}/{snapshot.PayloadBytesReceived} B),数据报发送/接收={snapshot.DatagramsSent}/{snapshot.DatagramsReceived} 个({snapshot.DatagramBytesSent}/{snapshot.DatagramBytesReceived} B)。";
}
private static string BuildChineseErrorSummary(TransportMetricsSnapshot snapshot)
{
var totalErrors = snapshot.SendErrors + snapshot.ReceiveErrors + snapshot.OtherErrors;
if (totalErrors == 0)
{
return "错误统计:未记录到传输错误。";
}
var busiestErrorStage = snapshot.ErrorCountsByStage.Count == 0
? "无"
: snapshot.ErrorCountsByStage.OrderByDescending(pair => pair.Value).ThenBy(pair => pair.Key, StringComparer.Ordinal).First().Key;
return $"错误统计:总数={totalErrors},发送={snapshot.SendErrors},接收={snapshot.ReceiveErrors},其他={snapshot.OtherErrors},最高频阶段={busiestErrorStage}。";
}
private static string BuildChineseLifecycleSummary(TransportMetricsSnapshot snapshot)
{
if (snapshot.ApplicationSessionsTracked == 0)
{
return "生命周期摘要:未捕获到共享会话状态快照。";
}
var states = snapshot.ApplicationSessionStateCounts.Count == 0
? "无"
: string.Join(",", snapshot.ApplicationSessionStateCounts.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}={pair.Value}"));
var heartbeatReady = snapshot.ApplicationSessionSummaries.Count(session => session.CanSendHeartbeat);
var reconnectPending = snapshot.ApplicationSessionSummaries.Count(session => session.NextReconnectAtUtc.HasValue);
return $"生命周期摘要:已跟踪={snapshot.ApplicationSessionsTracked},可发送心跳={heartbeatReady},等待重连={reconnectPending},状态分布={states}。";
}
private static string BuildChineseHealthSummary(TransportMetricsSnapshot snapshot)
{
if (snapshot.SessionsWithDiagnostics == 0)
{
return "健康摘要:未捕获到 KCP 会话诊断数据。";
}
var states = snapshot.SessionStateCounts.Count == 0
? "无"
: string.Join(",", snapshot.SessionStateCounts.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}={pair.Value}"));
return $"健康摘要:平均 RTT={snapshot.AverageSmoothedRttMs:F1} 毫秒,峰值 RTT={snapshot.PeakSmoothedRttMs} 毫秒,WaitSnd={snapshot.TotalWaitSendCount},在途重传段={snapshot.TotalRetransmittedSegmentsInFlight},累计观察到的重传次数={snapshot.TotalObservedRetransmissionSends},状态分布={states}。";
}
private static string BuildApplicationSessionKey(string scope, string remoteEndPoint)
{
var normalizedScope = string.IsNullOrWhiteSpace(scope) ? "default" : scope;
var normalizedRemote = string.IsNullOrWhiteSpace(remoteEndPoint) ? "default" : remoteEndPoint;
return normalizedScope + "|" + normalizedRemote;
}
private static TransportApplicationSessionSnapshot Clone(TransportApplicationSessionSnapshot snapshot)
{
if (snapshot == null)
{
return null;
}
return new TransportApplicationSessionSnapshot
{
Scope = snapshot.Scope,
RemoteEndPoint = snapshot.RemoteEndPoint,
ConnectionState = snapshot.ConnectionState,
CanSendHeartbeat = snapshot.CanSendHeartbeat,
LastRoundTripTimeMs = snapshot.LastRoundTripTimeMs,
LastFailureReason = snapshot.LastFailureReason,
LastLivenessUtc = snapshot.LastLivenessUtc,
LastHeartbeatSentUtc = snapshot.LastHeartbeatSentUtc,
NextReconnectAtUtc = snapshot.NextReconnectAtUtc,
CurrentServerTick = snapshot.CurrentServerTick,
ObservedAtUtc = snapshot.ObservedAtUtc
};
}
private PeerAccumulator GetOrCreatePeer(IPEndPoint remoteEndPoint)
{
var key = FormatEndPoint(remoteEndPoint) ?? "unknown";
@@ -485,8 +1027,30 @@ namespace Network.NetworkTransport
public long DatagramBytesSent { get; set; }
public long DatagramsReceived { get; set; }
public long DatagramBytesReceived { get; set; }
public string SessionLifecycleState { get; set; } = "active";
public TransportSessionDiagnosticsSnapshot SessionDiagnostics { get; private set; } = new();
public int PeakSmoothedRttMs { get; private set; }
public int PeakRetransmissionTimeoutMs { get; private set; }
public int PeakWaitSendCount { get; private set; }
public int PeakRetransmittedSegmentsInFlight { get; private set; }
public long ObservedRetransmissionSends { get; private set; }
public long ObservedLossSignals { get; private set; }
public Dictionary<string, long> ErrorCountsByStage { get; } = new(StringComparer.Ordinal);
public void RecordDiagnostics(TransportSessionDiagnosticsSnapshot diagnostics)
{
SessionLifecycleState = string.IsNullOrWhiteSpace(diagnostics.LifecycleState)
? SessionLifecycleState
: diagnostics.LifecycleState;
SessionDiagnostics = diagnostics;
PeakSmoothedRttMs = Math.Max(PeakSmoothedRttMs, diagnostics.SmoothedRttMs);
PeakRetransmissionTimeoutMs = Math.Max(PeakRetransmissionTimeoutMs, diagnostics.RetransmissionTimeoutMs);
PeakWaitSendCount = Math.Max(PeakWaitSendCount, diagnostics.WaitSendCount);
PeakRetransmittedSegmentsInFlight = Math.Max(PeakRetransmittedSegmentsInFlight, diagnostics.RetransmittedSegmentsInFlight);
ObservedRetransmissionSends = Math.Max(ObservedRetransmissionSends, diagnostics.ObservedRetransmissionSends);
ObservedLossSignals = Math.Max(ObservedLossSignals, diagnostics.ObservedLossSignals);
}
public TransportPeerMetricsSnapshot ToSnapshot()
{
return new TransportPeerMetricsSnapshot
@@ -504,6 +1068,14 @@ namespace Network.NetworkTransport
DatagramBytesSent = DatagramBytesSent,
DatagramsReceived = DatagramsReceived,
DatagramBytesReceived = DatagramBytesReceived,
SessionLifecycleState = SessionLifecycleState,
SessionDiagnostics = SessionDiagnostics,
PeakSmoothedRttMs = PeakSmoothedRttMs,
PeakRetransmissionTimeoutMs = PeakRetransmissionTimeoutMs,
PeakWaitSendCount = PeakWaitSendCount,
PeakRetransmittedSegmentsInFlight = PeakRetransmittedSegmentsInFlight,
ObservedRetransmissionSends = ObservedRetransmissionSends,
ObservedLossSignals = ObservedLossSignals,
ErrorCountsByStage = ErrorCountsByStage.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal)
};
}
@@ -0,0 +1,26 @@
using System;
using System.IO;
namespace Network.NetworkTransport
{
public sealed class TransportMetricsOptions
{
public static TransportMetricsOptions Default { get; } = new();
public string ReportDirectory { get; set; }
public TextWriter ConsoleWriter { get; set; }
public bool WriteJsonReport { get; set; } = true;
public bool WriteTextSummaryReport { get; set; } = true;
public bool WriteDiagnosisReport { get; set; } = true;
public bool EmitConsoleSummary { get; set; } = true;
public int MaxPeerSummariesInTextReport { get; set; } = 5;
public int MaxPeerSummariesInConsole { get; set; } = 3;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 34979a7bd532d8e4b8f80a2f9fcfdea2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,44 @@
using System;
using System.IO;
using System.Linq;
namespace Network.NetworkTransport
{
public static class TransportMetricsReportLocator
{
public static string GetDefaultReportDirectory()
{
return Path.Combine(Directory.GetCurrentDirectory(), "Logs", "transport-metrics");
}
public static string TryGetLatestDiagnosisPath(string reportDirectory = null)
{
var directory = string.IsNullOrWhiteSpace(reportDirectory)
? GetDefaultReportDirectory()
: reportDirectory;
if (!Directory.Exists(directory))
{
return null;
}
return new DirectoryInfo(directory)
.GetFiles("*.diagnosis.txt", SearchOption.TopDirectoryOnly)
.OrderByDescending(file => file.LastWriteTimeUtc)
.ThenByDescending(file => file.Name, StringComparer.Ordinal)
.Select(file => file.FullName)
.FirstOrDefault();
}
public static string ReadLatestDiagnosisText(string reportDirectory = null)
{
var path = TryGetLatestDiagnosisPath(reportDirectory);
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
return null;
}
return File.ReadAllText(path);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2858757643e325e409331d4c4df1f956
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: