Cocoa for Scientists (Part XXIX): This is the Message

·

·

Site Navigation


Latest Posts

In our last installment, we began a journey into the world of lowish-level networking on Mac OS X and iPhone. The first tutorial introduced the topic, and delved into Bonjour, which is a framework that helps devices find each other. In this tutorial, we are going to learn how you can make devices connect and talk to each other after they have been introduced, a process best described as ‘messaging’. We’ll be developing some basic messaging classes that will run on Mac and iPhone.

Sockets

As discussed last time, networking in Mac OS X and iPhone OS is based on BSD sockets. We will not be using these directly, but will instead use the open source class AsyncSocket, which will do most of the heavy lifting for us.

A socket can have a few basic tasks:

  • It can listen for attempts to connect to a particular network port.
  • It can attempt to connect to a listening socket.
  • It can send/receive a stream of data once a connection has been established.

If you are writing a server application, your application will need to listen on a particular port for any attempts to connect to it. If you are writing a client application, it will need to connect to a listening socket on a server. And once a connection has been established, both client and server need to be able to send (receive) data to (from) each other.

Streams

Sockets work with very simple streams of data. They do not know anything about the content of the data, and — from the perspective of the socket — the stream is homogenous and continuous, with no structure whatsoever. The meaning and content of the data stream is left to the messaging level, which is the primary focus of this tutorial.

Messaging

A messaging class or library establishes a system for structuring the data in a stream into messages that have some meaning to the communicating applications. There are two common ways to structure messages:

  1. Use a fixed size header that describes the message payload.
  2. Use a particular pattern of data to terminate a message.

The problem with a continuous stream of data is that you don’t know where one message ends, and the next begins. The approaches above are designed to address this issue.

The first approach breaks a message into two parts: the header, and the payload. The header is of fixed size, and describes how much payload data there is, as well as providing any other metadata, such as what type of data is contained in the payload. The receiver then knows how much data needs to be read before a new message begins.

The second approach doesn’t use a header, but a record termination sequence. The receiver looks at each piece of incoming data, and if a particular pattern is encountered, it assumes the message is complete. This is very similar to how periods are used to delineate sentences in written language.

The only downside to this is that you have to be sure that the termination sequence you are using cannot naturally occur in the data that is being transmitted, otherwise messages could be cut short. For example, if you are sending HTML data, and you decide to use the sequence <body> to terminate your messages, the receiver will see the <body> tag in a HTML page as a message terminator. So you need to make sure that you choose a sequence that will not occur in the message payload.

We will be using the header-payload approach for our messaging classes.

MTMessage

The messaging classes are MTMessage and MTMessageBrokerMTMessage is a model class that represents a single message. It has a tag, which can be used to distinguish different types of messages, and a payload, which is simply an instance of the class NSData.

@interface MTMessage : NSObject <NSCoding> {
    int tag;
    NSData *dataContent;
}

-(int)tag;
-(void)setTag:(int)value;

-(NSData *)dataContent;
-(void)setDataContent:(NSData *)value;

@end

The MTMessage class conforms to the NSCoding protocol, which makes it very easy to package up into an instance of NSData, and send to a remote system via a socket.

MTMessageBroker

The MTMessageBroker class is what actually sends and receives messages, and where most of the interesting code is.

#import <Foundation/Foundation.h>

@class AsyncSocket;
@class MTMessage;
@class MTMessageBroker;

@interface NSObject (MTMessageBrokerDelegateMethods)

-(void)messageBroker:(MTMessageBroker *)server didSendMessage:(MTMessage *)message;
-(void)messageBroker:(MTMessageBroker *)server didReceiveMessage:(MTMessage *)message;
-(void)messageBrokerDidDisconnectUnexpectedly:(MTMessageBroker *)server;

@end

@interface MTMessageBroker : NSObject {
    AsyncSocket *socket;
    BOOL connectionLostUnexpectedly;
    id delegate;
    NSMutableArray *messageQueue;
    BOOL isPaused;
}

-(id)initWithAsyncSocket:(AsyncSocket *)socket;

-(id)delegate;
-(void)setDelegate:(id)value;

-(AsyncSocket *)socket;

-(void)sendMessage:(MTMessage *)newMessage;

-(void)setIsPaused:(BOOL)yn;
-(BOOL)isPaused;

@end

MTMessageBroker works with a delegate, which is informed whenever a message is finished being sent, or a new message is received. To send a message, you simply call the sendMessage: method and pass in an instance of MTMessage. All going well, this will be sent via the AsyncSocket to the remote system, where it will be unpacked, and passed to the delegate on that system via the method messageBroker:didReceiveMessage:.

To create a message broker, you already need to have established a connection, with an instance of AsyncSocket ready to send and receive data. We’ll see how that is done in the next section; in this section, we’ll see how data is sent and received using the socket.

In the initializer of MTMessageBroker, a call is made to the readDataToLength:withTimeout:tag: method of AsyncSocket.

-(id)initWithAsyncSocket:(AsyncSocket *)newSocket {
    if ( self = [super init] ) {
        if ( [newSocket canSafelySetDelegate] ) {
            socket = [newSocket retain];
            [newSocket setDelegate:self];
            messageQueue = [NSMutableArray new];
            [socket readDataToLength:MessageHeaderSize withTimeout:SocketTimeout tag:0];
        }
        else {
            NSLog(@"Could not change delegate of socket");
            [self release];
            self = nil;
        }
    }
    return self;
}

The readDataToLength:withTimeout:tag: method is asynchronous, so it returns immediately before any data has been read; however, invoking it causes the socket to wait for data, and to notify its delegate when it has been received.

The method is told how much data it should wait for; the MessageHeaderSize variable corresponds to a single 64-bit unsigned integer (ie, 8 bytes), which represents the size of the payload. You can set a timeout, but the SocketTimeout constant is set to -1.0, which means there is effectively no timeout in this case. The tag is a convenience: you can use it on the receiver to identifier what type of data is being sent. In MTMessageBroker, a tag of 0 represents header data, and a tag of 1 represents the payload data.

Sending Data

The method for sending messages looks like this:

-(void)sendMessage:(MTMessage *)message {
    [messageQueue addObject:message];
    NSData *messageData = [NSKeyedArchiver archivedDataWithRootObject:message];
    UInt64 header[1];
    header[0] = [messageData length]; 
    header[0] = CFSwapInt64HostToLittle(header[0]);  // Send header in little endian byte order
    [socket writeData:[NSData dataWithBytes:header length:MessageHeaderSize] withTimeout:SocketTimeout tag:(long)0];
    [socket writeData:messageData withTimeout:SocketTimeout tag:(long)1];
}

This uses an NSKeyedArchiver to serialize the MTMessage passed in. It then determines how big this payload is using the NSData length method, and puts this number into an 8-byte, unsigned integer (UInt64), which is to be used as the fixed-size message header.

Now you have to be careful, because numbers on different devices are not all created equal. This comes back to endianness, which relates to significance of each byte in the binary representation of the number. Some devices are little-endian, which means that the first byte in memory is the least numerically significant, and others are big-endian, with the first byte being the most significant.

What this means in practice is that you need to ensure that, when you pass numbers as raw data, account is taken of byte order. In this case, we choose to send the header in little endian order. The Core Foundation framework makes it very easy to account for this by providing the function CFSwapInt64HostToLittle, which takes a native 64-bit unsigned integer, and converts it to little endian order.

    header[0] = CFSwapInt64HostToLittle(header[0]);  // Send header in little endian byte order

On a little endian system, this will have no effect, but on a big endian system, the byte order will be reversed.

The sendMessage: method sends the little endian integer using the AsyncSocket method writeData:length:withTimeout:tag:. It then uses the same method to send the payload.

    [socket writeData:[NSData dataWithBytes:header length:MessageHeaderSize] withTimeout:SocketTimeout tag:(long)0];
    [socket writeData:messageData withTimeout:SocketTimeout tag:(long)1];

Receiving Data

We have already seen in the initializer method that in order to receive data from AsyncSocket, you have to call a method like readDataToLength:timeout:tag:, and then implement the appropriate callback. Here is the callback from MTMessageBroker.

-(void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    if ( tag == 0 ) {
        // Header
        UInt64 header = *((UInt64*)[data bytes]);
        header = CFSwapInt64LittleToHost(header);  // Convert from little endian to native
        [socket readDataToLength:(CFIndex)header withTimeout:SocketTimeout tag:(long)1];
    }
    else if ( tag == 1 ) { 
        // Message body. Pass to delegate
        if ( delegate && [delegate respondsToSelector:@selector(messageBroker:didReceiveMessage:)] ) {
            MTMessage *message = [NSKeyedUnarchiver unarchiveObjectWithData:data];
            [delegate messageBroker:self didReceiveMessage:message];
        }

        // Begin listening for next message
        if ( !isPaused ) [socket readDataToLength:MessageHeaderSize withTimeout:SocketTimeout tag:(long)0];
    }
    else {
        NSLog(@"Unknown tag in read of socket data %d", tag);
    }
}

This method looks at the tag of the message, and takes action accordingly. If the tag is 0, it is a header, and — after applying the appropriate byte swapping — it reinvokes the readDataToLength:withTimeout:tag: method in order to receive the payload of the message. The amount of data it needs to read is now known, because that was the value sent in the header.

If the tag is 1, we are dealing with the payload, and a keyed unarchiver is used to deserialize the MTMessage object, which is then passed to the message broker delegate method messageBroker:didReceiveMessage:.

After the incoming payload has been processed, it is important to again make a call to readDataToLength:withTimeout:tag: to listen for the next incoming header. This chain of calls should not be broken, otherwise the socket will just fill up with data, and communications will be blocked.

One thing you might worry about when beginning low-level networking is what happens if one send ‘overtakes’ another on the network, and appears on the remote system first. Well, you needn’t worry, because the TCP protocol, and the AsyncSocket class, guarantee that this will not happen. Data is received by the AsyncSocket delegate on the receiver in the same order it was sent on the sender. It is not possible for the message payload to arrive after its corresponding header.

Listening Sockets

That pretty much covers MTMessageBroker, and sending and receiving data using AsyncSocket. What we haven’t covered yet is actually setting the socket up in the first place.

As mentioned in the beginning, apart from sending and receiving streams of data, a socket can also take on the role of listening for a new connection. The ServerController method startService sets up such socket.

-(void)startService {
    // Start listening socket
    NSError *error;
    self.listeningSocket = [[[AsyncSocket alloc] initWithDelegate:self] autorelease];
    if ( ![self.listeningSocket acceptOnPort:0 error:&error] ) {
        NSLog(@"Failed to create listening socket");
        return;
    }

    // Advertise service with bonjour
    NSString *serviceName = [NSString stringWithFormat:@"Cocoa for Scientists on %@", 
        [[NSProcessInfo processInfo] hostName]];
    netService = [[NSNetService alloc] initWithDomain:@"" 
        type:@"_cocoaforscientists._tcp." 
        name:serviceName 
        port:self.listeningSocket.localPort];
    netService.delegate = self;
    [netService publish];
}

The AsyncSocket is first initialized with the method initWithDelegate:, at which point a call is made to acceptOnPort:error:. This is the method that causes the socket to listen for attempts to connect. By passing ‘0’ for the port number, the system will assign an available port automatically. The port assigned is accessible via the socket’s localPort property, which has been used in the code above to initialize the Bonjour service.

If an attempt is made to connect to the port assigned to the listening socket, it creates a new AsyncSocket object with the same delegate, and that new socket calls the delegate methods onSocketWillConnect: and onSocket:didConnectToHost:port:.

-(BOOL)onSocketWillConnect:(AsyncSocket *)sock {
    if ( self.connectionSocket == nil ) {
        self.connectionSocket = sock;
        return YES;
    }
    return NO;
}

...

-(void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port {
    MTMessageBroker *newBroker = [[[MTMessageBroker alloc] initWithAsyncSocket:sock] autorelease];
    newBroker.delegate = self;
    self.messageBroker = newBroker;
}

The first method is invoked before the connection is established, and gives you a chance to reject the connection. In this case, we simply test to see whether there is already a connection in place, and if so, reject any new one by returning NO. In other applications, it is perfectly conceivable that you would have multiple connections to multiple remote machines at the same time.

If the onSocketWillConnect: delegate method returns YESAsyncSocket forms the connection, and then invokes the onSocket:didConnectToHost:port: delegate method. In the implementation above, the new socket is used to create the message broker object.

Client

The code is largely similar to the server, except that it is up to the client app to make contact in order to establish a connection. This occurs when the net service of the server has been resolved:
-(void)netServiceDidResolveAddress:(NSNetService *)service {
    NSError *error;
    self.connectedService = service;
    self.socket = [[[AsyncSocket alloc] initWithDelegate:self] autorelease];
    [self.socket connectToAddress:service.addresses.lastObject error:&error];
}

The method connectToAddress:error: is used to attempt to connect. The address of the server is retrieved from the resolved net service.

Apart from this small deviation, the connection code for the client is very similar to that of the server.

-(BOOL)onSocketWillConnect:(AsyncSocket *)sock {
    if ( messageBroker == nil ) {
        [sock retain];
        return YES;
    }
    return NO;
}

-(void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port {      
    MTMessageBroker *newBroker = [[[MTMessageBroker alloc] initWithAsyncSocket:socket] autorelease];
    [sock release];
    newBroker.delegate = self;
    self.messageBroker = newBroker;
    self.isConnected = YES;
}

The AsyncSocket delegate method onSocketWillConnect: is invoked first, followed by onSocket:didConnectToHost:port:. (Note that it is necessary to retain the newly formed AsyncSocket in onSocketWillConnect: if you want to go ahead with the connection.)

Sending Messages

With a message broker on both ends of the connection, sending data between client and server is very easy. The sender, in this case the client app, invokes the sendMessage: method of MTMessageBroker.

-(IBAction)send:(id)sender {
    NSData *data = [textView.string dataUsingEncoding:NSUTF8StringEncoding];
    MTMessage *newMessage = [[[MTMessage alloc] init] autorelease];
    newMessage.tag = 100;
    newMessage.dataContent = data;
    [self.messageBroker sendMessage:newMessage];
}

The receiver, in this case the server app, receives this message via a delegate method invocation.

-(void)messageBroker:(MTMessageBroker *)server didReceiveMessage:(MTMessage *)message {
    if ( message.tag == 100 ) {
        textView.string = [[[NSString alloc] initWithData:message.dataContent encoding:NSUTF8StringEncoding] autorelease];
    }
}

It checks the message tag so that it knows what sort of data it contains, and then processes the data content appropriately.

As you can see, neither the sending or receiving method is very extensive. The messaging approach makes it pretty trivial to send even complex object graphs between apps.

Trying It Out

To test out the applications, build and run each. Then, on the client app, click the ‘Search’ button to look for servers. You should see your server appear in the list. Select it, and click the ‘Connect’ button. Finally, enter some text into the text view at the bottom of the client window, and click ‘Send’. You should see it appear in the Server window.

Here we have demonstrated the sending of a simple string, but the content of your data is immaterial to the messaging classes. You could send files or complex object graphs the same way. The only prerequisite is that you need to be able to serialize the data into an instance of NSData (eg with NSKeyedArchiver).

Concluding

That’s it for our short tour of messaging. For educations sake, we have looked at creating a simple messaging framework on top of the sockets layer. You should also be aware that there are existing messaging frameworks that can do this for you, and they may be appropriate depending on your use case. For example, if you only need to communicate between two Macs, and not with an iPhone or some other platform, you could consider using the Distributed Objects framework, which is built into Cocoa.


Leave a Reply

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