Search code examples
pythontcpudpservertwisted

Detect when TCP is congested python twisted socket server


I'm working on a realtime MMO game, and have a working TCP server (along with game client), but now I'm considering using UDP for constantly updating other player's postions(to greatly reduce random game stuffer from TCP congestion control!) I'd love some help from people smarter than me in this stuff (I'm new to python/twisted, and couldn't find this info elsewhere ;) )

Currently, my server accepts connections with a simple Twisted Protocol. Eg.

''' TCP reciever '''
class TCPProtocol(Protocol):

    def connectionMade(self):
        #add to list of connected clients
        factory.clients.append(self)

    def dataReceived(self, data):
        pass


#setup factory and TCP protocol class
factory = Factory()
factory.protocol = TCPProtocol
factory.clients = []
reactor.listenTCP(1959, factory)

@@@@@@@@@@@@@@ UPDATE @@@@@@@@@@@@@ : How can I implement congestion checking for each Protocol instance seperately? Please start me off with something, inside the code sample below: (where it says 'HELP HERE PLEASE'!) Am I thinking of this wrongly? Any guidance would be awesome, thanks!

from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor

KServerIP='127.0.0.1'
KServerPortTCP=8000
#KClientPortUDP=7055

''' TCP reciever '''
class TCPProtocol(Protocol):

    def connectionMade(self):
        #add to list of connected clients
        factory.clients.append(self)

        #set client variables (simplified)
        self.pos_x=100
        self.pos_y=100

        #add to game room (to recieve updates)
        mainGameRoom.clientsInRoom.append(self)

    def dataReceived(self, data):
        pass

    #send custom byte message to client (I have special class to read it)
    def sendMessageToClient(self, message, isUpdate):

''' @@@@@@@@@@@@@@@@@  HELP HERE PLEASE!  @@@@@@@@@@@      
        if isUpdate and (CHECK IF CLIENT IS CONGESTED??? )
            return  (and send next update when not congested)'''
     '''@@@@@@@@@@@@@@@@@  HELP HERE PLEASE!  @@@@@@@@@@@   '''

        #if not congested, write update!
        msgLen = pack('!I', len(message.data))
        self.transport.write(msgLen) #add length before message
        self.transport.write(message.data)

#simplified version of my game room
#this room runs the game, and clients recieve pos updates for 
#everyone in this room (up to 50 people)
dt_gameloop=1.0/60 #loop time difference
dt_sendClientUpdate=0.1 #update intervar
class GameRoom(object):
    #room constants
    room_w=1000
    room_h=1000

    def __init__(self):
        super(GameRoom, self).__init__()
        #schedule main game loop
        l=task.LoopingCall(self.gameLoop)
        l.start(dt_gameloop) # call every X seconds
        #schedule users update loop
        l=task.LoopingCall(self.sendAllClientsPosUpdate)
        l.start(dt_sendClientUpdate) # call every X seconds

    def gameLoop(self):
        #game logic runs here (60 times a second),  EG.
        for anUpdateClient in self.clientsInRoom:
            anUpdateClient.pos_x+=10
            anUpdateClient.pos_y+=10

    #send position update every 0.1 seconds, 
    #send all player positions to all clients
    def sendAllClientsPosUpdate(self):

        message = MessageWriter()
        message.writeByte(MessageGameLoopUpdate) #message type

        #create one byte message containing info for all players
        message.writeInt(len(self.clientsInRoom)) #num items to read
        for aClient in self.clientsInRoom:
            message.writeInt(aClient.ID)
            message.writeFloat( aCell.pos_x )#pos
            message.writeFloat( aCell.pos_y )

        #send message to all clients
        for aClient in self.clientsInRoom:
            aClient.sendMessageToClient(message, True)

#setup factory and TCP protocol class
factory = Factory()
factory.protocol = TCPProtocol
factory.clients = []
reactor.listenTCP(KServerPortTCP, factory)

#simple example, one game room
mainGameRoom=GameRoom()


print "Server started..."
reactor.run()

Solution

  • You probably don't need UDP (yet).

    The first thing that you say is that you want to "reduce network congestion ... from TCP". That is not what UDP does. UDP allows you to work around congestion control, which actually increases network congestion. Until you're aware of how to implement your own congestion control algorithms, UDP traffic on high-latency connections is just going to cause packet storms which overwhelm your server and flood your users' network connections, making them unusable.

    The important thing about sending movement packets in a real-time game is that you always want to ensure that you don't waste time "catching up" with old movement packets when a new position is already available. In Twisted, you can use the producer and consumer APIs to do that on a TCP connection just fine, like so:

    from zope.interface import implementer
    from twisted.internet.protocol import Protocol
    from twisted.internet.interfaces import IPullProducer
    
    def serializePosition(position):
        "... take a 'position', return some bytes ..."
    
    @implementer(IPullProducer)
    class MovementUpdater(Protocol, object):
        def updatePosition(self, newPosition):
            if newPosition != self.currentPosition:
                self.currentPosition = newPosition
                self.needToSendPosition()
    
        waitingToSend = False
    
        def needToSendPosition(self):
            if not self.waitingToSend:
                self.waitingToSend = True
                self.transport.registerProducer(self, False)
    
        def resumeProducing(self):
            self.transport.write(serializePosition(self.currentPosition))
            self.transport.unregisterProducer()
            self.waitingToSend = False
    
        def stopProducing(self):
            "nothing to do here"
    

    Every time the game needs to send a new position, it can call updatePosition to update the player's current position. updatePosition first updates the current position, then calls needToSendPosition which marks the connection as needing to send a position update. This registers the protocol as a producer for its transport, which will cause resumeProducing to be called each time write-buffer space is available. As soon as resumeProducing is called, we send whatever the latest position is - if updatePosition is called 500 times while the network is congested, only one update will be sent as soon as the congestion alleviates.

    This is a bit oversimplified, because each transport can only have one producer at a time, and your game server will probably have lots of different position updates to send to clients, so you will need a multiplexor which aggregates all the position updates from multiple clients, and also some code to order messages so that things other than position updates still get through but that position updates have priority.

    This might seem like extra work if you're going to do UDP anyway, but if you're going to do UDP correctly and actually get any benefit from it, you will need to implement something very much like this anyway, so this won't be wasted.