ConnectionHealthMonitor.cs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading.Tasks;
  5. namespace Microsoft.Xna.Framework.Net
  6. {
  7. /// <summary>
  8. /// Monitors connection health using heartbeats and detects disconnections.
  9. /// </summary>
  10. public class ConnectionHealthMonitor
  11. {
  12. private const int HEARTBEAT_INTERVAL_MS = 3000; // Every 3 seconds (LAN)
  13. private const int HEARTBEAT_TIMEOUT_MS = 9000; // 3 missed = disconnect (fast for LAN)
  14. private readonly Dictionary<string, GamerConnectionState> connections = new Dictionary<string, GamerConnectionState>();
  15. private uint sequenceNumber = 0;
  16. private NetworkSession session;
  17. private bool isRunning = false;
  18. private class GamerConnectionState
  19. {
  20. public DateTime LastHeartbeat { get; set; } = DateTime.UtcNow;
  21. public int MissedHeartbeats { get; set; } = 0;
  22. public TimeSpan LastRtt { get; set; } = TimeSpan.Zero;
  23. public uint LastSequenceNumber { get; set; } = 0;
  24. }
  25. /// <summary>
  26. /// Starts monitoring connection health for the given session.
  27. /// </summary>
  28. public void StartMonitoring(NetworkSession session)
  29. {
  30. if (isRunning)
  31. return;
  32. this.session = session;
  33. isRunning = true;
  34. // Initialize connection tracking for all remote gamers
  35. lock (connections)
  36. {
  37. foreach (var gamer in session.AllGamers.Where(g => !g.IsLocal))
  38. {
  39. connections[gamer.Id] = new GamerConnectionState();
  40. }
  41. }
  42. // Start heartbeat sender task
  43. Task.Run(async () => await HeartbeatLoopAsync());
  44. }
  45. /// <summary>
  46. /// Stops monitoring.
  47. /// </summary>
  48. public void StopMonitoring()
  49. {
  50. isRunning = false;
  51. }
  52. private async Task HeartbeatLoopAsync()
  53. {
  54. while (isRunning && session != null && !session.disposed)
  55. {
  56. try
  57. {
  58. // Get the session's local gamer (not the static one, which can be stale)
  59. var localGamer = session.LocalGamers.FirstOrDefault();
  60. if (localGamer == null)
  61. continue; // No local gamer, skip this heartbeat
  62. // Send heartbeat to all remote gamers
  63. var heartbeat = new HeartbeatMessage
  64. {
  65. Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
  66. SequenceNumber = sequenceNumber++,
  67. GamerCount = session.AllGamers.Count,
  68. GamerId = localGamer.Id
  69. };
  70. var writer = new PacketWriter();
  71. heartbeat.Serialize(writer);
  72. var data = writer.GetData();
  73. // Send to each remote gamer individually (peer-to-peer)
  74. var remoteGamers = session.AllGamers.Where(g => !g.IsLocal).ToList();
  75. Console.WriteLine($"[HEARTBEAT-SEND] Sending to {remoteGamers.Count} remote gamer(s)");
  76. foreach (var gamer in remoteGamers)
  77. {
  78. Console.WriteLine($"[HEARTBEAT-SEND] -> {gamer.Gamertag} ({gamer.Id})");
  79. session.SendDataToGamer(gamer, data, SendDataOptions.None);
  80. }
  81. // Check for disconnections
  82. CheckForTimeouts();
  83. await Task.Delay(HEARTBEAT_INTERVAL_MS);
  84. }
  85. catch (Exception ex)
  86. {
  87. Console.WriteLine($"[HEARTBEAT] Error in heartbeat loop: {ex.Message}");
  88. }
  89. }
  90. }
  91. private void CheckForTimeouts()
  92. {
  93. if (session == null)
  94. return;
  95. var now = DateTime.UtcNow;
  96. var disconnected = new List<NetworkGamer>();
  97. lock (connections)
  98. {
  99. foreach (var kvp in connections.ToList())
  100. {
  101. var state = kvp.Value;
  102. var timeSinceLastHB = (now - state.LastHeartbeat).TotalMilliseconds;
  103. if (timeSinceLastHB > HEARTBEAT_TIMEOUT_MS)
  104. {
  105. // Mark for removal
  106. var gamer = session.AllGamers.FirstOrDefault(g => g.Id == kvp.Key);
  107. if (gamer != null)
  108. {
  109. disconnected.Add(gamer);
  110. Console.WriteLine($"[HEARTBEAT] {gamer.Gamertag} timed out (no response for {timeSinceLastHB:F0}ms)");
  111. }
  112. }
  113. }
  114. }
  115. // Remove disconnected gamers (outside lock to avoid deadlock)
  116. foreach (var gamer in disconnected)
  117. {
  118. // Use reflection to call internal RemoveGamer method
  119. var removeMethod = session.GetType().GetMethod("RemoveGamer",
  120. System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
  121. removeMethod?.Invoke(session, new object[] { gamer });
  122. lock (connections)
  123. {
  124. connections.Remove(gamer.Id);
  125. }
  126. }
  127. }
  128. /// <summary>
  129. /// Called when HeartbeatMessage is received.
  130. /// </summary>
  131. public void OnHeartbeatReceived(string gamerId, HeartbeatMessage heartbeat)
  132. {
  133. Console.WriteLine($"[HEARTBEAT-RECV] Received from gamer {gamerId}, seq {heartbeat.SequenceNumber}");
  134. lock (connections)
  135. {
  136. if (!connections.ContainsKey(gamerId))
  137. {
  138. // New remote gamer
  139. connections[gamerId] = new GamerConnectionState();
  140. Console.WriteLine($"[HEARTBEAT-RECV] Created new connection state for {gamerId}");
  141. }
  142. var state = connections[gamerId];
  143. state.LastHeartbeat = DateTime.UtcNow;
  144. state.MissedHeartbeats = 0;
  145. state.LastSequenceNumber = heartbeat.SequenceNumber;
  146. }
  147. // Send reply for RTT measurement using session's local gamer (not static property)
  148. var localGamer = session?.LocalGamers.FirstOrDefault();
  149. var reply = new HeartbeatReplyMessage
  150. {
  151. RequestTimestamp = heartbeat.Timestamp,
  152. ReplyTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
  153. GamerId = localGamer?.Id ?? string.Empty
  154. };
  155. var writer = new PacketWriter();
  156. reply.Serialize(writer);
  157. // Find the gamer and send reply
  158. var gamer = session?.AllGamers.FirstOrDefault(g => g.Id == gamerId);
  159. Console.WriteLine($"[HEARTBEAT-REPLY] Found gamer: {gamer?.Gamertag ?? "NULL"}, sending reply");
  160. if (gamer != null)
  161. {
  162. session?.SendDataToGamer(gamer, writer.GetData(), SendDataOptions.None);
  163. }
  164. else
  165. {
  166. Console.WriteLine($"[HEARTBEAT-REPLY] ERROR: Could not find gamer {gamerId} to send reply!");
  167. }
  168. }
  169. /// <summary>
  170. /// Called when HeartbeatReplyMessage is received (for RTT calculation).
  171. /// </summary>
  172. public void OnHeartbeatReplyReceived(string gamerId, HeartbeatReplyMessage reply)
  173. {
  174. Console.WriteLine($"[HEARTBEAT-REPLY-RECV] Received reply from {gamerId}");
  175. lock (connections)
  176. {
  177. if (connections.TryGetValue(gamerId, out var state))
  178. {
  179. var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
  180. var rtt = TimeSpan.FromMilliseconds(now - reply.RequestTimestamp);
  181. state.LastRtt = rtt;
  182. // Update gamer's RTT for QoS display
  183. var gamer = session?.AllGamers.FirstOrDefault(g => g.Id == gamerId);
  184. gamer?.UpdateRoundtripTime(rtt);
  185. }
  186. }
  187. }
  188. /// <summary>
  189. /// Called when a new gamer joins to start tracking them.
  190. /// </summary>
  191. public void OnGamerJoined(NetworkGamer gamer)
  192. {
  193. if (gamer == null || gamer.IsLocal)
  194. return;
  195. lock (connections)
  196. {
  197. connections[gamer.Id] = new GamerConnectionState();
  198. }
  199. }
  200. /// <summary>
  201. /// Called when a gamer leaves to stop tracking them.
  202. /// </summary>
  203. public void OnGamerLeft(NetworkGamer gamer)
  204. {
  205. if (gamer == null)
  206. return;
  207. lock (connections)
  208. {
  209. connections.Remove(gamer.Id);
  210. }
  211. }
  212. }
  213. }