Skip to content

Commit

Permalink
Merge pull request #66 from richardschneider/subtypes
Browse files Browse the repository at this point in the history
DNS-SD subtypes
  • Loading branch information
richardschneider authored Jun 21, 2019
2 parents 05722b8 + 5528bcd commit 531ac28
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions doc/articles/subtype.md
Original file line number Diff line number Diff line change
@@ -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);
```
3 changes: 3 additions & 0 deletions doc/articles/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 28 additions & 0 deletions src/ServiceDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,24 @@ public void QueryServiceInstances(string service)
Mdns.SendQuery(service + ".local", type: DnsType.PTR);
}

/// <summary>
/// Asks instances of the specified service with the subtype to send details.
/// </summary>
/// <param name="service">
/// The service name to query. Typically of the form "_<i>service</i>._tcp".
/// </param>
/// <param name="subtype">
/// The feature that is needed.
/// </param>
/// <remarks>
/// When an answer is received the <see cref="ServiceInstanceDiscovered"/> event is raised.
/// </remarks>
/// <seealso cref="ServiceProfile.ServiceName"/>
public void QueryServiceInstances(string service, string subtype)
{
Mdns.SendQuery($"{subtype}._sub.{service}.local", type: DnsType.PTR);
}

/// <summary>
/// Asks instances of the specified service to send details.
/// accepts unicast and/or broadcast answers.
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions src/ServiceProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@ public ServiceProfile(string instanceName, string serviceName, ushort port, IEnu
/// </remarks>
public List<ResourceRecord> Resources { get; set; } = new List<ResourceRecord>();

/// <summary>
/// A list of service features implemented by the service instance.
/// </summary>
/// <value>
/// The default is an empty list.
/// </value>
/// <seealso href="https://tools.ietf.org/html/rfc6763#section-7.1"/>
public List<string> Subtypes { get; set; } = new List<string>();

/// <summary>
/// Add a property of the service to the <see cref="TXTRecord"/>.
/// </summary>
Expand Down
70 changes: 70 additions & 0 deletions test/ServiceDiscoveryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PTRRecord>().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()
{
Expand Down Expand Up @@ -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()
{
Expand Down
7 changes: 7 additions & 0 deletions test/ServiceProfileTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,12 @@ public void TTLs()
Assert.AreEqual(TimeSpan.FromMinutes(75), service.Resources.OfType<TXTRecord>().First().TTL);
Assert.AreEqual(TimeSpan.FromSeconds(120), service.Resources.OfType<AddressRecord>().First().TTL);
}

[TestMethod]
public void Subtypes()
{
var service = new ServiceProfile("x", "_sdtest._udp", 1024);
Assert.AreEqual(0, service.Subtypes.Count);
}
}
}

0 comments on commit 531ac28

Please sign in to comment.