using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Threading; namespace Hazel.Udp { partial class UdpConnection { /// /// Class to hold packet data /// public class PingPacket : IRecyclable { private static readonly ObjectPool PacketPool = new ObjectPool(() => new PingPacket()); public readonly Stopwatch Stopwatch = new Stopwatch(); internal static PingPacket GetObject() { return PacketPool.GetObject(); } public void Recycle() { Stopwatch.Stop(); PacketPool.PutObject(this); } } internal ConcurrentDictionary activePingPackets = new ConcurrentDictionary(); /// /// The interval from data being received or transmitted to a keepalive packet being sent in milliseconds. /// /// /// /// Keepalive packets serve to close connections when an endpoint abruptly disconnects and to ensure than any /// NAT devices do not close their translation for our argument. By ensuring there is regular contact the /// connection can detect and prevent these issues. /// /// /// The default value is 10 seconds, set to System.Threading.Timeout.Infinite to disable keepalive packets. /// /// public int KeepAliveInterval { get { return keepAliveInterval; } set { keepAliveInterval = value; ResetKeepAliveTimer(); } } private int keepAliveInterval = 1500; public int MissingPingsUntilDisconnect { get; set; } = 6; private volatile int pingsSinceAck = 0; /// /// The timer creating keepalive pulses. /// private Timer keepAliveTimer; /// /// Starts the keepalive timer. /// protected void InitializeKeepAliveTimer() { keepAliveTimer = new Timer( HandleKeepAlive, null, keepAliveInterval, keepAliveInterval ); } private void HandleKeepAlive(object state) { if (this.State != ConnectionState.Connected) return; if (this.pingsSinceAck >= this.MissingPingsUntilDisconnect) { this.DisposeKeepAliveTimer(); this.DisconnectInternal(HazelInternalErrors.PingsWithoutResponse, $"Sent {this.pingsSinceAck} pings that remote has not responded to."); return; } try { this.pingsSinceAck++; SendPing(); } catch { } } // Pings are special, quasi-reliable packets. // We send them to trigger responses that validate our connection is alive // An unacked ping should never be the sole cause of a disconnect. // Rather, the responses will reset our pingsSinceAck, enough unacked // pings should cause a disconnect. private void SendPing() { ushort id = (ushort)Interlocked.Increment(ref lastIDAllocated); byte[] bytes = new byte[3]; bytes[0] = (byte)UdpSendOption.Ping; bytes[1] = (byte)(id >> 8); bytes[2] = (byte)id; PingPacket pkt; if (!this.activePingPackets.TryGetValue(id, out pkt)) { pkt = PingPacket.GetObject(); if (!this.activePingPackets.TryAdd(id, pkt)) { throw new Exception("This shouldn't be possible"); } } pkt.Stopwatch.Restart(); WriteBytesToConnection(bytes, bytes.Length); Statistics.LogReliableSend(0); } /// /// Resets the keepalive timer to zero. /// protected void ResetKeepAliveTimer() { try { keepAliveTimer?.Change(keepAliveInterval, keepAliveInterval); } catch { } } /// /// Disposes of the keep alive timer. /// private void DisposeKeepAliveTimer() { if (this.keepAliveTimer != null) { this.keepAliveTimer.Dispose(); } foreach (var kvp in activePingPackets) { if (this.activePingPackets.TryRemove(kvp.Key, out var pkt)) { pkt.Recycle(); } } } } }