using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; namespace Crysome.Common.Network; public sealed class RudpChannel : IDisposable { private sealed class Pending { public uint Seq; public byte[][] Datagrams; public DateTime SentAt; public DateTime LastSent; public int Retries; } private sealed class FragBuf { public byte[][] Frags; public int RecvCount; public DateTime Created = DateTime.UtcNow; public bool IsComplete { get { if (Frags != null) { return RecvCount >= Frags.Length; } return false; } } } public const int HeaderSize = 19; private const int WireMtuSafePayload = 1200; public const byte F_RELIABLE = 1; public const byte F_UNRELIABLE = 2; public const byte F_ACK = 4; public const byte F_SYN = 8; public const byte F_SYNACK = 16; public const byte F_FIN = 32; public const byte F_KEEPALIVE = 64; private const int InitRto = 500; private const int MaxRto = 16000; private const int MaxRetries = 15; private const int KeepaliveMs = 5000; private const int TimeoutMs = 30000; private const int MaxInFlight = 64; private readonly UdpClient _socket; private readonly bool _ownsSocket; private uint _sendSeq; private uint _recvSeq; private uint _unrelSendSeq; private uint _unrelLastSeq; private readonly object _seqLock = new object(); private readonly ConcurrentDictionary _pending = new ConcurrentDictionary(); private readonly SortedDictionary _ooo = new SortedDictionary(); private readonly object _oooLock = new object(); private readonly ConcurrentDictionary _fragBufs = new ConcurrentDictionary(); private readonly ConcurrentDictionary _relFragBufs = new ConcurrentDictionary(); private int _rtoMs = 500; private bool _rtoDoubledThisCycle; private DateTime _lastActivity = DateTime.UtcNow; private CancellationTokenSource _cts; private bool _disposed; private readonly SemaphoreSlim _sendWindow = new SemaphoreSlim(64, 64); private readonly object _statsLock = new object(); private double _srtt = 100.0; private double _rttvar = 50.0; public uint SessionId { get; private set; } public IPEndPoint Remote { get; private set; } public bool IsAlive { get { if (!_disposed) { return (DateTime.UtcNow - _lastActivity).TotalMilliseconds < 30000.0; } return false; } } public double RttMs { get; private set; } = 100.0; public double LossRate { get; private set; } public long BytesSent { get; private set; } public long BytesRecv { get; private set; } public long PktsSent { get; private set; } public long PktsLost { get; private set; } public event Action ReliableReceived; public event Action UnreliableFrameReceived; public event Action Disconnected; public event Action CongestionChanged; public RudpChannel(uint sessionId, IPEndPoint remote, int localPort = 0) { SessionId = sessionId; Remote = remote; _socket = new UdpClient(localPort); _socket.Client.ReceiveBufferSize = 8388608; _socket.Client.SendBufferSize = 8388608; _ownsSocket = true; } public RudpChannel(uint sessionId, IPEndPoint remote, UdpClient sharedSocket) { SessionId = sessionId; Remote = remote; _socket = sharedSocket; _ownsSocket = false; } public void Start() { _cts = new CancellationTokenSource(); if (_ownsSocket) { Task.Run(delegate { OwnedReceiveLoop(_cts.Token); }); } Task.Run(delegate { RetransmitLoop(_cts.Token); }); Task.Run(delegate { KeepaliveLoop(_cts.Token); }); } public void Dispose() { if (_disposed) { return; } _disposed = true; try { _cts?.Cancel(); } catch { } if (_ownsSocket) { try { _socket?.Close(); } catch { } } int count = _pending.Count; _pending.Clear(); for (int i = 0; i < count + 64; i++) { try { _sendWindow.Release(); } catch { } } } public void SendReliable(byte[] payload) { if (_disposed) { return; } _sendWindow.Wait(); if (_disposed) { _sendWindow.Release(); return; } int num = Math.Max(1, (payload.Length + 1200 - 1) / 1200); uint num2; lock (_seqLock) { num2 = ++_sendSeq; } byte[][] array = new byte[num][]; for (ushort num3 = 0; num3 < num; num3++) { int num4 = num3 * 1200; int payLen = Math.Min(1200, payload.Length - num4); array[num3] = BuildDatagram(1, num2, 0u, num3, (ushort)num, 0, payload, num4, payLen); } Pending value = new Pending { Seq = num2, Datagrams = array, SentAt = DateTime.UtcNow, LastSent = DateTime.UtcNow }; _pending[num2] = value; bool flag = num > 4; byte[][] array2 = array; foreach (byte[] dg in array2) { RawSend(dg); if (flag) { Thread.Sleep(2); } } PktsSent++; } public void SendUnreliable(byte packetTypeId, byte[] payload) { if (_disposed) { return; } uint seqNum; lock (_seqLock) { seqNum = ++_unrelSendSeq; } int num = Math.Max(1, (payload.Length + 1200 - 1) / 1200); for (ushort num2 = 0; num2 < num; num2++) { int num3 = num2 * 1200; int payLen = Math.Min(1200, payload.Length - num3); byte[] dg = BuildDatagram(2, seqNum, 0u, num2, (ushort)num, packetTypeId, payload, num3, payLen); RawSend(dg); if (num > 16 && (num2 & 0xF) == 15 && num2 < num - 1) { Thread.Sleep(1); } } } public void SendSyn() { byte[] dg = BuildDatagram(8, 0u, 0u, 0, 1, 0, new byte[0], 0, 0); RawSend(dg); } public void SendSynAck() { byte[] dg = BuildDatagram(16, SessionId, 0u, 0, 1, 0, new byte[0], 0, 0); RawSend(dg); } public void SendFin() { byte[] dg = BuildDatagram(32, 0u, 0u, 0, 1, 0, new byte[0], 0, 0); try { RawSend(dg); } catch { } } public void FeedDatagram(byte[] datagram) { ProcessDatagram(datagram); } private async void OwnedReceiveLoop(CancellationToken ct) { while (!ct.IsCancellationRequested && !_disposed) { try { ProcessDatagram((await _socket.ReceiveAsync()).Buffer); } catch (ObjectDisposedException) { break; } catch { } } } private void ProcessDatagram(byte[] dg) { if (dg == null || dg.Length < 19) { return; } _lastActivity = DateTime.UtcNow; BytesRecv += dg.Length; BitConverter.ToUInt32(dg, 0); byte b = dg[4]; uint seq = BitConverter.ToUInt32(dg, 5); uint ackSeq = BitConverter.ToUInt32(dg, 9); ushort fragIdx = BitConverter.ToUInt16(dg, 13); ushort fragCount = BitConverter.ToUInt16(dg, 15); byte typeId = dg[17]; int payLen = dg.Length - 19; if ((b & 4) != 0) { HandleAck(ackSeq); } else if ((b & 0x40) == 0) { if ((b & 0x20) != 0) { TriggerDisconnect(); } else if ((b & 1) != 0) { HandleReliableData(seq, fragIdx, fragCount, dg, payLen); } else if ((b & 2) != 0) { HandleUnreliableData(seq, typeId, fragIdx, fragCount, dg, payLen); } } } private void HandleAck(uint ackSeq) { if (_pending.TryRemove(ackSeq, out var value)) { double totalMilliseconds = (DateTime.UtcNow - value.SentAt).TotalMilliseconds; UpdateRtt(totalMilliseconds); try { _sendWindow.Release(); } catch { } } } private void HandleReliableData(uint seq, ushort fragIdx, ushort fragCount, byte[] dg, int payLen) { int num = 19; if (payLen < 0 || num + payLen > dg.Length) { return; } byte[] array = new byte[payLen]; if (payLen > 0) { Buffer.BlockCopy(dg, num, array, 0, payLen); } if (fragCount > 1) { FragBuf orAdd = _relFragBufs.GetOrAdd(seq, (uint _) => new FragBuf { Frags = new byte[fragCount][] }); if (fragIdx >= orAdd.Frags.Length || orAdd.Frags[fragIdx] != null) { return; } orAdd.Frags[fragIdx] = array; Interlocked.Increment(ref orAdd.RecvCount); if (!orAdd.IsComplete) { return; } _relFragBufs.TryRemove(seq, out var _); SendAck(seq); int num2 = 0; byte[][] frags = orAdd.Frags; foreach (byte[] array2 in frags) { if (array2 != null) { num2 += array2.Length; } } byte[] array3 = new byte[num2]; int num4 = 0; frags = orAdd.Frags; foreach (byte[] array4 in frags) { if (array4 != null) { Buffer.BlockCopy(array4, 0, array3, num4, array4.Length); num4 += array4.Length; } } array = array3; } else { SendAck(seq); } lock (_oooLock) { uint num5 = _recvSeq + 1; if (seq == num5 || _recvSeq == 0) { _recvSeq = seq; DispatchReliable(array); while (true) { uint num6 = _recvSeq + 1; if (_ooo.TryGetValue(num6, out var value2)) { _ooo.Remove(num6); _recvSeq = num6; DispatchReliable(value2); continue; } break; } } else if ((int)(seq - num5) > 0 && !_ooo.ContainsKey(seq)) { _ooo[seq] = array; } } } private void HandleUnreliableData(uint seq, byte typeId, ushort fragIdx, ushort fragCount, byte[] dg, int payLen) { lock (_seqLock) { if (_unrelLastSeq != 0 && (int)(seq - _unrelLastSeq) <= 0) { return; } } int num = 19; if (payLen < 0 || num + payLen > dg.Length) { return; } byte[] array = new byte[payLen]; if (payLen > 0) { Buffer.BlockCopy(dg, num, array, 0, payLen); } if (fragCount <= 1) { lock (_seqLock) { _unrelLastSeq = seq; } DispatchUnreliable(typeId, array); return; } FragBuf orAdd = _fragBufs.GetOrAdd(seq, (uint _) => new FragBuf { Frags = new byte[fragCount][] }); if (fragIdx >= orAdd.Frags.Length || orAdd.Frags[fragIdx] != null) { return; } orAdd.Frags[fragIdx] = array; Interlocked.Increment(ref orAdd.RecvCount); if (!orAdd.IsComplete) { return; } _fragBufs.TryRemove(seq, out var _); lock (_seqLock) { if (_unrelLastSeq != 0 && (int)(seq - _unrelLastSeq) <= 0) { return; } _unrelLastSeq = seq; } int num2 = 0; byte[][] frags = orAdd.Frags; foreach (byte[] array2 in frags) { if (array2 != null) { num2 += array2.Length; } } byte[] array3 = new byte[num2]; int num4 = 0; frags = orAdd.Frags; foreach (byte[] array4 in frags) { if (array4 != null) { Buffer.BlockCopy(array4, 0, array3, num4, array4.Length); num4 += array4.Length; } } DispatchUnreliable(typeId, array3); } private void DispatchReliable(byte[] data) { Task.Run(delegate { try { this.ReliableReceived?.Invoke(data); } catch { } }); } private void DispatchUnreliable(byte typeId, byte[] data) { Task.Run(delegate { try { this.UnreliableFrameReceived?.Invoke(typeId, data); } catch { } }); } private async void RetransmitLoop(CancellationToken ct) { while (!ct.IsCancellationRequested && !_disposed) { await Task.Delay(50, ct).ContinueWith(delegate { }); DateTime utcNow = DateTime.UtcNow; FragBuf value; foreach (KeyValuePair fragBuf in _fragBufs) { if ((utcNow - fragBuf.Value.Created).TotalSeconds > 10.0) { _fragBufs.TryRemove(fragBuf.Key, out value); } } foreach (KeyValuePair relFragBuf in _relFragBufs) { if ((utcNow - relFragBuf.Value.Created).TotalSeconds > 60.0) { _relFragBufs.TryRemove(relFragBuf.Key, out value); } } _rtoDoubledThisCycle = false; foreach (KeyValuePair item in _pending) { Pending value2 = item.Value; if ((utcNow - value2.LastSent).TotalMilliseconds < (double)_rtoMs) { continue; } if (value2.Retries >= 15) { if (_pending.TryRemove(value2.Seq, out var _)) { PktsLost++; UpdateLossRate(); try { _sendWindow.Release(); } catch { } } continue; } value2.Retries++; value2.LastSent = utcNow; if (!_rtoDoubledThisCycle) { _rtoMs = Math.Min(_rtoMs * 2, 16000); _rtoDoubledThisCycle = true; } bool flag = value2.Datagrams.Length > 4; byte[][] datagrams = value2.Datagrams; foreach (byte[] dg in datagrams) { RawSend(dg); if (flag) { Thread.Sleep(1); } } } UpdateCongestionMetric(); if ((utcNow - _lastActivity).TotalMilliseconds > 30000.0) { TriggerDisconnect(); } } } private async void KeepaliveLoop(CancellationToken ct) { while (!ct.IsCancellationRequested && !_disposed) { await Task.Delay(5000, ct).ContinueWith(delegate { }); if (!_disposed) { try { byte[] dg = BuildDatagram(64, 0u, 0u, 0, 1, 0, new byte[0], 0, 0); RawSend(dg); } catch { } continue; } break; } } private void SendAck(uint seq) { byte[] dg = BuildDatagram(4, 0u, seq, 0, 1, 0, new byte[0], 0, 0); try { RawSend(dg); } catch { } } private void UpdateRtt(double sample) { lock (_statsLock) { _rttvar = 0.75 * _rttvar + 0.25 * Math.Abs(_srtt - sample); _srtt = 0.875 * _srtt + 0.125 * sample; RttMs = _srtt; _rtoMs = Math.Max(200, Math.Min(16000, (int)(_srtt + 4.0 * _rttvar))); } } private void UpdateLossRate() { double val = ((PktsSent > 0) ? ((double)PktsLost / (double)PktsSent) : 0.0); LossRate = Math.Min(1.0, val); } private void UpdateCongestionMetric() { UpdateLossRate(); double num = Math.Min(1.0, RttMs / 2000.0); double lossRate = LossRate; double obj = Math.Min(1.0, num * 0.5 + lossRate * 0.5); try { this.CongestionChanged?.Invoke(obj); } catch { } } private void TriggerDisconnect() { if (_disposed) { return; } Dispose(); try { this.Disconnected?.Invoke(); } catch { } } private byte[] BuildDatagram(byte flags, uint seqNum, uint ackNum, ushort fragIdx, ushort fragCount, byte packetTypeId, byte[] payload, int payOffset, int payLen) { byte[] array = new byte[19 + payLen]; Buffer.BlockCopy(BitConverter.GetBytes(SessionId), 0, array, 0, 4); array[4] = flags; Buffer.BlockCopy(BitConverter.GetBytes(seqNum), 0, array, 5, 4); Buffer.BlockCopy(BitConverter.GetBytes(ackNum), 0, array, 9, 4); Buffer.BlockCopy(BitConverter.GetBytes(fragIdx), 0, array, 13, 2); Buffer.BlockCopy(BitConverter.GetBytes(fragCount), 0, array, 15, 2); array[17] = packetTypeId; array[18] = 0; if (payLen > 0) { Buffer.BlockCopy(payload, payOffset, array, 19, payLen); } return array; } private void RawSend(byte[] dg) { if (_disposed || dg == null) { return; } try { _socket.Send(dg, dg.Length, Remote); BytesSent += dg.Length; } catch { } } public static bool ParseHeader(byte[] dg, int len, out uint sessionId, out byte flags, out uint seqNum, out uint ackNum, out ushort fragIdx, out ushort fragCount, out byte packetTypeId) { sessionId = 0u; flags = 0; seqNum = 0u; ackNum = 0u; fragIdx = 0; fragCount = 0; packetTypeId = 0; if (dg == null || len < 19) { return false; } sessionId = BitConverter.ToUInt32(dg, 0); flags = dg[4]; seqNum = BitConverter.ToUInt32(dg, 5); ackNum = BitConverter.ToUInt32(dg, 9); fragIdx = BitConverter.ToUInt16(dg, 13); fragCount = BitConverter.ToUInt16(dg, 15); packetTypeId = dg[17]; return true; } }