AddThis Social Bookmark Button

Print

Networking in Cocoa

by Michael Beam, coauthor of Cocoa in a Nutshell
05/13/2003

Editor's Note: We're happy to welcome Mike Beam back from his stint coauthoring Cocoa in a Nutshell with James Duncan Davidson.

In the last column we learned a bit about Unix network programming with the sockets API. Today we're going to finish the RCE chat program we started several columns ago. We'll need a healthy dose of sockets programming from the last column and a sprinkle of the Foundation framework with the class NSFileHandle.

The RCE "Protocol"

The first thing we're going to do is establish the RCE chat protocol. It is a very simple protocol that prescribes the format for chat messages between two clients. A chat message will be a character string with three components: the string "_rce_" (which provides a way of identifying the string as an RCE message), the name of the message sender, and the message's content. Each of these components is separated by a colon ":". For example, a message from me would look like the following:

_rce_:Mike:How's it going?

NSFileHandle for Socket Communications

In the previous column we learned how to create a socket and how to read and write data to the socket using the standard Unix file I/O functions, read() and write(). Both of these functions take a file descriptor that identifies a file on disk or an open socket. In the case of networking, read() and write() handle socket I/O. We also saw in the last column how we had to call accept() in an infinite for-loop so that our program could handle new connections from clients.

The Foundation framework wraps up this I/O functionality in the class NSFileHandle, which provides an interface for writing to and reading from a file or communications channel such as a socket. NSFileHandle's I/O functionality is particularly well suited for socket communications since it provides ways of listening for connections, accepting them, and reading data asynchronously in a background thread. NSFileHandle alerts objects to new connections and received data using notifications, which is a familiar Cocoa programming practice.

Asynchronous background communication means that we can issue a command to read from the socket, and it will execute in a separate thread while the main thread continues with its work. The first thing we do is wait for new connections, accepting them when they arrive. The last column showed how this was done with a for-loop and a call to accept(). NSFileHandle provides similar functionality in the method acceptConnectionInBackgroundAndNotify. When we invoke this method a new thread will be launched that the file handle uses to listen for and accept new connections. When the file handle does accept a new connection, it will post an NSFileHandleConnectionAcceptedNotification to the notification center and continue listening for more connection requests. This is how we accept socket connections using NSFileHandle.

Related Reading

Cocoa in a Nutshell
A Desktop Quick Reference
By Michael Beam, James Duncan Davidson

The next step is to read data from the file handle, which is also done asynchronously in the background. To tell a file handle to read data we invoke the method readInBackgroundAndNotify. When the file handle has read all of the data it received it will post an NSFileHandleReadCompletionNotification notification to the notification center. Objects interested in obtaining data from a socket register to receive this notification. When such a notification is posted, the notification object passed to the observer contains the data that the file handle read. This data is accessible from the notification object's userInfo dictionary. The notification object of this notification is the file handle that posted the notification. We will see below how to use this object to reinitiate a read request. This notification also contains a userInfo dictionary that contains the actual data that was read in an NSData NSFileHandleNotificationDataItem.

The flip-side to reading is writing data to a socket. This is done simply with the method writeData:, which takes an NSData object containing the data to be sent to the other end of the socket connection. There is nothing asynchronous about writeData:. It simply writes the data to the socket and returns.

To create a file handle used for socket communication, we use the initializer initWithFileDescriptor:, which takes the file descriptor that is returned by socket(). A more generalized initializer is initWithFileDescriptor:closeOnDealloc:. This allows us to explicitly specify whether or not the socket should be automatically closed when the file handle is released. The default behavior of initWithFileDescriptor: is to setup the file handle to not close the socket when released.

We will see in a moment how all of this fits together in our program.

ChatWindowController

The class ChatWindowController is a subclass of NSWindowController and is responsible for owning and interacting with chat windows. In addition to creating this class, we must create a nib that contains the chat window itself. ChatWindowController will be made the File's Owner of this nib, and we will set up the initializer of ChatWindowController to properly load the nib.

ChatWindowController is a simple class with five methods and three instances variables. To begin, create a new Objective-C NSWindowController subclass and name it ChatWindowController. The first thing we want to do before building our interface is to setup the class header, since it will be needed in Interface Builder. The class interface file (ChatWindowController.h) contents are the following:

#import <AppKit/AppKit.h>

@interface ChatWindowController : NSWindowController {
    IBOutlet NSTextView *textView;
    NSFileHandle *fileHandle;
    NSString *myName;
}
- (id)initWithConnection:(NSFileHandle *)aFileHandle myName:(NSString *)me;
- (IBAction)sendMessage:(id)sender;
- (void)receiveMessage:(NSNotification *)notification;
- (void)postMessage:(NSString *)message fromPerson:(NSString *)person;
- (void)windowWillClose:(NSNotification *)notification;
@end

The first method of this class is initWithConnection:myName:, which, through aFileHandle, sets up the chat window with a connection to the RCE client of whomever we're chatting with; it sets the myName instance variable to the myName: parameter of this method. The method sendMessage: is the action of the text field in which the user types their message. When the user hits the enter key, the message will be sent to the peer in this method. The method receiveMessage: is registered in the notification center as the method to invoke when data becomes available on the file handle fileHandle, which is connected to the peer. Next we have the method postMessage:fromPerson:. We use it to display a message in the running conversation text view, which is assigned to the outlet instance variable textView. The final method is windowWillClose:, which is a delegate method of NSWindow; in Interface Builder we will be assigning ChatWindowController to be the delegate of the actual chat window.

The Chat Window Interface

Before we go on to implement these methods, let's step into Interface Builder to build our interface. In Interface Builder create a new nib from the File menu. From the Starting Point dialog choose Empty from the list of nib templates. To this nib add a window from the Cocoa-Windows palette. To that window add a text view from the Cocoa-Data palette, and add a text field from the Cocoa-Views palette. Arrange these two objects in the window such that the text view is higher in the window, above the text field. Next, select both of these objects and make them subviews of an NSSplitView by selecting Make subviews of->Split View from the Layout menu. Arrange this split view so that it fills the window. From the Size inspector (Command-3) change the autosizing of the split view so that both interior struts are springs. This will cause the split view to resize with the window. The image below shows how my chat window looks.

Screen shot.
A simple chat window.

The next thing we need to do is import the ChatWindowController header. Do this by dragging ChatWindowController.h from Project Builder into the nib window, or by selecting Read Files... from the Classes menu, and finding and choosing ChatWindowController.h in the file browser. After the class interface has been imported, select File's Owner in the nib window and change its class to ChatWindowController from the Custom Class inspector (Command-5). By changing the class to ChatWindowController, File's Owner will take on all of the outlets and actions that we created for this class in Project Builder. We're now ready to make connections between ChatWindowController and the chat window.

First we want to make the action of the text field the sendMessage: action of File's Owner. Drag a wire from the text field to File's Owner and make this connection. Next, we want to connect the text view in the upper portion of the chat window to the textView outlet of File's Owner. Do so by dragging a wire from File's Owner to the text view and making the connection.

Now we want to double-check that the window outlet (which is defined by ChatWindowController's superclass, NSWindowController) of File's Owner is connected to the chat window. If it isn't, make this connection. The last thing to do is set ChatWindowController to be the delegate of the chat window. Do this by dragging a wire from the window to File's Owner and making the connection to the delegate outlet.

With all of this completed save the nib as ChatWindow.nib. When the Save File sheet appears, you must navigate to the project directory, and save the nib under the .lproj directory of your primary language--English.lproj in my case. After clicking OK, you will be prompted add the file to the project, make sure the check box is checked next to the target name for your project, and click the Add button. We're now ready to implement ChatWindowController.

Initializing and destroying ChatWindowController objects

Now we implement ChatWindowController. Let's start with initWithConnection:myName:. This method has the following implementation:

- (id)initWithConnection:(NSFileHandle *)aFileHandle myName:(NSString *)me
{
self = [super initWithWindowNibName:@"ChatWindow"];

if ( self ) {
    fileHandle = [aFileHandle retain];
    myName = [me copy];

    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self
           selector:@selector(receiveMessage:)
               name:NSFileHandleReadCompletionNotification
             object:fileHandle];

    [fileHandle readInBackgroundAndNotify];
}
return self;
}

The first thing we do is invoke the initializer of the superclass, which is initWithWindowNibName:. To this method we pass the name of the nib, ChatWindow, that contains the interface that will be owned and managed by the window controller. This method of NSWindowController takes the necessary steps needed to load the nib into memory. Assuming all went as planned with initWithWindowNibName:, we now assign objects to our instance variables by retaining the file handle passed in the first parameter of the method invocation and copying the string from the second parameter.

The reason that we copy, rather than retain the string is that the string passed to the initializer may possibly be an instance of NSMutableString, which can be changed by other objects not under the control of ChatWindowController (actually, in our program this probably isn't a very great concern). By copying me instead of retaining it, we preserve the state of the string as it was when the ChatWindowController object was initialized. Thus, if me is indeed a mutable string, any other object may change it without disturbing the value of the myName instance variable in ChatWindowController. The reason for this is that retain simply increments the reference count of the object identified by name, while copy will make a new object that is identical to the original (well, almost: copy will make an immutable copy of the object. If we want a mutable copy then we must use a related method, mutableCopy, which is only supported by classes that conform to the NSMutableCopying protocol).

After we get the objects passed to the initializer, we register for a notification. The notification we are interested in is NSFileHandleReadCompletionNotification, which fileHandle will post whenever it finishes reading new data. Any single instance of ChatWindowController is only interested in those notifications posted by its own fileHandle object, so we pass fileHandle in the object: argument to thus restrict which notifications ChatWindowController receives. If we had passed nil here, then every instance of ChatWindowController would respond to read completion notifications posted by any file handle in RCE, which would certainly be problematic if we had several chat windows open at the same time. The method that is invoked in response to this notification is receiveMessage:, which we discuss below.

The last thing we do before moving on is to tell the file handle to begin waiting for and reading any received data. This is done by sending a readDataInBackgroundAndNotify message to fileHandle.

dealloc

Whenever you create the initializer for a class, you should always create a dealloc method to ensure that objects assigned to instance variables and owned by the class are properly released. In initWithConnection:myName: we claimed ownership over two objects: fileHandle and myName. Both copy and retain have the effect of incrementing the reference count of the receiver object. Thus, we must send a release message to these two objects in the dealloc method of ChatWindowController:

- (void)dealloc
{
    [fileHandle release];
    [myName release];

    [super dealloc];
}

The last thing we always do in dealloc is to send a dealloc message to super so that the superclass has a chance to perform similar cleanup operations.

There are a couple of things missing from this dealloc method. A file handle provides a way of reading and writing data to an open file or socket. In our program, the fileHandle object is the only link we have to a socket, so if we destroy the file handle we will be left with an open socket that we are unable to close. If we had kept track of the socket file descriptor that we had originally used to initialize the file handle, then we could close the socket using the close function. However, we don't do that, so we have to close the socket through the file handle, which is done with the closeFile method of NSFileHandle. We invoke this method immediately before releasing the file handle, which makes the next iteration of dealloc the following:

- (void)dealloc 
{
    [fileHandle closeFile];
    [fileHandle release]; 
    [myName release]; 
    
    [super dealloc]; 
}

Objects that have been registered to receive notifications should always remove themselves from the notification center before they are destroyed. If we don't take care to do this we create problems with the notification center trying to send messages to a nonexistant object. So, we finish off our dealloc implementation with the following:

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [fileHandle closeFile];
    [fileHandle release];
    [myName release];

    [super dealloc];
}

Sending and Receiving Messages

Before I get to the methods that deal with reading and writing to the file handle, I'd like to show the implementation of the postMessage:fromPerson: method, since this is used by both sendMessage: and receiveMessage:. This method is used to display a message in the text view, and has the following implementation:

- (void)postMessage:(NSString *)message fromPerson:(NSString *)person
{
    NSString *str = [NSString stringWithFormat:@"%@: %@\n", person, message];
    NSAttributedString *aStr = [[NSAttributedString alloc] initWithString:str];
    
    [[textView textStorage] appendAttributedString:aStr];
    [aStr release];
}

In this method all we are doing is concatenating the person and message strings such that they are separated by a colon, creating an attributed string, and displaying that string in the text view. A newline character is tacked onto the end of the string so that messages don't run together on the same line.

sendMessage:

Let's implement the action of the message of the message text field, sendMessage:. This method works by reading the string from the text field, packaging the UTF8 representation of the string in an NSData object, and writing that data object to the socket through fileHandle. It looks like this:

- (IBAction)sendMessage:(id)sender
{
    NSString *message = [NSString stringWithFormat:@"_rce_:%@:%@", 
                                  myName, [sender stringValue]];
    NSDate *messageData = [NSData dataWithBytes:[message UTF8String]
                                         length:[message length]];
    [fileHandle writeData:messageData];

    [self postMessage:[sender stringValue] fromPerson:@"Me"];
    [sender setStringValue:@""];
}

First, we construct the message string according to our protocol using NSString's stringWithFormat:. As you can see we have the "_rce_" identifier string, the name of whoever is sending the message, and the message text all separated by colons. We use NSData's convenience constructor dataWithBytes:length: to create a data object containing the message. The data contents are 8-bit characters, which we obtain with UTF8String method of NSString. With the data object in hand, we write it to the socket using NSFileHandle's writeData: method. Next, we invoke postMessage:fromPerson: to display the message in the conversation view. We pass [sender stringValue] here rather than message since message is formatted for the application, rather than for being viewed by humans. To finish things off we clear the message text field by setting the string value to an empty string.

receiveMessage:

At the other end of the connection a peer RCE client will be listening for incoming messages. When data is received on the socket, the file handle will post a notification alerting observers to this fact. Our chat window controller is an observer for the NSFileHandleReadCompletionNotification, and receiveMessage: is invoked in response to this notification. The receiveMessage: method is a bit more complicated that sendMessage:--the added complexity stems from the fact that we wish to only display incoming messages that are formatted according to the RCE protocol. To this end we add several checks to make sure the formatting is correct before posting the message to the chat window. Additionally, we must add a bit of logic to this method so that the chat window controller can display the chat window if it is not already visible. This will be true when a chat is first started. Let's take a look at this method:

- (void)receiveMessage:(NSNotification *)notification
{
    NSData *messageData = [[notification userInfo]
                    objectForKey:NSFileHandleNotificationDataItem];
    
    if ( [messageData length] == 0 ) {
        [fileHandle readInBackgroundAndNotify];
        return;
    }

    NSString *message = [NSString stringWithUTF8String:[messageData bytes]];
    NSArray *msgComponents = [message componentsSeparatedByString:@":"];

    if ( [msgComponents count] != 3 ) {
        [fileHandle readInBackgroundAndNotify];
        return;
    }

    if ( ![[msgComponents objectAtIndex:0] isEqualToString:@"_rce_"] ) {
        [fileHandle readInBackgroundAndNotify];
        return;
    }

    if ( ![[self window] isVisible] )
        [self showWindow:nil];

    [self postMessage:[msgComponents objectAtIndex:2]
           fromPerson:[msgComponents objectAtIndex:1]];

    [fileHandle readInBackgroundAndNotify];    
}

The first and last lines are the most important. As was mentioned earlier, when an NSFileHandle object receives a readInBackgroundAndNotify message it will sit in the background waiting for incoming data. When data is received, the file handle will read the data from the socket, and store it in an NSData object. It then posts the NSFileHandleReadCompletionNotification notification. At this point receiveMessage: is invoked and steps in to obtain the data from the file handle. The file handle makes the data it read accessible to notification observers through the notification's userInfo dictionary in the key NSFileHandleNotificationDataItem. What we see in the first line is how we obtain this data object.

Once receiveMessage: has been invoked the file handle is no longer waiting for data. It has done what was asked of it, read data and notified others that it has done so. Thus, after we handle the read data notification we must tell the file handle to continue listening for data by re-invoking readInBackgroundAndNotify. We do this not only in the last line of the method, but at any point before the method returns, which occurs in the first three if-statements.

In the first if-statement we check that there is actually data to work with. Sockets do some communication behind the scenes that may not necessarily produce consumable data, but may still trigger a notification. We don't want to proceed with the method if there is no data to work with, so we tell the file handle to continue waiting for data, and we return from the method (indeed, stringWithUTF8String: will raise an exception if the argument is nil).

Next we convert the data contents into an NSString object and parse that string into its components. An NSString object is created from the data using the convenience constructor stringWithUTF8String:. Following the protocol we break the string apart at the colons, which is accomplished using the NSString method componentsSeparatedByString:. This method returns an array that contains each component as a string.

The RCE protocol specifies that a properly formatted method has three components, and its first component is the string "_rce_". We check both of these conditions in the next two if-statements. If either of these tests fails, we tell the file handle to go on waiting for data, and exit the method. If the message is determined to be properly formatted then we post the message to the text view using postMessage:fromPerson:. The message contents come from the third element of the msgComponents array, and the fromPerson: argument comes from the second element of the msgComponents array. Of course, we want to make sure the window is visible, so we check for this and show the window if necessary before posting the message.

The Delegate Method

The last thing we have to do before finishing up with this class is implement an NSWindow delegate method, windowWillClose:. In a moment we will see that Controller does not keep track of any of the chat window controllers it creates, so we have no way of releasing them from Controller. As a result, the window controller itself is ultimately responsible for destroying itself at the appropriate time, which is when the window closes.

This is where windowWillClose: comes in. In this method we simply send a release message to the window controller, self:

- (void)windowWillClose:(NSNotification *)notification
{
    [self release];
}

With that, our ChatWindowController class is complete. Now we must take a look at the necessary modifications of the Controller class, which we began implementing in previous installments of this column.

Back to Controller

What we have seen is how to implement the ChatWindowController class. It is the responsibility of the class Controller to instantiate this class, and thus display a chat window, when two things happen: when the user double-clicks a name in the buddy list, and when another RCE client initiates a conversation by sending a first message. To this end we implement two additional methods in the Controller class: openNewChatWindowAsChatInitiator: and openNewChatWindowAsMessageReceiver:. These two methods, however, are not the only changes we will be making to Controller. We must setup the table view so that is can respond to double clicks, and we must add some code for setting up sockets and notifications for the server functionality of RCE.

awakeFromNib

Previously this method had the very simple duty of giving the service name text field an initial value. What we want to do today is set the target and double action of the discovered services table view. Some controls have a double action, which is a method that is invoked in response to a double-click in the control. NSTableView is one such class that will send this action when a row is double-clicked in the view. To support this behavior we make awakeFromNib the following:

- (void)awakeFromNib
{
    // Give nameField an initial value
    [nameField setStringValue:NSFullUserName()];

    // Set the double-click action of discoveredServicesList
    [discoveredServicesList setTarget:self];
    SEL actionSel = @selector(openNewChatWindowAsChatInitiator:);        
    [discoveredServicesList setDoubleAction:actionSel];
}

As you can see, the method openNewChatWindowAsChatInitiator: will be invoked when the user double-clicks a name in the list of known services.

toggleServiceActivation:

When we left off with RCE several columns ago, we had the following implementation for the method toggleServiceActivation:, which the action of the service start/stop button.

- (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;
    }
}

The method setupSocket was really just a place holder method, and today I want to move its functionality into setupService, so we're going to remove setupSocket from Controller altogether. Additionally, when we shut down the service there are some things we will need to do related to closing the server socket, so I want to place that code into a new method, stopService. This method will also take care of stopping the actual Rendezvous service, so we can remove [service stop] from the above method. toggleServiceActivation: should thus have the following implementation:

- (IBAction)toggleServiceActivation:(id)sender
{
    switch ( [sender state] ) {
        case NSOnState:
            [self setupService];
            [self setupBrowser];

            [nameField setEnabled:NO];
            break;

        case NSOffState:
            [self stopService];

            [serviceBrowser stop];
            [domainBrowser stop];

            [nameField setEnabled:YES];
            break;
    }
}

setupService

Previously, setupService: had the following implementation:

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

This created a service on port number 12345. One important change we need to make is to let the system decide what port to serve RCE on. By letting the system decide at runtime the port number for the RCE server, we can run multiple instances of RCE at the same time on the same machine. If we had left the port hard-coded to 12345 then we would have run into problems when two instances of RCE attempted to bind to the same port. Ultimately, we don't need to know the port number for the service, since Rendezvous will take care of communicating all of those details to clients.

Before we get to the new implementation of setupService, we need to have Controller import the Unix sockets headers and the interface for ChatWindowController. At the top of Controller.m add the following:

#import "ChatWindowController.h"
#import <sys/socket.h>
#import <netinet/in.h>

And now, let's take a look at the new setupService::

- (void)setupService
{
    struct sockaddr_in addr;
    int sockfd;

    // Create a socket
    sockfd = socket( AF_INET, SOCK_STREAM, 0 );

    // Setup its address structure
    bzero( &addr, sizeof(struct sockaddr_in));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl( INADDR_ANY );
    addr.sin_port = htons( 0 );

    // Bind it to an address and port
    bind( sockfd, (struct sockaddr *)&addr, sizeof(struct sockadd));

    // Set it listening for connections
    listen( sockfd, 5 );

    // Find out what port the socket was bound to
    int namelen = sizeof(struct sockaddr_in);
    getsockname( sockfd, (struct sockaddr *)&addr, &namelen );

    // Create an NSFileHandle to communicate with the socket
    listeningSocket = [[NSFileHandle alloc] 
                                     initWithFileDescriptor:sockfd];

    // Register for NSFileHandle socket-related notification
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self
        selector:@selector(openNewChatWindowAsMessageReceiver:)
        name:NSFileHandleConnectionAcceptedNotification
        object:nil];

    // Begin waiting for and accepting connections
    [listeningSocket acceptConnectionInBackgroundAndNotify];

    // Setup Rendezvous service
    service = [[NSNetService alloc] initWithDomain:@""
                                    type:@"_rce._tcp."
                                    name:[nameField stringValue]
                                    port:addr.sin_port];
    [service setDelegate:self];
    [service publish];
}

The first thing we do is create a new socket using the socket() function. Nothing unusual here: we're just creating a socket and catching the returned file descriptor in the sockfd variable.

Next we setup the socket's address structure, which must be done prior to calling the bind() function. In the last column on networking we talked about the role of the struct sockaddr_in type, so we won't go into detail about it here. We first initialize the contents of the structure to zero. The address family is set to AF_INET, the same address family that was specified when we created the socket. In the next two lines we specify what address and port the socket should be bound to. By setting addr.sin_addr.s_addr to INADDR_ANY, we tell bind() to bind the socket to any of the host's addresses, rather than one specific address. In the next line we set the port number to zero, which will cause bind() to choose an available port for the socket.

After configuring the address structure, the next thing we do is call bind(), and then listen(). bind() associates the socket with a port and address (or addresses), and listen() puts the socket in a state ready to accept new connections from clients. Again, these two functions were discussed in the previous column, so we won't go into detail about them here.

Finding Out the Port Number

To properly initialize an instance of NSNetService we must provide the port number from which the service is accessible. In our original version of RCE, we hard coded the port number 12345. However, in this new version we have bind() select a port for us, and as a result we have to do a tiny bit of work to find out what port bind() choose for us.

To do this we use the function getsockname(), which has the following signature:

int getsockname( int s, struct sockaddr *name, int *namelen );

What the call getsockname() does for us is fill a sockaddr structure, name, with the address and port information for the specified socket, s. getsockname() returns 0 if it's successful; it returns -1 otherwise. The namelen parameter is used for two things: to tell getsockname() the size of memory space pointed to by name, and when getsockname() is done, namelen contains the actual size of the returned address structure.

In the method above we set namelen to the size of the struct sockaddr_in type. In the next line we make our call to getsockname(), passing sockfd as the socket descriptor and the address (memory, not network) of addr. Here we are reusing the addr structure: getsockname() will overwrite whatever was previously contained in this structure. After getsockname() is finished, addr will contain the port number for the socket in its sin_port member. Later in the method we see that we pass addr.sin_port to the port: parameter of the net service initializer.

Create an NSFileHandle, register for notifications, begin waiting for connections

The last thing we do before creating the net service is register the Controller object with the notification center to observe the notification NSFileHandleConnectionAcceptedNotification. We learned earlier in the article that this is the notification posted by a socket file handle when the underlying socket receives and accepts a new connection, which will happen when the chat server receives a new message from another chat client. The method that is invoked in response to this notification is openNewChatWindowAsMessageReceiver:. Note that we pass nil as the object: parameter, which is contrary to what we did in ChatWindowController. We can get away with this here since there is always at most only one server socket per RCE application, and there is no danger of multiple file handles posting connection accepted notifications. Finally, to startup the chat server, we send an acceptConnectionInBackgroundAndNotify message to the file handle listeningSocket. This will tell the socket to listen for, and accept connections.

stopService

The method stopService is used to shut down the server socket, and to stop the net service. Its implementation is straightforward, and performs the same set of operations that we saw in ChatWindowController's dealloc method:

- (void)stopService
{
    [service stop];
    [[NSNotificationCenter defaultCenter] removeObserver:self];

    [listeningSocket closeFile];
    [listeningSocket release];
}

openNewChatWindowAsMessageReceiver:

Now let's take a look at the method that will be invoked when a new connection is established with the chat server: openNewChatWindowAsMessageReceiver:. This method has the following implementation:

- (void)openNewChatWindowAsMessageReceiver:(NSNotification *)notification
{
    NSFileHandle *remoteFH = [[notification userInfo] 
                       objectForKey:NSFileHandleNotificationFileHandleItem];

    ChatWindowController *chatWC;
    chatWC = [[ChatWindowController alloc] initWithConnection:remoteFH
                                           myName:[nameField stringValue]];
}

As we know, this method is invoked whenever our chat server socket receives data from another chat client. In the previous column we learned about the accept() routine, and how it will return a new socket file descriptor for the connection to the remote client. This returned socket is what we use to communicate with the client, so that the server socket can continue listening for new connection requests. In a similar manner, NSFileHandle will create a new file handle that is used to communicate with the socket connected to the remote client. This allows us to continue using the existing file handle to listen for new connections. The file handle representing the near end of the connection is passed to us in the notification object's userInfo dictionary and is accessed using the key NSFileHandleNotificationFileHandleItem. We assign this file handle to the variable remoteFH.

Following that we instantiate ChatWindowController, and initialize it with the remote file handle, remoteFH, and the name of our local service. ChatWindowController will handle the rest, including opening the chat window when the first message is actually received (this method is invoked when a client makes a connection to the local RCE server).

openNewChatWindowAsChatInitiator:

The last method we need to write is openNewChatWindowAsChatInitiator:, which has the following implementation:

- (void)openNewChatWindowAsChatInitiator:(id)sender
{
    // Obtain remote service based on selected name in list
    NSNetService *remoteService;
    remoteService = [discoveredServices objectAtIndex:[sender selectedRow]];
    
    // Get the socket address structure for the remote service
    NSData *address = [[remoteService addresses] objectAtIndex:0];

    // Create a socket that will be used to connect to the other client
    int sockfd = socket( AF_INET, SOCK_STREAM, 0 );
    connect( sockfd, [address bytes], [address length] );

    // Create a file handle for this socket
    NSFileHandle *remoteFH;
    remoteFH = [[NSFileHandle alloc] initWithFileDescriptor:sockfd];
    [remoteFH autorelease];

    // Open a window with a connection to the remote client
    ChatWindowController *chatWC;
    chatWC = [[ChatWindowController alloc] initWithConnection:remoteFH
                                           myName:[nameField stringValue]];
    [chatWindow showWindow:nil];
}

In the first line we obtain the NSNetService instance representing the chat client we wish to connect to. We can discover the index of the row that the user double-clicked by sending a selectedRow message to the table view, which is the sender of the openChatWindowAsChatIntiator: method. This index is then used to retrieve the corresponding net service from the array discoveredServices.

In the next line we obtain the address structure that tells us how to connect to the remote service's socket. This is done using the addresses method of NSNetService, which returns an NSArray of data objects, one for each address that is valid for the service.

The next important thing that we do is create a socket using the socket function and connect to a server socket using the function connect. To connect to a remote socket we must provide the address information for the socket we wish to connect to. This is the same information returned by an addresses message to the remote service instance. To access a pointer to the actual sockaddr struct we use NSData's bytes method, and the size of the sockaddr structure is obtained with the length method.

After creating and connecting the socket we create an NSFileHandle that will be our I/O interface to the socket. This file handle is initialized with the method initWithFileDescriptor:, to which we provide the socket file descriptor returned by socket. Note that we send this file handle an autorelease method since we are not interested in owning it: we are simply providing it for the chat window controller which will retain it in the initializer method.

Finally, to finish this method off we have three lines of code to create and display the chat window. We pass remoteFH as the connection file handle, and as before we pass [nameField stringValue] for the myName: parameter.

Give It a Go

It's finally time to compile the code and run it. We want to do a couple of things to make sure that everything is running correctly.

The first thing we should do is check that everything starts up properly when you "sign on". When you click the Start Chat Service button you will see a series of messages in the standard output providing feedback about the net service. If the service started properly, you should see your name in the list.

lsof

Next we want to check that the server socket was created properly. We do this using the program lsof, a command-line utility for listing information about open files, including sockets, used by applications. We can use this program to learn more about how RCE--or any program--uses sockets. Running lsof returns information about open files for all processes running your machine. Here's a sample of what it knows about iTunes (I had to do wrap each of the three lines. The "\\" in each line is not part of the lsof output):

[southpark:~] mike% lsof
[...lots of open files...]
iTunes     2521 mike   14u  inet 0x0238dfac        0t0      TCP     \\
                            *:3689 (LISTEN)
iTunes     2521 mike   16u  VREG       14,2    3939857   433783     \\
                            / -- iTunes 4 Music Library
iTunes     2521 mike   17r  VREG       14,2    7165777   252967     \\
                            /Users/mike/Music/Seefeel/Quique/Industrious.mp3
[...and it continues on...]

These are three out of 22 open file descriptors lsof found for iTunes. In the first line we see the server socket to which people connect to access my shared music. In the next line we have the iTunes music library file, and the last line is a reference to the song I was listening to when I ran lsof.

To sort through the information provided by lsof to get only information about RCE's sockets I did the following:

[southpark:~] mike% lsof | grep RCE | grep inet

The first grep following lsof filters out everything except lines of text containing RCE (the program name, analogous to iTunes), and the second grep filters the RCE lines so that only information on sockets is returned. If you run this command in the terminal you should see one line that identifies RCE's listening server socket, which for me was the following:

[southpark:~] mike% lsof | grep RCE | grep inet
RCE       11554 mike    8u  inet 0x03564c9c        0t0      TCP *:51604 (LISTEN)

This tells us that RCE properly created its server socket, and it is listening on port 51604. The asterisk preceding the port number means that the socket is listening on all of the machine's addresses (i.e. localhost, southpark.local., etc).

Running Two RCE's

Now let's try running two instances of RCE and having a chat with ourselves. Make a copy of RCE by option-dragging RCE.app from the Products group in Project Builder onto the Desktop. Launch this new copy of RCE with a service name other than the default, and then run RCE from Project Builder. After you start both services you should see two different names in the buddy list. From one, select the name of the other RCE client, and double-click it. After you type in a message into the text field you should see a window pop up containing the message. (You might need to move the windows around since the window will be opened at the location specified in Interface Builder, which will be the same for both instances of RCE.)

The End

There you have it. Hopefully everything worked out as planned. It may take a bit of work, since what we did today was somewhat complicated. If you need help, download the completed project here.

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.