MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Programming With Cocoa

Incorporating Rendezvous into Your Cocoa Applications, Part 2

11/15/2002

Editor's note -- In the first part of this multi-part series, Mike Beam discussed some of the thinking behind Rendezvous (ZeroConf) and how to incorporate this functionality into your Cocoa applications. Now, in part two, he shows you how to build a simple Rendezvous client similar to iChat.

Our Application

Today we'll build the Rendezvous service publisher and browser, and in the next column I'll show you how to give the application the ability to talk to other instances of the application on a network.

In a fit of creative inspiration, I decided to call this application RCE, which stands for Rendezvous Chat Example. Stirring, isn't it?

So that we don't spend half of the column talking about how to build the UI for RCE, I've included in a project folder a completed interface that has been wired up to a controller object. The project folder can be downloaded here.

Let's take a look at the interface so we all know what we're going to be working with. This is the interface that I came up with:

Screen shot.     Screen shot.

On the left we see RCE turned off; on the right it has been started, and as you can see I am the only person running the RCE service on my network. Heed the warning, especially since it contains a typo and is colored red! Blech!

On the left you see the state of the interface when it first launches. The field at the top is where we specify the name of the service that we'll be publishing. This field is connected to the class Controller through the outlet nameField. Since this is a chat application, the service name defaults to the long name of the currently logged in user.

Below nameField is the button that starts and stops the service. You can see in the screenshot on the right how the button title changes from "Start Chat Service" to "Stop Chat Service" and back again when it is clicked. This button is wired to Controller's toggleServiceActivation: action.

The final element of the interface is the table "Mi Amigos" (assuming you're friends with all of the folks on your network). This is a single-column table that displays the list of discovered services on the network. The names that appear in the table are the same names specified in nameField.

Building Cocoa Applications: A Step by Step Guide

Related Reading

Building Cocoa Applications: A Step by Step Guide
By Simson Garfinkel, Michael Mahoney

In the right you see only my name, since I was the only one running RCE when I took the screenshot (my sales department is working on that). The outlet for the table is discoveredServicesList in the class Controller. Controller has been set up as a data source for discoveredServicesList, so we won't have to implement the NSTableViewDataSource methods ourselves.

Implementing the Application

Let's start implementing this application. The first thing we want to do is perform any initialization in awakeFromNib. Right now there's really only one thing we need to do, and that is fill nameField with some initial value, which as I said would be the full name of the currently logged in user. This name is obtained using the Foundation function NSFullUserName, which we use in awakeFromNib, like so:

- (void)awakeFromNib
{
    [nameField setStringValue:NSFullUserName()];
}

The target of the "Start Chat Service" button is the action in Controller toggleServiceActivation:. In this method we call a number of methods that configure different parts of the application when the service is activated, and some methods that do some cleanup when the service is deactivated. When starting a service it is necessary to configure a socket, and an instance of NSNetService that will advertise our service.

Since our application is both a service publisher and consumer of services -- that is, it browses for them and ultimately uses them -- we have to set up the service browser, as well. This is the standard behavior of chat applications that both announce your presence to other chat clients when you sign on (the service publication side of business), and also display others who are signed on (the service browsing side of business).

Here is the implementation for toggleServiceActivation:

- (IBAction)toggleServiceActivation:(id)sender
{
    switch ( [sender state] ) {
	       case NSOnState:
	           [self setupSocket];
	           [self setupService];
	           [self setupBrowser];
	           [nameField setEnabled:NO];
	           break;
	
        case NSOffState:
	           [serviceBrowser stop];
            [domainBrowser stop];
	           [service stop];
	           [nameField setEnabled:YES];
	           break;
    }
}

This action is invoked whenever the Start/Stop Service Button is pressed. When this action is invoked, we do a switch statement based on the state of the switch. If the state is NSOnState, indicating that the service should be started, we do what we need to do to set up the various components of the chat service, which includes building a socket, creating and publishing the associated service, and setting up the browser. Finally, we disable the name field to prevent the user from changing the name in the field while the service is running. If the state is NSOffState, then we take the necessary steps to remove the service from the network and stop browsing for other services. This is done by sending stop messages to our NSNetService and NSNetServiceBrowser objects. After stopping these things, we have to re-enable the service name text field so that the user can change the name of their service, if they so choose.

setupSocket

Sockets are the topic of the next column, but I want to touch on them here because setting up a socket is the first step to creating and publishing a service. Sockets are, for programmers, endpoints of network communications. When a connection between a server and a client has been established, there is a pair of sockets associated with each other: one on the client and one on the server. Data is transferred from the client to the server when the client writes data to its socket representing the server. The server obtains the data sent by the client by reading available data from its socket representing the client. This chain of events is true in the opposite direction as well.

When a server starts, it creates what is known as a listening socket. This is a socket that is "listening" for connections from clients. The listening socket for an application is what we need to set up for our net service.

One could look at NSNetService and Rendezvous as doing nothing more than advertising the presence of a listening socket. This socket is given a name, which is the service name, and a type, which is the service type discussed above. Browsers searching for services of a specific type will then get, in response, the name of our particular service, and when the service is resolved, the client gets the address and port number for the server's listening socket to which it can connect. So you can see how, from the programmer's perspective, NSNetService really only advertises the existence of a socket to which interested clients may connect.

NSFileHandle is used to communicate over a socket. Again, the details of this will be discussed in the next column, but for the purposes of setupSocket, we're going to create one now.

- (void)setupSocket
{ 
    socketPort = [[NSSocketPort alloc] initWithTCPPort:12345];
    listeningSocket = [[NSFileHandle alloc]
                         initWithFileDescriptor:[socketPort socket]];
}

In this code the variables socketPort and listeningSocket are both instance variables in Controller. So in the class interface we have to add the following:

NSSocketPort *socketPort;
NSFileHandle *listeningSocket;

In setupSocket, we do two things. We use NSSocketPort to create a socket bound to the port 12345 using the initializer initWithTCPPort:. In the next line we instantiate and initialize an NSFileHandle using the method initWithFileDescriptor:. The file descriptor we pass in this method is the socket file descriptor returned by a socket message to our NSSocketPort object. Again, we will talk more about this in the next column, in addition to an alternative to using NSSocketPort here.

This method is a pretty simple method. NSNetService will advertise the existence of the socket we create in this method. Of course, right now our socket does absolutely nothing, but that will change in the next column. For now, we'll pretend that our socket on port 12345 is set up to do really important things that makes advertising it with Rendezvous worthwhile (although, since Rendezvous is so easy to use, even if the service behind your socket is a paragon of mediocrity, it would still be worthwhile to advertise it with Rendezvous).

Interestingly, all of the documentation regarding Rendezvous states that creating a socket is a necessary first step to creating a service. The truth, however, is that we can advertise a service without creating a socket. This is totally pointless, since clients won't have anything to which to connect. The point is that we are on the honor system to make sure we have a functioning service provider before we advertise it. NSNetService doesn't perform any check to make sure that there is a live socket on the port number we specify.

setupService

The next thing we do after setting up the socket is to set up an actual instance of NSNetService. This is done in the method setupService, which appears below:

- (void)setupService
{
    service = [[NSNetService alloc] initWithDomain:@""
					                          type:@"_rce._tcp."
					                          name:[nameField stringValue]
					                          port:12345];
	   [service setDelegate:self];
    [service publish];
}

The variable service is another instance variable that must be added to the class interface:

NSNetService *service;

Initializing an instance of NSNetService is done with the method initWithDomain:type:name:port:. The first argument, initWithDomain:, is the domain in which we wish to register our service. Currently, this class only supports registration in the local domain, .local. Rather than explicitly specify .local as the domain, we pass an empty string that NSNetService takes to mean "register this service in the default domain," which is .local.

The next argument, type:, is a string that identifies the service type and transport layer. For this parameter we pass _rce._tcp. The NSNetService object will advertise our service as type rce. The reason these elements are prepended by an underscore is so that the mDNS responder can identify them as information about the service, rather than as being part of the host name. (The type: string is actually part of a larger string used by the mDNS responder that contains the host name and service description: for example, Mike._rce._tcp.southpark.local..)

In the next parameter, name:, we specify the name of the service. This name should be a name that has meaning to human users, rather than some indecipherable incantation of characters that computers really like to use. This is, in fact, the name that will appear in the browser lists of other instances of RCE running on the network. For our service, we set the name of the service to the string contained in the text field nameField.

Finally, we supply the port number to which our socket is bound. Supplying the port number in the initialization is actually the only contact NSNetService has with the networking functionality of our application. All NSNetService does is say to those who care (i.e. apps searching for _rce._tcp service instances) that "I'm running a service over here on port 12345 and it's of type _rce._tcp. If you ask, I'll give you the address of the socket that's serving this service and you can hook up with it." Your service is the pretty girl at the party, and NSNetService is her drunk friend giving out her name and phone number to anyone who asks.

NSNetService has two areas of functionality: it functions to publish services that we create, and it also works to resolve the addresses of discovered services. In the current section, we are concerned with the publication side of business. NSNetService declares delegate methods that are relevant to both publication and resolution, all of which are listed here:

The functionality of the two sets of methods are largely parallel, and serve to notify the delegate of the progress of a publish or resolution operation, and of any errors that may arise during these operations. We will discuss the resolution-specific delegate methods below, and here we will say a word about the publication-relevant delegate methods.

Each of these delegate methods passes as a parameter the net service object that invoked the method. In netServiceWillPublish: and netServiceDidStop:, the only argument is the net service object that sent the message. The method netService:didNotPublish: passes the NSNetService object in the first argument, and an error dictionary in the second argument.

These first two methods can be used to update the user interface to reflect the status of service publication. We won't do anything too fancy here, just print a message to the standard output with NSLog indicating where we are in publishing, as shown here:

- (void)netServiceWillPublish:(NSNetService *)sender
{
    NSLog( @"Publishing service %@", [sender name] );
}

- (void)netServiceDidStop:(NSNetService *)sender
{
    NSLog( @"Stopping service %@", [sender name] );
}

NSNetService objects notify their delegates of errors by invoking their respective didNotPublish: or didNotResolve: methods. Information about the error is passed in an error dictionary as the last argument to this method. The error dictionary contains objects for the keys NSNetServicesErrorCode and NSNetServicesErrorDomain. The object for NSNetServicesErrorDomain tells us whether the error occurred in the mach network layer or in the NSNetService object. This information is usually less useful than the constant passed for NSNetServicesErrorCode. The possible error codes are listed in the table below.

Constant Description
NSNetServicesUnknownError An unknown error occurred.
NSNetServicesCollisionError The service could not be published because the name is already in use either locally or on another system.
NSNetServicesNotFoundError The service could not be found on the network.
NSNetServicesActivityInProgress The net service cannot process the request at this time.
NSNetServicesBadArgumentError An invalid argument was used when creating the NSNetService object.
NSNetServicesCancelledError The client cancelled the action.
NSNetServicesInvalidError The net service was improperly configured.

Setting Up The Browser

Browsing is where Rendezvous really shines: it is the pinnacle of user-friendliness when a user, without any effort, can see listed, before his eyes, the services available for his use. You just can't get much simpler. Fortunately for us application developers, wiring up a Rendezvous service browser is nearly as simple.

NSNetServiceBrowser supports two kinds of searching: we can search for domains on which services might exist, and we can search for all services on a specific domain. We will explore both, but only the latter is supported in the RCE user interface.

As we did with NSNetService, we will implement delegate methods of NSNetServiceBrowser that will drive the interface in response to discovery events. For example, if a service is registered on the network, our NSNetServiceBrowser object will notify its delegate, the Controller object, of the discovery by invoking the delegate method netServiceBrowser:didFindService:moreComing:.

Before we get to implementing the delegate methods, we'll set up the browser object in the method setupBrowser. This method is the following:

- (void)setupBrowser
{
    if ( !serviceBrowser ) {
	       serviceBrowser = [[NSNetServiceBrowser alloc] init];
	       [serviceBrowser setDelegate:self];
    }
    
    if ( !domainBrowswer) {
        domainBrowser = [[NSNetServiceBrowser alloc] init];
        [domainBrowser setDelegate:self];
    }
    
    if ( !discoveredServices )
	       discoveredServices = [[NSMutableArray alloc] init];

    [domainBrowser searchForAllDomains];
    [serviceBrowser searchForServicesOfType:@"_rce._tcp." inDomain:@""];
}

First we do some lazy initialization of the serviceBrowser and domainBrowser objects: if they don't exist yet, we create them, otherwise we continue on with the method. We do the same thing with the mutable array discoveredServices. All three of these objects should be declared as instance variables in the interface, which will give us the final set of instance variable declarations shown here:

@interface Controller : NSObject
{
    NSNetService *service;
    NSNetServiceBrowser *serviceBrowser;
    NSNetServiceBrowser *domainBrowser;
    NSMutableArray *discoveredServices;
    NSSocketPort *socketPort;
    NSFileHandle *listeningSocket;
    
    IBOutlet id discoveredServicesList;
    IBOutlet id nameField;
}
.
.
.
@end

Creating an instance of NSNetServiceBrowser is as easy as invoking alloc and init in NSNetServiceBrowser. At the same time we create each instance of NSNetServiceBrowser, we assign self as the delegate so our Controller object can be alerted to newly-discovered domains, services and other events that occur in the event of searching. Next we do the same trick for the mutable array discoveredServices, which is declared in the interface as an instance variable. This array will provide the services table with its data through the NSTableView data source methods we will implement. Finally, we begin a search of services in the default domain that are of type _rce._tcp. by invoking the searchForServicesOfType:inDomain: method.

Note that an instance of NSNetServiceBrowser cannot simultaneously search for both domains and services: it's one or the other. However, if you did want to search for both domains and services concurrently, you could do so by having two instances of NSNetServiceBrowser: one to search for domains, and the other to search for services. This is exactly what we do in our application. While domain and service discovery are both supported, only service browsing is supported in our UI. Domain discovery is reported to the user only through output to stdout.

NSNetServiceBrowser Delegate Methods

NSNetServiceBrowser declares the following seven delegate methods:

In each of these methods (except the first two), the browser object that invoked the method is passed in the netServiceBrowser: argument. In the first two, the browser object invoking the method is passed as the sole argument. For the sake of brevity, I will not type out the common netServiceBrowser prefix when referring to these methods.

The first three methods listed above, didNotSearch:, willSearch, and didStopSearch, notify the delegate about the progress or status of a search operation. Just before a search commences, when the search browser object has determined that the network is present and the browser object is configured correctly, it will invoke the method willSearch. The method didStopSearch is invoked in the delegate in response to a stop message being sent to the browser object that is currently searching.

NSNetServiceBrowser has an error-reporting method: netServiceBrowser:didNotSearch:. This method passes to the delegate the browser object reporting the error, as well as an error dictionary that contains the same keys as the NSNetService error dictionary described earlier.

The pair of methods didFindService:moreComing: and didRemoveService:moreComing: are used to report to the delegate the discovery and removal of services. The Service: arguments to these methods are instances of NSNetService representing the Rendezvous network service that has been discovered, or that was removed from the network.

The objects passed to us in didFindService:moreComing: and didRemoveService:moreComing: are not the same objects that services are published with. Rather, they are simply NSNetService instances that represent those services as something to which to connect. Unlike instances of NSNetService that are used to publish services, these NSNetService objects are appropriate only for resolving the discovered service.

If you look at the NSNetService class reference you will see an initializer in addition to the one that we have already discussed. This additional initializer is initWithDomain:type:name:, and it is used to initialize resolution-appropriate instances of NSNetService. Note that it is identical to the publication-appropriate initializer we used earlier, except we can't specify a port. If you attempt to send a publish message to a net service object initialized with this method, then the delegate's netService:didNotPublish: method will be invoked with an NSNetServicesBadArgumentError. We never have to create resolution-appropriate net service objects ourselves, since it is all taken care of by NSNetServiceBrowser, but I thought it was worth mentioning.

Typically, when a service is discovered and reported by didFindService:moreComing:, we immediately send a resolve message to the net service object passed to us. This takes care of determining the connection information before it is needed by the user. The concern is that resolving the address and port number of a service may take a non-trivial amount of time. Since we can begin the resolution process in the background, we do so to avoid delaying the connection when the user actually requests it.

The final pair of methods, didFindDomain:moreComing: and didRemoveDomain:moreComing:, are used to notify the delegate when domains have been discovered, and when previously-discovered domains have disappeared. NSNetServiceBrowser has two methods for searching for domains: searchForAllDomains and searchForRegistrationDomains. A registration domain is one that we have the authority to register a service on: .local is a registration domain.

In the last four methods we've discussed, I have yet to mention anything about the moreComing: argument, which is a BOOL. When a service browser object is searching, it may get responses from several hosts running services within a short period of time. The service browser, however, can only report discovered domains or services one at a time.

The idea behind the moreComing: flag is that when the service browser is reporting discovered services, it uses this flag to notify the delegate whether or not there are additional services to report that were discovered at the same time. When the moreComing: flag is NO, the method should update the user interface. By checking the value of this flag, the delegate can cut down on the number of expensive user interface updates that may need to be performed.

Let's go through and implement our net service browser delegate methods now. Since we don't have a user interface for displaying discovered domains, we will simply log any discovered and removed domains:

- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser 
	    didFindDomain:(NSString *)domainString 
	    moreComing:(BOOL)moreComing
{
    NSLog( @"Discovered the domain %@", domainString );
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser 
	    didRemoveDomain:(NSString *)domainString 
	    moreComing:(BOOL)moreComing
{
    NSLog( @"Removing the domain %@", domainString );
}

Discovered services will be displayed in the table discoveredServicesList. The net services whose names are displayed in this list are stored in the array discoveredServices. When a service is found, it is resolved and added to the array. When a service is removed from the network, it is removed from the array. These operations are done in the didFindService:moreComing: and didRemoveService:moreComing: methods shown here:

- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser 
	    didFindService:(NSNetService *)aNetService 
	    moreComing:(BOOL)moreComing
{
    [discoveredServices addObject:aNetService];
    
    [aNetService setDelegate:self];
    [aNetService resolve];
    
    if ( moreComing == NO )
	       [discoveredServicesList reloadData];
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser 
	    didRemoveService:(NSNetService *)aNetService 
	    moreComing:(BOOL)moreComing
{
    [discoveredServices removeObject:aNetService];
    
    if ( moreComing == NO )
	       [discoveredServicesList reloadData];
}

Notice in these methods how we use the moreComing flag to control when the table's data is reloaded: only when there are no more changes to report do we update the interface. Also note in didFindService:moreComing: that we set self as the delegate of the discovered service. This is so that the controller object can be notified of the progress of resolving the net service. For example, we can add implementations for the NSNetService delegate methods netServiceDidResolveAddress:, netService:didNotResolve:, and netServiceWillResolve:. The first notifies the delegate of a successful address resolution, the second is invoked in the event of an error, and the third tells us when the network is prepared for resolving. For the sake of completeness, we will add the following simple implementations for these methods:

- (void)netService:(NSNetService *)sender 
        didNotResolve:(NSDictionary *)errorDict
{
    NSLog( @"There was an error while attempting to resolve %@.", 
                                                      [sender name] );
}

- (void)netServiceDidResolveAddress:(NSNetService *)sender
{
    NSLog( @"successfully resolved address for %@.", [sender name] );
}

- (void)netServiceWillResolve:(NSNetService *)sender
{
    NSLog( @"Attempting to resolve address for %@.", [sender name] );
}

The next method we need to implement is netServiceBrowserDidStopSearch:. In this method we do two things: we remove all previously-discovered net service objects from the discoveredServices array, and we reload the table view to reflect the new state of the discoveredServices array:

- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)browser
{
    if ( aNetServiceBrowser == serviceBrowser ) {
	       [discoveredServices removeAllObjects];
	       [discoveredServicesList reloadData];
    }    
}

We have two objects performing two different searches, both with the same delegate. Since this method will be invoked by both objects when they are sent stop messages, we need to check which browser object is invoking this method. If the argument browser is the serviceBrowser object, only then do we remove the services and update the UI.

Finally, after all of this work, we have a working Rendezvous service publisher and browser. You should now be able to compile it, run it, and see your own name appear when you publish the service. If you have access to additional Macs on your network, try dropping a copy of this app on them and starting it up. When testing RCE, you must be connected to a network for your own service name to appear. The current mDNS responder implementation in Jaguar has a bug that does not recognize the loopback interface as a valid network interface, preventing your service from working properly in the absence of another network interface.

As always, a complete project folder is available for download here.

Final Thoughts

The ZeroConf working group has the following to say with respect to the technology they are developing:

It is important to understand that the purpose of Zeroconf is not solely to make current personal computer networking easier to use, though this is certainly a useful benefit. The long-term goal of Zeroconf is to enable the creation of entirely new kinds of networked products, products that today would simply not be commercially viable because of the inconvenience and support costs involved in setting up, configuring, and maintaining a network to allow them to operate.

This is an interesting perspective that should make all developers pause and take a look at this technology. Stop and think about that. Apple says that if you have a product that does networking, it should advertise its service using Rendezvous. This, I think, is where we will see Rendezvous and ZeroConf networking take root: in existing applications that we already know and use. Once developers get a feel for Rendezvous, then we will begin to see apps be built that were too difficult to support previously. Already we are seeing collaboration apps, like the recently-released iStorm, that use Rendezvous. Peer-to-peer apps will become more common. I suppose the end result of all of this is to make the flow of information between peers and colleagues and friends as easy as flipping the power switch.

What we saw here was how to use Rendezvous for Cocoa. My hope is that you will take this and run with it. If your thing isn't Cocoa, but perhaps cross-platform Unix applications, or programming on any platform, then download the Rendezvous source code and play around with it -- it's well worth a look if you have had any exposure to Unix network programming. You might see something you like. Right now, I'm working on a side project to embed a multicast DNS responder in the software I work on at my day job. If everything goes as planned (which appears to be the case thus far) I'll write something up about it to share. This is really cool stuff, the kind of thing I always imagined that computers should be able to do.

Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.


Read more Programming With Cocoa columns.

Return to the Mac DevCenter.

Copyright © 2009 O'Reilly Media, Inc.