Yortw / RSSDP

Really Simple Service Discovery Protocol - a 100% .Net implementation of the SSDP protocol for publishing custom/basic devices, and discovering all device types on a network.
http://yortw.github.io/RSSDP/
MIT License
288 stars 67 forks source link

Needing to call SearchAsync() repeatedly and unable to receive notifications from remote devices #115

Closed trzy closed 2 years ago

trzy commented 2 years ago

Hi,

I'm having a couple of issues:

  1. Using the callback-based example in the documentation to search for broadcasts that I publish (using your publisher example directly), I only receive three callback events. I notice in the code that you re-send each UDP message thrice, so this makes sense. But I never get any more after that unless I keep calling SearchAsync() repeatedly. I thought that StartListeningForNotifications() should eliminate the need for that?

  2. I only receive the broadcasts generated by the same device. That is, if I start the app on a different computer on the same network, I do not see those broadcasts (and that instance, in turn, only sees its own broadcasts). UPnP is enabled on my wireless router. I will sit down with WireShark tomorrow to see if I can detect the packets at all, I haven't tried that yet. But I wonder whether I'm just using the code incorrectly.

Pardon the incredibly messy code (I am just trying to learn how this works before developing a proper implementation) but here is my listener (this is in a Unity app):

public class RssdpTest : MonoBehaviour
{
    private SsdpDeviceLocator _DeviceLocator;
    private Task<IEnumerable<DiscoveredSsdpDevice>> _searchTask = null;

    // Call this method from somewhere in your code to start the search.
    public void BeginSearch()
    {
        _DeviceLocator = new SsdpDeviceLocator();

        // (Optional) Set the filter so we only see notifications for devices we care about 
        // (can be any search target value i.e device type, uuid value etc - any value that appears in the 
        // DiscoverdSsdpDevice.NotificationType property or that is used with the searchTarget parameter of the Search method).
        _DeviceLocator.NotificationFilter = "upnp:rootdevice";

        // Connect our event handler so we process devices as they are found
        _DeviceLocator.DeviceAvailable += deviceLocator_DeviceAvailable;

        // Enable listening for notifications (optional)
        _DeviceLocator.StartListeningForNotifications();

        // Perform a search so we don't have to wait for devices to broadcast notifications 
        // again to get any results right away (notifications are broadcast periodically).
//        _DeviceLocator.SearchAsync();
        Debug.Log("Started device search");
    }

    // Process each found device in the event handler
    async void deviceLocator_DeviceAvailable(object sender, DeviceAvailableEventArgs e)
    {
        //Device data returned only contains basic device details and location of full device description.
        Debug.Log("Found " + e.DiscoveredDevice.Usn + " at " + e.DiscoveredDevice.DescriptionLocation.ToString());

        //Can retrieve the full device description easily though.
        var fullDevice = await e.DiscoveredDevice.GetDeviceInfo();
        Debug.Log("FriendlyName=" + fullDevice.FriendlyName);
        Debug.Log("FullDeviceType=" + fullDevice.FullDeviceType);
    }

    private void Start()
    {
        BeginSearch();
    }

    private void FixedUpdate()
    {
        if (_searchTask == null)
        {
            _searchTask = _DeviceLocator.SearchAsync();
        }
        else
        {
            if (_searchTask.IsCompleted)
            {
                _searchTask = null;
            }
        }
    }
}

And here is the publisher:

public class RssdpTest2 : MonoBehaviour
{
    private SsdpDevicePublisher _Publisher;
    private HttpListener _listener;
    private string _deviceDescriptionDocument;

    // Call this method from somewhere to actually do the publish.
    public void PublishDevice(string ourIPAddress)
    {
        // As this is a sample, we are only setting the minimum required properties.
        var deviceDefinition = new SsdpRootDevice()
        {
            CacheLifetime = TimeSpan.FromMinutes(30), //How long SSDP clients can cache this info.
            Location = new Uri("http://" + ourIPAddress + ":6810/Description.xml"), // Must point to the URL that serves your devices UPnP description document. 
            DeviceTypeNamespace = "my-namespace",
            DeviceType = "MyCustomDevice",
            FriendlyName = "Custom Device 1",
            Manufacturer = "Me",
            ModelName = "MyCustomDevice",
            Uuid = Guid.NewGuid().ToString()
        };

        _deviceDescriptionDocument = deviceDefinition.ToDescriptionDocument();
        Debug.Log("Device Description Document: " + _deviceDescriptionDocument);

        _Publisher = new SsdpDevicePublisher();
        _Publisher.AddDevice(deviceDefinition);
    }

    private void StartHTTPServer()
    {
        // https://thoughtbot.com/blog/using-httplistener-to-build-a-http-server-in-csharp
        int port = 6810;
        _listener = new HttpListener();
        _listener.Prefixes.Add("http://*:" + port.ToString() + "/");
        _listener.Start();
        Receive();
        Debug.LogFormat("Started HTTP server");
    }

    private void Receive()
    {
        _listener.BeginGetContext(new AsyncCallback(ListenerCallback), _listener);
    }

    private void Stop()
    {
        _listener.Stop();
    }

    private void ListenerCallback(IAsyncResult result)
    {
        if (_listener.IsListening)
        {
            var context = _listener.EndGetContext(result);
            var request = context.Request;

            Debug.LogFormat("HTTP server received request: {0}", request.Url);

            var response = context.Response;
            response.StatusCode = (int)HttpStatusCode.OK;
            response.ContentType = "text/plain";
            byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(_deviceDescriptionDocument);
            response.OutputStream.Write(responseBytes, 0, responseBytes.Length);
            response.OutputStream.Close();  // end response

            // Next!
            Receive();
        }
    }

    private IEnumerator Start()
    {
        string ourIPAddress = null;
        var host = Dns.GetHostEntry(Dns.GetHostName());
        foreach (var ip in host.AddressList)
        {
            if (ip.AddressFamily == AddressFamily.InterNetwork)
            {
                ourIPAddress = ip.ToString();
            }
        }

        Debug.LogFormat("Our IP Address = {0}", ourIPAddress);

        yield return new WaitForSeconds(5);
        if (ourIPAddress != null)
        {
            StartHTTPServer();
            PublishDevice(ourIPAddress: ourIPAddress);
        }
    }

    private void OnDestroy()
    {
        Stop();
    }
}

Any tips would be appreciated.

Thanks!

trzy commented 2 years ago

Some additional information: I think we can rule out network issues. I ran a WireShark capture and found that packets do arrive from the other device (IP 100 is my PC and 107 is the Android device I am running the same code on):

1 0.000000 192.168.0.100 239.255.255.250 SSDP 217 M-SEARCH HTTP/1.1 2 10.153707 192.168.0.100 239.255.255.250 SSDP 136 M-SEARCH HTTP/1.1 3 10.256424 192.168.0.100 239.255.255.250 SSDP 136 M-SEARCH HTTP/1.1 4 10.356536 192.168.0.100 239.255.255.250 SSDP 136 M-SEARCH HTTP/1.1 5 23.256799 192.168.0.100 239.255.255.250 SSDP 413 NOTIFY HTTP/1.1 6 23.356948 192.168.0.100 239.255.255.250 SSDP 413 NOTIFY HTTP/1.1 7 23.457076 192.168.0.100 239.255.255.250 SSDP 413 NOTIFY HTTP/1.1 8 23.557221 192.168.0.100 239.255.255.250 SSDP 411 NOTIFY HTTP/1.1 9 23.657563 192.168.0.100 239.255.255.250 SSDP 411 NOTIFY HTTP/1.1 10 23.757906 192.168.0.100 239.255.255.250 SSDP 411 NOTIFY HTTP/1.1 11 23.858051 192.168.0.100 239.255.255.250 SSDP 422 NOTIFY HTTP/1.1 12 23.958170 192.168.0.100 239.255.255.250 SSDP 422 NOTIFY HTTP/1.1 13 24.058339 192.168.0.100 239.255.255.250 SSDP 422 NOTIFY HTTP/1.1 14 24.158553 192.168.0.100 239.255.255.250 SSDP 463 NOTIFY HTTP/1.1 15 24.258679 192.168.0.100 239.255.255.250 SSDP 463 NOTIFY HTTP/1.1 16 24.358833 192.168.0.100 239.255.255.250 SSDP 463 NOTIFY HTTP/1.1 17 27.987695 192.168.0.188 239.255.255.250 SSDP 217 M-SEARCH HTTP/1.1 18 28.993433 192.168.0.188 239.255.255.250 SSDP 217 M-SEARCH HTTP/1.1 19 29.994276 192.168.0.188 239.255.255.250 SSDP 217 M-SEARCH HTTP/1.1 20 30.995189 192.168.0.188 239.255.255.250 SSDP 217 M-SEARCH HTTP/1.1 21 71.685383 192.168.0.188 239.255.255.250 SSDP 211 M-SEARCH HTTP/1.1 22 72.696623 192.168.0.188 239.255.255.250 SSDP 211 M-SEARCH HTTP/1.1 23 73.696874 192.168.0.188 239.255.255.250 SSDP 211 M-SEARCH HTTP/1.1 24 74.697067 192.168.0.188 239.255.255.250 SSDP 211 M-SEARCH HTTP/1.1 25 83.482031 192.168.0.107 239.255.255.250 SSDP 136 M-SEARCH HTTP/1.1 26 83.963206 192.168.0.107 239.255.255.250 SSDP 136 M-SEARCH HTTP/1.1 27 84.062279 192.168.0.107 239.255.255.250 SSDP 136 M-SEARCH HTTP/1.1 28 89.251050 192.168.0.107 239.255.255.250 SSDP 396 NOTIFY HTTP/1.1 29 89.345021 192.168.0.107 239.255.255.250 SSDP 396 NOTIFY HTTP/1.1 30 89.447675 192.168.0.107 239.255.255.250 SSDP 396 NOTIFY HTTP/1.1 31 89.549279 192.168.0.107 239.255.255.250 SSDP 394 NOTIFY HTTP/1.1 32 89.653658 192.168.0.107 239.255.255.250 SSDP 394 NOTIFY HTTP/1.1 33 89.759647 192.168.0.107 239.255.255.250 SSDP 394 NOTIFY HTTP/1.1 34 89.843073 192.168.0.107 239.255.255.250 SSDP 405 NOTIFY HTTP/1.1 35 89.950249 192.168.0.107 239.255.255.250 SSDP 405 NOTIFY HTTP/1.1 36 90.050953 192.168.0.107 239.255.255.250 SSDP 405 NOTIFY HTTP/1.1 37 90.150848 192.168.0.107 239.255.255.250 SSDP 446 NOTIFY HTTP/1.1 38 90.251845 192.168.0.107 239.255.255.250 SSDP 446 NOTIFY HTTP/1.1 39 90.353418 192.168.0.107 239.255.255.250 SSDP 446 NOTIFY * HTTP/1.1

For some reason, however, I am not getting notification events.

Digging into the code further, I see now that what Rssdp is getting are NOTIFY messages. That's how the client sees is alerted to itself. But these NOTIFY messages aren't received from the other (remote) client in SsdpCommunicationServer::ProcessMessage(). Yet, Wireshark sees them. Hmm...

trzy commented 2 years ago

Ah... the issue seems to be Unity :( I suspect something about async execution is not working as intended in Unity. Everything works fine in a console app. Long shot... but maybe you've dealt with Unity before and might know where the problem could be? It's clearly still receiving some messages on the socket it is listening on.

trzy commented 2 years ago

It was Windows Firewall (which seems to somehow keep resetting its rules for the application in question).