using System; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Cysharp.Threading.Tasks; namespace ShrinkNetwork { public sealed class ShrinkKcpClientTransport : IShrinkNetworkAsyncTransport { private readonly string _host; private readonly int _port; private readonly long _sessionId; private readonly ShrinkKcpTransportOptions _options; private readonly object _syncRoot = new(); private CancellationTokenSource _cts; private UdpClient _udpClient; private ShrinkKcpPeer _peer; private long _handshakeNonce; private int _started; private int _connected; public ShrinkKcpClientTransport(string host, int port, ShrinkKcpTransportOptions options = null, long sessionId = 1) { if (string.IsNullOrWhiteSpace(host)) throw new ArgumentException("Host cannot be empty.", nameof(host)); if (port <= 0 || port > 65535) throw new ArgumentOutOfRangeException(nameof(port)); _host = host.Trim(); _port = port; _sessionId = sessionId; _options = (options ?? new ShrinkKcpTransportOptions()).Clone(); _options.Validate(); } public bool IsStarted => Volatile.Read(ref _started) == 1; public event Action OnEvent; public void Start() { if (Interlocked.Exchange(ref _started, 1) == 1) return; _cts = new CancellationTokenSource(); _udpClient = new UdpClient(AddressFamily.InterNetwork); _udpClient.Client.ReceiveBufferSize = _options.ReceiveBufferSize; _udpClient.Connect(_host, _port); _handshakeNonce = CreateHandshakeNonce(); ReceiveLoopAsync(_cts.Token).Forget(); HandshakeLoopAsync(_cts.Token).Forget(); UpdateLoopAsync(_cts.Token).Forget(); } public void Stop() { var remote = $"{_host}:{_port}"; var wasConnected = Interlocked.Exchange(ref _connected, 0) == 1; if (Interlocked.Exchange(ref _started, 0) == 0) return; try { var peer = _peer; if (peer != null) SendDatagram(ShrinkKcpTransportProtocol.CreateDisconnect(peer.ConversationId)); } catch { } _cts?.Cancel(); lock (_syncRoot) { _peer?.Dispose(); _peer = null; try { _udpClient?.Close(); } catch { } _udpClient = null; } if (wasConnected) OnEvent?.Invoke(ShrinkNetworkTransportEvent.Disconnected(_sessionId, remote)); } public void Send(long sessionId, byte[] packetData) { SendAsync(sessionId, packetData).Forget(); } public UniTask SendAsync(long sessionId, byte[] packetData) { if (!IsStarted) throw new InvalidOperationException("Transport is not started."); if (sessionId != _sessionId) throw new InvalidOperationException($"Unsupported session id {sessionId}. This transport only supports {_sessionId}."); if (Volatile.Read(ref _connected) != 1) throw new InvalidOperationException("KCP client is not connected."); lock (_syncRoot) { _peer?.Send(packetData ?? Array.Empty()); } return UniTask.CompletedTask; } private async UniTaskVoid HandshakeLoopAsync(CancellationToken cancellationToken) { var startedAt = DateTime.UtcNow; try { while (!cancellationToken.IsCancellationRequested && Volatile.Read(ref _connected) == 0) { if ((DateTime.UtcNow - startedAt).TotalMilliseconds > _options.ConnectTimeoutMs) throw new TimeoutException($"KCP connect timed out: {_host}:{_port}"); SendDatagram(ShrinkKcpTransportProtocol.CreateConnectRequest(_handshakeNonce, _options.ConversationId)); await Task.Delay(_options.HandshakeRetryMs, cancellationToken); } } catch (OperationCanceledException) { } catch (Exception ex) { ShrinkNetworkLogger.Exception(ex); ShrinkNetworkLogger.Error($"[ShrinkNetwork] KCP connect failed: {_host}:{_port} {ex.Message}"); Stop(); } } private async UniTaskVoid ReceiveLoopAsync(CancellationToken cancellationToken) { try { while (!cancellationToken.IsCancellationRequested) { UdpReceiveResult result; try { result = await _udpClient.ReceiveAsync(); } catch (ObjectDisposedException) { break; } catch (SocketException) when (!IsStarted) { break; } HandleDatagram(result.Buffer, result.RemoteEndPoint); } } catch (Exception ex) { ShrinkNetworkLogger.Exception(ex); ShrinkNetworkLogger.Warn($"[ShrinkNetwork] KCP receive loop ended: {ex.Message}"); } finally { Stop(); } } private async UniTaskVoid UpdateLoopAsync(CancellationToken cancellationToken) { try { while (!cancellationToken.IsCancellationRequested) { var shouldStop = false; lock (_syncRoot) { _peer?.Tick(packet => OnEvent?.Invoke(ShrinkNetworkTransportEvent.Packet(_sessionId, packet))); if (_peer != null && Volatile.Read(ref _connected) == 1 && DateTime.UtcNow.Ticks - _peer.LastReceiveUtcTicks > TimeSpan.FromMilliseconds(_options.IdleTimeoutMs).Ticks) { ShrinkNetworkLogger.Warn($"[ShrinkNetwork] KCP idle timeout: {_host}:{_port}"); shouldStop = true; } } if (shouldStop) { Stop(); return; } await Task.Delay(_options.UpdateIntervalMs, cancellationToken); } } catch (OperationCanceledException) { } } private void HandleDatagram(byte[] datagram, IPEndPoint remoteEndPoint) { if (datagram == null || datagram.Length == 0) return; if (ShrinkKcpTransportProtocol.TryReadConnectAccept(datagram, out var nonce, out var conversationId)) { if (nonce != _handshakeNonce) return; lock (_syncRoot) { if (_peer == null) { _peer = new ShrinkKcpPeer(conversationId, _options, payload => SendDatagram(ShrinkKcpTransportProtocol.CreateDataPacket(conversationId, payload))); } } if (Interlocked.Exchange(ref _connected, 1) == 0) OnEvent?.Invoke(ShrinkNetworkTransportEvent.Connected(_sessionId, remoteEndPoint.ToString())); return; } if (ShrinkKcpTransportProtocol.TryReadDisconnect(datagram, out var disconnectedConversationId)) { if (_peer != null && _peer.ConversationId == disconnectedConversationId) Stop(); return; } if (!ShrinkKcpTransportProtocol.TryReadDataPacket(datagram, out var dataConversationId, out var payloadOffset, out var payloadLength)) return; lock (_syncRoot) { if (_peer == null || _peer.ConversationId != dataConversationId) return; _peer.Input(datagram, payloadOffset, payloadLength, packet => OnEvent?.Invoke(ShrinkNetworkTransportEvent.Packet(_sessionId, packet))); } } private void SendDatagram(byte[] datagram) { if (_udpClient == null) return; _udpClient.Send(datagram, datagram.Length); } private static long CreateHandshakeNonce() { Span buffer = stackalloc byte[sizeof(long)]; RandomNumberGenerator.Fill(buffer); return BitConverter.ToInt64(buffer); } } }