Just An Application

November 14, 2013

Service Discovery In Android And iOS: Part Six – iOS Take Two

At the Core Foundation level we have the CFNetServices API which supports the registration and discovery of services using DNS/mDNS.

1.0 CFNetServiceBrowser

A CFNetServiceBrowser is the CFNetServices equivalent of an NSNetServiceBrowser.

2.0 CFNetService

A CFNetService is the CFNetServices equivalent of an NSNetService.

3.0 Synchronous vs. Aynchronous

Unlike the NSNetServiceBrowser and NSNetService methods the equivalent CFNetServiceBrowser and CFNetService functions can be used synchronously or asynchronously.

By default the functions are synchronous.

To make them asynchronous for a given CFNetServiceBrowser or CFNetService it must be added to a CFRunLoop.

4.0 Searching For Services Asynchronously

To search for services of a given type using the CFNetworkServices API we need to

  1. create a CFNetServiceBrowser

  2. make it asynchronous

  3. start the search

4.1 CFNetServiceBrowserCreate

We can create a CFNetServiceBrowser using the function CFNetServiceBrowserCreate which is declared like this

    CFNetServiceBrowserRef CFNetServiceBrowserCreate (
                               CFAllocatorRef                    alloc,
                               CFNetServiceBrowserClientCallBack clientCB,
                               CFNetServiceClientContext*        clientContext);

The alloc argument is a reference to an allocator to use when creating the CFNetServiceBrowser or more likely the constant

    kCFAllocatorDefault

4.1.1 The clientCB Argument

The clientCB argument is a pointer to a function of type CFNetServiceBrowserClientCallBack.

This function will be invoked when a service is found or if an error occurs.

The function type CFNetServiceBrowserClientCallBack is declared like this

    typedef void (*CFNetServiceBrowserClientCallBack) (
                       CFNetServiceBrowserRef browser,
                       CFOptionFlags          flags,
                       CFTypeRef              domainOrService,
                       CFStreamError*         error,
                       void*                  info);;

The browser argument is the CFNetServiceBrowserRef that was returned from the call to CFNetServiceBrowserCreate.

The callback function is also invoked when searching for domains so the flags argument is used to distinguish between the two uses.

It is also used to distinguish between the discovery of a service and the disappearance of a previously discovered service.

When a service is discovered the value of the flags argument will be zero (0).

When a previously discovered service disappears the value of the flags argument will be

   kCFNetServiceFlagRemove

When being invoked on the discovery or disappearance of a service the domainOrService argument will be a
CFNetServiceRef.

If an error has occurred, the error and domain fields of error argument will be set.

The info argument is the value of the info field of the CFNetServiceClientContext passed to the call to CFNetServiceBrowserCreate.

4.1.2 The clientContext Argument

The clientContext argument is a pointer to a CFNetServiceClientContext which is declared like this.

    struct CFNetServiceClientContext {
        CFIndex                            version;
        void*                              info;
        CFAllocatorRetainCallBack          retain;
        CFAllocatorReleaseCallBack         release;
        CFAllocatorCopyDescriptionCallBack copyDescription;
    };
        
    typedef struct CFNetServiceClientContext CFNetServiceClientContext;

The info field can be used to store a pointer to an ‘object’ which will be passed to the callback function passed via the clientCB argument.

If the retain and release fields are not NULL then the functions specified will be invoked when the implementation wishes to retain and release the ‘object’ specified in the info field.

It is not clear from the documentation whether the contents of the CFNetServiceClientContext are copied.

A const qualifier is sometimes a clue, but a little experimentation shows that they are, so it is safe to stack allocate.

4.2 CFNetServiceBrowserScheduleWithRunLoop

We can add our newly created CFNetServiceBrowser to a CFRunLoop using the function CFNetServiceBrowserScheduleWithRunLoop which is declared like this

    void CFNetServiceBrowserScheduleWithRunLoop(
             CFNetServiceBrowserRef browser,
             CFRunLoopRef           runLoop,
             CFStringRef            runLoopMode);

The easiest way to use this is to add the CFNetServiceBrowser to the main CFRunLoop in the default mode like so

    CFNetServiceBrowserScheduleWithRunLoop(
        browser, 
        CFRunLoopGetMain(), 
        kCFRunLoopDefaultMode);

4.3 CFNetServiceBrowserSearchForServices

Once we have made our CFNetServiceBrowse asynchronous we can start the search for services by calling the function CFNetServiceBrowserSearchForServices which is declared like this

    Boolean CFNetServiceBrowserSearchForServices (
                CFNetServiceBrowserRef browser,
                CFStringRef            domain,
                CFStringRef            serviceType,
                CFStreamError*         error);

The browser argument should be the CFNetServiceBrowserRef returned from the call to CFNetServiceBrowserCreate.

The domain argument should be the absolute name of the domain in which to search, e.g.,

    "local."

The serviceType argument should be the domain relative type of the services to search for, e.g.,

    "_ipp._tcp."

The documentation for the error argument is a tad confusing.

It states

A pointer to a CFStreamError structure, that, if an error occurs, will be set to the error and the error’s domain and passed to your callback function.

The

… and passed to your callback

bit does not appear to be true.

If it is not possible to start the search then the function returns false immediately and the domain and error fields of the CFStreamError are set.

5.0 Resolving A Service Asynchronously

Once a service has been found we need to resolve it …

To do this asynchronously we need to

  1. make it possible to get the result asynchronously

  2. make the CFNetService asynchronous

  3. start the resolution

5.1 CFNetServiceSetClient

To get the results from CFNetService functions when running asynchronously we must associate a callback function and a context with the CFNetService first.

We do this using the function CFNetServiceSetClient which is declared like this

    Boolean CFNetServiceSetClient(
                CFNetServiceRef            theService,
                CFNetServiceClientCallBack clientCB,
                CFNetServiceClientContext* clientContext);

5.1.1 The clientCB Argument

The clientCB argument is a pointer to a function of type CFNetServiceClientCallBack

This function will be invoked when the service is resolved or an error occurs.

The function type CFNetServiceClientCallBack is declared like this

    typedef void (*CFNetServiceClientCallBack) (
                      CFNetServiceRef theService,
                      CFStreamError*  error,
                      void*           info);

The info argument is the value of the info field of the CFNetServiceClientContext passed as the clientContext argument in the call to the CFNetServiceSetClient

If an error has occurred, the error and domain fields of error argument will be set.

5.1.2 The clientContext Argument

The clientContext argument is a pointer to a CFNetServiceClientContext which we have already seen used with the function CFNetServiceBrowserCreate.

5.2 CFNetServiceScheduleWithRunLoop

We can add our CFNetService instance to a CFRunLoop using the function CFNetServiceScheduleWithRunLoop which is declared like this.

    void CFNetServiceScheduleWithRunLoop(
             CFNetServiceRef theService,
             CFRunLoopRef    runLoop,
             CFStringRef     runLoopMode);

The easiest way to use this is to add the CFNetService to the main CFRunLoop in the default mode like so

    CFNetServiceScheduleWithRunLoop(service, CFRunLoopGetMain(), kCFRunLoopDefaultMode);

5.3 CFNetServiceResolveWithTimeout

Once we have made our CFNetService asynchronous we can start the resolution bey calling the function
CFNetServiceResolveWithTimeout which is declared like this

    Boolean CFNetServiceResolveWithTimeout(
                CFNetServiceRef theService,
                CFTimeInterval  timeout,
                CFStreamError*  error);

The timeout argument specifies the amount of time in seconds that the implementation should wait for the resolution to complete If the value is less than or equal to zero the implementation will wait indefinitely.

If it not possible to start the resolution the function returns false immediately and the domain and error fields of the CFStreamError argument are set.

6.0 The FindServices Class

Here is the FindServices class re-written to use the CFNetServices API.

    //
    //  FindServices.m
    //  XperTakeTwo
    //
    //  Created by Simon Lewis on 12/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
    
    #import "Service.h"
    
    #import "FindServices.h"
    
    @interface FindServices ()
    
    @property NSString*             type;
    @property NSString*             domain;
    
    @property NSMutableDictionary*  services;
    
    
    - (void)serviceFound:(CFNetServiceRef)theService;
    
    - (void)serviceLost:(CFNetServiceRef)theService;
    
    
    - (void)resolved:(CFNetServiceRef)theService;
    
    - (void)resolveFailed:(CFNetServiceRef)theService withError:(CFStreamError*)theError;
    
    
    - (void)stopBrowser;
    
    - (void)stopService:(CFNetServiceRef)theService;
    
    - (void)log:(NSString*)theMessage service:(CFNetServiceRef)theService;
    
    @end
    
    @implementation FindServices
    {
        CFNetServiceBrowserRef      browser;
    }
    
    - (FindServices*)initWithType:(NSString*)theType andDomain:(NSString*)theDomain
    {
        self = [super init];
        if (self != nil)
        {
            self.type     = theType;
            self.domain   = theDomain;
            self.services = [NSMutableDictionary dictionaryWithCapacity:8];
        }
        return self;
    }
    
    
    
    static void browserCallBack(
                    CFNetServiceBrowserRef  theBrowser,
                    CFOptionFlags           theFlags,
                    CFTypeRef               theDomainOrService,
                    CFStreamError*          theError,
                    void*                   theInfo)
    {
        NSLog(@"browserCallBack");
    
        if ((theError->error) != 0)
        {
            NSLog(@"error: %d\n", (int)theError->error);
        }
        else
        if ((theFlags & kCFNetServiceFlagIsDomain) != 0)
        {
            NSLog(@"domain !\n");
        }
        else // service
        if ((theFlags & kCFNetServiceFlagRemove) == 0)
        {
            [(__bridge FindServices*)theInfo serviceFound:(CFNetServiceRef)theDomainOrService];
        }
        else
        {
            [(__bridge FindServices*)theInfo serviceLost:(CFNetServiceRef)theDomainOrService];
        }
    }
    
    - (void)start
    {
        CFNetServiceClientContext   context;
    
        memset(&context, 0, sizeof(context));
        context.info = (__bridge void *)(self);
    
        browser = CFNetServiceBrowserCreate(kCFAllocatorDefault, browserCallBack, &context);
    
        CFNetServiceBrowserScheduleWithRunLoop(browser, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
    
        Boolean       status;
        CFStreamError error;
    
        status = CFNetServiceBrowserSearchForServices(
                     browser,
                     (__bridge CFStringRef)self.domain,
                     (__bridge CFStringRef)self.type,
                     &error);
        if (status == 0)
        {
            NSLog(@"error.error == %d\n", (int)error.error);
            [self stopBrowser];
        }
    }

    static void resolveCallBack(
                    CFNetServiceRef theService,
                    CFStreamError*  theError,
                    void*           theInfo)
    {
        NSLog(@"resolveCallback");

        if (theError->error == 0)
        {
            [(__bridge FindServices*)theInfo resolved:theService];
        }
        else
        {
            [(__bridge FindServices*)theInfo resolveFailed:theService withError:theError];
        }
    }
    
    - (void)serviceFound:(CFNetServiceRef)theService
    {
        [self log:@"service found: %@.%@%@" service:theService];
    
        CFNetServiceClientContext   context;
    
        memset(&context, 0, sizeof(context));
        context.info = (__bridge void *)(self);
    
        CFNetServiceSetClient(theService, resolveCallBack, &context);
    
        CFNetServiceScheduleWithRunLoop(theService, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
    
        Boolean       status;
        CFStreamError error;
    
        status = CFNetServiceResolveWithTimeout(theService, 0.0, &error);
        if (status == 0)
        {
            NSLog(@"error.error == %d\n", (int)error.error);
    
           [self stopService:theService];
        }
    }
    
    - (void)serviceLost:(CFNetServiceRef)theService
    {
        [self log: @"service lost: %@.%@%@" service:theService];
    
        NSString* name   = [NSString stringWithFormat:
                                @"%@.%@%@",
                                CFNetServiceGetName(
                                    theService),
                                CFNetServiceGetType(
                                    theService),
                                CFNetServiceGetDomain(
                                    theService)];
        Service* service = [self.services objectForKey:name];
    
        if (service != nil)
        {
            [service lost];
            [self.services removeObjectForKey:name];
            [self.delegate findServices:self didLoseService:service];
        }
        else
        {
            [self log: @"service lost but was not found: %@.%@%@" service:theService];
        }
    }
    
    - (void)resolved:(CFNetServiceRef)theService
    {
        [self log:@"service resolved: %@.%@%@" service:theService];
    
        CFArrayRef addresses = CFNetServiceGetAddressing(theService);
    
        if (CFArrayGetCount(addresses) != 0)
        {
            Service* service = [[Service alloc] init:theService];
    
            [self.services setObject:service forKey:service.name];
            [self.delegate findServices:self didFindService:service];
        }
        else
        {
            NSLog(@"service has 0 addresses: lost ?");
        }
    }
    
    - (void)resolveFailed:(CFNetServiceRef)theService withError:(CFStreamError *)theError
    {
        [self log:@"service resolvedFailed: %@.%@%@" service:theService];
        [self stopService:theService];
    }
    
    - (void)stopBrowser
    {
        CFNetServiceBrowserUnscheduleFromRunLoop(browser, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
        CFNetServiceBrowserInvalidate(browser);
        CFNetServiceBrowserStopSearch(browser, NULL);
    }
    
    - (void)stopService:(CFNetServiceRef)theService
    {
        CFNetServiceClientContext   context;
    
        memset(&context, 0, sizeof(context));
    
        CFNetServiceUnscheduleFromRunLoop(theService, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
        CFNetServiceSetClient(theService, NULL, &context);
        CFNetServiceCancel(theService);
    }
    
    - (void)log:(NSString*)theMessage service:(CFNetServiceRef)theService
    {
        NSLog(
            theMessage,
            CFNetServiceGetName(
                theService),
            CFNetServiceGetType(
                theService),
           CFNetServiceGetDomain(
               theService));
    }
    
    @end

7.0 Examples

In each case FindServices is looking for services of type

    "_ipp._tcp."

in the domain

    "local."

In each case the log output is from FindServices and its delegate running on an iPad running iOS 7.0.

7.1 A Single IPPServer

A single instance of the CUPS test server IPPServer with the name ipp_server_1 running on a Mac and then being stopped.

    ...

    2013-11-14 14:07:07.561 XperTakeTwo[665:60b] browserCallBack
    2013-11-14 14:07:07.564 XperTakeTwo[665:60b] service found: ipp_server_1._ipp._tcp.local.
    2013-11-14 14:07:07.593 XperTakeTwo[665:60b] resolveCallback
    2013-11-14 14:07:07.595 XperTakeTwo[665:60b] service resolved: ipp_server_1._ipp._tcp.local.
    2013-11-14 14:07:07.597 XperTakeTwo[665:60b] findServices:didFindService:<Service: 0x14e86990>
    2013-11-14 14:07:13.931 XperTakeTwo[665:60b] browserCallBack
    2013-11-14 14:07:13.933 XperTakeTwo[665:60b] service lost: ipp_server_1._ipp._tcp.local.
    2013-11-14 14:07:13.935 XperTakeTwo[665:60b] findServices:didLoseService::<Service: 0x14e86990>

    ...

7.2 A Single Printer

A printer being turned on and then turned a couple of minutes later.

Note the lines shown in bold for emphasis.

When the printer is turned off the resolveCallback function is being called for a second time but this time the CFNetService has no addresses.

This is the same thing that happened in this case when using the NSNetServiceBrowser/NSNetService API. (see here)

    ...

    2013-11-14 14:07:55.177 XperTakeTwo[671:60b] browserCallBack
    2013-11-14 14:07:55.180 XperTakeTwo[671:60b] service found: Canon MG6200 series._ipp._tcp.local.
    2013-11-14 14:07:55.250 XperTakeTwo[671:60b] resolveCallback
    2013-11-14 14:07:55.252 XperTakeTwo[671:60b] service resolved: Canon MG6200 series._ipp._tcp.local.
    2013-11-14 14:07:55.255 XperTakeTwo[671:60b] findServices:didFindService:<Service: 0x1452e060>

    2013-11-14 14:09:05.838 XperTakeTwo[671:60b] resolveCallback
    2013-11-14 14:09:05.840 XperTakeTwo[671:60b] service resolved: Canon MG6200 series._ipp._tcp.local.
    2013-11-14 14:09:05.843 XperTakeTwo[671:60b] service has 0 addresses: lost ?

    2013-11-14 14:09:06.910 XperTakeTwo[671:60b] browserCallBack
    2013-11-14 14:09:06.912 XperTakeTwo[671:60b] service lost: Canon MG6200 series._ipp._tcp.local.
    2013-11-14 14:09:06.915 XperTakeTwo[671:60b] findServices:didLoseService::<Service: 0x1452e060>

    ...

Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

November 3, 2013

Service Discovery In Android And iOS: Part Four – Doing It Properly In Java

Filed under: DNS, DNS Based Service Discovery, mDNS — Tags: , , , — Simon Lewis @ 8:37 pm

To do mDNS based service discovery in Java we would need to be able to

  • send and receive UDP multicast packets
  • read and write DNS messages

Fortunately both of theses things are eminently doable.

1.0 Multicasting

UDP multicasting we get for free courtesy of the class java.net.MulticastSocket.

1.1 Creating A Multicast Socket

Creating a multicast socket is just like creating a unicast socket but in addition we must call the method

    public void joinGroup(InetAddress mcastaddr)
                throws
                    IOException

to join the multicast group we wish to send to, e.g.,

    ...

    socket = new MulticastSocket();
    group  = InetAddress.getByName(MDNS_GROUP_ADDRESS);
    socket.joinGroup(group);
    
    ...

where MDNS_GROUP_ADDRESS is defined as

    private static final String MDNS_GROUP_ADDRESS = "224.0.0.251";

1.2 Sending

Sending a datagram is also the same as in the unicast case, e.g.,

    ...
    
    packet.setAddress(group);
    packet.setPort(MDNS_PORT);
    socket.send(packet);

    ...

where packet is an instance of java.net.DatagramPacket, group is the IV we initialized when creating the socket and MDNS_PORT isdefined as

    private static final int MDNS_PORT = 5353;

1.3 Receiving

Receiving on a multicast socket is exactly the same as on a unicast socket

2.0 Reading A DNS Message

The class MessageReader is responsible for reading incoming DNS messages. Only responses are handled as that is all we expect to see.

The components of the response are handed to an instance of ResponseHandler as they are read.

These are the methods that read the message.

    ...
    
    public void read(ResponseHandler theHandler)
                throws
                    Exception
    {
        int id        = readUnsignedShort();
        int bitfields = readUnsignedShort();
    
        if ((bitfields & QR_RESPONSE_BIT) == 0)
        {
            // query !
        }
        else
        if ((bitfields & RCODE_BITS) == 0)
        {
            theHandler.beginResponse();
    
            int qdcount = readUnsignedShort();
            int ancount = readUnsignedShort();
            int nscount = readUnsignedShort();
            int arcount = readUnsignedShort();
    
            readQuestions(qdcount, theHandler);
            readRecords(Category.ANSWER, ancount, theHandler);
            readRecords(Category.AUTHORITY, nscount, theHandler);
            readRecords(Category.ADDITIONAL, arcount, theHandler);
            if (offset != end)
            {
                throw new Exception();
            }
            theHandler.endResponse();
        }
        else
        {
            // error !
        }
    }
	
    private void readQuestions(int theCount, ResponseHandler theHandler)
                 throws
                     Exception
    {
        for (int i = 0; i < theCount; i++)
        {
            theHandler.question(
                           readNodeName(),
                           TYPE.get(readUnsignedShort()),
                           CLASS.get(readUnsignedShort()));
        }
    }
	
    private void readRecords(Category theCategory, int theCount, ResponseHandler theHandler)
                 throws
                     Exception
    {
        for (int i = 0; i < theCount; i++)
        {
            readRecord(theCategory, theHandler);
        }
    }
	
    private void readRecord(Category theCategory, ResponseHandler theHandler)
                 throws
                     Exception
    {
        String name     = readNodeName();
        int    type     = readUnsignedShort();
        int    klass    = readUnsignedShort();
        int    ttl      = readInt();
        int    rdlength = readUnsignedShort();
    
        switch (type)
        {
            case DNS.T_A:
    
                theHandler.a(theCategory, name, ttl, makeRDATA(rdlength));
                break;
    
            case DNS.T_AAAA:
    
                theHandler.aaaa(theCategory, name, ttl, makeRDATA(rdlength));
                break;
    
            case DNS.T_PTR:
    
                theHandler.ptr(theCategory, name, ttl, readNodeName());
                break;
    
            case DNS.T_TXT:
    
                theHandler.txt(theCategory, name, ttl, makeRDATA(rdlength));
                break;
    
            case DNS.T_SRV:
    
                theHandler.srv(
                               theCategory,
                               name,
                               ttl,
                               readUnsignedShort(),
                               readUnsignedShort(),
                               readUnsignedShort(),
                               readNodeName());
                break;
    
            default:
    
                theHandler.rr(theCategory, name, type, klass, ttl, makeRDATA(rdlength));
        }
    }
    
    ...

3.0 Writing a DNS Message

This is left as an exercise for the reader.

4.0 A Very Simple API

To do DNS base service discovery we need the ability to make queries and see the responses.

The class xper.net.mdns.Client defines the method

    public void query(String theName, TYPE theType)
                throws
                    Exception

which can be used to make queries.

An instance of the Client class can be obtained by calling the method

    public static Client getClient(ResponseHandler theHandler)

where the ResponseHandler interface is defined as follows

    public interface ResponseHandler
    {
        public enum Category
        {
            ANSWER,
            AUTHORITY,
            ADDITIONAL,
        }
	
        //
	
        public void beginResponse();
	
        //
	
        public void question(String theName, TYPE theType, CLASS theClass);
	
        //
	
        public void a(Category theCategory, String name, int ttl, RDATA makeRDATA);
	
        public void aaaa(Category theCategory, String name, int ttl, RDATA makeRDATA);
    
        public void ptr(Category theCategory, String theName, int theTTL, String thePtr);
	
        public void srv(
                        Category theCategory, 
                        String   theName, 
                        int      theTTL, 
                        int      thePriority, 
                        int      theWeight, 
                        int      thePort, 
                        String   theTarget);
	
        public void txt(Category theCategory, String theName, int theTTL, RDATA theData);
	
        //
	
        public void rr(Category theCategory, String theName, int theType, int theClass, int theTTL, RDATA theData);
    
        //
	
        public void endResponse();
    
    }

5.0 The FindServices Class

The FindServices class uses the ‘API’ to find services of a given type.

    // FindServices.java
    
    // Copyright (c) 2013 By Simon Lewis. All Rights Reserved.
    
    package xper.net.sd;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import android.util.Log;
    
    import xper.net.dns.core.CLASS;
    import xper.net.dns.core.RDATA;
    import xper.net.dns.core.ResponseHandler;
    import xper.net.dns.core.TYPE;
    import xper.net.mdns.Client;
    
    public final class FindServices
                       implements
                           ResponseHandler
    {
        public FindServices(String theType, ServiceListener theListener)
        {
            client   = Client.getClient(this);
            type     = theType;
            listener = theListener;
            services = new HashMap<String, ServiceData>();
        }
        
        //
        
        public void start()
                    throws
                        Exception
        {
            log("start");
            client.query(type, TYPE.PTR);
        }
        
        //
        
        @Override
        public void beginResponse()
        {
            log(">> beginResponse");
        }
        
        @Override
        public void question(String theName, TYPE theType, CLASS theClass)
        {
            log("question: " + theName + " " + theType + " " + theClass);
        }
        
        @Override
        public void a(Category theCategory, String theName, int theTTL, RDATA theData)
        {
            log(theCategory + " A: " + theName);
        }
        
        @Override
        public void aaaa(Category theCategory, String theName, int theTTL, RDATA theData)
        {
            log(theCategory + " AAAA: " + theName);
        }
        
        @Override
        public void ptr(Category theCategory, String theName, int theTTL, String thePtr)
        {
            log(theCategory + " PTR: " + thePtr);
		
            switch (theCategory)
            {
                case ANSWER:
        
                    ptr(theName, thePtr);
                    break;
        
                default:
        
                    ;
            }
        }
        
        @Override
        public void srv(
                        Category theCategory, 
                        String   theName, 
                        int      theTTL, 
                        int      thePriority, 
                        int      theWeight, 
                        int      thePort, 
                        String   theTarget)
        {
            log(theCategory + " SRV: " + theName);
		
            switch (theCategory)
            {
                case ANSWER:
                case ADDITIONAL:
        
                    srv(theName, thePriority, theWeight, thePort, theTarget);
                    break;
        
                default:
        
                    ;
            }
        }
        
        @Override
        public void txt(Category theCategory, String theName, int theTTL, RDATA theData)
        {
            log(theCategory + " TXT: " + theName);
		
            switch (theCategory)
            {
                case ANSWER:
                case ADDITIONAL:
        
                    txt(theName, theData);
                    break;
        
                default:
        
                    // Ignore
                    ;
            }
        }
        
        @Override
        public void rr(Category theCategory, String theName, int theType, int theClass, int theTTL, RDATA theData)
        {
            log(theCategory + " rr: " + theName + " type == " + theType + " class == " + theClass);
        }
        
        @Override
        public void endResponse()
        {
            for (ServiceData s: services.values())
            {
                try
                {
                    if (!s.haveSRV)
                    {
                        client.query(s.name, TYPE.SRV);
                    }
                    else
                    if (!s.haveTXT)
                    {
                        client.query(s.name, TYPE.TXT);
                    }
                    else
                    if (!s.found)
                    {
                        log("Found service " + s.name);
                        s.found = true;
                        listener.serviceFound(s);
                    }
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
            log("<< endResponse");
        }
            
        //
            
        private void ptr(String theName, String theServiceName)
        {
            ServiceData service    = getService(theServiceName);
            
            if (service == null)
            {
                addService(theServiceName);
            }
            else
            {
                // ????
            }
        }
            
        private void srv(String theName, int thePriority, int theWeight, int thePort, String theTarget)
        {
            ServiceData service = getService(theName);
            
            if ((service != null) && !service.haveSRV)
            {
                log("priority == " + thePriority);
                log("weight   == " + theWeight);
                log("port     == " + thePort);
                log("target   == " + theTarget);
			
                service.target   = theTarget;
                service.port     = thePort;
                service.priority = thePriority;
                service.weight   = theWeight;
                service.haveSRV  = true;
            }
            else
            {
                log("Unsolicited SRV record for " + theName + " !");
            }
        }
            
        private void txt(String theName, RDATA theData)
        {
            ServiceData service = getService(theName);
            
            if ((service != null) && !service.haveTXT)
            {
                try
                {
                    service.keyValuePairs = makeKeyValuePairs(theData.asStrings());
                    service.haveTXT       = true;
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
            else
            {
                log("Unsolicited TXT record for " + theName + " !");
            }
        }
            
        //
            
        private Map<String, String> makeKeyValuePairs(List theRawStrings)
        {
            Map<String, String> keyValuePairs = new HashMap<String, String>();
            
            for (String s : theRawStrings)
            {
                String key   = null;
                String value = null;
                int    index = s.indexOf('=');
			
                if (index != -1)
                {
                    key   = s.substring(0, index);
                    value = s.substring(index + 1);
                }
                else
                {
                    key   = s;
                    value = "";
                }
                log("key == " + key + " value == " + value);
                if (keyValuePairs.get(key) == null)
                {
                    keyValuePairs.put(key, value);
                }
            }
            return keyValuePairs;
        }
            
        //
            
        private ServiceData getService(String theName)
        {
            return services.get(theName);
        }
            
        private void addService(String theName)
        {
            services.put(theName, new ServiceData(theName, type));
        }
            
        private void log(String theString)
        {
            Log.d("FindServices", theString);
        }
            
        //
            
        private Client                   client;
        private String                   type;
        private ServiceListener          listener;
        private Map<String, ServiceData> services;
            
            
        private static final class ServiceData
                                   implements
                                       Service
        {
            @Override
            public String getName()
            {
                return name;
            }
            
            @Override
            public String getType()
            {
                return type;
            }
            
            @Override
            public String getTarget() 
            {
                return target;
            }
            
            @Override
            public int getPort() 
            {
                return port;
            }
            
            @Override
            public int getPriority() 
            {
                return priority;
            }
            
            @Override
            public int getWeight() 
            {
                return weight;
            }
            
            @Override
            public Map<String, String> getKeyValuePairs()
            {
                return keyValuePairs;
            }
            
            ServiceData(String theName, String theType) 
            {
                name = theName;
                type = theType;
            }
            
            //
            
            private String              name;
            private String              type;
            private String              target;
            private int                 port;
            private int                 priority;
            private int                 weight;
            private Map<String, String> keyValuePairs;
            
            //
            
            private boolean             haveSRV;
            private boolean             haveTXT;
            private boolean             found;
        }
    }

6.0 Examples

The following examples all show the output from an instance of the FindServices class initialized with the service type

    _ipp._tcp.local.

All output is the result of running the output of adb logcat through the command grep FindServices

All the examples involve combinations of three hosts on a wifi network.

6.1 Example One

An Android device and a actual printer.

It takes three queries to obtain all the necessary information


    D/FindServices( 2549): start
    D/FindServices( 2549): >> beginResponse
    D/FindServices( 2549): question: _ipp._tcp.local. PTR IN
    D/FindServices( 2549): ANSWER PTR: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 2549): << endResponse
    D/FindServices( 2549): >> beginResponse
    D/FindServices( 2549): question: Canon MG6200 series._ipp._tcp.local. SRV IN
    D/FindServices( 2549): ANSWER SRV: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 2549): priority == 0
    D/FindServices( 2549): weight   == 0
    D/FindServices( 2549): port     == 631
    D/FindServices( 2549): target   == 7D300C000000.local.
    D/FindServices( 2549): << endResponse
    D/FindServices( 2549): >> beginResponse
    D/FindServices( 2549): question: Canon MG6200 series._ipp._tcp.local. TXT IN
    D/FindServices( 2549): ANSWER TXT: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 2549): key == txtvers value == 1
    D/FindServices( 2549): key == rp value == ipp/printer
    D/FindServices( 2549): key == note value ==
    D/FindServices( 2549): key == qtotal value == 1
    D/FindServices( 2549): key == priority value == 15
    D/FindServices( 2549): key == ty value == Canon MG6200 series
    D/FindServices( 2549): key == product value == (Canon MG6200 series)
    D/FindServices( 2549): key == pdl value == application/octet-stream,image/urf,image/jpeg
    D/FindServices( 2549): key == adminurl value == http://7D300C000000.local.
    D/FindServices( 2549): key == usb_MFG value == Canon
    D/FindServices( 2549): key == usb_MDL value == MG6200 series
    D/FindServices( 2549): key == usb_CMD value == URF
    D/FindServices( 2549): key == UUID value == 00000000-0000-1000-8000-8887177D300C
    D/FindServices( 2549): key == URF value == CP1,PQ4-5,RS600,SRGB24,W8,DM3,OB9,OFU0
    D/FindServices( 2549): key == Color value == T
    D/FindServices( 2549): key == Duplex value == T
    D/FindServices( 2549): key == Scan value == T
    D/FindServices( 2549): key == mac value == 88:87:17:7D:30:0C
    D/FindServices( 2549): Found service Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 2549): << endResponse

6.2 Example Two

An Android device and a Mac.

The Mac is running an instance of the CUPS 1.7.0 test server ippserver with the name ipp_server_1 is which listening on port 6363.

The MacOS X mDNS implementation, mDNSResponder, is following the recommendation in RFC 6763 and is returning the relevant SRV and TXT records as well as the AAAA and A records as additional records in the response to the original query.


    D/FindServices( 2815): start
    D/FindServices( 2815): >> beginResponse
    D/FindServices( 2815): question: _ipp._tcp.local. PTR IN
    D/FindServices( 2815): ANSWER PTR: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2815): ADDITIONAL SRV: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2815): priority == 0
    D/FindServices( 2815): weight   == 0
    D/FindServices( 2815): port     == 6363
    D/FindServices( 2815): target   == Simons-Computer.local.
    D/FindServices( 2815): ADDITIONAL TXT: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2815): key == rp value == ipp/print
    D/FindServices( 2815): key == ty value == Test Printer
    D/FindServices( 2815): key == adminurl value == http://Simons-Computer.local:6363/
    D/FindServices( 2815): key == product value == (Printer)
    D/FindServices( 2815): key == pdl value == application/pdf,image/jpeg,image/pwg-raster
    D/FindServices( 2815): key == Color value == F
    D/FindServices( 2815): key == Duplex value == F
    D/FindServices( 2815): key == usb_MFG value == Test
    D/FindServices( 2815): key == usb_MDL value == Printer
    D/FindServices( 2815): ADDITIONAL AAAA: Simons-Computer.local.
    D/FindServices( 2815): ADDITIONAL A: Simons-Computer.local.
    D/FindServices( 2815): Found service ipp_server_1._ipp._tcp.local.
    D/FindServices( 2815): << endResponse

6.3 Example Three

An Android device and a Mac.

The Mac is running two instances of ippserver.

As in Example Two all the necessary information is contained in the reply to the original query.

        
    D/FindServices( 2929): start
    D/FindServices( 2929): >> beginResponse
    D/FindServices( 2929): question: _ipp._tcp.local. PTR IN
    D/FindServices( 2929): ANSWER PTR: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2929): ANSWER PTR: ipp_server_2._ipp._tcp.local.
    D/FindServices( 2929): ADDITIONAL SRV: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2929): priority == 0
    D/FindServices( 2929): weight   == 0
    D/FindServices( 2929): port     == 6363
    D/FindServices( 2929): target   == Simons-Computer.local.
    D/FindServices( 2929): ADDITIONAL TXT: ipp_server_1._ipp._tcp.local.
    D/FindServices( 2929): key == rp value == ipp/print
    D/FindServices( 2929): key == ty value == Test Printer
    D/FindServices( 2929): key == adminurl value == http://Simons-Computer.local:6363/
    D/FindServices( 2929): key == product value == (Printer)
    D/FindServices( 2929): key == pdl value == application/pdf,image/jpeg,image/pwg-raster
    D/FindServices( 2929): key == Color value == F
    D/FindServices( 2929): key == Duplex value == F
    D/FindServices( 2929): key == usb_MFG value == Test
    D/FindServices( 2929): key == usb_MDL value == Printer
    D/FindServices( 2929): ADDITIONAL SRV: ipp_server_2._ipp._tcp.local.
    D/FindServices( 2929): priority == 0
    D/FindServices( 2929): weight   == 0
    D/FindServices( 2929): port     == 6364
    D/FindServices( 2929): target   == Simons-Computer.local.
    D/FindServices( 2929): ADDITIONAL TXT: ipp_server_2._ipp._tcp.local.
    D/FindServices( 2929): key == rp value == ipp/print
    D/FindServices( 2929): key == ty value == Test Printer
    D/FindServices( 2929): key == adminurl value == http://Simons-Computer.local:6364/
    D/FindServices( 2929): key == product value == (Printer)
    D/FindServices( 2929): key == pdl value == application/pdf,image/jpeg,image/pwg-raster
    D/FindServices( 2929): key == Color value == F
    D/FindServices( 2929): key == Duplex value == F
    D/FindServices( 2929): key == usb_MFG value == Test
    D/FindServices( 2929): key == usb_MDL value == Printer
    D/FindServices( 2929): ADDITIONAL AAAA: Simons-Computer.local.
    D/FindServices( 2929): ADDITIONAL A: Simons-Computer.local.
    D/FindServices( 2929): Found service ipp_server_2._ipp._tcp.local.
    D/FindServices( 2929): Found service ipp_server_1._ipp._tcp.local.
    D/FindServices( 2929): << endResponse
    

6.4 Example Four

An Android device, a printer and a Mac running an instance of ippserver.

Just to show it can be done.


    D/FindServices( 3453): start
    D/FindServices( 3453): >> beginResponse
    D/FindServices( 3453): question: _ipp._tcp.local. PTR IN
    D/FindServices( 3453): ANSWER PTR: ipp_server_1._ipp._tcp.local.
    D/FindServices( 3453): ADDITIONAL SRV: ipp_server_1._ipp._tcp.local.
    D/FindServices( 3453): priority == 0
    D/FindServices( 3453): weight   == 0
    D/FindServices( 3453): port     == 6363
    D/FindServices( 3453): target   == Simons-Computer.local.
    D/FindServices( 3453): ADDITIONAL TXT: ipp_server_1._ipp._tcp.local.
    D/FindServices( 3453): key == rp value == ipp/print
    D/FindServices( 3453): key == ty value == Test Printer
    D/FindServices( 3453): key == adminurl value == http://Simons-Computer.local:6363/
    D/FindServices( 3453): key == product value == (Printer)
    D/FindServices( 3453): key == pdl value == application/pdf,image/jpeg,image/pwg-raster
    D/FindServices( 3453): key == Color value == F
    D/FindServices( 3453): key == Duplex value == F
    D/FindServices( 3453): key == usb_MFG value == Test
    D/FindServices( 3453): key == usb_MDL value == Printer
    D/FindServices( 3453): ADDITIONAL AAAA: Simons-Computer.local.
    D/FindServices( 3453): ADDITIONAL A: Simons-Computer.local.
    D/FindServices( 3453): Found service ipp_server_1._ipp._tcp.local.
    D/FindServices( 3453): << endResponse
    D/FindServices( 3453): >> beginResponse
    D/FindServices( 3453): question: _ipp._tcp.local. PTR IN
    D/FindServices( 3453): ANSWER PTR: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 3453): << endResponse
    D/FindServices( 3453): >> beginResponse
    D/FindServices( 3453): question: Canon MG6200 series._ipp._tcp.local. SRV IN
    D/FindServices( 3453): ANSWER SRV: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 3453): priority == 0
    D/FindServices( 3453): weight   == 0
    D/FindServices( 3453): port     == 631
    D/FindServices( 3453): target   == 7D300C000000.local.
    D/FindServices( 3453): << endResponse
    D/FindServices( 3453): >> beginResponse
    D/FindServices( 3453): question: Canon MG6200 series._ipp._tcp.local. TXT IN
    D/FindServices( 3453): ANSWER TXT: Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 3453): key == txtvers value == 1
    D/FindServices( 3453): key == rp value == ipp/printer
    D/FindServices( 3453): key == note value ==
    D/FindServices( 3453): key == qtotal value == 1
    D/FindServices( 3453): key == priority value == 15
    D/FindServices( 3453): key == ty value == Canon MG6200 series
    D/FindServices( 3453): key == product value == (Canon MG6200 series)
    D/FindServices( 3453): key == pdl value == application/octet-stream,image/urf,image/jpeg
    D/FindServices( 3453): key == adminurl value == http://7D300C000000.local.
    D/FindServices( 3453): key == usb_MFG value == Canon
    D/FindServices( 3453): key == usb_MDL value == MG6200 series
    D/FindServices( 3453): key == usb_CMD value == URF
    D/FindServices( 3453): key == UUID value == 00000000-0000-1000-8000-8887177D300C
    D/FindServices( 3453): key == URF value == CP1,PQ4-5,RS600,SRGB24,W8,DM3,OB9,OFU0
    D/FindServices( 3453): key == Color value == T
    D/FindServices( 3453): key == Duplex value == T
    D/FindServices( 3453): key == Scan value == T
    D/FindServices( 3453): key == mac value == 88:87:17:7D:30:0C
    D/FindServices( 3453): Found service Canon MG6200 series._ipp._tcp.local.
    D/FindServices( 3453): << endResponse


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

Service Discovery In Android And iOS: Part Three – If You Want Something Done Properly … mDNS And DNS Service Discovery Basics

To continue with the wild over simplifications, Multicast DNS (mDNS) defines a special node with the label

    local

immediately beneath the root ot the DNS name space.

The DNS name space sub-tree beneath the local node is not global but confined to the hosts directly attached to a given network.

There is no dedicated DNS server responsible for managing a local sub-tree.

Instead individual hosts attached to a network can each allocate nodes within the local sub-tree themselves. This means that different hosts can allocate nodes with the same name with associated resource records of the same type and class.

For queries to work in this context they are sent via multicast UDP so that they can be seen by all the hosts in the local network which may have allocated nodes in the sub-tree. Queries may result in multiple responses each from a different host.

2.0 Service Discovery Using DNS

RFC 6763 DNS-Based Service Discovery specifies how the DNS name space and DNS resource records can be used to make services discoverable.

2.1 Service Types

The label of a node which represents a service type starts with an underscore (‘_’).

A node which represents a service type is either a child of a node with the label

    _tcp

meaning that the service is available via TCP, or a child of a node with the label

    _udp

which, curiously, does not necessarily mean that the service is available via UDP, although it can be, but that the service is available via some protocol other than TCP.

For example the node

    _ipp._tcp.local.

represents the IPP over TCP service type in the local domain

2.2 Service Instances

A service instance, i.e., an entity which implements a service of a given type, is represented by child node of the service type node, e.g.,

    ipp-printer._ipp._tcp.local.

2.3 Service Type Resource Records

A service type node has an associated PTR record for each service instance of that service type. The data in each PTR record is the name of the node representing the service instance.

2.4 Service Instance Records

A service instance node has an associated SRV record.

The SRV record data identifies the host on which the service instance is running and the port it is listening on.

A service instance node also has an associated TXT record.

2.4.1 Service Instance TXT Records

The data of a service instance TXT record comprises one or more of strings of the form

    key=value

The key value pairs provide additional information about the service.

2.5 Service Discovery

To discover all the instances of a given service type it is only necessary to query the appropriate service type node for its PTR records.

Each PTR record in the answer will identify a service instance.

The node for each service instance can then be queried for its SRV and TXT records.

2.6 Service Discovery And The Additional Records Section

The standard recommends that the associated SRV and TXT records of the service instancea be included in the additional records section of the response to the query to the service type node for its PTR records.

In theory therefore it is possible to obtain all the information about the available instances of a given service type with a single query, subject to limits on the datagram size.


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

Blog at WordPress.com.

%d bloggers like this: