using System;
using System.Buffers;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using WsjtxUtils.WsjtxMessages;
using WsjtxUtils.WsjtxMessages.Messages;
namespace WsjtxUtils.WsjtxUdpServer
{
///
/// A UDP server for WSJT-X clients
///
public class WsjtxUdpServer : IDisposable
{
///
/// Size of the datagram buffers in bytes
///
private readonly int _datagramBufferSize;
///
/// The socket used for communications
///
private readonly Socket _socket;
///
/// The target handler for WSJT-X messages
///
private readonly IWsjtxUdpMessageHandler _messageHandler;
///
/// Source for cancellation tokens
///
private CancellationTokenSource? _cancellationTokenSource;
///
/// The datagram handling task
///
private Task? _handleDatagramsTask;
///
/// Get whether or not this object has been disposed.
///
public bool IsDisposed { get; private set; }
///
/// Is the specified address multicast
///
public bool IsMulticast { get; private set; }
///
/// Is the server currently running
///
public bool IsRunning { get; private set; }
///
/// The endpoint the server is bound to
///
public IPEndPoint LocalEndpoint { get; private set; }
///
/// Constructor for WSJT-X UDP server
///
///
///
///
///
public WsjtxUdpServer(IWsjtxUdpMessageHandler wsjtxUdpMessageHandler, IPAddress address, int port = 2237, int datagramBufferSize = 1500)
{
// set the message handling object
_messageHandler = wsjtxUdpMessageHandler;
// size of the buffers to allocate for reading/writing
_datagramBufferSize = datagramBufferSize;
// check if the address is multicast and setup accordingly
IsMulticast = IsAddressMulticast(address);
LocalEndpoint = IsMulticast ?
new IPEndPoint(IPAddress.Any, port) :
new IPEndPoint(address, port);
// setup UDP socket allowing for shared addresses
_socket = new Socket(SocketType.Dgram, ProtocolType.Udp)
{
ExclusiveAddressUse = false
};
_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
_socket.Bind(LocalEndpoint);
// if multicast join the group
if (IsMulticast)
_socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership,
new MulticastOption(address, LocalEndpoint.Address));
}
///
/// Destructor
///
~WsjtxUdpServer()
{
Dispose(false);
}
///
/// Start the UDP sever and process datagrams
///
///
public void Start(CancellationTokenSource? cancellationTokenSource = default)
{
if (IsRunning)
throw new InvalidOperationException("The server is already running.");
IsRunning = true;
_cancellationTokenSource = cancellationTokenSource ?? new CancellationTokenSource();
_handleDatagramsTask = HandleDatagramLoopAsync(_cancellationTokenSource.Token);
}
///
/// Stop the UDP server and datagram processing
///
public void Stop()
{
if (!IsRunning)
throw new InvalidOperationException("The server is not running.");
try
{
_cancellationTokenSource?.Cancel();
_handleDatagramsTask?.Wait(1000);
}
catch (AggregateException aggregateException)
{
aggregateException.Handle(ex => ex is TaskCanceledException);
}
finally
{
IsRunning = false;
}
}
///
/// Send a WSJT-X message to the specified endpoint
///
///
///
///
/// The number of bytes sent
public int SendMessageTo(EndPoint remoteEndpoint, T message) where T : WsjtxMessage, IWsjtxDirectionIn
{
if (string.IsNullOrEmpty(message.Id))
throw new ArgumentException($"The client id can not be null or empty when sending {typeof(T).Name}.", nameof(message));
var datagramBuffer = ArrayPool.Shared.Rent(_datagramBufferSize);
try
{
var bytesWritten = message.WriteMessageTo(datagramBuffer);
return _socket.SendTo(datagramBuffer, bytesWritten, SocketFlags.None, remoteEndpoint);
}
finally
{
ArrayPool.Shared.Return(datagramBuffer);
}
}
///
/// Send a WSJT-X message to the specified endpoint
///
///
///
///
///
/// The number of bytes sent
public async ValueTask SendMessageToAsync(EndPoint remoteEndpoint, T message, CancellationToken cancellationToken = default) where T : WsjtxMessage, IWsjtxDirectionIn
{
if (string.IsNullOrEmpty(message.Id))
throw new ArgumentException($"The client id can not be null or empty when sending {typeof(T).Name}.", nameof(message));
var datagramBuffer = ArrayPool.Shared.Rent(_datagramBufferSize);
try
{
var bytesWritten = message.WriteMessageTo(datagramBuffer);
return await _socket.SendToAsync(new ArraySegment(datagramBuffer, 0, bytesWritten), SocketFlags.None, remoteEndpoint);
}
finally
{
ArrayPool.Shared.Return(datagramBuffer);
}
}
///
/// Dispose
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Private Methods
///
/// Dispose
///
///
private void Dispose(bool disposing)
{
if (IsDisposed)
return;
// unmanaged items
if (disposing)
{
// managed items
_cancellationTokenSource?.Dispose();
_socket?.Dispose();
}
IsDisposed = true;
}
///
/// Process incoming datagrams
///
///
///
private async Task HandleDatagramLoopAsync(CancellationToken cancellationToken)
{
var datagramBuffer = new byte[_datagramBufferSize];
while (!cancellationToken.IsCancellationRequested)
{
// wait for and read the next datagram into the buffer
#if NETFRAMEWORK
var result = await _socket.ReceiveFromAsync(new ArraySegment(datagramBuffer), SocketFlags.None, LocalEndpoint);
#else
var result = await _socket.ReceiveFromAsync(datagramBuffer, SocketFlags.None, LocalEndpoint, cancellationToken);
#endif
var message = datagramBuffer.AsMemory().DeserializeWsjtxMessage();
// get the correct handler for the given message type
_ = message?.MessageType switch
{
MessageType.Clear => _messageHandler.HandleClearMessageAsync(this, (Clear)message, result.RemoteEndPoint, cancellationToken),
MessageType.Close => _messageHandler.HandleClosedMessageAsync(this, (Close)message, result.RemoteEndPoint, cancellationToken),
MessageType.Decode => _messageHandler.HandleDecodeMessageAsync(this, (Decode)message, result.RemoteEndPoint, cancellationToken),
MessageType.Heartbeat => _messageHandler.HandleHeartbeatMessageAsync(this, (Heartbeat)message, result.RemoteEndPoint, cancellationToken),
MessageType.LoggedADIF => _messageHandler.HandleLoggedAdifMessageAsync(this, (LoggedAdif)message, result.RemoteEndPoint, cancellationToken),
MessageType.QSOLogged => _messageHandler.HandleQsoLoggedMessageAsync(this, (QsoLogged)message, result.RemoteEndPoint, cancellationToken),
MessageType.Status => _messageHandler.HandleStatusMessageAsync(this, (Status)message, result.RemoteEndPoint, cancellationToken),
MessageType.WSPRDecode => _messageHandler.HandleWSPRDecodeMessageAsync(this, (WSPRDecode)message, result.RemoteEndPoint, cancellationToken),
_ => null // no handler for unsupported messages
};
}
}
#endregion
#region Static Methods
///
/// Determine if the address is a multicast group
///
///
///
public static bool IsAddressMulticast(IPAddress address)
{
if (address.IsIPv6Multicast)
return true;
var addressBytes = address.GetAddressBytes();
if (addressBytes.Length == 4)
return addressBytes[0] >= 224 && addressBytes[0] <= 239;
return false;
}
#endregion
}
}