Cocoa for Scientists Bonjour and How Do You Do?

·

·

Many of you will have noticed that last week Apple lifted their NDA on the iPhone SDK, which means we are now free to talk about it. You will undoubtedly read a lot about what is in the SDK, on blogs, and in Apple’s own documentation, but one aspect of iPhone development that I found to be poorly covered was networking. I don’t mean downloading a web page, or copying files from an FTP server, I mean the low-level stuff. How do you find and talk to another iPhone or Mac? The options available on the iPhone are also available on the Mac, so it seemed like a good topic for this series.

Low-Level Networking

One of the reasons you won’t find too many articles about low-level networking in Cocoa is two-fold:

  1. There are good high-level networking options — on the Mac.
  2. Low-level networking is quite hard.

The Mac has a technology called ‘Distributed Objects’ (DO), which makes it quite easy for one process to talk to another, even if it is on another computer. The downside of DO is that it works only on Macs, not on iPhones, and not on any other platform. If you want to communicate iPhone-to-iPhone, Mac-to-iPhone, iPhone-to-PlatformX, or Mac-to-PlatformX, you will need something more low-level.

This brings us to point (2): low-level networking can be a little tricky, which means you will probably want to build your own ‘Messaging Library’, to reduce the complexity and chance of bugs. This is a set of functions and classes that allow you to easily send messages — chunks of data — from one system to another, without having to think too much about it.

Sockets

At the lowest level of OS X, we have BSD sockets. A socket on one computer can be connected to a socket on another computer, and raw data can be streamed across it. Although you can use the C API for BSD sockets directly, it is quite complex, and — when using Cocoa — you are better off using BSD sockets indirectly via a socket-abstraction class.

Apple actually supplies such a class, the ever versatile NSFileHandle. Yes, you read right, Mac OS X basically sees sockets as special file handles, which you can serially read from, or write to.

Using NSFileHandle is one option, but probably not the best. For a more advanced socket-abstraction class, you need to look further afield. The one we will be using is AsyncSocket, an open-source class which is both powerful, easy to use, and will run on iPhone and Mac.

We will be building our messaging layer on top of AsyncSocket, but we are going to leave that for the next tutorial. For now, we need to address a more pressing issue: how do our devices find each other?

Finding Each Other

In this tutorial, we are going to look at the first challenge of connecting two systems, that of locating each other on a local network. For this, we are going to use Apple’s Bonjour. For the overly pedantic, Bonjour is Apple’s implementation of Zeroconf, but you really don’t need to know too much about how it works to use it.

The Cocoa interface to Bonjour has two classes: NSNetServiceBrowser and NSNetService. A NSNetService represents any service that a server would like to vend. For example, I’ve been working recently on getting the Mac and iPhone versions of Mental Case to sync via wifi; in that case, the service vended was simply the ability to perform a certain type of sync.

The server publishes its service, and any clients on the network can then look for the service using the NSNetServiceBrowser class. This class seeks out the services of a particular type on the local network, and reports them. It does not setup any sort of connection — it just finds and reports the existence of services. (Note that a ‘local network’ in the context of Bonjour exists between two devices when they can send each other data without that data passing through a router, or any other form of network address translation.)

Client and Server

Let’s see how it works in practice. We need two separate projects for this: a client application, and a server application. You can run both on the same computer at the same time, and everything will work fine.

Download the client project, and the server project, and build and run each at the same time.

To begin the client searching for services, click the ‘Search’ button in the client app. The server should appear in the list, indicating it was located. You can now select the server, and press Connect. Though nothing will appear to change, the connection has now been made.

That’s the extent of this week’s functionality; not very exciting, but it will get better next time, when we will try to send an image from one app to the other.

The Server

Now let’s see how it works. First take a look in the Server project at the ServerController class. The header file looks like this

#import <Cocoa/Cocoa.h>

@interface ServerController : NSObject {
    NSNetService *netService;
}

@end

This is a very simple class, with no public methods, and only a single instance variable to store the NSNetService that will be published.

The implementation is also quite straightforward:

#import "ServerController.h"

@interface ServerController ()

-(void)startService;
-(void)stopService;

@end


@implementation ServerController

-(void)awakeFromNib {    
    [self startService];
}

-(void)startService {
    netService = [[NSNetService alloc] initWithDomain:@"" type:@"_cocoaforsci._tcp." 
        name:@"" port:7865];
    netService.delegate = self;
    [netService publish];
}

-(void)stopService {
    [netService stop];
    [netService release]; 
    netService = nil;
}

-(void)dealloc {
    [self stopService];
    [super dealloc];
}

#pragma mark Net Service Delegate Methods
-(void)netService:(NSNetService *)aNetService didNotPublish:(NSDictionary *)dict {
    NSLog(@"Failed to publish: %@", dict);
}

@end

By far the most interesting method is startService, which gets called from awakeFromNib when the application launches. A new net service is created using the initializer initWithDomain:type:name:port:.

netService = [[NSNetService alloc] initWithDomain:@"" type:@"_cocoaforsci._tcp." 
    name:@"" port:7865];

You can explicitly enter a network domain to publish the service on, but most of the time you want it published on all local domains, which is achieved by passing an empty string. The ‘type’ is very important, because it declares what sort of service is being provided. Here we have made up a service called ‘cocoaforsci’, which makes use of the TCP protocol. The name of the type should be 14 characters or less in length. The leading underscores and periods are important, and cannot be left out. Be careful not to leave off the trailing period, which is easy to do, and can lead to copious hair pulling.

The name of service is supplied next. A common mistake is to think that this name should be the same for all instances of the service, on any server, but this is not the case. In fact, it must be a unique name on the local network. If two servers are running, each must use a different name, which uniquely identifies the service on that particular machine. If you pass an empty string, as we have here, Bonjour will use the host’s computer name.

NSNetService has a delegate, which it reports to. On the server side, we are only really interested in whether the service fails to publish, so the netService:didNotPublish: delegate method has been included.

The Client

The client is a bit more involved. It has to create an NSNetServiceBrowser, and then handle any services that are located via delegate method invocations. The header file looks like this

#import <Cocoa/Cocoa.h>

@interface ClientController : NSObject {
    BOOL isConnected;
    NSNetServiceBrowser *browser;
    NSNetService *connectedService;
    NSMutableArray *services;
    IBOutlet NSArrayController *servicesController;
}

@property (readonly, retain) NSMutableArray *services;
@property (readonly, assign) BOOL isConnected;

-(IBAction)search:(id)sender;
-(IBAction)connect:(id)sender;

@end

Aside from the NSNetServiceBrowser, there are instance variables to store the service that the client is currently connected to, as well as a mutable array to store all of the services that have been found by the browser, so they can be listed in the table view.

The implementation looks like this

#import "ClientController.h"

@interface ClientController ()

@property (readwrite, retain) NSNetServiceBrowser *browser;
@property (readwrite, retain) NSMutableArray *services;
@property (readwrite, assign) BOOL isConnected;
@property (readwrite, retain) NSNetService *connectedService;

@end

@implementation ClientController

@synthesize browser;
@synthesize services;
@synthesize isConnected;
@synthesize connectedService;

-(void)awakeFromNib {
    services = [NSMutableArray new];
    self.browser = [[NSNetServiceBrowser new] autorelease];
    self.browser.delegate = self;
    self.isConnected = NO;
}

-(void)dealloc {
    self.connectedService = nil;
    self.browser = nil;
    [services release];
    [super dealloc];
}

-(IBAction)search:(id)sender {
    [self.browser searchForServicesOfType:@"_cocoaforsci._tcp." inDomain:@""];
}

-(IBAction)connect:(id)sender {
    NSNetService *remoteService = servicesController.selectedObjects.lastObject;
    remoteService.delegate = self;
    [remoteService resolveWithTimeout:0];
}

#pragma mark Net Service Browser Delegate Methods
-(void)netServiceBrowser:(NSNetServiceBrowser *)aBrowser didFindService:(NSNetService *)aService moreComing:(BOOL)more {
    [servicesController addObject:aService];
}

-(void)netServiceBrowser:(NSNetServiceBrowser *)aBrowser didRemoveService:(NSNetService *)aService moreComing:(BOOL)more {
    [servicesController removeObject:aService];
    if ( aService == self.connectedService ) self.isConnected = NO;
}

-(void)netServiceDidResolveAddress:(NSNetService *)service {
    self.isConnected = YES;
    self.connectedService = service;
}

-(void)netService:(NSNetService *)service didNotResolve:(NSDictionary *)errorDict {
    NSLog(@"Could not resolve: %@", errorDict);
}

@end

The NSNetServiceBrowser is created in the awakeFromNib method

-(void)awakeFromNib {
    services = [NSMutableArray new];
    self.browser = [[NSNetServiceBrowser new] autorelease];
    self.browser.delegate = self;
    self.isConnected = NO;
}

Here we have used the new method, which is equivalent to alloc followed by init. You do not initially need to configure the browser in any way, other than to set the delegate so that it can report any services it finds.

When you are ready to begin searching for a particular type of service, such as when the user presses the Search button, you invoke the method searchForServicesOfType:inDomain:.

-(IBAction)search:(id)sender {
    [self.browser searchForServicesOfType:@"_cocoaforsci._tcp." inDomain:@""];
}

The same rules apply as before: be careful with your dots and dashes in the service type, and pass an empty string if you want to search all local domains.

Resolving Services

There are actually two phases to connecting to a service: the first is to find it on the network, and the second is to resolve it. Before you can use a service, you need to find out if it is available, and retrieve information about the service such as port number and IP address. This is what resolving the service entails.

When the browser finds a service, or indeed removes one, it invokes a delegate method:

-(void)netServiceBrowser:(NSNetServiceBrowser *)aBrowser didFindService:(NSNetService *)aService moreComing:(BOOL)more {
    [servicesController addObject:aService];
}

-(void)netServiceBrowser:(NSNetServiceBrowser *)aBrowser didRemoveService:(NSNetService *)aService moreComing:(BOOL)more {
    [servicesController removeObject:aService];
    if ( aService == self.connectedService ) self.isConnected = NO;
}

netServiceBrowser:didFindService:moreComing: is called whenever a service is found (not resolved). In this case, we add the service to our array. The netServiceBrowser:didRemoveService:moreComing: method is called if a service is removed from the network. In both cases, the moreComing flag can be used to know if the browser is finished reporting services, or if more are on the way.

When the client actually needs to connect to a particular service, such as when the user selects a service and presses the Connect button, the client needs to know details such as port number and IP address — it needs to resolve the service.

-(IBAction)connect:(id)sender {
    NSNetService *remoteService = servicesController.selectedObjects.lastObject;
    remoteService.delegate = self;
    [remoteService resolveWithTimeout:0];
}

The resolveWithTimeout: method is invoked on the NSNetService. If the service is successfully resolved, the netServiceDidResolveAddress: method is invoked, and, if not, netService:didNotResolve: is called.

-(void)netServiceDidResolveAddress:(NSNetService *)service {
    self.isConnected = YES;
    self.connectedService = service;
}

-(void)netService:(NSNetService *)service didNotResolve:(NSDictionary *)errorDict {
    NSLog(@"Could not resolve: %@", errorDict);
}

In this example, the connected service is stored, and no other action undertaken. Next time, we will take the resolved service, and use the information stored in it to connect to a remote socket on the server, so that data can be transferred.

Location, Location, Location

That’s it for this time. If your head is swimming a bit, welcome to the wonderful world of networking. Things that you would think should be simple, such as looking up a service, can actually turn out to be harder than you would think. Why? Because, by its nature, networking is slow, which means that networking APIs work asynchronously, so that your program doesn’t lock up every time you perform a network operation.

What this means is that instead of easy-to-understand, synchronous function calls, you get non-linear callbacks instead, in the form of delegate method invocations. That’s life I guess. Can’t be helped.

Next time we will move into the wonderful world of sockets, and building your own messaging library.

Update: Changed the name of the service to ‘cocoaforsci’, to fit in the 14 character limit. Also passed empty string for the service name, and changed the resolve timeout to 0.


Leave a Reply

Your email address will not be published. Required fields are marked *