If you’re willing to forgo
GameKit’s Bluetooth and work with WiFi, you can duplicate many of
GameKit’s features on all iPhones including the older first generation
units. Recipe 1 introduces BonjourHelper.
[GameKitHelper sharedInstance].sessionID = @"Typing Together";
[GameKitHelper sharedInstance].dataDelegate = self;
[GameKitHelper assignViewController:self];
Substituting BonjourHelper for GameKitHelper
requires very few programming changes. It uses the same initialization
steps, and the data delegate receives an identical set of callbacks. You
do need to omit the space in the session ID, a step that isn’t needed
in GameKit. GameKit encrypts its session IDs to produce a guaranteed
no-space proper Bonjour identifier. BonjourHelper’s plain-text
approach means spaces are off-limits. Limit your session ID names to
simple alphanumeric text with 14 characters or fewer. Refer to RFC 2782
service types (http://www.dns-sd.org/ServiceTypes.html) for details. The BonjourHelper code transforms the session ID into a standard Bonjour identifier (i.e., _typingtogether._tcp.).
[BonjourHelper sharedInstance].sessionID = @"TypingTogether";
[BonjourHelper sharedInstance].dataDelegate = self;
[BonjourHelper assignViewController:self];
That’s not to say that the functionality and implementation are identical. With BonjourHelper, both units must be on the same network. You lose the pretty GameKit peer connection controller sequence . Instead, BonjourHelper provides a simple alert, as shown in Figure 12-8. Beyond that, BonjourHelper basically provides the same peer-to-peer connectivity and data flow as GameKit.
Registering Bonjour Names and Ports
You should register any Bonjour names you plan to
use for commercial release with the DNS Service Discovery organization.
Registration ensures that your service names and protocols will not
overlap or conflict with any other vendor. A list of currently
registered services is maintained at http://www.dns-sd.org/ServiceTypes.html.
These names must conform to the RFC 2782 standard. Submit your protocol name to [email protected].
Include the up-to-14-character name of the Bonjour service, a longer
descriptive name, the contact information (name and e-mail address) of
the person registering the service, and an information page URL. Specify
the transportation protocol (i.e., _tcp or _udp) and a list of any TXT
record keys used.
It may take some time for the volunteers at the dns-sd.org
site to process and respond to your query. Delays on the order of weeks
are not uncommon. You may need to resubmit, so keep a copy of all your
information.
If you plan to use a fixed port (most Bonjour
implementations randomly pick a port at runtime to use), you’ll want to
submit an application for a registered port number with IANA, the
Internet Assigned Numbers Authority, as well. IANA provides a central
repository for port registrations and will, at some time, be merged with
the dns-sd registry. IANA often takes a year or longer to finish
registering new protocol port numbers.
Duplex Connection
For simplicity, BonjourHelper
works by establishing a duplex connection. Each device provides both a
client and a host. This avoids any issues about trying to get two peers
to negotiate with each other and assume the proper server and client
roles without both of them ending up as client or server at the same
time.
When resolving addresses, the helper ensures that
the unit will not connect to itself. It demands a unique IP address
that doesn’t match the local one. If the incoming address does match, it
just continues looking. The host needs no such checks; outgoing client
connections are limited to foreign addresses.
When the helper has established an outgoing
connection and accepted an incoming one, it stops looking for any
further peers and considers itself fully connected. The helper updates
the Connect/Disconnect button if a view controller has been set.
Reading Data
Recipe 1
cannot use a simple read loop, that is, request data, read it, and
repeat. Reading data is blocking. A read loop prevents an application
from handling its server duties at the same time as its client duties.
Instead, this class uses the nonblocking hasDataAvailable
check before asking for new data. A delayed selector adds a natural
interval into the poll allowing each host time to update and prepare new
data before being barraged by a new request.
Closing Connections
Connections can break in several ways. Users can
quit an application, they can press the Disconnect button in the sample,
or they can move out of range of the connection. BonjourHelper
checks for disconnects exclusively from the server point of view. This
simplifies its implementation, assuming that a lost client equates to a
lost host and avoids the issue of multiple user notifications, i.e.,
“Lost connection to server” and “Lost connection to client” for both
ends of the duplex connection.
Note
For space considerations, this listing of Recipe 1 omits a number of basic IP utilities, including stringFromAddress:, addressFromString:address:, and localIPAddress.
Recipe 1. BonjourHelper Provides GameKit-like Connectivity over WiFi
#define DO_DATA_CALLBACK(X, Y) if (sharedInstance.dataDelegate && \
[sharedInstance.dataDelegate respondsToSelector:@selector(X)]) \
[sharedInstance.dataDelegate performSelector:@selector(X) \
withObject:Y];
#define BARBUTTON(TITLE, SELECTOR) [[[UIBarButtonItem alloc] \
initWithTitle:TITLE style:UIBarButtonItemStylePlain \
target:[BonjourHelper class] action:SELECTOR] autorelease]
@implementation BonjourHelper
@synthesize server;
@synthesize browser;
@synthesize inConnection;
@synthesize outConnection;
@synthesize dataDelegate;
@synthesize viewController;
@synthesize sessionID;
@synthesize isConnected;
@synthesize hud;
static BonjourHelper *sharedInstance = nil;
BOOL inConnected;
BOOL outConnected;
+ (BonjourHelper *) sharedInstance
{
if(!sharedInstance) sharedInstance = [[self alloc] init];
return sharedInstance;
}
#pragma mark Class utilities
+ (void) assignViewController: (UIViewController *) aViewController
{
// By assigning the optional view controller, this class
// takes charge of the connect/disconnect button
sharedInstance.viewController = aViewController;
if (sharedInstance.viewController)
sharedInstance.viewController.navigationItem.rightBarButtonItem
= BARBUTTON(@"Connect", @selector(connect));
}
#pragma mark Handshaking
- (void) updateStatus
{
// Must be connected to continue
if (!(self.inConnection && self.outConnection) ||
!(inConnected && outConnected))
{
self.isConnected = NO;
return;
}
// Send callback, dismiss HUD, update bar button
self.isConnected = YES;
DO_DATA_CALLBACK(connectionEstablished, nil);
[self.hud dismissWithClickedButtonIndex:1 animated:YES];
if (self.viewController)
self.viewController.navigationItem.rightBarButtonItem =
BARBUTTON(@"Disconnect", @selector(disconnect));
}
// Upon resolving address, create a connection to that address
// and request data
- (void)netServiceDidResolveAddress:(NSNetService *)netService
{
NSArray* addresses = [netService addresses];
if (addresses && addresses.count)
{
for (int i = 0; i < addresses.count; i++)
{
// The IP utility implementations can be found in
// They are omitted here for space considerations.
struct sockaddr* address =
(struct sockaddr*)[[addresses objectAtIndex:i] bytes];
NSString *addressString =
[BonjourHelper stringFromAddress:address];
if (!addressString) continue;
if ([addressString hasPrefix:
[BonjourHelper localIPAddress]])
{
printf("Will not resolve with self. \
Continuing to browse.\n");
continue;
}
printf("Found a matching external service\n");
printf("My address: %s\n",
[[BonjourHelper localIPAddress] UTF8String]);
printf("Remote address: %s\n", [addressString UTF8String]);
// Stop browsing for services
[self.browser stop];
[netService release];
// Create an outbound connection to this new service
self.outConnection = [[[TCPConnection alloc]
initWithRemoteAddress:address] autorelease];
[self.outConnection setDelegate:self];
[self performSelector:@selector(checkForData)];
[self updateStatus];
return;
}
}
[netService stop];
}
- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
didFindService:(NSNetService *)netService
moreComing:(BOOL)moreServicesComing
{
// start to resolve the service that was found
[[netService retain] setDelegate:self];
[netService resolveWithTimeout:0.0f];
}
+ (void) startBrowsingForServices
{
// look for matching Bonjour services. The double-retain was
// added for security. You can almost certainly discard it.
sharedInstance.browser =
[[[NSNetServiceBrowser alloc] init] retain];
[sharedInstance.browser setDelegate:sharedInstance];
NSString *type = [TCPConnection
bonjourTypeFromIdentifier:sharedInstance.sessionID];
[sharedInstance.browser searchForServicesOfType:type
inDomain:@"local"];
}
+ (void) publish
{
// Publish service to peers
sharedInstance.server =
[[[TCPServer alloc] initWithPort:0] autorelease];
[sharedInstance.server setDelegate:sharedInstance];
[sharedInstance.server startUsingRunLoop:
[NSRunLoop currentRunLoop]];
[sharedInstance.server enableBonjourWithDomain:@"local"
applicationProtocol:sharedInstance.sessionID
name:[self localHostname]];
}
+ (void) initConnections
{
// Return to base unconnected state
[sharedInstance.browser stop];
[sharedInstance.server stop];
sharedInstance.inConnection = nil;
sharedInstance.outConnection = nil;
sharedInstance.isConnected = NO;
inConnected = NO;
outConnected = NO;
}
- (void)alertView:(UIAlertView *)alertView
clickedButtonAtIndex:(NSInteger)buttonIndex
{
// Handle user request to cancel connecting
if (buttonIndex) return;
[BonjourHelper disconnect];
}
+ (void) connect
{
if (sharedInstance.viewController)
sharedInstance.viewController.navigationItem.rightBarButtonItem
= nil;
if (!sharedInstance.sessionID)
sharedInstance.sessionID = @"Sample Session";
// Create activity view with cancel button
sharedInstance.hud = [[[UIAlertView alloc]
initWithTitle:
@"Searching for connection peer on your local network"
message:@"\n\n" delegate:sharedInstance
cancelButtonTitle:@"Cancel" otherButtonTitles:nil]
autorelease];
[sharedInstance.hud show];
// Add the progress wheel
UIActivityIndicatorView *aiv = [[[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:
UIActivityIndicatorViewStyleWhiteLarge] autorelease];
[aiv startAnimating];
aiv.center = CGPointMake(
sharedInstance.hud.bounds.size.width / 2.0f,
sharedInstance.hud.bounds.size.height/2.0f);
[sharedInstance.hud addSubview:aiv];
// Prepare for duplex connection
[self initConnections];
[self startBrowsingForServices];
[self publish];
}
+ (void) disconnect
{
// disable current connections
[sharedInstance.inConnection invalidate];
[sharedInstance.outConnection invalidate];
[self initConnections];
// stop server
[sharedInstance.server stop];
[sharedInstance updateStatus];
// reset
[sharedInstance.hud dismissWithClickedButtonIndex:1 animated:YES];
if (sharedInstance.viewController)
sharedInstance.viewController.navigationItem.rightBarButtonItem
= BARBUTTON(@"Connect", @selector(connect));
}
#pragma mark Data Handling
- (void) checkForData
{
// Perform a blocking receive only when data is available
if (!self.outConnection) return;
if ([self.outConnection hasDataAvailable])
[self.outConnection receiveData];
[self performSelector:@selector(checkForData)
withObject:self afterDelay:0.1f];
}
+ (void) sendData: (NSData *) data
{
if (!sharedInstance.outConnection) return;
BOOL success = [sharedInstance.outConnection sendData:data];
if (success) {
DO_DATA_CALLBACK(sentData:, nil); }
else {
DO_DATA_CALLBACK(sentData:, @"Data could not be sent.");}
}
- (void) connection:(TCPConnection*)connection
didReceiveData:(NSData*)data;
{
// Redirect data callback
DO_DATA_CALLBACK(receivedData:, data);
}
#pragma mark Connection Handlers
- (BOOL) server:(TCPServer*)server
shouldAcceptConnectionFromAddress:(const struct sockaddr*)address
{
// Accept connections only while not connected
return !self.isConnected;
}
- (void) connectionDidFailOpening:(TCPConnection*)connection
{
// Handled a fail open
if (!connection) return;
NSString *addressString = [BonjourHelper
stringFromAddress:connection.remoteSocketAddress];
[BonjourHelper disconnect];
if (addressString)
[ModalAlert say:@"Error while opening %@ connection (from %@).\
Wait a few seconds or relaunch before trying to connect\n
again.", (connection == self.inConnection) ? @"incoming" :
@"outgoing", addressString];
else
printf("Failed to open connection from unknown address\n");
}
- (void) server:(TCPServer*)server
didCloseConnection:(TCPServerConnection*)connection
{
// Handle a newly closed connection
if (!connection) return;
NSString *addressString = [BonjourHelper
stringFromAddress:connection.remoteSocketAddress];
if (!addressString) return;
BOOL wasConnected = self.isConnected;
[BonjourHelper disconnect];
printf("Lost connection from %s\n", [addressString UTF8String]);
if (wasConnected)
[ModalAlert say:@"Disconnected from peer (%@). You are no \
longer connected to another device.", addressString];
else
[ModalAlert say:@"Peer was lost before full connection could \
be established."];
}
- (void) server:(TCPServer*)server
didOpenConnection:(TCPServerConnection*)connection
{
// Set the connection but wait for it to fully open
self.inConnection = connection;
[self updateStatus];
[connection setDelegate:self];
}
- (void) connectionDidOpen: (TCPConnection *) connection
{
// Fully opened connection
printf("Connection did open: %s\n", (connection ==
self.inConnection) ? "incoming" : "outgoing");
if (connection == self.inConnection) inConnected = YES;
if (connection == self.outConnection) outConnected = YES;
[self updateStatus];
}
- (void) connectionDidClose: (TCPConnection *)connection
{
// Closed connection
printf("Connection did close: %s\n", (connection ==
self.inConnection) ? "incoming" : "outgoing");
if (connection == self.inConnection) inConnected = NO;
if (connection == self.outConnection) outConnected = NO;
[self updateStatus];
}
@end
|