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
-
create a
CFNetServiceBrowser
-
make it asynchronous
-
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
-
make it possible to get the result asynchronously
-
make the
CFNetService
asynchronous -
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.