How To Make a Multiplayer iPhone Game Hosted on Your Own Server Part 1

Ray Wenderlich
Create a simple multiplayer game hosted on your own server with Game Center matchmaking!

Create a simple multiplayer game hosted on your own server with Game Center matchmaking!

A while back, I wrote a tutorial on How To Make A Simple Multiplayer Game with Game Center.

That tutorial showed you how to use Game Center to create a peer-to-peer match. This means that packets of game data were sent directly between the connected devies, and there was no central game server.

However, sometimes you might want to create your own game server, and have your game clients connect to that rather than talking to each other directly. This can have the following advantages:

  • The maximum number of players in a peer-to-peer games is 4, but a hosted game can have up to 16.
  • With a central server, you can add neat features such as centrally storing data, dynamically modifying game state and performing calculations, metrics, and more.
  • With a central server, you have a built-in authority that can make important decisions and keep track of the official game state.

Even though there’s all these great advantages, there’s one big disadvantage – there’s not a heck of a lot of documentation on how to actually do this!

But never fear – a while back this won the weekly tutorial vote in the sidebar, so all you have to do is follow along! :]

Before going through this tutorial, you should do the following tutorials first (or have equivalent knowledge):

It’s also very helpful to have at least a basic familiarity with Python for this tutorial, since we’ll use it to construct the server. If you are completely new to Python, check out the official Python tutorial.

I realize this is a lot of prerequisites (plus this tutorial is super-long!), but making your own game server is not the easiest task in the world! :]

It is however a lot of fun to make your own server, and one of the best learning experiences you can have. So I encourage you all to grab some caffeine and have fun learning how to make your own game server with me!

Getting Started

We’re going to start out with the same starter code that we used in the Game Center peer-to-peer tutorial.

This way, you can focus on adding the code related to network programming rather than having to worry about creating a game too.

Plus, since it’s the same game we used in the Game Center peer-to-peer tutorial, you’ll get to see where we have to diverge to make it a hosted match instead.

So download the starter code and run the project, and you should see something like this:

Simple Cocos2D Racing Game Starter Code

Take a look through the project to refresh your memory how it works. It’s all pretty simple stuff – but just needs some networking code so the kid on the tricycle can have a fighting chance!

Authenticating with Game Center

Even though we’re going to host the game on our own server, we’re still going to authenticate the user with game center.

This keeps us from having to create an account system and authentication system for users on our own server. It’s also nice for users because they don’t have to sign into an account for our app in particular, plus they can easily invite people from their Game Center friends list.

So let’s start by getting the authentication code in. This should be review from the Game Center peer-to-peer tutorial.

First of all, open up Resources\Info.plist and set the Bundle identifier that you made previously for your app. For example, mine was “com.razeware.catrace”:

Setting the Bundle Identifier in your info.plist

Second, add the GameKit framework to your project. To do this, select the CatRace project in the upper left of Groups & Files, select the Build Phases tab, expand the “Link Binary with Libraries” section, and click the “+” button.

Select GameKit.framework, and click Add. Change the type from Required to Optional, and your screen should look like the following:

Adding Game Kit Framework to Xcode 4 Project

Next, let’s add the code to authenticate the local user with Game Center. This will be mostly review, with a bit of new stuff added in too.

Go to File\New\New File, choose iOS\Cocoa Touch\Objective-C class, and click Next. Enter NSObject for Subclass of, click Next, name the new class NetworkController.m, and click Save.

Open up NetworkController.h and replace it with the following:

#import <Foundation/Foundation.h>
#import <GameKit/GameKit.h>
 
typedef enum {
    NetworkStateNotAvailable,
    NetworkStatePendingAuthentication,
    NetworkStateAuthenticated,    
} NetworkState;
 
@protocol NetworkControllerDelegate
- (void)stateChanged:(NetworkState)state;
@end
 
@interface NetworkController : NSObject {
    BOOL _gameCenterAvailable;
    BOOL _userAuthenticated;
    id <NetworkControllerDelegate> _delegate;
    NetworkState _state;
}
 
@property (assign, readonly) BOOL gameCenterAvailable;
@property (assign, readonly) BOOL userAuthenticated;
@property (assign) id <NetworkControllerDelegate> delegate;
@property (assign, readonly) NetworkState state;
 
+ (NetworkController *)sharedInstance;
- (void)authenticateLocalUser;
 
@end

This class is similar to the GCHelper class from the Game Center peer-to-peer tutorial, but we renamed it to NetworkController because now it’s about more than just Game Center – it will manage communictation with our custom server as well.

Note that we have started creating an enum called NetworkState. This will keep track of the various states that we’ll pass through as we get set up for a match. Right now, we just have three states – Game Center not available, pending Game Center authentication, and authenticated with Game Center.

We also declare a protocol and a delegate so that we can notify a listener (our HelloWorldLayer in this case) when the network state changes. We’ll update the label at the top of the screen so we can easily tell what’s going on.

Next, switch to NetworkController.m and replace it with the following:

#import "NetworkController.h"
 
@implementation NetworkController
@synthesize gameCenterAvailable = _gameCenterAvailable;
@synthesize userAuthenticated = _userAuthenticated;
@synthesize delegate = _delegate;
@synthesize state = _state;
 
#pragma mark - Helpers
 
static NetworkController *sharedController = nil;
+ (NetworkController *) sharedInstance {
    if (!sharedController) {
        sharedController = [[NetworkController alloc] init];
    }
    return sharedController;
}
 
- (BOOL)isGameCenterAvailable {
    // check for presence of GKLocalPlayer API
    Class gcClass = (NSClassFromString(@"GKLocalPlayer"));
 
    // check if the device is running iOS 4.1 or later
    NSString *reqSysVer = @"4.1";
    NSString *currSysVer = [[UIDevice currentDevice] systemVersion];
    BOOL osVersionSupported = ([currSysVer compare:reqSysVer 
                                           options:NSNumericSearch] != NSOrderedAscending);
 
    return (gcClass && osVersionSupported);
}
 
- (void)setState:(NetworkState)state {
    _state = state;
    if (_delegate) {
        [_delegate stateChanged:_state];
    }
}
 
#pragma mark - Init
 
- (id)init {
    if ((self = [super init])) {
        [self setState:_state];
        _gameCenterAvailable = [self isGameCenterAvailable];
        if (_gameCenterAvailable) {
            NSNotificationCenter *nc = 
            [NSNotificationCenter defaultCenter];
            [nc addObserver:self 
                   selector:@selector(authenticationChanged) 
                       name:GKPlayerAuthenticationDidChangeNotificationName 
                     object:nil];
        }
    }
    return self;
}
 
#pragma mark - Authentication
 
- (void)authenticationChanged {    
 
    if ([GKLocalPlayer localPlayer].isAuthenticated && !_userAuthenticated) {
        NSLog(@"Authentication changed: player authenticated.");
        [self setState:NetworkStateAuthenticated];
        _userAuthenticated = TRUE; 
    } else if (![GKLocalPlayer localPlayer].isAuthenticated && _userAuthenticated) {
        NSLog(@"Authentication changed: player not authenticated");
        _userAuthenticated = FALSE;
        [self setState:NetworkStateNotAvailable];
    }
 
}
 
- (void)authenticateLocalUser { 
 
    if (!_gameCenterAvailable) return;
 
    NSLog(@"Authenticating local user...");
    if ([GKLocalPlayer localPlayer].authenticated == NO) {     
        [self setState:NetworkStatePendingAuthentication];
        [[GKLocalPlayer localPlayer] authenticateWithCompletionHandler:nil];        
    } else {
        NSLog(@"Already authenticated!");
    }
}
 
@end

Read over this carefully and make sure you understand everything – it’s mostly review from last time.

The only new bit is the new setState method, that notifies the delegate of the network state change when it occurs (so it can update its label).

OK, now let’s make use of this! Open up AppDelegate.m and make the following changes:

// Add to top of file
#import "NetworkController.h"
 
// Add at top of applicationDidFinishLaunching
[[NetworkController sharedInstance] authenticateLocalUser];

This starts the authentication process as soon as possible when the app launches.

Next switch to HelloWorldLayer.h and make the following changes:

// Add to top of file
#import "NetworkController.h"
 
// Modify @interface declaration as to add NetworkControllerDelegate protocol
@interface HelloWorldLayer : CCLayer <NetworkControllerDelegate>

Finally, switch to HelloWorldLayer.m and make the following changes:

// Add at bottom of init
[NetworkController sharedInstance].delegate = self;
[self stateChanged:[NetworkController sharedInstance].state];
 
// Add new method right above dealloc
- (void)stateChanged:(NetworkState)state {
    switch(state) {
        case NetworkStateNotAvailable:
            debugLabel.string = @"Not Available";
            break;
        case NetworkStatePendingAuthentication:
            debugLabel.string = @"Pending Authentication";
            break;
        case NetworkStateAuthenticated:
            debugLabel.string = @"Authenticated";
            break;        
    }
}

This is just the code that updates the debug label based on the network state.

Compile and run, and you should see yourself get authenticated with Game Center!

User Authenticated with Game Center

If this doesn’t work right for you, it might be that an old version of the app is still cached somewhere. Go to your iOS simulator menu and choose “Reset Content and Settings” and quit the simulator, and hopefully it should work OK for you.

Creating A Simple Server

The next step is to add some code to have our game connect to your own server.

But before you can do that, you’ll need a simple server to connect to, of course!

We’re going to use Python and the Twisted framework to create the server, just like Cesare showed you how to do in the socket based app tutorial. It’s one of the easiest ways to make a server – and let’s face it, Python rules!

You can run your server on a central server you own (I use Linode), or you can run it on your local machine for testing.

Wherever you choose to run the server, make sure you have Python and Twisted installed as discussed in Cesare’s tutorial.

Then create a new file named CatRaceServer.py, and add the following code:

from twisted.internet.protocol import Factory, Protocol
from twisted.internet import reactor
 
class CatRaceFactory(Factory):
    def __init__(self):
        self.protocol = CatRaceProtocol
 
class CatRaceProtocol(Protocol):
 
    def log(self, message):
        print message  
 
    def connectionMade(self):
        self.log("Connection made")
 
    def connectionLost(self, reason):
        self.log("Connection lost: %s" % str(reason))
 
factory = CatRaceFactory()
reactor.listenTCP(1955, factory)
print "Cat Race server started"
reactor.run()

This is a bare-bones server that listens for connections on port 1955 and just prints out when someone connects or disconnects. Bonus points if you know why I chose 1955! :]

Then run your server with the following command:

python CatRaceServer.py 

You should see “Cat Race server started” and it should just sit there. You can verify you can actually connect by opening a Terminal on your development machine and issuing the following command (but substitute your own domain name or IP address where your server is running):

~ rwenderlich$ telnet www.razeware.com 1955
Trying 74.207.227.16...
Connected to razeware.com.
Escape character is '^]'.
^]
telnet> quit
Connection closed.

On the other side, you should see output like this:

Cat Race server started
Connection made
Connection lost: [SNIP] Connection was closed cleanly.

If you have trouble connecting, make sure you don’t have a firewall blocking that port – and if you’re running locally, you might need to configure your router to allow you to access the listening port. However, this sort of configuration is outside of the scope of this tutorial!

Once you have it working, read on – time to add the code to have our app connect to the server!

Connecting To Your Server

To connect to the server, we’re going to create a socket connection to that port just like we did in Cesare’s tutorial.

This should be review, so let’s dive right in! Start by making the following changes to NetworkController.h:

// Add to the end of the NetworkState enum
NetworkStateConnectingToServer,
NetworkStateConnected,
 
// Modify @interface declaration to add NSStreamDelegate
@interface NetworkController : NSObject <NSStreamDelegate> {
 
// Inside @interface
NSInputStream *_inputStream;
NSOutputStream *_outputStream;
BOOL _inputOpened;
BOOL _outputOpened;
 
// After @interface
@property (retain) NSInputStream *inputStream;
@property (retain) NSOutputStream *outputStream;
@property (assign) BOOL inputOpened;
@property (assign) BOOL outputOpened;

This creates two new network states. NetworkStateConnectingToServer is when we’re not connected to the server but are trying, and NetworkStateConnected is when we’re successfully connected to the server.

We also create two instance variables/properties for the input and output stream we’ll use to read/write to the socket, and mark the NetworkController as implementing the NSStreamDelegate protocol, so we can receive callbacks for the streams.

Switch to NetworkController.m and make the following changes:

// Add to top of file
@synthesize inputStream = _inputStream;
@synthesize outputStream = _outputStream;
@synthesize inputOpened = _inputOpened;
@synthesize outputOpened = _outputOpened;
 
// Add after init method
#pragma mark - Server communication
 
- (void)connect {    
 
    [self setState:NetworkStateConnectingToServer];
 
    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;
    CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)@"www.razeware.com", 1955, &readStream, &writeStream);
    _inputStream = (NSInputStream *)readStream;
    _outputStream = (NSOutputStream *)writeStream;
    [_inputStream setDelegate:self];
    [_outputStream setDelegate:self];
    [_inputStream setProperty:(id)kCFBooleanTrue forKey:(NSString *)kCFStreamPropertyShouldCloseNativeSocket];
    [_outputStream setProperty:(id)kCFBooleanTrue forKey:(NSString *)kCFStreamPropertyShouldCloseNativeSocket];
    [_inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [_outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [_inputStream open];
    [_outputStream open];
}
 
- (void)disconnect {
 
    [self setState:NetworkStateConnectingToServer];
 
    if (_inputStream != nil) {
        self.inputStream.delegate = nil;
        [_inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [_inputStream close];
        self.inputStream = nil;
    } 
    if (_outputStream != nil) {
        self.outputStream.delegate = nil;
        [self.outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [self.outputStream close];
        self.outputStream = nil;
    }
}
 
- (void)reconnect {
    [self disconnect];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
        [self connect];
    });
}

The connect method is similar to the initNetworkCommunication method in Cesare’s tutorial, except it updates the state and sets a fancy variable called kCFStreamPropertyShouldCloseNativeSocket.

If you set kCFStreamPropertyShouldCloseNativeSocket to true like we do here, it means that when you close the stream it closes the socket too, which is exactly what we want to happen!

Also, don’t forget to replace the DNS name/IP and port in connect with the settings for your own server!

The disconnect method is a handy method that shuts down the input and output streams and puts us in a nice clean state.

Finally, the reconnect method disconnects, and then schedules the connect to be called in 5 seconds. The call to dispatch_after is a fancy trick that uses GCD to schedule a block of code to be run 5 seconds in the future. For more information on GCD, check out the Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial.

Once you’re ready, add the following methods right after reconnect:

- (void)inputStreamHandleEvent:(NSStreamEvent)eventCode {
 
    switch (eventCode) {
        case NSStreamEventOpenCompleted: {
            NSLog(@"Opened input stream");
            _inputOpened = YES;
            if (_inputOpened && _outputOpened && _state == NetworkStateConnectingToServer) {
                [self setState:NetworkStateConnected];
                // TODO: Send message to server
            }
        } 
        case NSStreamEventHasBytesAvailable: {                       
            if ([_inputStream hasBytesAvailable]) {                
                NSLog(@"Input stream has bytes...");
                // TODO: Read bytes                
            }
        } break;
        case NSStreamEventHasSpaceAvailable: {
            assert(NO); // should never happen for the input stream
        } break;
        case NSStreamEventErrorOccurred: {
            NSLog(@"Stream open error, reconnecting");
            [self reconnect];
        } break;
        case NSStreamEventEndEncountered: {
            // ignore
        } break;
        default: {
            assert(NO);
        } break;
    }    
}
 
- (void)outputStreamHandleEvent:(NSStreamEvent)eventCode {            
    switch (eventCode) {
        case NSStreamEventOpenCompleted: {
            NSLog(@"Opened output stream");
            _outputOpened = YES;
            if (_inputOpened && _outputOpened && _state == NetworkStateConnectingToServer) {
                [self setState:NetworkStateConnected];
                // TODO: Send message to server
            }
        } break;
        case NSStreamEventHasBytesAvailable: {
            assert(NO);     // should never happen for the output stream
        } break;
        case NSStreamEventHasSpaceAvailable: {
            NSLog(@"Ok to send");
            // TODO: Write bytes
        } break;
        case NSStreamEventErrorOccurred: {
            NSLog(@"Stream open error, reconnecting");
            [self reconnect];
        } break;
        case NSStreamEventEndEncountered: {
            // ignore
        } break;
        default: {
            assert(NO);
        } break;
    }
}
 
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode { 
    dispatch_async(dispatch_get_main_queue(), ^(void) {
        if (aStream == _inputStream) {
            [self inputStreamHandleEvent:eventCode];
        } else if (aStream == _outputStream) {
            [self outputStreamHandleEvent:eventCode];
        }
    });
}

Starting at the bottom, the stream:handleEvent method gets called whenever an event happens on the input stream of the output stream, because we registered this class as the delegate of the streams.

One big gotcha here is that this method isn’t guaranteed to be called in the main thread. Because we’re going to be accessing some shared state in this method (instance variables on this class), we use GCD to run the handling code on the main thread.

Then we have the routines to handle the input and output stream events. Right now, they don’t really do that much except reconnect on an error, and handle the NSStreamEventOpenCompleted event.

The NSStreamEventOpenCompleted event is called when the sockets finish opening. So here we set a flag when each side has finished opening, and when both sides are connected we switch the state to connected.

Soon we’ll add some code to send an initial message over to the server when the client first connects, but now let’s try all this new code out.

Make these last changes to NetworkController.m:

// Add inside authenticationChanged, right after _userAuthenticated = TRUE
[self connect]; 
 
// Add inside authenticationChanged, right after _userAuthenticated = FALSE
[self disconnect];

This sets the code up to connect to the server after the user is authenticated with Game Center.

And finally, switch to HelloWorldLayer.m and add the following debug lines for the two new network states to the end of stateChanged:

case NetworkStateConnectingToServer:
    debugLabel.string = @"Connecting to Server";
    break;
case NetworkStateConnected:
    debugLabel.string = @"Connected";
    break;

Compile and run, and you should see your app successfully connect to your own server!

Game connected to your own server

Writing Socket Data

When you look at sample code online, people often write to the output stream willy-nilly assuming it will always work.

However, it turns out sometimes you can’t write to an output stream, because it is full! To be safe, you should only write to it when it tells you there are bytes available, with the NSStreamEventHasSpaceAvailable message.

So we’ll keep a buffer of data to write, and only send it to the output stream when it tells us it has space available.

But how do we know how much to write? Well, you just make a reasonable guess (we’ll try 1024) and give it your best shot. The output stream will return to you how much actually got written (which could be less than 1024), so you update your buffer accordingly.

So try this out by making the following changes to NetworkController.h:

// Inside @interface
NSMutableData *_outputBuffer;
BOOL _okToWrite;
 
// After @interface
@property (retain) NSMutableData *outputBuffer;
@property (assign) BOOL okToWrite;

This creates the data buffer for the output as mentioned above, and also a variable that keeps track of whether it’s currently OK to write to the output buffer.

Then make the following changes to NetworkController.m:

// Add in synthesize section
@synthesize outputBuffer = _outputBuffer;
@synthesize okToWrite = _okToWrite;
 
// Add at top of connect
self.outputBuffer = [NSMutableData data];
 
// Add in disconnect, at end of _outputStream != nil case:
self.outputBuffer = nil;
 
// Add above outputStreamHandleEvent
- (BOOL)writeChunk {
    int amtToWrite = MIN(_outputBuffer.length, 1024);
    if (amtToWrite == 0) return FALSE;
 
    NSLog(@"Amt to write: %d/%d", amtToWrite, _outputBuffer.length);
 
    int amtWritten = [self.outputStream write:_outputBuffer.bytes maxLength:amtToWrite];
    if (amtWritten < 0) {
        [self reconnect];
    }
    int amtRemaining = _outputBuffer.length - amtWritten;
    if (amtRemaining == 0) {
        self.outputBuffer = [NSMutableData data];
    } else {
        NSLog(@"Creating output buffer of length %d", amtRemaining);
        self.outputBuffer = [NSMutableData dataWithBytes:_outputBuffer.bytes+amtWritten length:amtRemaining];
    }
    NSLog(@"Wrote %d bytes, %d remaining.", amtWritten, amtRemaining);
    _okToWrite = FALSE;
    return TRUE;
}
 
// In outputStreamHandleVent, in NSStreamEventHasSpaceAvailable case
BOOL wroteChunk = [self writeChunk];
if (!wroteChunk) {
    _okToWrite = TRUE;
}

In writeChunk, we choose to write either the amount of data in the buffer or 1024, whichever is smaller, then try to write it.

The output stream will tell us how much actually got written – we use this to shorten our data buffer.

If we wrote something, set set _okToWrite to FALSE, because every time you write something, NSStreamEventHasSpaceAvailable will be called when there’s more space available. But if we didn’t write something, NSStreamEventHasSpaceAvailable won’t be called again until you write something, so we set _okToWrite to TRUE so we know it’s safe to write to the output stream right away next time someone adds to the output buffer.

OK – now we have the code that will write whatever’s in the output buffer out on the wire. We just need the code to put something into the output buffer – by marshalling the message parameters!

Marshalling Data

Marshalling is a fancy way of saying “take some data you want to send to the other side, and convert it into a sequence of bytes.”

You might think “why not use NSCoding? That’s a pretty handy way of converting objects to bytes.”

That would probably be a bad idea. When sending data across the network, you want the data to be as small as possible, because the smaller the data is the faster it will be transmitted and the less of the user’s bandwidth you will consume. NSCoding serialization is pretty verbose, especially when compared to handcrafted messages.

To make things easier, let’s write a helper class to handle all of the data marshalling. Go to File\New\New File, choose iOS\Cocoa Touch\Objective-C class, and click Next. Enter NSObject for Subclass of, click Next, name the new class MessageWriter.m, and click Save.

Replace MessageWriter.h with the following:

#import <Foundation/Foundation.h>
 
@interface MessageWriter : NSObject {
    NSMutableData * _data;
}
 
@property (retain, readonly) NSMutableData * data;
 
- (void)writeByte:(unsigned char)value;
- (void)writeInt:(int)value;
- (void)writeString:(NSString *)value;
 
@end

This is an object that creates an NSMutableData storing the message to send across the wire. It provides some methods you can use to write bytes, integers, and strings to the data buffer to be sent across.

Switch to MessageWriter.m and replace it with the following:

#import "MessageWriter.h"
 
@implementation MessageWriter
@synthesize data = _data;
 
- (id)init {
    if ((self = [super init])) {
        _data = [[NSMutableData alloc] init];
    }
    return self;
}
 
- (void)writeBytes:(void *)bytes length:(int)length {
    [_data appendBytes:bytes length:length];
}
 
- (void)writeByte:(unsigned char)value {
    [self writeBytes:&value length:sizeof(value)];
}
 
- (void)writeInt:(int)intValue {
    int value = htonl(intValue);
    [self writeBytes:&value length:sizeof(value)];
}
 
- (void)writeString:(NSString *)value {
    const char * utf8Value = [value UTF8String];
    int length = strlen(utf8Value) + 1; // for null terminator
    [self writeInt:length];
    [self writeBytes:(void *)utf8Value length:length];
}
 
- (void)dealloc {
    [_data release];
    [super dealloc];
}
 
@end

writeByte is pretty straightforward – it just appends the byte directly ot the NSMutableData.

writeInt is similar, except it has to deal with endianness.

If you’re not familiar with endianness, it turns out that different types of machines store bytes in different orders. Some machines store an integer with the least significant byte at the highest address, and some store the least significant byte at the lowest address.

This matters for network programming, because we don’t want the order at which integers are written to mess up our code. So to avoid this, we always send data in “network byte order”, which is big-endian btw.

Long story short, whenever you want to send data across the network it should be stored in network byte order. Luckily this is really easy – you can use the htonl and ntohl functions, which stand for “network to host long” and “host to network long”.

So writeInt converts the integer to network bytes order with htonl, then writes the resulting bytes.

Finally, writeString is a special case. When we send the string across, the other side needs to know how long the string is before reading it out. So we write the length of the string first, followed by the actual bytes of the string.

OK, let’s put this all together and make use of it! Start with NetworkController.h – add two new states to the NetworkState enum:

NetworkStatePendingMatchStatus,
NetworkStateReceivedMatchStatus,

The NetworkStatePendingMatchStatus is when we are sending our initial message over to the server and waiting for it to tell us what our match status is, and the NetworkStateReceivedMatchStatus is for when we receive this.

Then switch to NetworkController.m and make the following changes:

// Add to top of file, after #import
#import "MessageWriter.h"
 
@interface NetworkController (PrivateMethods)
- (BOOL)writeChunk;
@end
 
typedef enum {
    MessagePlayerConnected = 0,
    MessageNotInMatch,
} MessageType;
 
// Add right after init method
#pragma mark - Message sending / receiving
 
- (void)sendData:(NSData *)data {
 
    if (_outputBuffer == nil) return;
 
    int dataLength = data.length;
    dataLength = htonl(dataLength);
    [_outputBuffer appendBytes:&dataLength length:sizeof(dataLength)];
    [_outputBuffer appendData:data];
    if (_okToWrite) {
        [self writeChunk];
        NSLog(@"Wrote message");
    } else {
        NSLog(@"Queued message");
    }
}
 
- (void)sendPlayerConnected:(BOOL)continueMatch {
    [self setState:NetworkStatePendingMatchStatus];
 
    MessageWriter * writer = [[[MessageWriter alloc] init] autorelease];
    [writer writeByte:MessagePlayerConnected];
    [writer writeString:[GKLocalPlayer localPlayer].playerID];
    [writer writeString:[GKLocalPlayer localPlayer].alias];
    [writer writeByte:continueMatch];
    [self sendData:writer.data];
}
 
// In inputStreamHandleEvent, right after "TODO: Send message to server"
[self sendPlayerConnected:true];
 
// In outputSreamHandleevent, right after "TODO: Send message to server"
[self sendPlayerConnected:true];

Here we import the MessageWriter class we just wrote, and predeclare our writeChunk method in a private category. This is a little trick you can use to predeclare a method without putting it in the header file where other clases can be aware of it.

We then create an enum for our message types. There’s just two right now. MessagePlayerConnected is the “hello” message we’re about to send to the server as soon as we connect. MessageNotInMatch is the message we expect the server will send back to us, telling us we’re not already in a match.

sendData is a helper method that will take a buffer of data and enqueue it to the output buffer. It prepends it with the length of the buffer, which is important so the other side knows when it has fully received the message.

This is where the _okToWrite flag comes in handy. If it’s set, we can call writeChunk to write some of it to the outputStream right away – otherwise we have to wait for the next NSStreamEventHasSpaceAvailable.

sendPlayerConnected changes the state to NetworkStatePendingMatchStatus, and uses the MessageWriter to send a MessagePlayerConnected over to the other side. It contains the current player’s Game Center player ID, alias, and whether we want to continue any previous match.

Finally, we call sendPlayerConnected whenever the input/output streams are both opened.

One last change – switch to HelloWorldLayer.m and add the debug printouts for the new states at the end of stateChanged:

case NetworkStatePendingMatchStatus:
    debugLabel.string = @"Pending Match Status";
    break;
case NetworkStateReceivedMatchStatus:
    debugLabel.string = @"Received Match Status";
    break;

w00t – your code can now send a message over to your server! Compile and run your code, and you should see your debug label advances to a new state:

Writing data across a socket to your own game server

You should also see something like the following in the console log:

CatRace[37306:207] Opened output stream
CatRace[37306:207] Queued message
CatRace[37306:207] Ok to send
CatRace[37306:207] Amt to write: 32/32
Wrote 32 bytes, 0 remaining.
Ok to send

Of course, nothing happens because we haven’t updated our server to be able to read any incoming data! So let’s do that next.

Reading and Writing Socket Data from the Server

We’re going to completely revamp our server so it can read the incoming message, and send a response back. So delete everything you currently have in CatRaceServer.py, and replace it with the following code blocks. We’ll go over each section part by part.

from twisted.internet.protocol import Factory, Protocol
from twisted.internet import reactor
from struct import *
 
MESSAGE_PLAYER_CONNECTED = 0
MESSAGE_NOT_IN_MATCH = 1

Imports the Twisted libraries same as earlier, but also includes the built-in Python struct module, which is handy for parsing binary data.

Also declares two global constants for the two message types we’ll be using here.

class MessageReader:
 
    def __init__(self, data):
        self.data = data
        self.offset = 0
 
    def readByte(self):
        retval = unpack('!B', self.data[self.offset:self.offset+1])[0]
        self.offset = self.offset + 1
        return retval
 
    def readInt(self):
        retval = unpack('!I', self.data[self.offset:self.offset+4])[0]
        self.offset = self.offset + 4
        return retval
 
    def readString(self):
        strLength = self.readInt()
        unpackStr = '!%ds' % (strLength)
        retval = unpack(unpackStr, self.data[self.offset:self.offset+strLength])[0]
        self.offset = self.offset + strLength
        return retval

Much like the MessageWriter class we wrote earlier, this is a helper function to help unmarshal the data our app sent over. It keeps the buffer of data, and a pointer to where it’s currently reading.

  • readByte uses the “!B” format string, which means “unpack a byte in network order” at the current offset.
  • readInt uses the “!I” format string, which means “unpack an integer in network order” at the current offset.
  • readString reads the length of the string that follows, then builds a custom format string based on that length. For example, if strLength was 10, unpackStr would be “!10s”. This means “Read a 10-byte string.” It then uses unpackStr to actually unpack the string.
class MessageWriter:
 
    def __init__(self):
        self.data = ""
 
    def writeByte(self, value):        
        self.data = self.data + pack('!B', value)
 
    def writeInt(self, value):
        self.data = self.data + pack('!I', value)
 
    def writeString(self, value):
        self.writeInt(len(value))
        packStr = '!%ds' % (len(value))
        self.data = self.data + pack(packStr, value)

This is almost exactly the same as the MessageWriter class we wrote earlier, but now in Python! Notice it does the opposite of MessageReader.

class CatRacePlayer:
 
    def __init__(self, protocol, playerId, alias):
        self.protocol = protocol
        self.playerId = playerId
        self.alias = alias
        self.match = None
        self.posX = 25
 
    def __repr__(self):
        return "%s:%d" % (self.alias, self.posX)
 
    def write(self, message):
        message.writeString(self.playerId)
        message.writeString(self.alias)
        message.writeInt(self.posX)

This class stores the information about a player in a match. The __repr__ function is called whenever this class needs to be converted to a string, and the write method is a helper method to write out the player to a MessageWriter.

class CatRaceFactory(Factory):
    def __init__(self):
        self.protocol = CatRaceProtocol
        self.players = []
 
    def connectionLost(self, protocol):
        for existingPlayer in self.players:
            if existingPlayer.protocol == protocol:
                existingPlayer.protocol = None        
 
    def playerConnected(self, protocol, playerId, alias, continueMatch):
        for existingPlayer in self.players:
            if existingPlayer.playerId == playerId:
                existingPlayer.protocol = protocol
                protocol.player = existingPlayer
                if (existingPlayer.match):
                    print "TODO: Already in match case"
                else:
                    existingPlayer.protocol.sendNotInMatch()
                return
        newPlayer = CatRacePlayer(protocol, playerId, alias)
        protocol.player = newPlayer
        self.players.append(newPlayer)
        newPlayer.protocol.sendNotInMatch()

We modify the factory here to keep a list of the current players. When a player connects, it looks to see if the player ID is already in the list. If it is, it looks to see if the player is already in a match. Later on, we’ll return information about the match if the player’s already in one, but for now we just print a TODO.

If the player isn’t in the list, it makes a new player, adds it to the list, and sends a “not in match” message. This is what we expect to happen the first time a player connects to the server.

class CatRaceProtocol(Protocol):
 
    def __init__(self):
        self.inBuffer = ""
        self.player = None
 
    def log(self, message):
        if (self.player):
            print "%s: %s" % (self.player.alias, message)
        else:
            print "%s: %s" % (self, message) 
 
    def connectionMade(self):
        self.log("Connection made")
 
    def connectionLost(self, reason):
        self.log("Connection lost: %s" % str(reason))
        self.factory.connectionLost(self)
 
    def sendMessage(self, message):
        msgLen = pack('!I', len(message.data))
        self.transport.write(msgLen)
        self.transport.write(message.data)
 
    def sendNotInMatch(self):
        message = MessageWriter()
        message.writeByte(MESSAGE_NOT_IN_MATCH)
        self.log("Sent MESSAGE_NOT_IN_MATCH")
        self.sendMessage(message)

This is the first part of the updated CatRaceProtocol. We initialize the inBuffer to an empty string and the player to None. The log method is also updated to print out the current player’s alias, if it’s set.

  • connectionLost forwards the message on to the factory, so it can update the player’s protocol to None, to signify that the player has disconnected.
  • sendMessage is a lot like the version we wrote in Objective-C to send a message across – it write the length of the message followed by the message itself.
  • sendNotInMatch sends the MESSAGE_NOT_IN_MATCH over to the server. This message has no parameters.

Here’s the last bit of CatRaceProtocol:

    def playerConnected(self, message):
        playerId = message.readString()
        alias = message.readString()
        continueMatch = message.readByte()
        self.log("Recv MESSAGE_PLAYER_CONNECTED %s %s %d" % (playerId, alias, continueMatch))
        self.factory.playerConnected(self, playerId, alias, continueMatch)
 
    def processMessage(self, message):
        messageId = message.readByte()        
 
        if messageId == MESSAGE_PLAYER_CONNECTED:            
            return self.playerConnected(message)
 
        self.log("Unexpected message: %d" % (messageId))
 
    def dataReceived(self, data):
 
        self.inBuffer = self.inBuffer + data
 
        while(True):
            if (len(self.inBuffer) < 4):
                return;
 
            msgLen = unpack('!I', self.inBuffer[:4])[0]
            if (len(self.inBuffer) < msgLen):
                return;
 
            messageString = self.inBuffer[4:msgLen+4]
            self.inBuffer = self.inBuffer[msgLen+4:]
 
            message = MessageReader(messageString)
            self.processMessage(message)

Read this bottom-up. When dataReceived is called, we don’t know exactly how much data is received – it might be exactly one message, but it also might be more or less.

So we append it to a buffer, and then start looping through the buffer. We read the first integer, which tells us how much data we expect for the next message. If we have that much in the buffer, we read it out and pass it to processMessage (wrapped with a MessageReader).

processMessage reads the first byte to see the message type. Right now we can only handle the MESSAGE_PLAYER_CONNECTED – and if it’s that it calls another function to parse it.

playerConnected reads out the playerId, alias, and continueMatch parameters with the MessageReader helper methods, and passes the parsed information on to the factory method we discussed earlier.

factory = CatRaceFactory()
reactor.listenTCP(1955, factory)
print "Cat Race server started"
reactor.run()

The final bit didn’t change at all!

And that’s it – compile and run your server, and connect to it with your game, and you should see some output like this:

Cat Race server started
[SNIP]: Connection made
[SNIP]: Recv MESSAGE_PLAYER_CONNECTED G:1417937643 Riap 1
Riap: Sent MESSAGE_NOT_IN_MATCH

Congratulations – your server is now sending and receiving data! Let’s update your game to be able to read that MESSAGE_NOT_IN_MATCH message.

Reading Socket Data

Just like you saw in the Python script, when you receive a NSStreamEventHasBytesAvailable you don’t know how many bytes you’ll receive. It might be exactly the length of one message, but it could be shorter or greater than a message also.

So we need to add some code very similar to what was in the Python script, to add the data to an input buffer, and process it only when a complete message has arrived.

Let’s see what this looks like in code. Make the following changes to NetworkController.h:

// Inside @interface
NSMutableData *_inputBuffer;
 
// After @interface
@property (retain) NSMutableData *inputBuffer;

This creates a data buffer for the input buffer as mentioned above.

Switch to NetworkController.m and make the following changes:

// Add in synthesize section
@synthesize inputBuffer = _inputBuffer;
 
// Add at top of connect
self.inputBuffer = [NSMutableData data];
 
// Add in disconnect, at end of _inputStream != nil case:
self.inputBuffer = nil;
 
// Add placeholder method right after init
- (void)processMessage:(NSData *)data {
    // TODO: Process message
    NSLog(@"w00t got a message!  Implement me!");
}
 
// Add right above inputStreamHandleEvent
- (void)checkForMessages {
    while (true) {
        if (_inputBuffer.length < sizeof(int)) {
            return;
        }
 
        int msgLength = *((int *) _inputBuffer.bytes);
        msgLength = ntohl(msgLength);
        if (_inputBuffer.length < msgLength) {
            return;
        }
 
        NSData * message = [_inputBuffer subdataWithRange:NSMakeRange(4, msgLength)];
        [self processMessage:message];
 
        int amtRemaining = _inputBuffer.length - msgLength - sizeof(int);
        if (amtRemaining == 0) {
            self.inputBuffer = [[[NSMutableData alloc] init] autorelease];
        } else {
            NSLog(@"Creating input buffer of length %d", amtRemaining);
            self.inputBuffer = [[[NSMutableData alloc] initWithBytes:_inputBuffer.bytes+4+msgLength length:amtRemaining] autorelease];
        }        
 
    }
}
 
// In inputStreamHandleEvent, NSStreamEventHasBytesAvailable case, right after "Input stream has bytes..."
NSInteger       bytesRead;
uint8_t         buffer[32768];
 
bytesRead = [self.inputStream read:buffer maxLength:sizeof(buffer)];
if (bytesRead == -1) {
    NSLog(@"Network read error");
} else if (bytesRead == 0) {
    NSLog(@"No data read, reconnecting");
    [self reconnect];
} else {                
    NSLog(@"Read %d bytes", bytesRead);
    [_inputBuffer appendData:[NSData dataWithBytes:buffer length:bytesRead]];
    [self checkForMessages];
}

Let’s start at the bottom.

When the NSStreamEventHasBytesAvailable comes in, we read some random bytes that have come from the other side.
So we take whatever’s come, append them onto the input buffer, and call another method to check if we have any full messages yet.

The checkForMessages method makes sure there’s at least 4 bytes in the input buffer (for the length). If there is, it reads out the first four bytes and stores them into an integer we call msgLength (converted to host byte order).

We make sure we have that amount of data in the input buffer, and if so make a data buffer with that sub-range. We then pass it to a processMessage method for futher processing.

That’s it – compile and run your code and restart the server, and you should see some output like this in your game’s console log:

[SNIP] Input stream has bytes...
[SNIP] Read 5 bytes
[SNIP] w00t got a message!  Implement me!

Unmarshalling Data

Now that we have a buffer of data, we have to unmarshal it. Let’s write a helper class for this called MessageReader. It will be just like the Python version!

Go to File\New\New File, choose iOS\Cocoa Touch\Objective-C class, and click Next. Enter NSObject for Subclass of, click Next, name the new class MessageReader.m, and click Save.

Replace MessageReader.h with the following:

#import <Foundation/Foundation.h>
 
@interface MessageReader : NSObject {
    NSData * _data;
    int _offset;
}
 
- (id)initWithData:(NSData *)data;
 
- (unsigned char)readByte;
- (int)readInt;
- (NSString *)readString;
 
@end

This keeps track of the data buffer we’re reading and the current offset where we’re reading from, and some helper methods to read various types of data.

Switch to MessageReader.m and replace the contents with the following:

#import "MessageReader.h"
 
@implementation MessageReader
 
- (id)initWithData:(NSData *)data {
    if ((self = [super init])) {
        _data = [data retain];
        _offset = 0;
    }
    return self;
}
 
- (unsigned char)readByte {
    unsigned char retval = *((unsigned char *) (_data.bytes + _offset));
    _offset += sizeof(unsigned char);
    return retval;
}
 
- (int)readInt {
    int retval = *((unsigned int *) (_data.bytes + _offset));
    retval = ntohl(retval);
    _offset += sizeof(unsigned int);
    return retval;
}
 
- (NSString *)readString {
    int strLen = [self readInt];
    NSString *retval = [NSString stringWithCString:_data.bytes+_offset encoding:NSUTF8StringEncoding];
    _offset += strLen;
    return retval;
 
}
 
- (void)dealloc {
    [_data release];
    [super dealloc];
}
 
@end

You should understand how this works pretty well by now, so I’m not going to discuss this further here.

Now, we’re going to use this to parse the MessageNotInMatch message. Once we recieve this, we’ll bring up the Game Center matchmaker view controller and let it try to find a match for us. Once it finds a match, we’ll just log out the player IDs it finds.

Start by opening NetworkController.h and make the following changes:

// Add final three network states
NetworkStatePendingMatch,
NetworkStatePendingMatchStart,
NetworkStateMatchActive,
 
// Add inside NetworkControllerDelegate
- (void)setNotInMatch;
 
// Modify @interface to add GKMatchmakerViewController protocol
@interface NetworkController : NSObject <NSStreamDelegate, GKMatchmakerViewControllerDelegate> {
 
// Inside @interface
UIViewController *_presentingViewController;
GKMatchmakerViewController *_mmvc;
 
// After @interface
@property (retain) UIViewController *presentingViewController;
@property (retain) GKMatchmakerViewController *mmvc;
 
// After @properties
- (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers 
                 viewController:(UIViewController *)viewController;

Here we add the final three network states. NetworkStatePendingMatch means we’re waiting for the Game Center matchmaker to do its thing, NetworkStatePendingMatchStart means we’re waiting for the server to actually start the match, and NetworkStateMathActive means the game is on!

We also set the NetworkController as implementing the GKMatchmakerViewControllerDelegate and create a few instance variables/properties it needs. We also create a method the layer will use when it’s ready to look for a match.

Next switch to NetworkController.m and make the following changes:

// Add to top of file
#import "MessageReader.h"
 
// In @synthesize section
@synthesize presentingViewController = _presentingViewController;
@synthesize mmvc = _mmvc;
 
// Add right after setState:
- (void)dismissMatchmaker {
    [_presentingViewController dismissModalViewControllerAnimated:YES];
    self.mmvc = nil;
    self.presentingViewController = nil;
}
 
// Replace processMessage with the following
- (void)processMessage:(NSData *)data {
    MessageReader * reader = [[[MessageReader alloc] initWithData:data] autorelease];
 
    unsigned char msgType = [reader readByte];
    if (msgType == MessageNotInMatch) {
        [self setState:NetworkStateReceivedMatchStatus];
        [_delegate setNotInMatch];
    }
}
 
// Add code to bottom of file
#pragma mark - Matchmaking
 
- (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers 
                 viewController:(UIViewController *)viewController {
 
    if (!_gameCenterAvailable) return;
 
    [self setState:NetworkStatePendingMatch];
 
    self.presentingViewController = viewController;
    [_presentingViewController dismissModalViewControllerAnimated:NO];
 
    if (FALSE) {
 
        // TODO: Will add code here later!
 
    } else {
        GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease]; 
        request.minPlayers = minPlayers;     
        request.maxPlayers = maxPlayers;
 
        self.mmvc = [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];    
        _mmvc.hosted = YES;
        _mmvc.matchmakerDelegate = self;
 
        [_presentingViewController presentModalViewController:_mmvc animated:YES];
    }
}
 
// The user has cancelled matchmaking
- (void)matchmakerViewControllerWasCancelled:(GKMatchmakerViewController *)viewController {
    NSLog(@"matchmakerViewControllerWasCancelled");
    [self dismissMatchmaker];
}
 
// Matchmaking has failed with an error
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFailWithError:(NSError *)error {
    NSLog(@"didFailWithError: %@", error.localizedDescription);
    [self dismissMatchmaker];
}
 
// Players have been found for a server-hosted game, the game should start
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindPlayers:(NSArray *)playerIDs {
    NSLog(@"didFindPlayers");
    for (NSString *playerID in playerIDs) {
        NSLog(@"%@", playerID);
    }        
    if (_state == NetworkStatePendingMatch) {
        [self dismissMatchmaker]; 
        // TODO: Send message to server to start match, with given player Ids
    }
}
 
// An invited player has accepted a hosted invite.  Apps should connect through the hosting server and then update the player's connected state (using setConnected:forHostedPlayer:)
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didReceiveAcceptFromHostedPlayer:(NSString *)playerID __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_5_0) {
    NSLog(@"didReceiveAcceptFromHostedPlayer");    
}

There’s a lot of code here, but most of this is review from the previous Game Center tutorial.

The biggest significant new piece is we’re implementing the processMessage method to use the new MessageReader to parse the first byte to figure out the message type. If it’s the MessageNotInMatch method, we notify the delegate (the HelloWorldLayer in this case).

Before we switch to the HelloWorldLayer, we need to make a property for the RootViewController in the AppDelegate, because this is needed to present the matchmaker view controller. So switch to AppDelegate.h and add the property:

@property (nonatomic, retain) RootViewController * viewController;

Then switch to AppDelegate.m and synthesize it:

@synthesize viewController;

Finally switch to HelloWorldLayer.m and make the following changes:

// Add to top of file
#import "AppDelegate.h"
#import "NetworkController.h"
 
// Add final cases to stateChanged
case NetworkStatePendingMatch:
    debugLabel.string = @"Pending Match";
    break;
case NetworkStateMatchActive:
    debugLabel.string = @"Match Active";
    break;   
case NetworkStatePendingMatchStart:
    debugLabel.string = @"Pending Start";
    break;                  
 
// Add right above dealloc
- (void)setNotInMatch {
    AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;                
    [[NetworkController sharedInstance] findMatchWithMinPlayers:2 maxPlayers:2 viewController:delegate.viewController];
}

The most important part here is setNotInMatch – when this is called, we tell the network controller to look for a match.

Phew – finally done! Compile and run, and you’ll see the matchmaker GUI show up:

Displaying the GKMatchmakerViewController

If you run your game on the simulator and a device at the same time, you can actually use it to look for an auto-match, and once it finds a match you’ll see the player IDs it matched up logged out:

CatRace[5407:707] didFindPlayers
CatRace[5407:707] G:1036727375
CatRace[5407:707] G:1417937643

Allright, we have end-to-end communication! We send and received a message on our game, and also sent and received a message from our server. It will be a cakewalk from here!

Well, let’s face it – as much of a cakewalk as any networked game can be :P

Where to Go From Here?

This tutorial is so massive that WordPress is making me split it into two parts.

But I don’t want to leave you guys hanging here, because we haven’t even gotten to the cool part yet – making the match, and moving the players! For that, check out part 2! :D

In the meantime, if you have any questions or comments, please join the forum discussion below!

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments

18 Comments

[ 1 , 2 ]
  • Ian2400 wrote:My userAuthenticated BOOL will not get set for some reason. I verify that it gets set to TRUE properly when the user authentication changes, but the next time I get a notification it is set to FALSE even though it should not be. So whenever I get a repeat notification, it tries to connect to the server, opening a bunch of new sockets. How can I stop this, it is driving me nuts!


    Gah, nevermind. This was a problem caused by me because I had come up with my own solution awhile ago, and was trying to adapt my solution (just a socket based server) to use gamecenter using this tutorial. I hadn't removed an instance of my app initializing a new NetworkController.
    Ian2400
  • Good tutorial. However, I think you might want to reconsider the code for your writeChunk method.

    Where you write:
    Code: Select all

        int amtWritten = [self.outputStream write:_outputBuffer.bytes maxLength:amtToWrite];
        if (amtWritten < 0) {
            [self reconnect];
        }
        int amtRemaining = _outputBuffer.length - amtWritten;

    have you considered the case where amtWritten actually is less than zero, as in the if...? Since there is no return statement following the [self reconnect] line in the if... statement, the code in writeChunk would continue to process after the [self reconnect] statement. Then the line
    Code: Select all
    int amtRemaining = _outputBuffer.length - amtWritten;

    would result in amtRemaining = _outputBuffer.length - (-1), or +1. I think you're setting yourself up for a buffer overflow with this code.

    A safer way to write this method might be:
    Code: Select all
    - (BOOL)writeChunk
    {
       size_t writeDataSize = _writeData.length;
       
       if ( writeDataSize > 0 )
       {
          NSInteger sentSize;
          size_t dataSegmentSize = (writeDataSize >= 1024) ? 1024 : writeDataSize;
          uint8_t *sendBytes = (uint8_t *)[self.dataToWrite bytes];
          uint8_t buf[dataSegmentSize];
             
          memcpy(buf, sendBytes, dataSegmentSize);
          sentSize = [_outputStream write:buf maxLength:dataSegmentSize];

          // Check to see that the output stream returned something valid.
          if ( sentSize >= 0 )
          {
             if (writeDataSize - sentSize == 0)
                self.writeData = [NSMutableData data];
             else
                self.writeData = [NSMutableData dataWithBytes:(sendBytes + dataSegmentSize)
                                                       length:(writeDataSize - sentSize)];
             NSLog(@"%i bytes sent", sentSize);
             return TRUE;
          }
          
          // If the output stream returned an invalid value (-1), try a reconnect to the server.
          // The previous send attempt winds up ignored, and the continuing execution of
          // the method will return a FALSE. Since the previous send is ignored, self.writeData
          // is not reset, so effectively the previous send will be retried upon a successful
          // reconnect to the server.
          NSLog(@"send error, reconnecting to server");
          [self reconnectToServer];
       }
       
       return FALSE;
    }
    cmdahler
  • I'm having a problem with reading the message from the server.
    When I'm sending "hello" from the server, my game crashes in:

    NSData * message = [_inputBuffer subdataWithRange:NSMakeRange(4, msgLength)];

    The inputbuffer has 13 bytes and the msgLength is 11.
    The exception description I get:

    Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSConcreteMutableData subdataWithRange:]: range {4, 11} exceeds data length 13'

    I didn't quite understand why we subdata the inputbuffer? and why do I get this error?

    thanks..
    Kevin Hury
[ 1 , 2 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Vote for Our Next Tutorial!

Every week, we alternate between Gaming and Non-Gaming tutorial votes. This week: Gaming!

    Loading ... Loading ...

Last week's winner: Apple TestFlight Tutorial.

Suggest a Tutorial - Past Results

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in January: WatchKit.

Sign Up - January

Our Books

Our Team

Tutorial Team

  • René Cacheaux
  • Tony Dahbura

... 60 total!

Update Team

  • Ray Fix

... 12 total!

Editorial Team

... 17 total!

Code Team

  • Orta Therox

... 3 total!

Subject Matter Experts

... 4 total!