Networking in Cocoa
by Michael Beam, coauthor of Cocoa in a Nutshell05/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 |
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.
|
|
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.
-
problem sending non ascii characters to xp
2006-08-01 15:41:27 Nigeuk123 [View]
-
window opening broken in 10.4?
2005-08-15 14:57:55 Lost_and_Found [View]
-
NSTableView double click action not responding.
2003-11-04 22:31:07 anonymous2 [View]
-
NSTableView double click action not responding.
2003-11-20 02:31:48 anonymous2 [View]
-
connecting to ip modification
2003-07-09 19:51:23 anonymous2 [View]
-
connecting to ip modification
2003-07-28 20:45:00 anonymous2 [View]
-
Great set of articles!
2003-07-08 13:22:51 anonymous2 [View]
-
nib files gone bad
2003-07-01 21:02:23 psheldon [View]
-
IB single click action with multiple connection
2003-07-02 12:23:35 anonymous2 [View]
-
completed project controller modularity?
2003-07-01 20:56:02 psheldon [View]
-
completed project controller modularity?
2003-11-20 02:34:28 anonymous2 [View]
-
characterization of methods not appearing in header as overrides ?
2003-06-29 22:30:41 psheldon [View]
-
This is cocoa, but will the protocol work with Windows ports?
2003-06-06 04:01:10 anonymous2 [View]
-
about the dealloc and WindowWillClose
2003-05-30 10:15:46 jeff_yecn [View]
-
extend our RCE protocol
2003-05-29 22:57:06 anonymous2 [View]
-
a little improvement
2003-05-29 21:53:28 anonymous2 [View]
-
a little improvement
2003-05-29 22:06:43 anonymous2 [View]
-
a little improvement - another method for the colons
2003-09-04 03:10:07 anonymous2 [View]
-
Problems with mot exessage handling
2003-05-29 09:06:18 anonymous2 [View]
-
Problems with mot exessage handling
2003-05-30 00:14:50 anonymous2 [View]
-
Partial Reads
2003-05-17 03:20:56 anonymous2 [View]
-
sendMessage typo
2003-05-14 06:01:52 anonymous2 [View]
-
Weird pages? (2,3,4)
2003-05-13 23:51:37 anonymous2 [View]
-
It's all one page now
2003-05-14 14:58:07 Terrie Miller | [View]


