From 5354c33fc3a365f354d9b26f8d975ecaebcd3507 Mon Sep 17 00:00:00 2001 From: jules Date: Wed, 17 Oct 2018 15:24:36 +0100 Subject: [PATCH] Added some classes NetworkServiceDiscovery::Advertiser and NetworkServiceDiscovery::AvailableServiceList to implement a simple protocol for discovering and connecting devices on the LAN --- .../juce_NetworkServiceDiscovery.cpp | 190 ++++++++++++++++++ .../juce_NetworkServiceDiscovery.h | 126 ++++++++++++ modules/juce_events/juce_events.cpp | 1 + modules/juce_events/juce_events.h | 1 + 4 files changed, 318 insertions(+) create mode 100644 modules/juce_events/interprocess/juce_NetworkServiceDiscovery.cpp create mode 100644 modules/juce_events/interprocess/juce_NetworkServiceDiscovery.h diff --git a/modules/juce_events/interprocess/juce_NetworkServiceDiscovery.cpp b/modules/juce_events/interprocess/juce_NetworkServiceDiscovery.cpp new file mode 100644 index 0000000000..1e5474f5e0 --- /dev/null +++ b/modules/juce_events/interprocess/juce_NetworkServiceDiscovery.cpp @@ -0,0 +1,190 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +NetworkServiceDiscovery::Advertiser::Advertiser (const String& serviceTypeUID, + const String& serviceDescription, + int broadcastPortToUse, int connectionPort, + RelativeTime minTimeBetweenBroadcasts) + : Thread ("Discovery_broadcast"), + message (serviceTypeUID), broadcastPort (broadcastPortToUse), + minInterval (minTimeBetweenBroadcasts) +{ + message.setAttribute ("id", Uuid().toString()); + message.setAttribute ("name", serviceDescription); + message.setAttribute ("address", String()); + message.setAttribute ("port", connectionPort); + + startThread (2); +} + +NetworkServiceDiscovery::Advertiser::~Advertiser() +{ + stopThread (2000); + socket.shutdown(); +} + +void NetworkServiceDiscovery::Advertiser::run() +{ + if (! socket.bindToPort (0)) + { + jassertfalse; + return; + } + + while (! threadShouldExit()) + { + sendBroadcast(); + wait ((int) minInterval.inMilliseconds()); + } +} + +void NetworkServiceDiscovery::Advertiser::sendBroadcast() +{ + auto localAddress = IPAddress::getLocalAddress(); + message.setAttribute ("address", localAddress.toString()); + auto broadcastAddress = IPAddress::getInterfaceBroadcastAddress (localAddress); + auto data = message.createDocument ({}, true, false); + socket.write (broadcastAddress.toString(), broadcastPort, data.toRawUTF8(), (int) data.getNumBytesAsUTF8()); +} + +//============================================================================== +NetworkServiceDiscovery::AvailableServiceList::AvailableServiceList (const String& serviceType, int broadcastPort) + : Thread ("Discovery_listen"), serviceTypeUID (serviceType) +{ + socket.bindToPort (broadcastPort); + startThread (2); +} + +NetworkServiceDiscovery::AvailableServiceList::~AvailableServiceList() +{ + socket.shutdown(); + stopThread (2000); +} + +void NetworkServiceDiscovery::AvailableServiceList::run() +{ + while (! threadShouldExit()) + { + if (socket.waitUntilReady (true, 200) == 1) + { + char buffer[1024]; + auto bytesRead = socket.read (buffer, sizeof (buffer) - 1, false); + + if (bytesRead > 10) + if (auto xml = parseXML (String (CharPointer_UTF8 (buffer), + CharPointer_UTF8 (buffer + bytesRead)))) + if (xml->hasTagName (serviceTypeUID)) + handleMessage (*xml); + } + + removeTimedOutServices(); + } +} + +std::vector NetworkServiceDiscovery::AvailableServiceList::getServices() const +{ + const ScopedLock sl (listLock); + auto listCopy = services; + return listCopy; +} + +void NetworkServiceDiscovery::AvailableServiceList::handleAsyncUpdate() +{ + if (onChange != nullptr) + onChange(); +} + +void NetworkServiceDiscovery::AvailableServiceList::handleMessage (const XmlElement& xml) +{ + Service service; + service.instanceID = xml.getStringAttribute ("id"); + + if (service.instanceID.trim().isNotEmpty()) + { + service.description = xml.getStringAttribute ("name"); + service.address = IPAddress (xml.getStringAttribute ("address")); + service.port = xml.getIntAttribute ("port"); + service.lastSeen = Time::getCurrentTime(); + + handleMessage (service); + } +} + +static void sortServiceList (std::vector& services) +{ + auto compareServices = [] (const NetworkServiceDiscovery::Service& s1, + const NetworkServiceDiscovery::Service& s2) + { + return s1.instanceID < s2.instanceID; + }; + + std::sort (services.begin(), services.end(), compareServices); +} + +void NetworkServiceDiscovery::AvailableServiceList::handleMessage (const Service& service) +{ + const ScopedLock sl (listLock); + + for (auto& s : services) + { + if (s.instanceID == service.instanceID) + { + if (s.description != service.description + || s.address != service.address + || s.port != service.port) + { + s = service; + triggerAsyncUpdate(); + } + + s.lastSeen = service.lastSeen; + return; + } + } + + services.push_back (service); + sortServiceList (services); + triggerAsyncUpdate(); +} + +void NetworkServiceDiscovery::AvailableServiceList::removeTimedOutServices() +{ + const double timeoutSeconds = 5.0; + auto oldestAllowedTime = Time::getCurrentTime() - RelativeTime::seconds (timeoutSeconds); + + const ScopedLock sl (listLock); + + auto oldEnd = std::end (services); + auto newEnd = std::remove_if (std::begin (services), oldEnd, + [=] (const Service& s) { return s.lastSeen < oldestAllowedTime; }); + + if (newEnd != oldEnd) + { + services.erase (newEnd, oldEnd); + triggerAsyncUpdate(); + } +} + +} // namespace juce diff --git a/modules/juce_events/interprocess/juce_NetworkServiceDiscovery.h b/modules/juce_events/interprocess/juce_NetworkServiceDiscovery.h new file mode 100644 index 0000000000..63e3e4f657 --- /dev/null +++ b/modules/juce_events/interprocess/juce_NetworkServiceDiscovery.h @@ -0,0 +1,126 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + Contains classes that implement a simple protocol for broadcasting the availability + and location of a discoverable service on the local network, and for maintaining a + list of known services. +*/ +struct NetworkServiceDiscovery +{ + /** An object which runs a thread to repeatedly broadcast the existence of a + discoverable service. + + To use, simply create an instance of an Advertiser and it'll broadcast until + you delete it. + */ + struct Advertiser : private Thread + { + /** Creates and starts an Advertiser thread, broadcasting with the given properties. + @param serviceTypeUID A user-supplied string to define the type of service this represents + @param serviceDescription A description string that will appear in the Service::description field for clients + @param broadcastPort The port number on which to broadcast the service discovery packets + @param connectionPort The port number that will be sent to appear in the Service::port field + @param minTimeBetweenBroadcasts The interval to wait between sending broadcast messages + */ + Advertiser (const String& serviceTypeUID, + const String& serviceDescription, + int broadcastPort, + int connectionPort, + RelativeTime minTimeBetweenBroadcasts = RelativeTime::seconds (1.5)); + + /** Destructor */ + ~Advertiser(); + + private: + XmlElement message; + const int broadcastPort; + const RelativeTime minInterval; + DatagramSocket socket { true }; + + void run() override; + void sendBroadcast(); + }; + + //============================================================================== + /** + Contains information about a service that has been found on the network. + @see AvailableServiceList, Advertiser + */ + struct Service + { + String instanceID; /**< A UUID that identifies the particular instance of the Advertiser class. */ + String description; /**< The service description as sent by the Advertiser */ + IPAddress address; /**< The IP address of the advertiser */ + int port; /**< The port number of the advertiser */ + Time lastSeen; /**< The time of the last ping received from the advertiser */ + }; + + //============================================================================== + /** + Watches the network for broadcasts from Advertiser objects, and keeps a list of + all the currently active instances. + + Just create an instance of AvailableServiceList and it will start listening - you + can register a callback with its onChange member to find out when services + appear/disappear, and you can call getServices() to find out the current list. + @see Service, Advertiser + */ + struct AvailableServiceList : private Thread, + private AsyncUpdater + { + /** Creates an AvailableServiceList that will bind to the given port number and watch + the network for Advertisers broadcasting the given service type. + + This will only detect broadcasts from an Advertiser object with a matching + serviceTypeUID value, and where the broadcastPort matches. + */ + AvailableServiceList (const String& serviceTypeUID, int broadcastPort); + + /** Destructor */ + ~AvailableServiceList(); + + /** A lambda that can be set to recieve a callback when the list changes */ + std::function onChange; + + /** Returns a list of the currently known services. */ + std::vector getServices() const; + + private: + DatagramSocket socket { true }; + String serviceTypeUID; + CriticalSection listLock; + std::vector services; + + void run() override; + void handleAsyncUpdate() override; + void handleMessage (const XmlElement&); + void handleMessage (const Service&); + void removeTimedOutServices(); + }; +}; + +} // namespace juce diff --git a/modules/juce_events/juce_events.cpp b/modules/juce_events/juce_events.cpp index bee0e3dff2..a29883f92a 100644 --- a/modules/juce_events/juce_events.cpp +++ b/modules/juce_events/juce_events.cpp @@ -66,6 +66,7 @@ #include "interprocess/juce_InterprocessConnection.cpp" #include "interprocess/juce_InterprocessConnectionServer.cpp" #include "interprocess/juce_ConnectedChildProcess.cpp" +#include "interprocess/juce_NetworkServiceDiscovery.cpp" //============================================================================== #if JUCE_MAC || JUCE_IOS diff --git a/modules/juce_events/juce_events.h b/modules/juce_events/juce_events.h index 52552ce396..c7c1ef9c24 100644 --- a/modules/juce_events/juce_events.h +++ b/modules/juce_events/juce_events.h @@ -81,6 +81,7 @@ #include "interprocess/juce_InterprocessConnection.h" #include "interprocess/juce_InterprocessConnectionServer.h" #include "interprocess/juce_ConnectedChildProcess.h" +#include "interprocess/juce_NetworkServiceDiscovery.h" #if JUCE_LINUX #include "native/juce_linux_EventLoop.h"