using System; using System.Buffers; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using Impostor.Api.Net.Messages; using Microsoft.Extensions.ObjectPool; using Serilog; namespace Impostor.Hazel.Udp { /// /// Represents a client's connection to a server that uses the UDP protocol. /// /// public sealed class UdpClientConnection : UdpConnection { private static readonly ILogger Logger = Log.ForContext(); /// /// The socket we're connected via. /// private readonly UdpClient _socket; private readonly Timer _reliablePacketTimer; private readonly SemaphoreSlim _connectWaitLock; private Task _listenTask; /// /// Creates a new UdpClientConnection. /// /// A to connect to. public UdpClientConnection(IPEndPoint remoteEndPoint, ObjectPool readerPool, IPMode ipMode = IPMode.IPv4) : base(null, readerPool) { EndPoint = remoteEndPoint; RemoteEndPoint = remoteEndPoint; IPMode = ipMode; _socket = new UdpClient { DontFragment = false }; _reliablePacketTimer = new Timer(ManageReliablePacketsInternal, null, 100, Timeout.Infinite); _connectWaitLock = new SemaphoreSlim(1, 1); } ~UdpClientConnection() { Dispose(false); } private async void ManageReliablePacketsInternal(object state) { await ManageReliablePackets(); try { _reliablePacketTimer.Change(100, Timeout.Infinite); } catch { // ignored } } /// protected override ValueTask WriteBytesToConnection(byte[] bytes, int length) { return WriteBytesToConnectionReal(bytes, length); } private async ValueTask WriteBytesToConnectionReal(byte[] bytes, int length) { try { await _socket.SendAsync(bytes, length); } catch (NullReferenceException) { } catch (ObjectDisposedException) { // Already disposed and disconnected... } catch (SocketException ex) { await DisconnectInternal(HazelInternalErrors.SocketExceptionSend, "Could not send data as a SocketException occurred: " + ex.Message); } } /// public override async ValueTask ConnectAsync(byte[] bytes = null) { State = ConnectionState.Connecting; try { _socket.Connect(RemoteEndPoint); } catch (SocketException e) { State = ConnectionState.NotConnected; throw new HazelException("A SocketException occurred while binding to the port.", e); } try { _listenTask = Task.Factory.StartNew(ListenAsync, TaskCreationOptions.LongRunning); } catch (ObjectDisposedException) { // If the socket's been disposed then we can just end there but make sure we're in NotConnected state. // If we end up here I'm really lost... State = ConnectionState.NotConnected; return; } catch (SocketException e) { Dispose(); throw new HazelException("A SocketException occurred while initiating a receive operation.", e); } // Write bytes to the server to tell it hi (and to punch a hole in our NAT, if present) // When acknowledged set the state to connected await SendHello(bytes, () => { State = ConnectionState.Connected; InitializeKeepAliveTimer(); }); await _connectWaitLock.WaitAsync(TimeSpan.FromSeconds(10)); } private async Task ListenAsync() { // Start packet handler. await StartAsync(); // Listen. while (State != ConnectionState.NotConnected) { UdpReceiveResult data; try { data = await _socket.ReceiveAsync(); } catch (SocketException e) { await DisconnectInternal(HazelInternalErrors.SocketExceptionReceive, "Socket exception while reading data: " + e.Message); return; } catch (Exception) { return; } if (data.Buffer.Length == 0) { await DisconnectInternal(HazelInternalErrors.ReceivedZeroBytes, "Received 0 bytes"); return; } // Write to client. await Pipeline.Writer.WriteAsync(data.Buffer); } } protected override void SetState(ConnectionState state) { if (state == ConnectionState.Connected) { _connectWaitLock.Release(); } } /// /// Sends a disconnect message to the end point. /// You may include optional disconnect data. The SendOption must be unreliable. /// protected override async ValueTask SendDisconnect(MessageWriter data = null) { lock (this) { if (_state == ConnectionState.NotConnected) return false; _state = ConnectionState.NotConnected; } var bytes = EmptyDisconnectBytes; if (data != null && data.Length > 0) { if (data.SendOption != MessageType.Unreliable) { throw new ArgumentException("Disconnect messages can only be unreliable."); } bytes = data.ToByteArray(true); bytes[0] = (byte)UdpSendOption.Disconnect; } try { await _socket.SendAsync(bytes, bytes.Length, RemoteEndPoint); } catch { } return true; } /// protected override void Dispose(bool disposing) { State = ConnectionState.NotConnected; try { _socket.Close(); } catch { } try { _socket.Dispose(); } catch { } _reliablePacketTimer.Dispose(); _connectWaitLock.Dispose(); base.Dispose(disposing); } } }