// This file is provided under The MIT License as part of RiptideNetworking. // Copyright (c) Tom Weiland // For additional information please see the included LICENSE.md file or view it on GitHub: // https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md using System; using System.Net; using System.Net.Sockets; namespace Riptide.Transports.Udp { /// The kind of socket to create. public enum SocketMode { /// Dual-mode. Works with both IPv4 and IPv6. Both, /// IPv4 only mode. IPv4Only, /// IPv6 only mode. IPv6Only } /// Provides base send & receive functionality for and . public abstract class UdpPeer { /// public event EventHandler Disconnected; /// The default size used for the socket's send and receive buffers. protected const int DefaultSocketBufferSize = 1024 * 1024; // 1MB /// The minimum size that may be used for the socket's send and receive buffers. private const int MinSocketBufferSize = 256 * 1024; // 256KB /// How long to wait for a packet, in microseconds. private const int ReceivePollingTime = 500000; // 0.5 seconds /// Whether to create an IPv4 only, IPv6 only, or dual-mode socket. protected readonly SocketMode mode; /// The size to use for the socket's send and receive buffers. private readonly int socketBufferSize; /// The array that incoming data is received into. private readonly byte[] receivedData; /// The socket to use for sending and receiving. private Socket socket; /// Whether or not the transport is running. private bool isRunning; /// A reusable endpoint. private EndPoint remoteEndPoint; /// Initializes the transport. /// Whether to create an IPv4 only, IPv6 only, or dual-mode socket. /// How big the socket's send and receive buffers should be. protected UdpPeer(SocketMode mode, int socketBufferSize) { if (socketBufferSize < MinSocketBufferSize) throw new ArgumentOutOfRangeException(nameof(socketBufferSize), $"The minimum socket buffer size is {MinSocketBufferSize}!"); this.mode = mode; this.socketBufferSize = socketBufferSize; receivedData = new byte[Message.MaxSize]; } /// public void Poll() { Receive(); } /// Opens the socket and starts the transport. /// The IP address to bind the socket to, if any. /// The port to bind the socket to. protected void OpenSocket(IPAddress listenAddress = null, ushort port = 0) { if (isRunning) CloseSocket(); if (mode == SocketMode.IPv4Only) socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); else if (mode == SocketMode.IPv6Only) socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp) { DualMode = false }; else socket = new Socket(SocketType.Dgram, ProtocolType.Udp); IPAddress any = socket.AddressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any; socket.SendBufferSize = socketBufferSize; socket.ReceiveBufferSize = socketBufferSize; socket.Bind(new IPEndPoint(listenAddress == null ? any : listenAddress, port)); remoteEndPoint = new IPEndPoint(any, 0); isRunning = true; } /// Closes the socket and stops the transport. protected void CloseSocket() { if (!isRunning) return; isRunning = false; socket.Close(); } /// Polls the socket and checks if any data was received. private void Receive() { if (!isRunning) return; bool tryReceiveMore = true; while (tryReceiveMore) { int byteCount = 0; try { if (socket.Available > 0 && socket.Poll(ReceivePollingTime, SelectMode.SelectRead)) byteCount = socket.ReceiveFrom(receivedData, SocketFlags.None, ref remoteEndPoint); else tryReceiveMore = false; } catch (SocketException ex) { tryReceiveMore = false; switch (ex.SocketErrorCode) { case SocketError.Interrupted: case SocketError.NotSocket: isRunning = false; break; case SocketError.ConnectionReset: case SocketError.MessageSize: case SocketError.TimedOut: break; default: break; } } catch (ObjectDisposedException) { tryReceiveMore = false; isRunning = false; } catch (NullReferenceException) { tryReceiveMore = false; isRunning = false; } if (byteCount > 0) OnDataReceived(receivedData, byteCount, (IPEndPoint)remoteEndPoint); } } /// Sends data to a given endpoint. /// The array containing the data. /// The number of bytes in the array which should be sent. /// The endpoint to send the data to. internal void Send(byte[] dataBuffer, int numBytes, IPEndPoint toEndPoint) { try { if (isRunning) socket.SendTo(dataBuffer, numBytes, SocketFlags.None, toEndPoint); } catch(SocketException) { // May want to consider triggering a disconnect here (perhaps depending on the type // of SocketException)? Timeout should catch disconnections, but disconnecting // explicitly might be better... } } /// Handles received data. /// A byte array containing the received data. /// The number of bytes in used by the received data. /// The endpoint from which the data was received. protected abstract void OnDataReceived(byte[] dataBuffer, int amount, IPEndPoint fromEndPoint); /// Invokes the event. /// The closed connection. /// The reason for the disconnection. protected virtual void OnDisconnected(Connection connection, DisconnectReason reason) { Disconnected?.Invoke(this, new DisconnectedEventArgs(connection, reason)); } } }