using System.Net; using System.Net.Sockets; using System.Text; using Nonemm.Core; namespace Nonemm.Network; /// Keeps the stations of a multi-operator entry in step. Each contact is sent /// to every peer as it is logged, and contacts that arrive from a peer are /// handed to whoever owns the log. public sealed class StationNetwork : IDisposable { private readonly int port; private readonly string stationName; private readonly IReadOnlyList peers; private readonly CancellationTokenSource stopping = new(); private readonly UdpClient listener; private readonly UdpClient sender = new(); private Task? loop; public StationNetwork(int port, string stationName, IEnumerable peerAddresses) { this.port = port; this.stationName = stationName; peers = peerAddresses.Select(a => Endpoint(a, port)).OfType().ToList(); listener = new UdpClient(new IPEndPoint(IPAddress.Any, port)); sender.EnableBroadcast = true; } public string StationName => stationName; public IReadOnlyList Peers => peers; /// A contact another station logged. It arrives already scored; the log /// works its points and multipliers out again from the rules. public event EventHandler? ContactArrived; public event EventHandler? Failed; public void Start() => loop ??= Task.Run(() => ListenAsync(stopping.Token)); public async Task SendAsync(Qso qso, string myCallsign, CancellationToken cancellation = default) { byte[] message = Encoding.UTF8.GetBytes(ContactMessage.Write(qso, myCallsign, stationName)); foreach (IPEndPoint peer in Destinations()) { try { await sender.SendAsync(message, peer, cancellation).ConfigureAwait(false); } catch (SocketException e) { Failed?.Invoke(this, $"could not reach {peer}: {e.Message}"); } } } public void Dispose() { stopping.Cancel(); listener.Dispose(); sender.Dispose(); stopping.Dispose(); } /// With no peers named, the contact goes out as a broadcast, which is how a /// station that has just joined is found without configuring anything. private IEnumerable Destinations() => peers.Count > 0 ? peers : [new IPEndPoint(IPAddress.Broadcast, port)]; private async Task ListenAsync(CancellationToken cancellation) { while (!cancellation.IsCancellationRequested) { try { UdpReceiveResult received = await listener.ReceiveAsync(cancellation).ConfigureAwait(false); Qso? qso = ContactMessage.Read(Encoding.UTF8.GetString(received.Buffer)); if (qso is not null && qso.StationName != stationName) { ContactArrived?.Invoke(this, qso); } } catch (OperationCanceledException) { return; } catch (SocketException e) { Failed?.Invoke(this, e.Message); } } } private static IPEndPoint? Endpoint(string address, int defaultPort) { string[] parts = address.Split(':'); if (!IPAddress.TryParse(parts[0].Trim(), out IPAddress? host)) { return null; } int port = parts.Length > 1 && int.TryParse(parts[1], out int given) ? given : defaultPort; return new IPEndPoint(host, port); } }