using System; using System.Net.Sockets; using System.Net; using System.IO; using System.Threading.Tasks; using System.Collections.Generic; using System.Linq; using SBRL.Utilities; using System.Text; using System.Runtime.CompilerServices; namespace PixelHub.Net { /// /// Provides a mechanism to broadcast a program's presence on a multicast address and port. /// public class DiscoveryBeacon : IDisposable { TextWriter logger; UdpClient beacon; int emitFrequency = 2000; string role = "server"; /// /// Whether the beacon is currently active. /// If true then the beacon needs disposing of properly by calling the Dispose() method before the program ends. /// public bool Active { get; private set; } = true; /// /// Whether the logging output should be verbose. /// public bool Verbose { get; private set; } = false; /// /// The current IP address of the local machine. /// /// /// Setting this value affects the IP address that is broadcast by the beacon. /// public IPAddress LocalIP { get; set; } /// /// The name of the entwork interface to bind to. /// If you don't want to bind to a specific interface, leave this property alone. /// public string NetworkInterfaceName { get; set; } = string.Empty; /// /// The (multicast) IP address that the beacon should broadcast to. /// public IPAddress DestinationAddress { get; private set; } /// /// The multicast endpoint that the beacon will broadcast to. /// This value is dynamically generated from the DestinationAddress and Port properties. /// public IPEndPoint MulticastChannel { get; private set; } /// /// The port that the beacon should broadcast on. /// public int Port { get; set; } = 5050; /// /// The role that the program hosted at this beacon plays. Default: server. /// public string Role { get { return role; } private set { if (value.Contains(" ")) throw new InvalidDataException($"Error: Invalid role '{value}'."); role = value; } } /// /// The time, in milliseconds, that the beacon should wait between broadcasts. /// public int EmitFrequency { get { return emitFrequency; } set { emitFrequency = value; logger.WriteLine("Beacon broadcast frequency changed to {0}ms.", EmitFrequency); } } /// /// Initializes a new instance of the class. /// /// The destination address to broadcast to. /// The port to broadcast on. /// The place to send log messages to defaults to Console.Out. public DiscoveryBeacon(IPAddress inDestAddress, int inPort, TextWriter inLogger) { logger = inLogger; Port = inPort; DestinationAddress = inDestAddress; MulticastChannel = new IPEndPoint(DestinationAddress, Port); LocalIP = findLocalIP(); } /// /// Initializes a new instance of the class. /// /// The destination address to broadcast to. /// The port to broadcast on. public DiscoveryBeacon(IPAddress inDestAddress, int inPort) : this(inDestAddress, inPort, Console.Out) { } /// /// Initializes a new instance of the class. /// /// The destination address to broadcast to. public DiscoveryBeacon(IPAddress inDestAddress) : this(inDestAddress, 5050) { } /// /// Start the beacon up and broadcast the program's presence on a regular (See basis. /// public async Task Emit() { logger.WriteLine("Using {0} as the local IP Address.", LocalIP); connect(); logger.WriteLine("Beacon activated with broadcast interval of {0}ms.", EmitFrequency); while(true) { await emitOnce(); await Task.Delay(EmitFrequency); } } /// /// Finds the IP of the machine the beacon is running on. /// /// The first IP found that points to this machine. private IPAddress findLocalIP() { List localIPS = Utilities.GetLocalIps().ToList(); return localIPS[0]; } /// /// Connects the underlying UdpClient client. /// private void connect() { beacon = new UdpClient(Port); if(NetworkInterfaceName != string.Empty) { logger.WriteLine("Discovering interfaces."); beacon.Client.SetSocketOption( SocketOptionLevel.IP, SocketOptionName.MulticastInterface, (int)IPAddress.HostToNetworkOrder(NetTools.GetNetworkIndexByName4(NetworkInterfaceName)) ); logger.WriteLine("Choosing interface with name {0}", NetworkInterfaceName); } Active = true; beacon.JoinMulticastGroup(DestinationAddress); logger.WriteLine("One way beacon set up pointing at {0}.", MulticastChannel); } /// /// Emits a single presence broadcast from the beacon. /// private async Task emitOnce() { await Send($"{Role}@{LocalIP}:{Port}"); } /// /// Sends a message from the underlying UdpClient to the attached multicast address. /// /// The message to send. private async Task Send(string data) { await Send(MulticastChannel, data); } /// /// Sends a message to the given endpoint using the underlying UdpClient. /// /// The place to send the message to. /// The message payload. private async Task Send(IPEndPoint destination, string data) { if(Verbose) logger.WriteLine("Sending '{0}' to {1}.", Utilities.EscapeString(data), destination); byte[] payload = Encoding.UTF8.GetBytes(data); await Send(destination, payload); } /// /// Sends a raw byte array message to the given endpoint. /// This is the lowest level Send() method before the message is passed to the UcpClient itself. /// /// THe destination endpoint to send the message to. /// The byte array to use as the message body. private async Task Send(IPEndPoint destination, byte[] payload) { if (beacon == null) throw new InvalidOperationException("Error: Can't send data until you join a channel."); await beacon.SendAsync(payload, payload.Length, MulticastChannel); } /// /// Releases all resource used by the . /// /// /// Call when you are finished using the . The /// method leaves the in an unusable state. After /// calling , you must release all references to the /// so the garbage collector can reclaim the memory that the /// was occupying. public void Dispose() { if(Active) { beacon.Close(); Active = false; } } } }