diff --git a/README.md b/README.md index af1dd29..f8ad201 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ service or service instance. - Detects new and/or removed network interfaces - Supports multicasting on multiple network interfaces - Supports reverse address mapping +- Supports service subtypes (features) - Handles legacy unicast queries, see #61 ## Getting started diff --git a/doc/articles/subtype.md b/doc/articles/subtype.md new file mode 100644 index 0000000..3b4d63b --- /dev/null +++ b/doc/articles/subtype.md @@ -0,0 +1,38 @@ +# Subtypes + +Subtypes are used to define features implemented by a service instance. See +[RFC 6763 - 7.1 Selective Instance Enumeration (Subtypes)](https://tools.ietf.org/html/rfc6763#section-7.1) for the details. + + +## Finding service instances + +[QueryServiceInstances](xref:Makaretu.Dns.ServiceDiscovery.QueryServiceInstances*) is used to find the +all the instances of a service with a specific feature. +The [ServiceInstanceDiscovered](xref:Makaretu.Dns.ServiceDiscovery.ServiceInstanceDiscovered) event is raised +each time a service instance is discovered. + +```csharp +using Makaretu.Dns; + +var sd = new ServiceDiscovery(); +sd.ServiceInstanceDiscovered += (s, e) => +{ + Console.WriteLine($"service instance '{e.ServiceInstanceName}'"); +}; +sd.QueryServiceInstances("_myservice", "apiv2"); +``` + +## Advertising + +Create a [ServiceProfile](xref:Makaretu.Dns.ServiceProfile) with a feature +and then [Advertise](xref:Makaretu.Dns.ServiceDiscovery.Advertise*) it. Any queries for the service or +service instance will be answered with information from the profile. + +```csharp +using Makaretu.Dns; + +var profile = new ServiceProfile("x", "_myservice._udp", 1024); +profile.Subtypes.Add("apiv2"); +var sd = new ServiceDiscovery(); +sd.Advertise(profile); +``` \ No newline at end of file diff --git a/doc/articles/toc.yml b/doc/articles/toc.yml index 2ba6bb2..cfa5a07 100644 --- a/doc/articles/toc.yml +++ b/doc/articles/toc.yml @@ -4,5 +4,8 @@ href: ms.md - name: Service Discovery href: sd.md + items: + - name: Subtypes + href: subtype.md - name: Class Reference href: ../api/Makaretu.Dns.yml diff --git a/src/ServiceDiscovery.cs b/src/ServiceDiscovery.cs index 2347102..1a88698 100644 --- a/src/ServiceDiscovery.cs +++ b/src/ServiceDiscovery.cs @@ -167,6 +167,24 @@ public void QueryServiceInstances(string service) Mdns.SendQuery(service + ".local", type: DnsType.PTR); } + /// + /// Asks instances of the specified service with the subtype to send details. + /// + /// + /// The service name to query. Typically of the form "_service._tcp". + /// + /// + /// The feature that is needed. + /// + /// + /// When an answer is received the event is raised. + /// + /// + public void QueryServiceInstances(string service, string subtype) + { + Mdns.SendQuery($"{subtype}._sub.{service}.local", type: DnsType.PTR); + } + /// /// Asks instances of the specified service to send details. /// accepts unicast and/or broadcast answers. @@ -209,6 +227,16 @@ public void Advertise(ServiceProfile service) new PTRRecord { Name = service.QualifiedServiceName, DomainName = service.FullyQualifiedName }, authoritative: true); + foreach (var subtype in service.Subtypes) + { + var ptr = new PTRRecord + { + Name = $"{subtype}._sub.{service.QualifiedServiceName}", + DomainName = service.FullyQualifiedName + }; + catalog.Add(ptr, authoritative: true); + } + foreach (var r in service.Resources) { catalog.Add(r, authoritative: true); diff --git a/src/ServiceProfile.cs b/src/ServiceProfile.cs index dc8864f..27f1e2e 100644 --- a/src/ServiceProfile.cs +++ b/src/ServiceProfile.cs @@ -154,6 +154,15 @@ public ServiceProfile(string instanceName, string serviceName, ushort port, IEnu /// public List Resources { get; set; } = new List(); + /// + /// A list of service features implemented by the service instance. + /// + /// + /// The default is an empty list. + /// + /// + public List Subtypes { get; set; } = new List(); + /// /// Add a property of the service to the . /// diff --git a/test/ServiceDiscoveryTest.cs b/test/ServiceDiscoveryTest.cs index d854e7f..8437bb5 100644 --- a/test/ServiceDiscoveryTest.cs +++ b/test/ServiceDiscoveryTest.cs @@ -121,6 +121,39 @@ public void Advertises_ServiceInstance_Address() } } + [TestMethod] + public void Advertises_ServiceInstance_Subtype() + { + var service = new ServiceProfile("x2", "_sdtest-1._udp", 1024, new[] { IPAddress.Loopback }); + service.Subtypes.Add("_example"); + var done = new ManualResetEvent(false); + + var mdns = new MulticastService(); + mdns.NetworkInterfaceDiscovered += (s, e) => + mdns.SendQuery("_example._sub._sdtest-1._udp.local", DnsClass.IN, DnsType.PTR); + mdns.AnswerReceived += (s, e) => + { + var msg = e.Message; + if (msg.Answers.OfType().Any(p => p.DomainName == service.FullyQualifiedName)) + { + done.Set(); + } + }; + try + { + using (var sd = new ServiceDiscovery(mdns)) + { + sd.Advertise(service); + mdns.Start(); + Assert.IsTrue(done.WaitOne(TimeSpan.FromSeconds(1)), "query timeout"); + } + } + finally + { + mdns.Stop(); + } + } + [TestMethod] public void Discover_AllServices() { @@ -213,6 +246,43 @@ public void Discover_ServiceInstance() } } + [TestMethod] + public void Discover_ServiceInstance_with_Subtype() + { + var service1 = new ServiceProfile("x", "_sdtest-2._udp", 1024); + var service2 = new ServiceProfile("y", "_sdtest-2._udp", 1024); + service2.Subtypes.Add("apiv2"); + var done = new ManualResetEvent(false); + var mdns = new MulticastService(); + var sd = new ServiceDiscovery(mdns); + + mdns.NetworkInterfaceDiscovered += (s, e) => + { + sd.QueryServiceInstances("_sdtest-2._udp", "apiv2"); + }; + + sd.ServiceInstanceDiscovered += (s, e) => + { + if (e.ServiceInstanceName == service2.FullyQualifiedName) + { + Assert.IsNotNull(e.Message); + done.Set(); + } + }; + try + { + sd.Advertise(service1); + sd.Advertise(service2); + mdns.Start(); + Assert.IsTrue(done.WaitOne(TimeSpan.FromSeconds(1)), "instance not found"); + } + finally + { + sd.Dispose(); + mdns.Stop(); + } + } + [TestMethod] public void Discover_ServiceInstance_Unicast() { diff --git a/test/ServiceProfileTest.cs b/test/ServiceProfileTest.cs index 2ddeefe..4f0f95e 100644 --- a/test/ServiceProfileTest.cs +++ b/test/ServiceProfileTest.cs @@ -95,5 +95,12 @@ public void TTLs() Assert.AreEqual(TimeSpan.FromMinutes(75), service.Resources.OfType().First().TTL); Assert.AreEqual(TimeSpan.FromSeconds(120), service.Resources.OfType().First().TTL); } + + [TestMethod] + public void Subtypes() + { + var service = new ServiceProfile("x", "_sdtest._udp", 1024); + Assert.AreEqual(0, service.Subtypes.Count); + } } }