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();
}
}
}
}
}