Chapter 9. Online Play: Bonjour and Network Streams

In the previous chapter, you saw how easy it is to create a networked application using GameKit. GameKit is cool, but currently it only supports online play using Bluetooth. If you want your networked programs to play on first-generation iPhones and iPod touches, or if you want to let people play over their local Wi-Fi connection or the Internet, you need to go beyond GameKit. In this chapter, we're going to do just that.

We'll take our TicTacToe project from Chapter 8 and add online play to it. We'll use Bonjour to let you find other players on your local network, and then create objects using CFNetwork, Apple's low-level networking framework, and the Berkeley sockets API to listen on the network for other devices attempting to connect. We'll then use network streams to communicate back and forth with the remote device. By combining these, we can provide the same functionality over the network that GameKit currently provides over Bluetooth.

This Chapter's Application

We're going to continue working with our project from the previous chapter, adding functionality to the existing tic-tac-toe game. At the end of this chapter, when users press the New Game button, instead of being presented immediately with a list of peers, they will be presented with the option to select either Online or Nearby play (Figure 9-1).

When the New Game button is pressed, the users will now have the option to select between two different modes of play. Online will allow them to play over their Wi-Fi connection with other phones that are also on the Wi-Fi connection. Nearby will allow them to play over Bluetooth, as in the original version of the application.

Figure 9.1. When the New Game button is pressed, the users will now have the option to select between two different modes of play. Online will allow them to play over their Wi-Fi connection with other phones that are also on the Wi-Fi connection. Nearby will allow them to play over Bluetooth, as in the original version of the application.

If users select Nearby, they will move to the peer picker and continue just as they did in the original version of the game. If they select Online, they will get an application-generated list of devices on the local network that are available to play the game (Figure 9-2).

Our application's equivalent of the GameKit's peer picker

Figure 9.2. Our application's equivalent of the GameKit's peer picker

If either player selects a peer, the game will commence exactly as it did in the previous chapter, but the packets will be sent over the network, rather than over the Bluetooth connection.

Before we start updating our application, we need to look at a few frameworks and objects that we haven't used before, which are required to implement online play. Let's take a few minutes to talk about Bonjour, network streams, and how to listen for connections using CFNetwork, which is the low-level networking API used by all of the Cocoa classes that read from or write to the network.

Overview of the Process

Before we get down into the specific objects and method calls that we need to use to implement online network play, let's look at the process from a very high level.

When the user selects online play, the first thing we're going to do is set up a listener. A listener is code that monitors a specific network port for connections. Then we're going to publish a service using Bonjour that says, in effect, "Hey world, I'm listening on this port for tic-tac-toe game connections." At the same time, we'll look for other Bonjour services that are also advertising in the same way, and will present a list of any tic-tac-toe games we find to the user.

If the user taps a row, we will stop advertising and listening, and connect to the advertised service on the other machine. Once we have a connection established, either because our user tapped a service name or because our listener detected a connection from another machine, we will use that network connection to transfer data back and forth with our opponent, just as we did over Bluetooth.

Setting Up a Listener

For most of the tasks that we need to do to implement online play, we'll be able to leverage Foundation (Objective-C) objects. There are, for example, high-level objects for publishing and discovering Bonjour services, and for sending and receiving data over a network connection. The way we work with these will be very familiar to you, because they are all Objective-C classes that use delegates to notify your controller class when something relevant has occurred.

Note

Remember that Foundation is the name of the framework containing the general-purpose Objective-C classes that are shared between the iPhone and Mac, and includes such classes as NSString and NSArray. Core Foundation is the name given to the collection of C APIs upon which most Foundation objects are built. When you see the prefix CF, it is an indication that you are working with a procedural C framework, rather than one written in Objective-C.

Our first step is to set up a listener to detect connection requests from remote machines. This is one task for which we must dive down into CFNetwork, which is the networking library from Apple's Core Foundation, and also a bit into the Berkeley sockets API, which is an even lower-level network programming library atop which CFNetwork sits.

Here, we'll review some basic CFNetwork and socket programming concepts to help you understand what we're doing in this chapter.

Note

For the most part, you won't need to do socket programming when working with Objective-C. The vast majority of the networking functionality your applications will need can be handled by higher-level objects like NSURLRequest, as well as the numerous init methods that take NSURL parameters, such as NSString's stringWithContentsOfURL:encoding: error:. Listening for network connections is one of the rare situations in Cocoa Touch where you need to interact with the low-level socket API. If you are really interested in learning more about socket programming, we recommend a good and fairly comprehensive guide to low-level socket programming, Beej's Guide to Network Programming, which is available on the Web at http://beej.us/guide/bgnet/.

Callback Functions and Run Loop Integration

Because CFNetwork is a procedural C library, it has no concept of selectors, methods, self, or any of the other dynamic runtime goodies that make Objective-C so much fun. As a result, CFNetwork calls do not use delegates to notify you when something has happened and cannot call methods. CFNetwork doesn't know about objects, so it can't use an objet as a delegate.

CFNetwork integrates with your application's run loop. We haven't worked with it directly, but every iPhone program has a main loop that's managed by UIApplication. The main loop keeps running until it receives some kind of input that tells it to quit. In that loop, the application looks for inputs, such as fingers touching the screen or the phone being rotated, and dispatches events through the responder chain based on those inputs. During the run loop, the application also makes any other calls that are necessary, such as calling application delegate methods at the appropriate times.

The application allows you to register certain objects with the run loop. Each time through the run loop, those objects will have a chance to perform tasks and call out to delegates, in the case of Objective-C, or to callback functions, in the case of Core Foundation libraries like CFNetwork. We're not going to delve into the actual process of creating objects that can be registered in the run loop, but it's important to know that CFNetwork and many of the higher-level objective-C networking classes register with the run loop to do their work. This allows them to listen for network connection attempts, for example, or to check if data has been received without needing to create threads or fork child processes.

Because CFNetwork is a procedural library, when you register any CFNetwork functionality with the run loop, it uses good old-fashioned C callbacks when it needs to notify you that something has happened. This means that each of our socket callbacks must take the form of a C function that won't know anything about our application's classes—it's just a chunk of code. We'll look at how to deal with that in a moment.

Configuring a Socket

In order to listen for connections, we need to create a socket. A socket represents one end of a network connection, and we can leverage CFNetwork to create it. To do that, first we declare a CFSocketContext, which is a data structure specifically created for configuring a socket.

Declaring a Socket Context

When creating a socket, the CFSocketContext you define to configure it will typically look something like this:

CFSocketContext socketCtxt = {0, self, NULL, NULL, NULL};

The first value in the struct is a version number that always needs to be set to 0. Presumably, this could change at some point in the future, but at present, you need to set the version to 0, and never any other value.

The second item in the struct is a pointer that will be passed to any callback functions called by the socket we create. This pointer is provided specifically for application use. It allows us to pass any data we might need to the callback functions. We set this pointer to self. Why? Remember that we must implement those callback functions that don't know anything about objects, self, or which object triggered the callback. We include a pointer to self to give the callback function context for which object triggered the callback. If we didn't include a reference to the object that created the socket, our callback function probably wouldn't know what to do, since the rest of our program is implemented as objects, and the function wouldn't have a pointer to any objects.

Note

Because Core Foundation can be used outside Objective-C, the callbacks don't take Objective-C objects as arguments, and none of the Core Foundation code uses Objective-C objects. But in your implementation of a Core Foundation callback function, it is perfectly acceptable to use Objective-C objects, as long as your function is contained in a .m file rather than a .c file. Objective-C is a superset of C, and it's always okay to have any C functionality in your implementation files. Since Objective-C objects are actually just pointers, it's also okay to do what we've done here and pass a pointer to an Objective-C object in any field or argument that is documented as being for application use. C doesn't know about objects, but it does know about pointers and will happily pass the object pointer along to the callback function.

The other three items in this struct are function pointers for optional callback functions supported by CFSocket. The first two are for memory management: one that can be used to retain any objects that need to be retained, and a second that can be used to release objects that were retained in the previous callback. This is important when using CFNetwork from C, because the memory needs to be retained and released, just as with Objective-C objects. We're not going to use these because we do all our memory management in the context of our objects, so we pass NULL for both.

The last function pointer is a callback that can be used to provide a string description of the second element (the one where we specified self). In a complex application, you might use this last element to differentiate the different values that were passed to the callback. We pass NULL for this one also; since we only use the pointer to self, there's no need to differentiate anything.

Creating a Socket

Once we have our CFSocketContext, we call the function CFSocketCreate() to actually create the socket.

CFSocketRef socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM,
        IPPROTO_TCP, kCFSocketAcceptCallBack,
        (CFSocketCallBack)&listenerAcceptCallback, &socketCtxt);

The first argument is a constant that tells CFNetwork that we don't have any special memory allocation that needs to happen, so it can just use the default memory allocator to create the socket. CFAllocators are special objects used in Core Foundation to handle allocating memory. Because Core Foundation is C-based and not Objective-C–based, it can't do retain counting in quite the same way as in Objective-C, so memory management is handled through a fairly complex set of callbacks that allow you to allocate and release memory.

The second argument, PF_INET, identifies the protocol family to be used. This is a constant defined in the socket libraries that refers to the Internet Protocol (IP). The instances where you would use any other value when specifying a protocol family in a CFNetwork or socket API call are very few and far between, as the world has pretty much standardized on PF_INET at this point.

The third argument, SOCK_STREAM, is another constant from the socket library. There are two primary types of sockets commonly used in network programming: stream sockets and datagram sockets. Stream sockets are typically used with the Transmission Control Protocol (TCP), the most common transmission protocol used with IP. It's so commonly used that the two are often referred to together as TCP/IP. With TCP, a connection is opened, and then data can continuously be sent (or "streamed") to the remote machine (or received from the remote machine) until the connection is closed. Datagram sockets are typically used with an alternative, lesser-used protocol called User Datagram Protocol (UDP). With datagram sockets, the connection is not kept open, and each transmission of data is a separate event. UDP is a lightweight protocol that is less reliable than TCP but faster. It is sometimes used in certain online games where transmission speed is more important than maintaining absolute data integrity. We won't be implementing UDP-based services in this book.

The fourth argument identifies the transmission protocol we want our socket to use. Since we specified SOCK_STREAM for our socket type, we want to specify TCP as our transmission protocol, which is what the constant IPPROTO_TCP does.

For the fifth argument, we pass a CFNetwork constant that tells the socket when to call its callback function. There are a number of different ways you can configure CFSockets. We pass kCFSocketAcceptCallBack to tell it to automatically accept new connections, and then call our callback function only when that happens. In our callback method, we will grab references to the input and output streams that represent that connection, and then we won't need any more callbacks from the socket. We'll talk more about streams a little later in the chapter.

The sixth argument is a pointer to the function we want called when the socket accepts a connection. This is a pointer to a C function that we need to implement. This function must follow a certain format, which can be found in the CFNetwork documentation.

Note

Not to worry—we'll show you how to implement these callbacks once we get to our sample code in a bit. In the meantime, you might want to bookmark Apple's CFNetwork documentation, which can be found here:

http://developer.apple.com/mac/library/documentation/Networking/Conceptual/CFNetwork/Introduction/Introduction.html

The last argument is a pointer to the CFSocketContext struct we created. It contains the pointer to self that will be passed to the callback functions.

Once we've created the socket, we need to check socket to make sure it's not NULL. If it is NULL, then the socket couldn't be created. Here's what checking the socket for NULL might look like:

if (socket == NULL) {
        if (error) *error = [[NSError alloc]
            initWithDomain:kMyApplicationErrorDomain
            code:kNoSocketsAvailableError
            userInfo:nil];
        return NO;
    }

Specifying a Port for Listening

Our next task is to specify a port for our socket to listen on. A port is a virtual, numbered data connection. Port numbers run from 0 to 65535, with port 0 reserved for system use. Since we'll be advertising our service with Bonjour, we don't want to hard-code a port number and risk a conflict with another running program. Instead, we'll specify port 0, which tells the socket to pick an available port and use it.

In the following example, we set the listen port to any available port, and then determine which port was used. First, we need to declare a struct of the type sockaddr_in, which is a data structure from the socket API used for configuring a socket. The socket APIs are very old and are from a time when the names of data structures were kept intentionally terse, so forgive the cryptic nature of this code.

struct sockaddr_in addr4;
    memset(&addr4, 0, sizeof(addr4));
    addr4.sin_len = sizeof(addr4);
    addr4.sin_family = AF_INET;
    addr4.sin_port = 0;
    addr4.sin_addr.s_addr = htonl(INADDR_ANY);

Note

If you're wondering why the variable ends in 4, it's a clue that we're using IP version 4 (IPv4), currently the most widely used version of the protocol. Because of the widespread popularity of the Internet, at some point in the not-too-distant future, IPv4 will run out of addresses. IP version 6 (IPv6) uses a different addressing scheme with more available addresses. As a result, IPv6 sockets must be created using a different data structure, called sockaddr_storage instead of sockaddr. Although there's a clear need for additional addresses on the Internet, there's no need to use IPv6 when working on a local area network.

In order to pass this struct into a CFNetwork call, we need to turn it into an instance of NSData:

NSData *address4 = [NSData dataWithBytes:&addr4 length:sizeof(addr4)];

We can then use the Core Foundation function CFSocketSetAddress to tell the socket on which port it should listen. If CFSocketSetAddress fails, it will return a value other than kCFSocketSuccess, and we do appropriate error handling:

if (kCFSocketSuccess != CFSocketSetAddress(socket, (CFDataRef)address4)) {
    if (error) *error = [[NSError alloc]
        initWithDomain:kMyApplicationErrorDomain
code:kUnableToSetListenAddressErrorCode
            userInfo:nil];
    if (socket) CFRelease(socket);
    socket = NULL;
    return NO;
}

You might have noticed that we actually cast our NSData instance to CFDataRef. Foundation and Core Foundation have a very special relationship. Many of the Objective-C objects that we use from Foundation have counterparts in Core Foundation. Through a special process called toll-free bridging, many of those items can be used interchangeably, either as an Objective-C object or as a Core Foundation object. In the preceding code example, we're creating an instance of NSData and passing it into a CFNetwork function called CFSocketSetAddress(), which expects a pointer to a CFData object. When you see a Core Foundation datatype that ends in ref, that means it's a pointer to something. In this case, CFDataRef is a pointer to a CFData. Because CFData and NSData are toll-free bridged, it's okay to simply cast our NSData instance as a CFDataRef.

Note

The API documentation for Foundation objects identifies whether an object is toll-free bridged with a Core Foundation counterpart.

Finally, we need to copy the information back from the socket, because the socket will have updated the fields with the correct port and address that were actually used. We need to copy that data back into addr4 so we can determine which port number was used.

NSData *addr = [(NSData *)CFSocketCopyAddress(socket) autorelease];
    memcpy(&addr4, [addr bytes], [addr length]);
    uint16_t port = ntohs(addr4.sin_port);

Registering the Socket with the Run Loop

The last thing we need to do is to register our socket with our run loop. This will allow the socket to poll the specified port for connection attempts, and then call our callback function when a connection is received. Here is how we do that:

CFRunLoopRef cfrl = CFRunLoopGetCurrent();
CFRunLoopSourceRef source4 = CFSocketCreateRunLoopSource(kCFAllocatorDefault,
        socket, 0);
CFRunLoopAddSource(cfrl, source4, kCFRunLoopCommonModes);
CFRelease(source4);

Implementing the Socket Callback Function

Once our socket is registered with the run loop, any time that we receive a connection from a remote machine, the function we specified when we created the socket will be called. In that function, we need to create a pair of stream objects that represent the connection to the other machine. One of those stream objects will be used to receive data from the other machine, and the other one will be used to send data to the other machine.

Here's how you create the stream pair that represents the connection to the other machine:

static void listenerAcceptCallback (CFSocketRef theSocket, CFSocketCallBackType
theType, CFDataRef theAddress, const void *data, void *info) {

    if (theType == kCFSocketAcceptCallBack) {
        CFSocketNativeHandle socketHandle = *(CFSocketNativeHandle *)data;
        uint8_t name[SOCK_MAXADDRLEN];
        socklen_t namelen = sizeof(name);
        NSData *peer = nil;
        if (getpeername(socketHandle, (struct sockaddr *)name, &namelen) == 0) {
            peer = [NSData dataWithBytes:name length:namelen];
        }
        CFReadStreamRef readStream = NULL;
        CFWriteStreamRef writeStream = NULL;
        CFStreamCreatePairWithSocket(kCFAllocatorDefault, socketHandle,
            &readStream, &writeStream);
        if (readStream && writeStream) {
            CFReadStreamSetProperty(readStream,
                kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
            CFWriteStreamSetProperty(writeStream,
            kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);

            self.inStream = readStream;
self.outStream = writeStream;
        } else {
            close(socketHandle);
        }
        if (readStream) CFRelease(readStream);
        if (writeStream) CFRelease(writeStream);
    }
}

In this particular example, we're just storing a reference to the stream pair. We'll talk about how to use them a little later in the chapter.

Stopping the Listener

To stop listening for new connections, we must invalidate and release the socket. We don't need to remove it from the run loop, because invalidating the socket takes care of that for us. Here's all we need to do when we're finished with our CFSocket:

if (socket) {
    CFSocketInvalidate(socket);
    CFRelease(socket);
    socket = NULL;
}

Bonjour

In the previous chapter, when we were using GameKit's peer picker, each phone was able to find the other phone without the user typing in an IP address or DNS name. That was accomplished using Bonjour (also known as Zeroconf). Bonjour is a protocol specifically designed to let devices find each other on a network. If you buy a new printer and plug it into your AirPort base station, and then tell a Mac on the same network to add a new printer, the new printer will appear automatically. The printer's type will be discovered without the need to type in an IP address or manually search the network. That's Bonjour in action. When you're in the Finder and other Macs on your network show up automatically under the SHARED heading (Figure 9-3), that's also Bonjour doing its thing.

If you're young enough not to remember life before Bonjour, consider yourself lucky. Bonjour makes life much easier for computer users. In the "old days" (yes, we walked to school 10 miles through the snow uphill both ways), you needed to know a service or device's IP address to find it on your network. It was often a tedious, frustrating experience. We want life to be easy for our users, don't we? Well, of course we do. So, how do we use Bonjour?

The SHARED heading in the Finder's sidebar lists all other Macs on your network that have shared folders. This is just one of the many examples of where Bonjour is used in Mac OS X.

Figure 9.3. The SHARED heading in the Finder's sidebar lists all other Macs on your network that have shared folders. This is just one of the many examples of where Bonjour is used in Mac OS X.

Creating a Service for Publication

When you advertise a service on the network using Bonjour, it's called publishing the service. Published services will be available for other computers to discover and connect to. The process of discovering another published service on the network is called searching for services. When you find a service and wish to connect to it, you need to resolve the service to get information about the address and port on which the service is running or, alternatively, you can ask the resolved service for a connection in the form of streams.

To advertise an available service, you need to create an instance of a class called NSNetService. To do that, you provide four pieces of information:

  • Domain: The first piece of information is the domain, which is referring to a DNS domain name like www.apple.com. You pretty much always want to specify an empty string for the domain. Although the documentation for NSNetService says to pass @"local." instead of the empty string if you want to support only local connections, Technote QA1331 (http://developer.apple.com/mac/library/qa/qa2001/qa1331.html) clarifies this point and says that passing @"local." may make your application incompatible with future versions of Mac OS X. It says to always pass an empty string, and NSNetService will "do the right thing."

  • Service type: The second piece of information that needs to be passed in is your service type. This is a string that uniquely identifies the protocol or application being run, along with the transmission protocol it uses. This is used to prevent services of different types from trying to connect to each other, much like the session identifier we used in Chapter 8. Unlike GameKit session identifiers, Bonjour identifiers must follow a very specific formula; you can't use just any string. A valid Bonjour type begins with an underscore, followed by a string that identifies the service or protocol being advertised, followed by another period, another underscore, the transmission protocol, and then a terminating period. For Cocoa applications, your transmission type will almost always be TCP, so your Bonjour type will pretty much always end in ._tcp..

  • Name: The third piece of information you provide is a name that uniquely identifies this particular device on the network. This is the value that is displayed in the list in Figure 9-2. If you pass the empty string, Bonjour will automatically select the device name as set in iTunes, which is usually the owner's first name followed by the type of device (e.g., Dave's iPhone or Jeff's iPod touch). In most instances, the empty string is the best option for name, although you could solicit a desired name from your users if you wanted to let them specify a different name under which they would appear.

  • Port number: Finally, you need to specify the port number that your application is listening on. Each port can be used by only a single application at a time, so it's important that you don't select one that's already in use. In the previous section, we showed how to set up a listener and specify the port, or how to let it pick a port and then find out which one it picked. The number we retrieved from the listener is the number that should be passed here. When you create an instance of NSNetService, you are telling the world (or at least your local network) that there is a specific device or service listening on a specific port of this machine. You shouldn't advertise one unless you are actually listening.

Here's what allocating a new net service might look like:

NSNetService *svc = [[NSNetService alloc] initWithDomain:@""
    type:@"_myprogram._tcp."
    name:@""
    port:15000];

Publishing a Bonjour Service

Once you've created an instance of NSNetService, you need to take a few steps before NSNetService will start actually advertising your service:

  • First, you need to schedule the service in your application's run loop. We introduced run loop integration when we talked about creating a listener earlier in the chapter. Because we're using Foundation rather than Core Foundation, we schedule the service in the run loop using method calls instead of C function calls, but the process is comparable.

  • After we schedule the service in the run loop, we need to set a delegate so that the service can notify us when certain things happen, such as when NSNetService is finished publishing or if an error was encountered.

  • Finally, we need to actually publish the service, which causes it to start letting other devices on the network know about its existence.

These steps would typically look something like this:

[svc scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    [svc setDelegate:self];
    [svc publish];

Stopping a Bonjour Service

When you stop listening on a port, or simply don't want any new connections, you need to tell the net service to stop advertising using Bonjour, like so:

[svc stop];

All this does is tell the service not to advertise its existence. You can always start it back up again, by republishing it:

[svc publish];

Delegate Methods for Publication

Once you've scheduled your service in your application's run loop and have published the service, it will call methods on its delegate when certain things happen. The class that acts as the service's delegate should conform to the NSNetServiceDelegate protocol and should implement any of the methods that correspond to activities it needs to be notified about.

Several of the delegate methods are called during the publication process. For example, when the service has been configured successfully, and just before it begins advertising its existence, it will call the following method on its delegate:

-(void)netServiceWillPublish:(NSNetService *)netService;

This is a good place to do setup work or configuration that, for some reason, you don't want to occur if the publication isn't going to work. If you're providing feedback to the user about the status of the connection, you can also use this method to let the user know that the server is ready to accept connections.

Similarly, if the service fails to publish for some reason, it will notify its delegate of that as well, using the method netService:didNotPublish:. In that method, you should stop the service. Here is an example implementation of netService:didNotPublish::

- (void)netService:(NSNetService *)theNetService
        didNotPublish:(NSDictionary *)errorDict {
    NSNumber *errorDomain = [errorDict valueForKey:NSNetServicesErrorDomain];
    NSNumber *errorCode = [errorDict valueForKey:NSNetServicesErrorCode];
    NSLog(@"Unable to publish Bonjour service (Domain: %@, Error Code: %@)",
        errorDomain, errorCode);
    [theNetService stop];
}

The second argument to this delegate method is a dictionary that contains information about the error, including an error domain stored under the key NSNetServicesErrorDomain and an error code stored under the key NSNetServicesErrorCode. These two items will tell you more about why it failed.

Note

You can find a list of the error domains and error codes that Bonjour services can generate in the API documentation for NSNetService.

When the service stops, the delegate method netServiceDidStop: will be called, which will give you the opportunity to update the status or to reattempt publication if desired. Often, once a service stops, you are finished with the net service and just want to release the instance of NSNetService that stopped. Here's what the delegate method in that situation might look like:

- (void)netServiceDidStop:(NSNetService *)netService {
    netService.delegate = nil;
    self.netService = nil;
}

Searching for Published Bonjour Services

The process to discover published services on your local network is fairly similar to that of publishing a service. You first create an instance of NSNetServiceBrowser and set its delegate:

NSNetServiceBrowser *theBrowser = [[NSNetServiceBrowser alloc] init];
    theBrowser.delegate = self;

Then you call searchForServicesOfType:inDomain: to kick off the search. Unlike with NSNetService, you don't need to register a service browser with the run loop, though you do still need to specify a delegate; otherwise, you wouldn't ever find out about the other services. For the first argument, you pass the same Bonjour identifier that we discussed when we talked about publishing the domain. In the second argument, we follow Apple's recommendation and pass the empty string.

[theBrowser searchForServicesOfType:@"_myprogram._tcp" inDomain:@""];

Browser Delegate Methods

When the browser completes its configuration and is ready to start looking for services, it will call the following method on its delegate:

- (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)browser

You do not need to implement this method, as there are no actions you must take at this point for the browser to find other services. It's just notifying you in case you want to update the status or take some action before it starts looking.

If the browser was unable to start a search for some reason, it will call the delegate method netServiceBrowser:didNotSearch: on its delegate. When this happens, you should stop the browser and do whatever error reporting is appropriate for your application. Here is a simple example:

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
             didNotSearch:(NSDictionary *)errorDict {
    NSLog(@"Error browsing for service: %@", [errorDict
        objectForKey:NSNetServicesErrorCode]);
    [self.netServiceBrowser stop];
}

You should not release the browser at this point, even if you're finished with it. After you call the stop method here, or at any other time, it will trigger another delegate method call, which is where you should release the browser, like so:

- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)browser {
    browser.delegate = nil;
    self.netServiceBrowser = nil;
}

When the browser finds a new service, it will call the delegate method netServiceBrowser:didFindService:moreComing:. The second argument the browser will pass to this method is an instance of NSNetService that can be resolved into an address or port, or turned into a stream pair, which you'll see how to do in a minute.

Typically, when notified about a new service, you add it to an array or other collection, so that you can let your user select from the available services. If the browser knows that there are more services coming, it will indicate this by passing YES for the last argument, which allows you to skip updating the user interface unnecessarily. The following is an example of what an implementation of this method might look like in a table view controller. Notice that we sort the data and reload the table only if there are no more services coming.

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
           didFindService:(NSNetService *)aNetService
               moreComing:(BOOL)moreComing {
    if (![[self.publishedService name] isEqualToString:[aNetService name]])
        [discoveredServices addObject:aNetService];

    if (!moreComing) {
        [self.tableView reloadData];
        NSSortDescriptor *sd = [[NSSortDescriptor alloc] initWithKey:@"name"
            ascending:YES];
[discoveredServices sortUsingDescriptors:[NSArray arrayWithObject:sd]];
        [sd release];
    }
}

Another thing to notice here is that we're comparing browser's name to the name of another published service. This step is unnecessary if you haven't published a Bonjour service in your app. However, if you're both publishing and browsing, as we're going to do in our application, you typically don't want to display your own service to your users. If you've published one, it will be discovered by your browser, so you must manually exclude it from the list you show to the users.

Finally, if a service becomes unavailable, the browser will call another delegate method, which looks very similar to the last one, to let you know that one of the previously available services can no longer be found. Here's what that method might look like in a table view controller class:

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
         didRemoveService:(NSNetService *)aNetService
               moreComing:(BOOL)moreComing {
    [discoveredServices removeObject:aNetService];

    if(!moreComing)
        [self.tableView reloadData];
}

Resolving a Discovered Service

If you want to connect to any of the discovered services, you do it by resolving the instance of NSNetService that was returned by the browser in the netServiceBrowser:didFindService:moreComing: method. To resolve it, all you need to do is call the method resolveWithTimeout:, specifying how long it should attempt to connect, or 0.0 to specify no timeout. If you were storing the discovered services in an array called discoveredServices, here is how you would resolve one of the services in that array:

NSNetService *selectedService = [discoveredServices objectAtIndex:selectedIndex];
    selectedService.delegate = self;
    [selectedService resolveWithTimeout:0.0];

Discovered services do not need to be registered with the run loop the way published ones do. Once you call resolveWithTimeout:, the service will then call delegate methods to tell you that the service was resolved, or to tell you that it couldn't be resolved.

If the service could not be resolved, for whatever reason, it will call the delegate method netService:didNotResolve:. At a minimum, you should stop the net service here. You should also do whatever error checking is appropriate to your application. Here's a simple implementation of this delegate method:

- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict {
    [sender stop];
    NSNumber *errorDomain = [errorDict valueForKey:NSNetServicesErrorDomain];
    NSNumber *errorCode = [errorDict valueForKey:NSNetServicesErrorCode];
NSLog(@"Unable to resolve Bonjour service (Domain: %@, Error Code: %@)",
        errorDomain, errorCode);
}

If the discovered service resolved successfully, then the delegate method netServiceDidResolveAddress: will be called. You can call the methods hostName and port on the service to find out its location and connect to it manually. An easier option is to ask the net service for a pair of streams already configured to connect to the remote service. Here's an example implementation of that delegate method. Note, however, that we don't do anything with the streams yet.

- (void)netServiceDidResolveAddress:(NSNetService *)service {

    NSInputStream *tempIn = nil;
    NSOutputStream *tempOut = nil;
    if (![service getInputStream:&tempIn outputStream:&tempOut]){
        NSLog(@"Could not start game with remote device",
            @"Could not start game with remote device") ];
        return;
    }
    // Open and use the streams
}

Why didn't we do anything with the streams? Because streams are complex enough to deserve their very own section, so we will now, very smoothly, segue into...

Streams

In the previous sections, we demonstrated how to obtain a pair of streams, which represent a connection to another device. In the section on setting up a listener, we showed you how to get a pair of CFStream pointers when another computer is connected. When we looked at resolving services with Bonjour, we demonstrated how to get a pair of NSStreams (actually an NSInputStream and an NSOutputStream, but both are subclasses of NSStream) to represent the connection to the published services. So, now it's time to talk about how to use streams.

Before we go too far, we should remind you that CFStream and NSStream are toll-free bridged, so we're not really talking about different objects here. They're all stream objects. If they represent a connection designed to let you send data to another machine, they're an NSOutputStream instance; if they're designed to let you read the data sent by another machine, they are instances of NSInputStream.

Note

In this chapter, we use streams to pass data between different instances of our application over a network. However, streams are also useful in situations that don't involve network connections. For example, streams can be used to read and write files. Any type of data source or destination that sequential bits of data can be sent to or received from can be represented as a stream.

Opening a Stream

The first thing you need to do with any stream object is to open it. You can't use a stream that hasn't been opened.

Opening a stream tells it that you're ready to use it. Until it's open, a stream object really represents a potential rather than an actual stream. After you open a stream, you need to register it with your run loop, so that it can send and receive data without disrupting the flow of your application. And, as you've probably guessed, you need to set a delegate, so that the streams can notify you when things happen.

Here's what opening a pair of streams generally looks like:

[inStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
        forMode:NSDefaultRunLoopMode];
    [outStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
        forMode:NSDefaultRunLoopMode];

    inStream.delegate = self;
    outStream.delegate = self;

    if ([inStream streamStatus] == NSStreamStatusNotOpen)
        [inStream open];

    if ([outStream streamStatus] == NSStreamStatusNotOpen)
        [outStream open];

Just to be safe, we actually check the status of the stream and make sure it wasn't already opened elsewhere. With the streams retrieved from Bonjour or from a network listener, the streams won't be open, but we code defensively so we don't get burnt.

The Stream and Its Delegate

Streams have one delegate method—that's it, just one. But they call that one method whenever anything of interest happens on the streams. The delegate method is stream:handleEvent:, and it includes an event code that tells you what's going on with the stream. Let's look at the relevant event codes:

  • NSStreamEventOpenCompleted: When the stream has finished opening and is ready to allow data to be transferred, it will call stream:handleEvent: with the event code NSStreamEventOpenCompleted. Put another way, once the stream has finished opening, its delegate will receive the NSStreamEventOpenCompleted event. Until this event has been received, a stream should not be used. You won't receive any data from an input stream before this event happens, and any attempts to send data to an output stream before its receipt will fail.

  • NSStreamEventErrorOccurred: If an error occurs at any time with the stream, it will send its delegate the NSStreamEventErrorOccurred event. When this happens, you can retrieve an instance of NSError with the details of the error by calling streamError on the stream, like so:

    NSError *theError = [stream streamError];

    Note

    An error does not necessarily indicate that the stream can no longer be used. If the stream can no longer be used, you will also receive a separate event informing you of that.

  • NSStreamEventEndEncountered: If you encounter a fatal error, or the device at the other end of the stream disconnects, the stream's delegate will receive the NSStreamEventEndEncountered event. When this happens, you should dispose of the streams, because they no longer connect you to anything.

  • NSStreamEventHasBytesAvailable: When the device you are connected to sends you data, you will receive one or more NSStreamEventHasBytesAvailable events. One of the tricky things about streams is that you may not receive the data all at once. The data will come across in the same order it was sent, but it's not the case that every discrete send results in one and only one NSStreamEventHasBytesAvailable event. The data from one send could be split into multiple events, or the data from multiple sends could get combined into one event. This can make reading data somewhat complex. We'll look at how to handle that complexity a little later, when we implement online play in our tic-tac-toe game.

  • NSStreamEventHasSpaceAvailable: Streams, especially network streams, have a limit to how much data they can accept at a time. When space becomes available on the stream, it will notify its delegate by sending the NSStreamEventHasSpaceAvailable event. At this time, if there is any queued, unsent data, it is safe to send at least some of that data through the stream.

Receiving Data from a Stream

When notified, by receipt of an NSStreamEventHasBytesAvailable event, that there is data available on the stream, you can read the available data, or a portion of it, by calling read:maxLength: on the stream.

The first argument you need to pass is a buffer, or chunk of memory, into which the stream will copy the received data. The second parameter is the maximum number of bytes that your buffer can handle. This method will return the number of bytes actually read, or −1 if there was an error.

Here's an example of reading up to a kibibyte of data (yes, Virginia, there is such a thing as a kibibyte; check out this link to learn more: http://en.wikipedia.org/wiki/Kibibyte) from the stream:

uint8_t     buffer[1024];
NSInteger   bytesRead = [inStream read:buffer maxLength:1024];
if (bytesRead == −1) {
    NSError *error = [inStream streamError];
    NSLog(@"Error reading data: %@", [error localizedDescription]);
}

Note

You'll notice that when we deal with data to be sent over a network connection, we often choose datatypes like uint8_t or int16_t, rather than more common datatypes like char and int. These are datatypes that are specified by their byte size, which is important when sending data over a network connection. Conversely, the int datatype is based on the register size of the hardware for which it's being compiled. An int compiled for one piece of hardware might not be the same size as an int compiled for another piece of hardware.

In this case, we want to be able to specify a buffer in bytes, so we use a datatype that's always going to be 8 bits (1 byte) long on all hardware and every platform. The actual datatype of the buffer doesn't matter—what matters is the size of that datatype, because that will affect the size of the buffer we allocate. We know uint8_t will always be 1 byte long on all platforms and all hardware, and that fact will be obvious to any programmer looking at our code, since the byte size is part of the datatype name.

Sending Data Through the Stream

To send data to the connected device through the output stream, you call write:maxLength:, passing in a pointer to the data to send and the length of that data. Here's how you might send the contents of an NSData instance called dataToSend:

NSUInteger sendLength = [dataToSend length];
NSUInteger written = [outStream write:[dataToSend bytes] maxLength:sendLength];
if (written == −1) {
    NSError *error = [outStream streamError];
    NSLog(@"Error writing data: %@", [error localizedDescription]);
}

It's important at this point that you check written to make sure it matches sendLength. If it doesn't, that means only part of your data went through, and you need to resend the rest when you get another NSStreamEventHasBytesAvailable event from the stream.

Putting It All Together

As you can see, adding online play to a program can be complex. If we're not careful, we could end up with messy globs of networking code littered throughout our application, making it hard to maintain and debug. We're still trying to write our code generically, so our goal is to create objects that can be reused, preferably unmodified, in other applications and that encapsulate the new functionality we need.

In this case, fortunately, we already have something we can model our classes on: GameKit. As discussed in the previous chapter, communication in GameKit happens through an object called a GKSession. That object manages both sending data to the remote device and receiving data from it. We call a method and pass in an NSData instance to send data to the other device, and we implement a delegate method to receive data from it. We're going to follow this model to create a similar session object for online play. We'll create two new generic classes, along with a couple of categories to help us convert an array of objects to a stream of bytes and back again. We're also going to need a new view controller.

The category will contain functionality that will assist us in reassembling data sent over a stream. One of the new objects will be called OnlineSession, and it will function similarly to GKSession. Once a stream pair is received from either the listener or from resolving the net service, that stream pair can be used to create a new OnlineSession.

We're also going to create a class called OnlineListener, which will encapsulate all the functionality needed to listen for new connections. Our new view controller class will present a list of available peers, similar to the peer picker in GameKit.

Before we get started writing these new classes, let's consider how we're going to ensure that the NSData objects we send can be reassembled by the other device. Remember that we don't have any control over how many bytes are sent at a time. We might, for example, send a single NSData instance, and the other machine may get that NSData spread over 20 NSStreamEventHasBytesAvailable events. Or we might send a few instances at different times that could be received all together in one NSStreamEventHasBytesAvailable event. To make sure that we can reassemble the stream of bytes into an object, we'll first send the length of the object followed by its bytes. That way, no matter how the stream is divided up, it can always be reassembled. If the other device is told to expect 128 bytes, it knows to keep waiting for data until it gets all 128 bytes before it should reassemble it. The device will also know that if it gets more than 128 bytes, then there's another object.

Let's take all this information and get it into code before our heads explode, shall we?

Updating Tic-Tac-Toe for Online Play

We're going to continue working with the TicTacToe application from the previous chapter. If you don't already have it open, consider making a backup copy before continuing. Because the fundamental game isn't changing, we don't need to touch the existing nibs. Although we will need to make changes to TicTacToeViewController, we won't change any of the game logic.

Adding the Packet Categories

We need the ability to convert multiple NSData instances into a single stream of bytes containing the length of the data and then the actual bytes. We also need a way to take a stream of bytes and reassemble those back into NSData instances. We're going to use categories to add this functionality to existing classes.

Because the stream won't necessarily be able to handle all the data we have to send, we're going to maintain a queue of all the data waiting to be sent in an NSArray. One of our categories will be on NSArray and will return a single NSData instance that holds a buffer of bytes representing everything in the array. We're also going to write a category on NSData to take a stream of bytes held in an instance of NSData and parse it back into the original objects. These categories will contain a single method each. Since they represent two sides of the same operation, we're going to place both categories in a single pair of files, just to minimize project clutter.

With your TicTacToe project open, single-click the Classes folder in the Groups & Files pane and press

Adding the Packet Categories

Once the files have been created, single-click PacketCategories.h and replace the existing contents with the following:

#import <Foundation/Foundation.h>
#define kInvalidObjectException     @"Invalid Object Exception"

@interface NSArray(PacketSend)
-(NSData *)contentsForTransfer;
@end

@interface NSData(PacketSplit)
- (NSArray *)splitTransferredPackets:(NSData **)leftover;
@end

The one constant, kInvalidObjectException, will be used to throw an exception if our NSArray method is called on an array that contains objects other than instances of NSData. If we wanted to make this more robust, we might archive other objects into instances of NSData, throwing an exception only if the array contains an object that doesn't conform to NSCoding. For simplicity's sake and to be consistent with the approach used by GKSession, we're going to support just NSData instances in our application.

After that, we declare a category on NSArray that adds a single method called contentsForTransfer, which returns the entire contents of the array, ready to be sent through a stream to the other machine. The second category is on NSData. This method will reassemble all of the objects contained in a chunk of received data. In addition to returning an array with those objects, it also takes one argument called leftover. This pointer to a pointer will be used to return any incomplete objects. If an object is incomplete, the caller will need to wait for more bytes, append them to leftover, and then call this method again.

Switch over to PacketCategories.m and replace the existing contents with this:

#import "PacketCategories.h"

@implementation NSArray(PacketSend)
-(NSData *)contentsForTransfer {
    NSMutableData *ret = [NSMutableData data];
    for (NSData *oneData in self) {
        if (![oneData isKindOfClass:[NSData class]])
            [NSException raise:kInvalidObjectException format:
                @"arrayContentsForTransfer only supports instances of NSData"];

        uint64_t dataSize[1];
        dataSize[0] = [oneData length];
        [ret appendBytes:dataSize length:sizeof(uint64_t)];
        [ret appendBytes:[oneData bytes] length:[oneData length]];
    }
    return ret;
}
@end

@implementation NSData(PacketSplit)
- (NSArray *)splitTransferredPackets:(NSData **)leftover {

    NSMutableArray *ret = [NSMutableArray array];
    const unsigned char *beginning = [self bytes];
    const unsigned char *offset = [self bytes];
    NSInteger bytesEnd = (NSInteger)offset + [self length];

    while ((NSInteger)offset < bytesEnd) {
        uint64_t dataSize[1];
        NSInteger dataSizeStart = offset - beginning;
        NSInteger dataStart = dataSizeStart + sizeof(uint64_t);

        NSRange headerRange = NSMakeRange(dataSizeStart, sizeof(uint64_t));
        [self getBytes:dataSize range:headerRange];

        if ((dataStart + dataSize[0] + (NSInteger)offset) > bytesEnd) {
            NSInteger lengthOfRemainingData = [self length] - dataSizeStart;
            NSRange dataRange = NSMakeRange(dataSizeStart, lengthOfRemainingData);
            *leftover = [self subdataWithRange:dataRange];

            return ret;
        }

        NSRange dataRange = NSMakeRange(dataStart, dataSize[0]);
        NSData *parsedData = [self subdataWithRange:dataRange];

        [ret addObject:parsedData];
        offset = offset + dataSize[0] + sizeof(uint64_t);
}
    return ret;
}
@end

These two categories might appear a little intimidating because they're dealing with bytes, but they're really quite straightforward. The first just creates an instance of NSMutableData to hold the stream of bytes, and then iterates over the array. For each object, it first adds the length of the object as a 64-byte integer, and then appends the actual data bytes from the object. When it's finished iterating, it returns the mutable data that contains the formatted stream of bytes.

The second method might be a little more intimidating looking, but all it's doing is looping through the bytes of self, which will be an instance of NSData that holds data formatted by the previous method. It first reads a uint64_t, a 64-byte integer that should hold the length of the object that follows, and then reads that number of bytes into a new instance of NSData, which it adds to a mutable array that will be returned. It continues to do this until it reaches the end of the data. If it gets to the end of the data and has an incomplete object, it sends that object's data back to the calling method using that pointer to a pointer argument, leftover.

Implementing the Online Session Object

Now that we have a way to split up and recombine objects from the stream, let's write our OnlineSession object. Create a new file by selecting the Classes folder and pressing

Implementing the Online Session Object

Single-click OnlineSession.h and replace the current contents with this new version:

#import <Foundation/Foundation.h>
#define kOnlineSessionErrorDomain   @"Online Session Domain"
#define kFailedToSendDataErrorCode  1000
#define kDataReadErrorCode          1001

#define kBufferSize                 512

@class OnlineSession;
@protocol OnlineSessionDelegate
- (void)onlineSessionReadyForUse:(OnlineSession *)session;
@optional
- (void)onlineSession:(OnlineSession *)session
         receivedData:(NSData *)data;
- (void)onlineSession:(OnlineSession *)session
 encounteredReadError:(NSError *)error;
- (void)onlineSession:(OnlineSession *)session
encounteredWriteError:(NSError *)error;
- (void)onlineSessionDisconnected:(OnlineSession *)session;
@end

@interface OnlineSession : NSObject {
    id                          delegate;
NSInputStream               *inStream;
    NSOutputStream              *outStream;

    BOOL                        writeReady;
    BOOL                        readReady;

    NSMutableArray              *packetQueue;
    NSData                      *readLeftover;
    NSData                      *writeLeftover;
}
@property (nonatomic, assign) id<OnlineSessionDelegate> delegate;

- (id)initWithInputStream:(NSInputStream *)theInStream
             outputStream:(NSOutputStream *)theOutStream;
- (BOOL)sendData:(NSData *)data error:(NSError **)error;
- (BOOL)isReadyForUse;
@end

Let's take a look at what we're doing here. First, we define a few constants for an error domain for our session object, as well as some error codes to represent errors we might encounter:

#define kOnlineSessionErrorDomain   @"Online Session Domain"
#define kFailedToSendDataErrorCode  1000
#define kDataReadErrorCode          1001

After that, we define another constant that will set the size of our read buffer. Remember that when we read data from a stream, we need to create a buffer of a specific size, and then inform the stream of the maximum number of bytes we can accept in a single read operation. This constant will be used to allocate the memory and also will be passed in to the stream's read method as the maxLength parameter. Depending on the size of the data you need to transfer, you might want to tweak this value, but it's generally a good idea to read from the stream in small chunks. Apple typically recommends either 512 or 1024 per read. Since the data we send in our application is relatively small, we went with the smaller suggested value of 512.

#define kBufferSize                 512

Our session will have a delegate, and we will inform the delegate when certain things happen. We create a protocol to define the methods that our delegate can and must implement. Because we haven't yet declared our OnlineSession class (which will happen below the protocol), we use the @class keyword to tell the compiler that the class actually exists, even though the compiler hasn't seen it yet. The only required method is the one used to receive data from peers; however, we provide methods to inform the delegate of pretty much any stream event that the application might need to know about.

@class OnlineSession;
@protocol OnlineSessionDelegate
- (void)onlineSessionReadyForUse:(OnlineSession *)session;
@optional
- (void)onlineSession:(OnlineSession *)session
         receivedData:(NSData *)data;
- (void)onlineSession:(OnlineSession *)session
 encounteredReadError:(NSError *)error;
- (void)onlineSession:(OnlineSession *)session
encounteredWriteError:(NSError *)error;
- (void)onlineSessionDisconnected:(OnlineSession *)session;
@end

Next, we define the OnlineSession class and declare its instance variables:

@interface OnlineSession : NSObject {
    id                          delegate;

    NSInputStream               *inStream;
    NSOutputStream              *outStream;

    BOOL                        writeReady;
    BOOL                        readReady;

    NSMutableArray              *packetQueue;
    NSData                      *readLeftover;
    NSData                      *writeLeftover;

}

We have a delegate, a stream pair, a pair of BOOLs that will be used to keep track of whether the streams are ready to use, and then a mutable array to keep a queue of unsent data. When our write method is called, if the streams aren't ready or if there is no space available on the output stream, we'll queue up the data by adding it to the packetQueue array, and then send the queued data when we get a space available event from outStream. The last two instance variables are used to keep track of partial packets. Remember that we won't always be able to send an entire object, nor will we always receive objects in a single chunk, so we need a way to keep track of the leftover data.

The only one of our instance variables that ever needs to be changed by another class is delegate, so we declare a property for delegate. Unlike the underlying instance variable, we declare the property as id<OnlineSessionDelegate>, which means that we accept any object, but require it to conform to the OnlineSessionDelegate protocol that we defined earlier. If another object tries to assign a delegate that doesn't conform to that protocol, it will generate a compile-time warning, because we've declared the property this way.

@property (nonatomic, assign) id<OnlineSessionDelegate> delegate;

One really important thing to note here is that we use assign rather than retain for our delegate. This is a standard convention in Cocoa and Cocoa Touch. Generally speaking, objects should not retain their delegate unless there's a compelling reason to do so. As a result, your delegate properties should always be declared with the assign keyword.

Finally, we declare a whopping three instance methods in our header: an init method, a method to send data, and a method to determine if the session is ready for use.

- (id)initWithInputStream:(NSInputStream *)theInStream
             outputStream:(NSOutputStream *)theOutStream;
- (BOOL)sendData:(NSData *)data error:(NSError **)error;
- (BOOL)isReadyForUse;

Save your file.

Now, switch over to OnlineSession.m and replace the contents with the following:

#import "OnlineSession.h"
#import "PacketCategories.h"

@interface OnlineSession()
- (void)sendQueuedData;
@end

#pragma mark -
@implementation OnlineSession
@synthesize delegate;

#pragma mark -
- (id)initWithInputStream:(NSInputStream *)theInStream
             outputStream:(NSOutputStream *)theOutStream {
    if (self = [super init]) {

        inStream = [theInStream retain];
        outStream = [theOutStream retain];

        [inStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
            forMode:NSDefaultRunLoopMode];
        [outStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
            forMode:NSDefaultRunLoopMode];

        inStream.delegate = self;
        outStream.delegate = self;

        if ([inStream streamStatus] == NSStreamStatusNotOpen)
            [inStream open];

        if ([outStream streamStatus] == NSStreamStatusNotOpen)
            [outStream open];

        packetQueue = [[NSMutableArray alloc] init];
    }
    return self;
}

- (BOOL)sendData:(NSData *)data error:(NSError **)error {

    if (data == nil || [data length] == 0)
        return NO;

    [packetQueue addObject:data];

    if ([outStream hasSpaceAvailable])
        [self sendQueuedData];

    return YES;
}

- (BOOL)isReadyForUse {
    return readReady && writeReady;
}
- (void)dealloc {
    [inStream close];
    [inStream removeFromRunLoop:[NSRunLoop currentRunLoop]
        forMode:NSDefaultRunLoopMode];
    inStream.delegate = nil;
    [inStream release];

    [outStream close];
    [outStream removeFromRunLoop:[NSRunLoop currentRunLoop]
        forMode:NSDefaultRunLoopMode];
    outStream.delegate = nil;
    [outStream release];

    [packetqueue release];
    [writeLeftover release];
    [readLeftover release];

    [super dealloc];
}

- (void) stream:(NSStream *)stream
    handleEvent:(NSStreamEvent)eventCode {
    switch(eventCode) {
        case NSStreamEventOpenCompleted:
            if (stream == inStream)
                readReady = YES;
            else
                writeReady = YES;
            if ([self isReadyForUse] &&
                [delegate respondsToSelector:@selector(onlineSessionReadyForUse:)])
                [delegate onlineSessionReadyForUse:self];
            break;
         case NSStreamEventHasBytesAvailable:
            if (stream == inStream) {

                if ([inStream hasBytesAvailable]) {

                    NSMutableData *data = [NSMutableData data];

                    if (readLeftover != nil) {
                        [data appendData:readLeftover];
                        [readLeftover release];
                        readLeftover = nil;
                    }

                    NSInteger       bytesRead;
                    static uint8_t  buffer[kBufferSize];

                    bytesRead = [inStream read:buffer maxLength:kBufferSize];
                    if (bytesRead == −1 && [delegate respondsToSelector:
                        @selector(onlineSession:encounteredReadError:)]) {
                        NSError *error = [[NSError alloc]
                            initWithDomain:kOnlineSessionErrorDomain
                            code:kDataReadErrorCode userInfo:nil];
                        [delegate onlineSession:self encounteredReadError:error];
                        [error release];
return;
                    }
                    else if (bytesRead > 0) {
                        [data appendBytes:buffer length:bytesRead];

                        NSArray *dataPackets = [data splitTransferredPackets:
                            &readLeftover];

                        if (readLeftover)
                            [readLeftover retain];

                        for (NSData *onePacketData in dataPackets)
                            [delegate onlineSession:self
                                receivedData:onePacketData];
                    }
                }
            }
            break;
       case NSStreamEventErrorOccurred: {
            NSError *theError = [stream streamError];
            if (stream == inStream)
                if (delegate && [delegate respondsToSelector:
                    @selector(onlineSession:encounteredReadError:)])
                    [delegate onlineSession:self encounteredReadError:theError];
                else{
                    if (delegate && [delegate respondsToSelector:
                        @selector(onlineSession:encounteredWriteError:)])
                        [delegate onlineSession:self
                            encounteredWriteError:theError];
                }

            break;
        }
        case NSStreamEventHasSpaceAvailable:
            if (stream == outStream) {
                [self sendQueuedData];
            }
            break;
        case NSStreamEventEndEncountered:
            if (delegate && [delegate respondsToSelector:
                @selector(onlineSessionDisconnected:)])
                [delegate onlineSessionDisconnected:self];
                readReady = NO;
                writeReady = NO;
            break;
        default:
            break;
    }
}

- (void)sendQueuedData {

    if (writeLeftover == nil && [packetQueue count] == 0)
        return; // Nothing to send!

    NSMutableData *dataToSend = [NSMutableData data];
if (writeLeftover != nil) {
        [dataToSend appendData:writeLeftover];
        [writeLeftover release];
        writeLeftover = nil;
    }

    [dataToSend appendData:[packetQueue contentsForTransfer]];
    [packetQueue removeAllObjects];

    NSUInteger sendLength = [dataToSend length];
    NSUInteger written = [outStream write:[dataToSend bytes] maxLength:sendLength];

    if (written == −1) {
        if (delegate && [delegate respondsToSelector:
            @selector(onlineSession:encounteredWriteError:)])
            [delegate onlineSession:self encounteredWriteError:
                [outStream streamError]];
    }
    if (written != sendLength) {
        NSRange leftoverRange = NSMakeRange(written, [dataToSend length] - written);
        writeLeftover = [[dataToSend subdataWithRange:leftoverRange] retain];
    }
}

@end

This is a little gnarly looking, but we've already covered pretty much everything we do in this class. In the initWithInputStream:outputStream: method, we retain and keep a reference to the two streams, schedule them both with the run loop, and then open the streams if they aren't already open. We also create our mutable array to serve as our packet queue.

- (id)initWithInputStream:(NSInputStream *)theInStream
             outputStream:(NSOutputStream *)theOutStream {
    if (self = [super init]) {
        [theInStream retain];
        inStream = theInStream;

        [theOutStream retain];
        outStream = theOutStream;

        [inStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
            forMode:NSDefaultRunLoopMode];
        [outStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
            forMode:NSDefaultRunLoopMode];

        inStream.delegate = self;
        outStream.delegate = self;

        if ([inStream streamStatus] == NSStreamStatusNotOpen)
            [inStream open];

        if ([outStream streamStatus] == NSStreamStatusNotOpen)
            [outStream open];

        packetQueue = [[NSMutableArray alloc] init];
    }
return self;
}

In our sendData: method, we add the new object to the queue and, if there's space available, we call the method sendQueuedData, which will attempt to send as much of the queued data as the stream will take. If there is no space available on the stream, we don't do anything other than add the data to the queue. It will be sent when we are notified that there is space available on the stream.

- (BOOL)sendData:(NSData *)data error:(NSError **)error {

    if (data == nil || [data length] == 0)
        return NO;

    [packetQueue addObject:data];

    if ([outStream hasSpaceAvailable])
        [self sendQueuedData];

    return YES;
}

The isReadyForUse method just does a logical AND operation on the two BOOL instance variables that are used to track whether the two streams are available for use. It returns YES if both streams are ready; otherwise, it returns NO. You'll see where we set these values a little later in the chapter.

- (BOOL)isReadyForUse {
    return readReady && writeReady;
}

Our dealloc method is pretty standard. The only differences from the regular dealloc methods we're used to writing is that we need to close the streams, remove them from the run loop, and set their delegate to nil.

- (void)dealloc {
    [inStream close];
    [inStream removeFromRunLoop:[NSRunLoop currentRunLoop]
        forMode:NSDefaultRunLoopMode];
    inStream.delegate = nil;
    [inStream release];

    [outStream close];
    [outStream removeFromRunLoop:[NSRunLoop currentRunLoop]
        forMode:NSDefaultRunLoopMode];
    outStream.delegate = nil;
    [outStream release];

    [super dealloc];
}

The next method is a bit of a doozy. It's that stream delegate method we discussed earlier—the one that is called with different event codes. This is where most of the work happens in our OnlineSession class. Let's look at each of the event codes separately.

If we are notified that a stream has finished opening, we check which stream sent us the event and set the appropriate value to YES. After we do that, we check to see if both are YES and, if they are, we inform our delegate that the session is ready to use if the delegate has implemented that method.

case NSStreamEventOpenCompleted:
            if (stream == inStream)
                readReady = YES;
            else
                writeReady = YES;
            if ([self isReadyForUse] &&
                [delegate respondsToSelector:@selector(onlineSessionReadyForUse:)])
                [delegate onlineSessionReadyForUse:self];
            break;

If we are notified that there are bytes available on a stream, we first make sure we're getting this event from the input stream. In theory, we should never get this event from an output stream, but we code defensively just in case. We also create an instance of NSMutableData to hold the received data. If there is any leftover data, we combine the new data with the leftover data, which we will be stored in readLeftover, before processing it. This way, every time new data comes in, we have all the unprocessed data in one place.

case NSStreamEventHasBytesAvailable:
            if (stream == inStream) {

                if ([inStream hasBytesAvailable]) {

                    NSMutableData *data = [NSMutableData data];

                    if (readLeftover != nil) {
                        [data appendData:readLeftover];
                        [readLeftover release];
                        readLeftover = nil;
                    }

Now we read the data into a buffer. We check to make sure we didn't encounter an error and, if we did, we notify our delegate about the error.

NSInteger       bytesRead;
                    static uint8_t  buffer[kBufferSize];

                    bytesRead = [inStream read:buffer maxLength:kBufferSize];
                    if (bytesRead == −1 && [delegate respondsToSelector:
                        @selector(onlineSession:encounteredReadError:)]) {
                        NSError *error = [[NSError alloc]
                            initWithDomain:kOnlineSessionErrorDomain
                            code:kDataReadErrorCode userInfo:nil];
                        [delegate onlineSession:self encounteredReadError:error];
                        [error release];
                        return;
                    }

If there wasn't an error, we use the category method we created earlier to decode objects from the data we've received. If any objects were decoded, we inform our delegate. If there is any leftover data, we retain it so that it will be here the next time this event is called.

else if (bytesRead > 0) {
                        [data appendBytes:buffer length:bytesRead];

                        NSArray *dataPackets = [data splitTransferredPackets:
                            &readLeftover];

                        if (readLeftover)
                            [readLeftover retain];

                        for (NSData *onePacketData in dataPackets)
                            [delegate onlineSession:self
                                receivedData:onePacketData];
                    }
                }
            }
            break;

If we get an error event, all we do is pass it on to our delegate method.

case NSStreamEventErrorOccurred: {
            NSError *theError = [stream streamError];
            if (stream == inStream)
                if (delegate && [delegate respondsToSelector:
                    @selector(onlineSession:encounteredReadError:)])
                    [delegate onlineSession:self encounteredReadError:theError];
                else {
                    if (delegate && [delegate respondsToSelector:
                        @selector(onlineSession:encounteredWriteError:)])
                        [delegate onlineSession:self
                            encounteredWriteError:theError];
                }
            break;
        }

When the output stream tells us that there's space available, we call sendQueuedData to send any unsent data.

case NSStreamEventHasSpaceAvailable:
            if (stream == outStream) {
                [self sendQueuedData];
            }
            break;

Finally, if we are notified that the stream has been closed for any reason, we inform our delegate of the fact, and mark both streams as no longer ready for use. Because the streams act as a pair, we don't bother to check which one informed us—we just assume that if one is closed, both are closed.

case NSStreamEventEndEncountered:
            if (delegate && [delegate respondsToSelector:
                @selector(onlineSessionDisconnected:)])
                [delegate onlineSessionDisconnected:self];
readReady = NO;
                writeReady = NO;
            break;

We're almost finished with OnlineSession. We just need to look at the sendQueuedData method. First, if there's no leftover data and nothing queued, we return, because there's no reason to do anything else.

- (void)sendQueuedData {
    if (writeLeftover == nil && [packetQueue count] == 0)
        return; // Nothing to send!

We next create an instance of NSMutableData. We use that instance to combine the queued data with the leftover data into a single chunk, and then clear the queue.

NSMutableData *dataToSend = [NSMutableData data];

    if (writeLeftover!= nil) {
        [dataToSend appendData: writeLeftover];
        [writeLeftover release];
        writeLeftover = nil;
    }

    [dataToSend appendData:[packetQueue contentsForTransfer]];
    [packetQueue removeAllObjects];

Now that we have all the data that needs to be sent in a single instance of NSMutableData, we try to send it.

NSUInteger sendLength = [dataToSend length];
    NSUInteger written = [outStream write:[dataToSend bytes] maxLength:sendLength];

If we encountered a write error, we notify our delegate.

if (written == −1) {
        if (delegate && [delegate respondsToSelector:
            @selector(onlineSession:encounteredWriteError:)])
            [delegate onlineSession:self encounteredWriteError:
                [outStream streamError]];
    }

Then, if the amount sent is not equal to the length of the data we needed to send, we extract the part that didn't get sent and store it in writeLeftover, so we'll have it the next time we try to send queued data.

if (written != sendLength) {
        NSRange leftoverRange = NSMakeRange(written, [dataToSend length] - written);
         writeLeftover = [[dataToSend subdataWithRange:leftoverRange] retain];

    }
}

We're finished with OnlineSession.m. Make sure you save it.

Creating the Listener Object

As we discussed earlier in the chapter, we need to listen for network connections if we're going to advertise a service using Bonjour. Now, let's create a class to encapsulate listening for a network connection.

Create a new file using the same Objective-C class template we've used twice already in this chapter. Name the new file OnlineListener.m, and have it create OnlineListener.h for you as well.

Once the files are created, single-click OnlineListener.h and replace the current contents with the following:

#import <Foundation/Foundation.h>
#define kOnlineListenerErrorDomain @"Online Session Listener Session Domain"
#define kOnlineListenerErrorNoSocketsAvailable    1000
#define kOnlineListenerErrorCouldntBindToAddress  1001
#define kOnlineListenerErrorStreamError           1002

@class OnlineListener;
@protocol OnlineListenerDelegate
- (void) acceptedConnectionForListener:(OnlineListener *)theListener
                           inputStream:(NSInputStream *)theInputStream
                          outputStream:(NSOutputStream *)theOutputStream;
@optional
- (void) onlineListener:(OnlineListener *)theListener
       encounteredError:(NSError *)error;
@end

@interface OnlineListener : NSObject {
    id delegate;
    uint16_t port;
    CFSocketRef socket;
}
@property (nonatomic, assign) id<OnlineListenerDelegate> delegate;
@property uint16_t  port;

- (BOOL)startListening:(NSError **)error;
- (void)stopListening;

@end

Once again, we have constants for an error domain, a few error codes that we'll need, a formal protocol to define one method that this class's delegate must implement, and a second method that it can define if necessary. The required method will be called when a connection attempt is detected, and it will pass to the delegate the stream pair that was created. The optional method is called when a connection was attempted but failed.

We have three instance variables this time: a delegate, a port number, and a CFSocketRef, which is that CFNetwork socket object we discussed earlier in the chapter. We expose only the delegate and the port number as properties, because there's really no reason why external objects would need direct access to the socket. We're not using a specific port number, which means the object that creates the listener will need to retrieve the port number so it can pass it to Bonjour.

The class itself has only two methods: one to tell it to start listening and one to tell it to stop listening. Nice and simple.

Make sure you save OnlineListener.h before continuing.

Single-click OnlineListener.m and replace its contents with the following:

#import "OnlineListener.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <CFNetwork/CFSocketStream.h>

#pragma mark CFNetwork C Callbacks
static void onlineListenerAcceptCallback (CFSocketRef theSocket, CFSocketCallBackType theType, CFDataRef theAddress, const void *data, void *info) {
    OnlineListener *listener = (OnlineListener *)info;
    id listenerDelegate = listener.delegate;
    if (theType == kCFSocketAcceptCallBack) {
        CFSocketNativeHandle nativeSocket = *(CFSocketNativeHandle *)data;
        uint8_t name[SOCK_MAXADDRLEN];
        socklen_t namelen = sizeof(name);
        NSData *peer = nil;
        if (getpeername(nativeSocket, (struct sockaddr *)name, &namelen) == 0) {
            peer = [NSData dataWithBytes:name length:namelen];
        }
        CFReadStreamRef readStream = NULL;
        CFWriteStreamRef writeStream = NULL;
        CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocket, &readStream,
            &writeStream);
        if (readStream && writeStream) {
            CFReadStreamSetProperty(readStream,
                kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
            CFWriteStreamSetProperty(writeStream,
                kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
            if (listenerDelegate && [listenerDelegate respondsToSelector:
               @selector(acceptedConnectionForListener:inputStream:outputStream:)]){
               [listenerDelegate acceptedConnectionForListener:listener
                   inputStream:(NSInputStream *)readStream
                   outputStream:(NSOutputStream *)writeStream];
            }
        } else {
            close(nativeSocket);
            if ([listenerDelegate
                respondsToSelector:@selector(onlineListener:encounteredError:)]) {
                NSError *error = [[NSError alloc]
                    initWithDomain:kOnlineListenerErrorDomain
                    code:kOnlineListenerErrorStreamError userInfo:nil];
                [listenerDelegate onlineListener:listener encounteredError:error];
                [error release];
            }
        }
        if (readStream) CFRelease(readStream);
        if (writeStream) CFRelease(writeStream);
    }
}

#pragma mark -
@implementation OnlineListener
@synthesize delegate;
@synthesize port;
#pragma mark -
#pragma mark Listener Methods
- (BOOL)startListening:(NSError **)error {
    CFSocketContext socketCtxt = {0, self, NULL, NULL, NULL};
    socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, (CFSocketCallBack)&onlineListenerAcceptCallback, &socketCtxt);

    if (socket == NULL) {
        if (error) *error = [[NSError alloc]
            initWithDomain:kOnlineListenerErrorDomain
            code:kOnlineListenerErrorNoSocketsAvailable
            userInfo:nil];
        return NO;
    }

    int ret = 1;
    setsockopt(CFSocketGetNative(socket), SOL_SOCKET, SO_REUSEADDR, (void *)&ret,
        sizeof(ret));

    struct sockaddr_in addr4;
    memset(&addr4, 0, sizeof(addr4));
    addr4.sin_len = sizeof(addr4);
    addr4.sin_family = AF_INET;
    addr4.sin_port = 0;
    addr4.sin_addr.s_addr = htonl(INADDR_ANY);
    NSData *address4 = [NSData dataWithBytes:&addr4 length:sizeof(addr4)];

    if (kCFSocketSuccess != CFSocketSetAddress(socket, (CFDataRef)address4)) {
        if (error) *error = [[NSError alloc]
            initWithDomain:kOnlineListenerErrorDomain
            code:kOnlineListenerErrorCouldntBindToAddress
            userInfo:nil];
        if (socket)
            CFRelease(socket);
        socket = NULL;
        return NO;
    }

    NSData *addr = [(NSData *)CFSocketCopyAddress(socket) autorelease];
    memcpy(&addr4, [addr bytes], [addr length]);
    self.port = ntohs(addr4.sin_port);

    CFRunLoopRef cfrl = CFRunLoopGetCurrent();
    CFRunLoopSourceRef source4 = CFSocketCreateRunLoopSource(kCFAllocatorDefault,
        socket, 0);
    CFRunLoopAddSource(cfrl, source4, kCFRunLoopCommonModes);
    CFRelease(source4);

    return ret;
}

- (void)stopListening {
    if (socket) {
        CFSocketInvalidate(socket);
CFRelease(socket);
        socket = NULL;
    }
}

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

@end

We begin by importing some header files you may not have seen before. The first three are part of the old-school socket API. We need to include these because we use some of the constants and functions they contain when we set up our listener. We also import a CFNetwork header file used to retrieve streams from a CFSocket.

#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <CFNetwork/CFSocketStream.h>

Next, before our class implementation, we have a C function. This is our socket callback function that will be called whenever a connection attempt is detected.

static void onlineListenerAcceptCallback (CFSocketRef theSocket,
    CFSocketCallBackType theType, CFDataRef theAddress,
const void *data, void *info) {

Since this is a C function, it does not have access to Objective-C constructs such as self. So how do we access our delegate from within this function? When we created the socket, we created a socket context struct, and embedded a pointer to self in that struct. That embedded pointer is passed to this function as its last parameter, info. We'll cast that pointer to an instance of OnlineListener, which will give us access to the listener's delegate.

OnlineListener *listener = (OnlineListener *)info;
    id listenerDelegate = listener.delegate;

Next, we make sure that we got the right type of callback. Although we registered to receive only one type of callback, we still want to code defensively. Remember that CFWriteStreamRef is toll-free bridged to NSOutputStream, and CFReadStreamRef is toll-free bridged to NSInputStream, so once we've created the stream pair, we pass them to the listener's delegate.

if (theType == kCFSocketAcceptCallBack) {

Then we retrieve a stream pair that represents the connection that was made:

CFSocketNativeHandle nativeSocket = *(CFSocketNativeHandle *)data;
        uint8_t name[SOCK_MAXADDRLEN];
        socklen_t namelen = sizeof(name);
        NSData *peer = nil;
        if (getpeername(nativeSocket, (struct sockaddr *)name, &namelen) == 0) {
            peer = [NSData dataWithBytes:name length:namelen];
        }
        CFReadStreamRef readStream = NULL;
CFWriteStreamRef writeStream = NULL;
        CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocket, &readStream,
            &writeStream);
        if (readStream && writeStream) {
            CFReadStreamSetProperty(readStream,
                kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
            CFWriteStreamSetProperty(writeStream,
                kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
            if (listenerDelegate && [listenerDelegate respondsToSelector:
               @selector(acceptedConnectionForListener:inputStream:outputStream:)]){
               [listenerDelegate acceptedConnectionForListener:self
                   inputStream:inStream outputStream:outStream];
        } else {

If there was a problem, we close the socket and notify our delegate, assuming it has implemented the onlineListener:encounteredError: method.

close(nativeSocket);
            if ([listenerDelegate
                respondsToSelector:@selector(onlineListener:encounteredError:)]) {
                NSError *error = [[NSError alloc]
                    initWithDomain:kOnlineListenerErrorDomain
                    code:kOnlineListenerErrorStreamError userInfo:nil];
                [listenerDelegate onlineListener:listener encounteredError:error];
                [error release];
            }
        }

When we're finished, we release both of the streams. CFRelease acts like release in Objective-C. It doesn't deallocate the object, but just decrements the object's retain count. It will be the delegate's responsibility to retain the streams if it's going to continue using them.

if (readStream) CFRelease(readStream);
        if (writeStream) CFRelease(writeStream);
    }
}

The startListening: method goes through the process we described earlier to create and configure a socket, and register it with the run loop. If there's anything here that you're not comfortable with, go back and review the "Setting Up a Listener" section earlier in this chapter.

The next method is stopListening. All it needs to do is invalidate and release the socket.

- (void)stopListening {
    if (socket) {
        CFSocketInvalidate(socket);
        CFRelease(socket);
        socket = NULL;
    }
}

Creating the Peer Browser

Since we're not using GameKit when the user selects online play, we must implement our own controller class to display the available peers and to let the user select one of them. Our current controller class, TicTacToeViewController, will present this new view controller's view modally, which will add just a touch of complexity to our application. The new view controller class will create and be the delegate for an instance of NSNetServiceBrowser, but when the user selects a peer, it's actually TicTacToeViewController that will need to be the delegate for the resolved service, because the resolution will happen after the modal view has been dismissed.

Creating the Peer Browser Files

Create another new class, and just to shake things up, let's choose a different file template this time. Select UIViewController subclass, and make sure the UITableViewController subclass check box is not selected, but that the With XIB for user interface box is checked (Figure 9-4). Call this new file OnlinePeerBrowser.m, and have it create OnlinePeerBrowser.h also.

When choosing the file template for creating the peer browser, you should select UIViewController subclass and also check the box labeled With XIB for user interface.

Figure 9.4. When choosing the file template for creating the peer browser, you should select UIViewController subclass and also check the box labeled With XIB for user interface.

After it creates the file, you should drag OnlinePeerBrowser.xib to the Resources folder in the Groups & Files pane where it belongs.

Writing the Peer Browser Header

Single-click OnlinePeerBrowser.h and replace the contents with the following:

#import <UIKit/UIKit.h>

@interface OnlinePeerBrowser : UIViewController
        <UITableViewDelegate, UITableViewDataSource> {

    UITableView         *tableView;
    NSNetServiceBrowser *netServiceBrowser;

    NSMutableArray      *discoveredServices;
}
@property (nonatomic, retain) IBOutlet UITableView *tableView;
@property (nonatomic, retain) NSNetServiceBrowser *netServiceBrowser;
@property (nonatomic, retain) NSMutableArray *discoveredServices;
- (IBAction)cancel;
@end

Everything here should be understandable. Because we need a toolbar with a Cancel button on it, we're not subclassing UITableViewController, but we will be using a table view, so we conform our class to both UITableViewDelegate and UITableViewDataSource. We have an outlet that will point to the table view, and an action method for the Cancel button on the toolbar to call. We also declare an instance of NSNetServiceBrowser, which will be used to search for peers, and a mutable array, called discoveredServices, which will be used to keep track of the found services.

Building the Peer Browser Interface

Double-click OnlinePeerBrowser.xib to open Interface Builder. Once it opens, drag a Toolbar from the library to the window labeled View, and place it snugly against the bottom of the window. Double-click the toolbar's one button to edit the button's title, and change it to say Cancel. Press return to commit the title change.

The toolbar button should still be selected. Control-drag from the button to File's Owner, and select the cancel action to connect the button to that action method.

Next, drag a Table View from the library over to the window. As you move it over the View window, it should automatically resize itself to the space available above the toolbar. Drop the table onto the view so it takes up the remainder of the space. Press

Building the Peer Browser Interface

Save the nib and quit Interface Builder.

Implementing the Peer Browser View Controller

Single-click OnlinePeerBrowser.m. Replace the current contents of that file with the following:

Warning

Do not try to build the project yet. The following code relies on some changes to TicTacToeViewController that we haven't made yet.

#import "OnlinePeerBrowser.h"
#import "TicTacToeViewController.h"

@implementation OnlinePeerBrowser
@synthesize tableView;
@synthesize netServiceBrowser;
@synthesize discoveredServices;

#pragma mark -
#pragma mark Action Methods
- (IBAction)cancel {
    [self.netServiceBrowser stop];
    self.netServiceBrowser.delegate = nil;
    self.netServiceBrowser = nil;

    [(TicTacToeViewController *)self.parentViewController browserCancelled];
}

#pragma mark -
#pragma mark Superclass Overrides
- (void)viewDidLoad {
    NSNetServiceBrowser *theBrowser = [[NSNetServiceBrowser alloc] init];
    theBrowser.delegate = self;

    [theBrowser searchForServicesOfType:kBonjourType inDomain:@""];
    self.netServiceBrowser = theBrowser;
    [theBrowser release];

    self.discoveredServices = [NSMutableArray array];

}

- (void)viewDidUnload {
    self.tableView = nil;
}

- (void)dealloc {
    [tableView release];
    if (netServiceBrowser != nil) {
        [self.netServiceBrowser stop];
        self.netServiceBrowser.delegate = nil;
    }
    [netServiceBrowser release];
    [discoveredServices release];
    [super dealloc];
}
#pragma mark -
#pragma mark Table View Methods
- (NSInteger)tableView:(UITableView *)theTableView numberOfRowsInSection:(NSInteger)section {
    return [discoveredServices count];
}

- (NSString *)tableView:(UITableView *)theTableView titleForHeaderInSection:(NSInteger)section {
    return NSLocalizedString(@"Available Peers", @"Available Peers");
}

- (UITableViewCell *)tableView:(UITableView *)theTableView
        cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *identifier = @"Browser Cell Identifier";

    UITableViewCell *cell = [tableView
        dequeueReusableCellWithIdentifier:identifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
            reuseIdentifier:identifier] autorelease];
    }
    NSUInteger row = [indexPath row];
    cell.textLabel.text = [[discoveredServices objectAtIndex:row] name];
    return cell;
}

- (void)tableView:(UITableView *)theTableView
        didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNetService *selectedService = [discoveredServices
       objectAtIndex:[indexPath row]];
    selectedService.delegate = self.parentViewController;
    [selectedService resolveWithTimeout:0.0];

    TicTacToeViewController *parent =
        (TicTacToeViewController *)self.parentViewController;
    parent.netService = selectedService;

    [self.netServiceBrowser stop];

    [self.parentViewController dismissModalViewControllerAnimated:YES];
}

#pragma mark -
#pragma mark Net Service Browser Delegate Methods
- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)browser {
    self.netServiceBrowser.delegate = nil;
    self.netServiceBrowser = nil;
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
             didNotSearch:(NSDictionary *)errorDict {
    NSLog(@"Error browsing for service: %@", [errorDict
        objectForKey:NSNetServicesErrorCode]);
    [self.netServiceBrowser stop];
}
- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
           didFindService:(NSNetService *)aNetService
               moreComing:(BOOL)moreComing {
    TicTacToeViewController *parent =
        (TicTacToeViewController *)self.parentViewController;
    if (![[parent.netService name] isEqualToString:[aNetService name]]){
        [discoveredServices addObject:aNetService];
        NSSortDescriptor *sd = [[NSSortDescriptor alloc]
            initWithKey:@"name" ascending:YES];
        [discoveredServices sortUsingDescriptors:[NSArray arrayWithObject:sd]];
        [sd release];
    }

    if(!moreComing)
        [self.tableView reloadData];
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
         didRemoveService:(NSNetService *)aNetService
               moreComing:(BOOL)moreComing {
    [discoveredServices removeObject:aNetService];

    if(!moreComing)
        [self.tableView reloadData];
}

@end

Most of this is stuff we've talked about before, but it's worth stepping through so you understand how we implemented this controller. The action method, cancel, stops the service browser from looking for Bonjour services, and then calls a method that we will write shortly on the parent view controller, which will be an instance of TicTacToeViewController. This method will dismiss the modal view controller and will reset the user interface so that the New Game button is available. Since no opponent was selected, the user should have the option to begin a new game.

- (IBAction)cancel {
    [self.netServiceBrowser stop];
    self.netServiceBrowser.delegate = nil;
    self.netServiceBrowser = nil;

    [(TicTacToeViewController *)self.parentViewController browserCancelled];
}

In viewDidLoad, we create an instance of NSNetServiceBrowser and tell it to start searching for services. We specify a constant called kBonjourType, which will contain the Bonjour type identifier for our tic-tac-toe game. We also create the mutable array instance that we'll use to keep track of discovered services and that will drive the table.

- (void)viewDidLoad {
    NSNetServiceBrowser *theBrowser = [[NSNetServiceBrowser alloc] init];
    theBrowser.delegate = self;

    [theBrowser searchForServicesOfType:kBonjourType inDomain:@""];
    self.netServiceBrowser = theBrowser;
[theBrowser release];

    self.discoveredServices = [NSMutableArray array];
}

The viewDidUnload and dealloc methods are standard and shouldn't require any additional explanation. The first three table view methods are all standard as well. We have a table with a single section, and the row count for that section is dictated by the number of items in the discoveredServices array.

- (NSInteger)tableView:(UITableView *)theTableView numberOfRowsInSection:(NSInteger)section {
    return [discoveredServices count];
}

We also return a header for the one section to inform the user what they're viewing.

- (NSString *)tableView:(UITableView *)theTableView titleForHeaderInSection:(NSInteger)section {
    return NSLocalizedString(@"Available Peers", @"Available Peers");
}

The tableView:cellForRowAtIndexPath: method is also pretty much the same as many we've written in the past. It just displays the name of one of the discovered services in a cell using the default cell style.

- (UITableViewCell *)tableView:(UITableView *)theTableView
        cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *identifier = @"Browser Cell Identifier";

    UITableViewCell *cell = [tableView
        dequeueReusableCellWithIdentifier:identifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
            reuseIdentifier:identifier] autorelease];
    }
    NSUInteger row = [indexPath row];
    cell.textLabel.text = [[discoveredServices objectAtIndex:row] name];
    return cell;
}

When the user taps a row, tableView:didSelectRowAtIndexPath: is called, and we need to resolve the selected service. When we do that, we don't specify self as the delegate of the net service that was selected. Instead, we specify our parent view controller, which is the view controller that presented our view modally. In our application, that will be TicTacToeViewController, so when the net service resolves, TicTacToeViewController will be notified. This is a good thing, because after that, we stop the browser (we support only one peer in this game) and dismiss the modally presented view controller, meaning this instance won't be around to be notified when the service is resolved.

- (void)tableView:(UITableView *)theTableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    NSNetService *selectedService = [discoveredServices
       objectAtIndex:[indexPath row]];
selectedService.delegate = self.parentViewController;
    [selectedService resolveWithTimeout:0.0];

    TicTacToeViewController *parent =
        (TicTacToeViewController *)self.parentViewController;
    parent.netService = selectedService;

    [self.netServiceBrowser stop];

    [self.parentViewController dismissModalViewControllerAnimated:YES];
}

Next up are the NSNetServiceBrowser delegate methods. When we're notified that a search stopped, we set the browser's delegate to nil and release it by assigning nil to the netServiceBrowser property.

- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)browser {
    self.netServiceBrowser.delegate = nil;
    self.netServiceBrowser = nil;
}

If we are notified that the browser wasn't able to search, we log the error and stop the search. In a shipping application, you would probably also want to notify the user of the error. In the interest of not making this chapter any longer than it already is, we opted to just log it here, because this shouldn't be a very common occurrence; if it does happen, the user just won't see any peers, which is hardly catastrophic.

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
             didNotSearch:(NSDictionary *)errorDict {
    NSLog(@"Error browsing for service: %@", [errorDict
        objectForKey:NSNetServicesErrorCode]);
    [self.netServiceBrowser stop];
}

When the browser finds a service, it will call the next method. When that happens, we first check to make sure the service that was found wasn't the one that our parent view controller published. If it wasn't, then we add it to the array. If there are no more services coming, we reload the table so the user will see the new services in the view.

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
           didFindService:(NSNetService *)aNetService
               moreComing:(BOOL)moreComing {
    TicTacToeViewController *parent =
        (TicTacToeViewController *)self.parentViewController;
    if (![[parent.netService name] isEqualToString:[aNetService name]]){
        [discoveredServices addObject:aNetService];
        NSSortDescriptor *sd = [[NSSortDescriptor alloc]
            initWithKey:@"name" ascending:YES];
        [discoveredServices sortUsingDescriptors:[NSArray arrayWithObject:sd]];
        [sd release];
    }

    if(!moreComing)
        [self.tableView reloadData];
}

When we are notified that a service has become unavailable, we remove it from the array. Again, if there are no more services coming, we reload the table so the user sees the change.

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
         didRemoveService:(NSNetService *)aNetService
               moreComing:(BOOL)moreComing {
    [discoveredServices removeObject:aNetService];

    if(!moreComing)
        [self.tableView reloadData];
}

@end

Okay, save OnlinePeerBrowser.m.

We have just one last step to get online play working in our game, but it's a somewhat complicated step. We need to update TicTacToeViewController to use these new objects we've created when the user chooses online play.

Updating TicTacToeViewController to Support Online Play

Single-click TicTacToeViewController.h so we can make the changes necessary to support online play. Add the bold code shown here to your existing file.

#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>
#import "OnlineSession.h"
#import "OnlineListener.h"

#define kTicTacToeSessionID     @"com.apress.TicTacToe.session"
#define kTicTacToeArchiveKey    @"com.apress.TicTacToe"
#define kBonjourType            @"_tictactoe._tcp."

typedef enum GameStates {
    kGameStateBeginning,
    kGameStateRollingDice,
    kGameStateMyTurn,
    kGameStateOpponentTurn,
    kGameStateInterrupted,
    kGameStateDone
} GameState;

typedef enum BoardSpaces {
    kUpperLeft = 1000, kUpperMiddle, kUpperRight,
    kMiddleLeft, kMiddleMiddle, kMiddleRight,
    kLowerLeft, kLowerMiddle, kLowerRight
} BoardSpace;

typedef enum PlayerPieces {
    kPlayerPieceUndecided,
    kPlayerPieceO,
    kPlayerPieceX
} PlayerPiece;
@class TicTacToePacket;

@interface TicTacToeViewController : UIViewController
    <GKPeerPickerControllerDelegate, GKSessionDelegate, UIAlertViewDelegate
    , OnlineSessionDelegate, OnlineListenerDelegate>
 {
    UIButton    *newGameButton;
    UILabel     *feedbackLabel;

    GKSession   *session;
    NSString    *peerID;

    GameState   state;

    NSInteger   myDieRoll;
    NSInteger   opponentDieRoll;

    PlayerPiece piece;
    UIImage     *xPieceImage;
    UIImage     *oPieceImage;

    BOOL        dieRollReceived;
    BOOL        dieRollAcknowledged;

    // Online Play
    NSNetService     *netService;
    OnlineSession    *onlineSession;
    OnlineListener   *onlineSessionListener;

}
@property(nonatomic, retain) IBOutlet UIButton *newGameButton;
@property(nonatomic, retain) IBOutlet UILabel *feedbackLabel;

@property(nonatomic, retain) GKSession        *session;
@property(nonatomic, copy) NSString           *peerID;

@property GameState state;

@property(nonatomic, retain) UIImage *xPieceImage;
@property(nonatomic, retain) UIImage *oPieceImage;

@property (nonatomic, retain) NSNetService       *netService;
@property (nonatomic, retain) OnlineSession      *onlineSession;
@property (nonatomic, retain) OnlineListener     *onlineSessionListener;

- (IBAction)newGameButtonPressed;
- (IBAction)gameSpacePressed:(id)sender;
- (void)resetBoard;
- (void)startNewGame;
- (void)resetDieState;
- (void)sendPacket:(TicTacToePacket *)packet;
- (void)sendDieRoll;
- (void)checkForGameEnd;
- (void)handleReceivedData:(NSData *)data;
- (void)browserCancelled;
@end

Most of the new code is self-explanatory. We declared a constant that is a valid Bonjour type identifier for our game. That identifier is used both when we publish our service and when we search for other services. We also conform our class to the two protocols used by OnlineSession and OnlineListener for their delegates, and we add instance variables to hold an instance of those two classes. The former will be used to communicate with the other peer if we're in online play; the other will be used to listen for connections when we want to start a new game.

We also added two new methods. One is used by the OnlinePeerBrowser class and is called when the user presses the Cancel button. The other requires a little bit of explanation. In our original version of the app, we had a switch statement right in the data receive handler used by GameKit to inform us that there was received data. In order to avoid duplicating the logic that handles those received packets now that we have two potential sources of data, we're going to move the logic to its own method, which will then be called both from GameKit's data receive handler, as well as from the data receive handler for our online session object.

Save TicTacToeViewController.h.

Now, switch over to TicTacToeViewController.m. At the top of the file, add the bold code shown here.

#import "TicTacToeViewController.h"
#import "TicTacToePacket.h"
#import "OnlinePeerBrowser.h"

@interface TicTacToeViewController()
- (void)showErrorAlertWithTitle:(NSString *)title message:(NSString *)message;
@end

@implementation TicTacToeViewController
#pragma mark -
#pragma mark Synthesized Properties
@synthesize newGameButton;
@synthesize feedbackLabel;
@synthesize session;
@synthesize peerID;
@synthesize state;
@synthesize xPieceImage;
@synthesize oPieceImage;

@synthesize netService;
@synthesize onlineSession;
@synthesize onlineSessionListener;

#pragma mark -
#pragma mark Private Methods
- (void)showErrorAlertWithTitle:(NSString *)alertTitle message:(NSString *)message {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:alertTitle
          message:message delegate:self
          cancelButtonTitle:NSLocalizedString(@"Bummer", @"Bummer")
          otherButtonTitles:nil];
    [alert show];
[alert release];
}

#pragma mark -
#pragma mark Game-Specific Methods
- (IBAction)newGameButtonPressed {
...

Because we're going to be creating instances of OnlinePeerBrowser, we need to import its header. We also use an Objective-C extension to declare a new private method for showing error alerts. In the previous version of our app, we showed alerts in only a handful of places. We'll add another handful to support errors encountered during online play, and that means we are now going to have that same task, which requires multiple lines of code, in many different places. By creating a method that displays an alert, we can replace several lines of code in multiple places in our class with a one-line call to this method.

Next, look for the existing method called newGameButtonPressed. We're still going to use the peer picker, but we need to tell it that we're also supporting online play. Add the following code to the newGameButtonPressed method to do that:

- (IBAction)newGameButtonPressed {

    dieRollReceived = NO;
    dieRollAcknowledged = NO;

    newGameButton.hidden = YES;
    GKPeerPickerController*    picker;

    picker = [[GKPeerPickerController alloc] init];
    picker.delegate = self;

    picker.connectionTypesMask = GKPeerPickerConnectionTypeOnline |
                                 GKPeerPickerConnectionTypeNearby;
    [picker show];
}

Warning

Currently, GameKit can be used only if you are offering Nearby play. Offering Online play is optional with the peer picker, but offering Nearby is not optional. If you attempt to set the picker's connectionTypesMask without including GKPeerPickerConnectionTypeNearby, you will get an error at runtime.

Scroll down now to just after the checkForGameEnd method but before the viewDidLoad method. We need to add those two new methods we declared in our header, and this is a good place to do it. Add the two new methods in bold after the existing checkForGameEnd method, like so:

if (state == kGameStateDone)
        [self performSelector:@selector(startNewGame) withObject:nil
        afterDelay:3.0];
}
- (void)handleReceivedData:(NSData *)data {

    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc]
        initForReadingWithData:data];
    TicTacToePacket *packet = [unarchiver decodeObjectForKey:kTicTacToeArchiveKey];

    switch (packet.type) {
        case kPacketTypeDieRoll:
            opponentDieRoll = packet.dieRoll;
            TicTacToePacket *ack = [[TicTacToePacket alloc]
                initAckPacketWithDieRoll:opponentDieRoll];
            [self sendPacket:ack];
            [ack release];
            dieRollReceived = YES;
            break;
        case kPacketTypeAck:
            if (packet.dieRoll != myDieRoll) {
                NSLog(@"Ack packet doesn't match opponentDieRoll (mine: %d, 
Updating TicTacToeViewController to Support Online Play
send: %d", packet.dieRoll, myDieRoll); } dieRollAcknowledged = YES; break; case kPacketTypeMove:{ UIButton *theButton = (UIButton *)[self.view viewWithTag:packet.space]; [theButton setImage:(piece == kPlayerPieceO) ? xPieceImage : oPieceImage forState:UIControlStateNormal]; state = kGameStateMyTurn; feedbackLabel.text = NSLocalizedString(@"Your Turn", @"Your Turn"); [self checkForGameEnd]; } break; case kPacketTypeReset: if (state == kGameStateDone) [self resetDieState]; default: break; } if (dieRollReceived == YES && dieRollAcknowledged == YES) [self startGame]; } - (void)browserCancelled { [self dismissModalViewControllerAnimated:YES]; newGameButton.hidden = NO; feedbackLabel.text = @""; } #pragma mark - #pragma mark Superclass Overrides - (void)viewDidLoad { ...

Now look for the existing method called sendPacket: and delete it. We're going to replace it with a new version that can send over either a GKSession instance or an OnlineSession instance. Because the new sendPacket: is no longer a GameKit-specific method, we should put this version above viewDidLoad. Insert this new version of sendPacket: above viewDidLoad, directly below the two methods you just added.

Warning

It's very important that you delete the old version of the sendPacket: method. You cannot have two copies of the same method in a class. If you fail to delete the old one, you will get a compile error.

- (void) sendPacket:(TicTacToePacket *)packet {

    NSMutableData *data = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]
        initForWritingWithMutableData:data];
    [archiver encodeObject:packet forKey:kTicTacToeArchiveKey];
    [archiver finishEncoding];

    NSError *error = nil;

    if (session) {
        if (![session sendDataToAllPeers:data withDataMode:GKSendDataReliable
            error:&error]) {
            // You will do real error handling
            NSLog(@"Error sending data: %@", [error localizedDescription]);
        }
    }else {
        if (![onlineSession sendData:data error:&error]) {
            // Ditto
            NSLog(@"Error sending data: %@", [error localizedDescription]);
        }
    }
    [archiver release];
    [data release];
}

The only real difference here is that we check to see if session, which is the GameKit session, is nil. If it's not nil, then we send data using it. If it is nil, we know we're in online play, and we must use onlineSession.

Scroll down some more. If you typed your code exactly the way it appeared in the previous chapter, you should have a #pragma line that identifies when the peer picker delegate methods start. It should look something like this:

#pragma mark -
#pragma mark GameKit Peer Picker Delegate Methods

Right after that, we need to add another method. If you don't have that #pragma line, then just search for the first method that takes an instance of GKPeerPickercontroller * as an argument, and add the new method before that method.

The peer picker has a delegate method called peerPickerController:didSelect ConnectionType:. We didn't need to implement this method in the previous chapter because we supported only one connection type. If, as we've now done, we tell the peer picker to offer online play, when users make their choice, it will call this delegate method to inform us about which option was selected. If Online was selected, we need to dismiss the peer picker and take over manually. If Nearby was selected, we don't need to do anything. Add the following new method to handle online play:

- (void)peerPickerController:(GKPeerPickerController *)picker
        didSelectConnectionType:(GKPeerPickerConnectionType)type {
    if (type == GKPeerPickerConnectionTypeOnline) {
        picker.delegate = nil;
        [picker dismiss];
        [picker autorelease];

        OnlineListener *theListener = [[OnlineListener alloc] init];
        self.onlineSessionListener = theListener;
        theListener.delegate = self;
        [theListener release];

        NSError *error;
        if (![onlineSessionListener startListening:&error]) {
            [self showErrorAlertWithTitle:NSLocalizedString(
                @"Error starting listener", @"Error starting listener")
                message:NSLocalizedString(
                @"Unable to start online play", @"Unable to start")];
        }

        NSNetService *theService = [[NSNetService alloc] initWithDomain:@""
            type:kBonjourType name:@"" port:onlineSessionListener.port];
        self.netService = theService;
        [theService release];

        [self.netService scheduleInRunLoop:[NSRunLoop currentRunLoop]
            forMode:NSRunLoopCommonModes];
        [self.netService setDelegate:self];
        [self.netService publish];


        OnlinePeerBrowser *controller = [[OnlinePeerBrowser alloc]
            initWithNibName:@"OnlinePeerBrowser" bundle:nil];
        [self presentModalViewController:controller animated:YES];
        [controller release];
    }
}

After we dismiss the peer picker, we create and start an instance of OnlineListener, which will start listening for connections. We then start advertising our listener using Bonjour. After we do that, we create an instance of OnlinePeerBrowser and present it modally so the user can choose who to play against online, if more than one peer is available.

Down a little further in the file, there should be a method called session:didFailWithError:. We can shorten that method by a few lines, courtesy of our snazzy new error alert method, like so:

- (void)session:(GKSession *)theSession didFailWithError:(NSError *)error {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:
        NSLocalizedString(@"Error Connecting!", @"Error Connecting!")
message:NSLocalizedString(@"Unable to establish the connection.",
        @"Unable to establish the connection.")
        delegate:self
        cancelButtonTitle:NSLocalizedString(@"Bummer", @"Bummer")
        otherButtonTitles:nil];
    [alert show];
    [alert release];
    [self showErrorAlertWithTitle:NSLocalizedString(@"Peer Disconnected!",
        @"Peer Disconnected!") message:NSLocalizedString(
        @"Your opponent has disconnected, or the connection has been lost",
        @"Your opponent has disconnected, or the connection has been lost")];
    theSession.available = NO;
    [theSession disconnectFromAllPeers];
    theSession.delegate = nil;
    [theSession setDataReceiveHandler:nil withContext:nil];
    self.session = nil;
}

There are several other places in the existing code where you can make the same change. We're not going to show you every one, and it won't hurt anything to leave them as they are. But if you want to shorten your code, you can replace any of the existing code that shows an alert with a call to our new alert method.

Next, look for a method called receiveData:fromPeer:inSession:context:. It should be in with your other GameKit methods (you can just use the function pop-up to navigate to it). This method currently contains the logic to handle a packet received from the peer. Since we've moved this logic into handleReceivedData:, we can trim out the logic and replace it with a single call:

- (void)receiveData:(NSData *)data fromPeer:(NSString *)peer
    inSession: (GKSession *)theSession context:(void *)context {
    [self handleReceivedData:data];
}

Now, scroll down to the bottom of the file. We need to add a few delegate methods for NSNetService to handle resolving discovered services. We also need to add the delegate methods for OnlineSession and OnlineListener. We have a bunch of new methods to add. We're going to add a few methods at a time and then explain them, but all of the code from here until the end of the chapter should go at the end of the file, directly above the @end declaration.

First up are the delegate methods that are called when a net service failed to publish or when it is stopped. If the service couldn't publish, we throw up an error alert. When the service stops, we set its delegate to nil and release it.

#pragma mark -
#pragma mark Net Service Delegate Methods (Publishing)
- (void)netService:(NSNetService *)theNetService
     didNotPublish:(NSDictionary *)errorDict {
    NSNumber *errorDomain = [errorDict valueForKey:NSNetServicesErrorDomain];
    NSNumber *errorCode = [errorDict valueForKey:NSNetServicesErrorCode];
    [self showErrorAlertWithTitle:NSLocalizedString(@"Unable to connect",
        @"Unable to connect") message:[NSString
        stringWithFormat:NSLocalizedString(
        @"Unable to publish Bonjour service(%@/%@)",
@"Unable to publish Bonjour service(%@/%@)"), errorDomain, errorCode] ];

    [theNetService stop];
}

- (void)netServiceDidStop:(NSNetService *)netService {
    self.netService.delegate = nil;
    self.netService = nil;
}

Next up is an NSNetService delegate that is called whenever an error is encountered. This is called if an error is encountered either with publishing a service or resolving one. All we do is show an alert.

#pragma mark -
#pragma mark Net Service Delegate Methods (General)
- (void)handleError:(NSNumber *)error withService:(NSNetService *)service {
    [self showErrorAlertWithTitle:NSLocalizedString(@"A network error occurred.",
        @"A network error occurred.") message:[NSString stringWithFormat:
        NSLocalizedString(
        @"An error occurred with service %@.%@.%@, error code = %@",
        @"An error occurred with service %@.%@.%@, error code = %@"),
        [service name], [service type], [service domain], error]];
}

There are two delegate methods related to resolving discovered services: one is called if the service could not be resolved, and one is called if it resolves successfully. If it fails to resolve, we just show an alert and stop trying to resolve the service.

#pragma mark -
#pragma mark Net Service Delegate Methods (Resolving)
- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict {

    NSNumber *errorDomain = [errorDict valueForKey:NSNetServicesErrorDomain];
    NSNumber *errorCode = [errorDict valueForKey:NSNetServicesErrorCode];
    [self showErrorAlertWithTitle:NSLocalizedString(@"Unable to connect",
        @"Unable to connect") message:[NSString stringWithFormat:
        NSLocalizedString(@"Could not start game with remote device (%@/%@)",
        @"Could not start game with remote device (%@/%@)"), errorDomain,
        errorCode] ];
    [sender stop];
}

If it resolved successfully, then we stop listening for new connections and get the stream pair for the connection. If we're not able to get the stream pair, we show an error alert; otherwise, we create an OnlineSession object with the stream pair.

- (void)netServiceDidResolveAddress:(NSNetService *)service {

    [self.onlineSessionListener stopListening];
    self.onlineSessionListener = nil;

    NSInputStream *tempIn = nil;
    NSOutputStream *tempOut = nil;
    if (![service getInputStream:&tempIn outputStream:&tempOut]){
        [self showErrorAlertWithTitle:NSLocalizedString(@"Unable to connect",
            @"Unable to connect") message:NSLocalizedString(
            @"Could not start game with remote device",
@"Could not start game with remote device") ];
        return;
    }

    OnlineSession *theSession = [[OnlineSession alloc]
        initWithInputStream:tempIn outputStream:tempOut];
    theSession.delegate = self;
    self.onlineSession = theSession;
    [theSession release];
}

When an OnlineListener detects a connection, it notifies its delegate. In that case, we also create an OnlineSession object with the stream pair we got from the listener.

#pragma mark -
#pragma mark Online Session Listener Delegate Methods
- (void) acceptedConnectionForListener:(OnlineListener *)theListener
                           inputStream:(NSInputStream *)theInputStream
                          outputStream:(NSOutputStream *)theOutputStream {
    OnlineSession *theSession = [[OnlineSession alloc]
        initWithInputStream:theInputStream outputStream:theOutputStream];
    theSession.delegate = self;
    self.onlineSession = theSession;

    [theSession release];
}

Our OnlineSession object, regardless of whether it was created by resolving a service or by accepting a connection from another machine, will call onlineSessionReadyForUse: when both streams are open. In this method, we check to see if we're still presenting a modal view controller, which would be the case if we received a connection from another machine; if so, we dismiss it. Then we start a new game.

#pragma mark -
#pragma mark Online Session Delegate Methods
- (void)onlineSessionReadyForUse:(OnlineSession *)session {
    if (self.modalViewController)
        [self dismissModalViewControllerAnimated:YES];

    [self startNewGame];
}

When we receive data from the OnlineSession, all we need to do is pass that on to the handleReceivedData: method.

- (void)onlineSession:(OnlineSession *)session receivedData:(NSData *)data {
    [self handleReceivedData:data];
}

If any of the three OnlineSessionDelegate error methods are called, we throw up an error alert and kill the session.

- (void)onlineSession:(OnlineSession *)session
 encounteredReadError:(NSError *)error {
    [self showErrorAlertWithTitle:NSLocalizedString(@"Error reading",
        @"Error Reading") message:NSLocalizedString(@"Could not read sent packet",
        @"Could not read sent packet")];
    self.onlineSession = nil;
}
- (void)onlineSession:(OnlineSession *)session
    encounteredWriteError:(NSError *)error {
    [self showErrorAlertWithTitle:NSLocalizedString(@"Error Writing",
        @"Error Writing") message:NSLocalizedString(@"Could not send packet",
        @"Could not send packet")];
    self.onlineSession = nil;
}
- (void)onlineSessionDisconnected:(OnlineSession *)session {
    [self showErrorAlertWithTitle:NSLocalizedString(@"Peer Disconnected",
        @"Peer Disconnected") message:NSLocalizedString(
        @"Your opponent disconnected or otherwise could not be reached.",
        @"Your opponent disconnected or otherwise could not be reached")];
    self.onlineSession = nil;
}
@end

Time to Play

And with that marathon of changes, we have now implemented online play in our TicTacToe application. You can select Build and Run from the Build menu to try it out. About time, huh?

Online play is significantly more complex to implement than GameKit over Bluetooth, but there's good news. The OnlineSession and OnlineListener objects we just wrote are completely generic. Copy them to a new project, and you can use them unchanged. That means your next application that needs to support network play will be almost as easy to write as it would be to use GameKit.

Before we leave the topic of networking completely, we have one more chapter of network goodness for you. We're going to show you a variety of ways to retrieve information from web servers and RESTful web services.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.255.177