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

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 […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 6 of this article. Click here to view the first page.

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.

Contributors

Over 300 content creators. Join our team.