TCP Client/Server Using Built-In Panda Code

I’ve been working on client/server code for a game I’m starting to build, but had a hell of a time making this work with multiple client connections and retaining the ability to send datagrams to a specific client.

Here’s a brief rundown of how the code works, and the full code for both the Server and Client are below.

Server will open a TCP port and listen for connections. Once somebody has connected it will record a unique identifier (integer) for that connection and the pointer to the connection in a dictionary. You can then use this dictionary to send a datagram to any given client connected to your server.

The code in the manual will give you enough structure to be able to send a datagram to a single client, but if you’re a python newbie like me it doesn’t really point you towards handling multiple clients.

Right now the server will simply listen for datagrams, and will send a response datagram back to the connection that sent it. However you can easily modify this to plugin to a database for a more elaborate setup.

Client will open a TCP connection to the server and send it a datagram. It will then remain connected and listen for any datagrams sent by the server.

Yes this is pretty basic in terms of functionality, but it’s a starting point if you want to get into multiple-clients connecting to your server.

Thanks to drwr and powerpup118 for helping me do this, they were extremely helpful when I was trying to get this going and had a ton of questions in the scripting forum, specifically this thread.

If you are looking for ideas on how to use this with a database, I posted pseudo-code on how to broadcast a message to all connect clients that are in a specific zone (for a basic MMO chat server as an example) in the thread linked to in the above paragraph.

In a nutshell you get a unique integer identifier for the client when they connection, which you map to their specific connection when they join. Then you can just store the connectionID in a database with their character information. When you need to send a message to a specific character or group of characters, you query your database for the connectionID’s that match the characters you are trying to reach, and then use the dictionary to identify their specific connection and fire a datagram off to them.

I ran this code with 5 clients connecting to the server, and it had no problem differentiating which datagrams were sent to which connections, so it should be useable.

Caveats

  1. I have no idea if/how this will function on a linux box as I built it on a Windows machine. I don’t know if that makes any different.

  2. This code just gives you the framework for establishing multiple client connections to a central server. It doesn’t go beyond that scope. When I have more working code for this, I’ll be happy to post it as I’m working on a mutli-player RPG for it (basically a mini-MMORPG). I’ll be adding client authentication and a few other things in the immediate future, and will be happy to give the code out for it.

  3. The Client code isn’t very well documented as I was in a rush. However it’s pretty basic. Please note that both the client and server code posted below do not have the direct object window popup when the programs are running. I disabled them as it would just slow down the loading times of it all. If you want the directobject windows to appear, just comment out this line in both:

ConfigVariableString("window-type", "none").setValue("none")

Here’s the code…

Server

from direct.showbase.DirectObject import DirectObject  
from direct.task import Task
from pandac.PandaModules import ConfigVariableString
ConfigVariableString("window-type", "none").setValue("none")
import direct.directbase.DirectStart
# These are the  Panda libraries needed for everything else
# to function properly.  The configvariable bit disables the
# standard window that opens, as the server doesn't need to continually
# re-draw a window - it just needs to to output to the command prompt

from panda3d.core import *

from pandac.PandaModules import QueuedConnectionManager, QueuedConnectionReader, ConnectionWriter, QueuedConnectionListener, NetAddress
from direct.distributed.PyDatagram import PyDatagram
from direct.distributed.PyDatagramIterator import PyDatagramIterator
# panda's built-in networking code for TCP networking

class Server(DirectObject):
    def __init__( self ):
        self.port = 9099
        self.portStatus = "Closed"
        self.host = "localhost"
        self.backlog = 1000
        self.Connections = {}
        # basic configuration variables that are used by most of the
        # other functions in this class

        self.StartConnectionManager()
        # manages the connection manager
        
        self.DisplayServerStatus()
        # output a status box to the console which tells you the port
        # and any connections currently connected to your server
        
    def DisplayServerStatusTASK(self, task):
        # all this does is periodically load the status function below
        # add a task to display it every 30 seconds while you are doing 
        # any new coding
        self.DisplayServerStatus()
        return Task.again
    
    def DisplayServerStatus(self):
        print "\n----------------------------------------------------------------------------\n"
        print "SERVER STATUS:\n\n"
        print "Connection Manager Port: " + str(self.port)  + " [" + str(self.portStatus) + "]"
        for k, v in self.Connections.iteritems():
            print "Connection " + k

    ##################################################################
    #              TCP Networking Functions and Tasks
    ##################################################################
       
    def StartConnectionManager(self):
        # this function creates a connection manager, and then 
        # creates a bunch of tasks to handle connections
        # that connect to the server
        self.cManager = QueuedConnectionManager()
        self.cListener = QueuedConnectionListener(self.cManager, 0)
        self.cReader = QueuedConnectionReader(self.cManager, 0)
        self.cWriter = ConnectionWriter(self.cManager,0)
        self.tcpSocket = self.cManager.openTCPServerRendezvous(self.port,self.backlog)
        self.cListener.addConnection(self.tcpSocket)
        self.portStatus = "Open"
        taskMgr.add(self.ConnectionManagerTASK_Listen_For_Connections,"Listening for Connections",-39)
        # This task listens for new connections
        taskMgr.add(self.ConnectionManagerTASK_Listen_For_Datagrams,"Listening for Datagrams",-40)
        # This task listens for new datagrams
        taskMgr.add(self.ConnectionManagerTASK_Check_For_Dropped_Connections,"Listening for Disconnections",-41)
        # This task listens for disconnections
        
    def ConnectionManagerTASK_Listen_For_Connections(self, task):
        if(self.portStatus == "Open"):
        # This exists in case you want to add a feature to disable your
        # login server for some reason.  You can just put code in somewhere
        # to set portStatus = 'closed' and your server will not
        # accept any new connections
            if self.cListener.newConnectionAvailable():
                print "CONNECTION"
                rendezvous = PointerToConnection()
                netAddress = NetAddress()
                newConnection = PointerToConnection()
                if self.cListener.getNewConnection(rendezvous,netAddress,newConnection):
                    newConnection = newConnection.p()
                    self.Connections[str(newConnection.this)] = rendezvous
                    # all connections are stored in the self.Connections
                    # dictionary, which you can use as a way to assign
                    # unique identifiers to each connection, making
                    # it easy to send messages out
                    self.cReader.addConnection(newConnection)   
                    print "\nSOMEBODY CONNECTED"
                    print "IP Address: " + str(newConnection.getAddress())
                    print "Connection ID: " + str(newConnection.this)
                    print "\n"
                    # you can delete this, I've left it in for debugging
                    # purposes
                    self.DisplayServerStatus()                
                    # this fucntion just outputs the port and
                    # current connections, useful for debugging purposes
        return Task.cont  

    def ConnectionManagerTASK_Listen_For_Datagrams(self, task):
        if self.cReader.dataAvailable():
            datagram=NetDatagram()  
            if self.cReader.getData(datagram):
                print "\nDatagram received, sending response"
                myResponse = PyDatagram()
                myResponse.addString("GOT YER MESSAGE")
                self.cWriter.send(myResponse, datagram.getConnection())
                # this was just testing some code, but the server will
                # automatically return a 'GOT YER MESSAGE' datagram
                # to any connection that sends a datagram to it
                # this is where you add a processing function here
                #myProcessDataFunction(datagram)
        return Task.cont

    def ConnectionManagerTASK_Check_For_Dropped_Connections(self, task):
        # if a connection has disappeared, this just does some house 
        # keeping to officially close the connection on the server,
        # if you don't have this task the server will lose track
        # of how many people are actually connected to you
        if(self.cManager.resetConnectionAvailable()):
            connectionPointer = PointerToConnection()
            self.cManager.getResetConnection(connectionPointer)
            lostConnection = connectionPointer.p()
            # the above pulls information on the connection that was lost
            print "\nSOMEBODY DISCONNECTED"
            print "IP Address: " + str(lostConnection.getAddress())
            print "ConnectionID: " + str(lostConnection.this)
            print "\n"
            del self.Connections[str(lostConnection.this)]
            # remove the connection from the dictionary
            self.cManager.closeConnection(lostConnection)
            # kills the connection on the server
            self.DisplayServerStatus()
        return Task.cont
 

Server = Server()
run()

Client


from direct.showbase.DirectObject import DirectObject   

from pandac.PandaModules import ConfigVariableString
ConfigVariableString("window-type", "none").setValue("none")
import direct.directbase.DirectStart
from direct.task import Task
from pandac.PandaModules import QueuedConnectionManager, QueuedConnectionReader, ConnectionWriter, QueuedConnectionListener
from direct.distributed.PyDatagram import PyDatagram
from direct.distributed.PyDatagramIterator import PyDatagramIterator

class Client(DirectObject):
    def __init__( self ):
        print "Initializing client test"
        
        self.port = 9099
        self.ip_address = "localhost"
        self.timeout = 3000             # 3 seconds to timeout
        
        self.cManager = QueuedConnectionManager()
        self.cListener = QueuedConnectionListener(self.cManager, 0)
        self.cReader = QueuedConnectionReader(self.cManager, 0)
        self.cWriter = ConnectionWriter(self.cManager,0)
        
        self.Connection = self.cManager.openTCPClientConnection(self.ip_address, self.port, self.timeout)
        if self.Connection:
            taskMgr.add(self.tskReaderPolling,"read the connection listener",-40)
            # this tells the client to listen for datagrams sent by the server
            
            print "Connected to Server"
            self.cReader.addConnection(self.Connection)
            PRINT_MESSAGE = 1
            myPyDatagram = PyDatagram()
            myPyDatagram.addUint8(100)
            # adds an unsigned integer to your datagram
            myPyDatagram.addString("first string of text")
            # adds a string to your datagram
            myPyDatagram.addString("second string of text")
            # adds a second string to your datagram
            self.cWriter.send(myPyDatagram, self.Connection)
            # fires it off to the server
            
            #self.cManager.closeConnection(self.Connection)
            #print "Disconnected from Server"
            # uncomment the above 2 lines if you want the client to
            # automatically disconnect.  Or you can just
            # hit CTRL-C twice when it's running to kill it
            # in windows, I don't know how to kill it in linux
        else:
            print "Not connected to Server"
        
    def tskReaderPolling(self, task):
        if self.cReader.dataAvailable():
            datagram=PyDatagram()  
            if self.cReader.getData(datagram):
                self.processServerMessage(datagram)
        return Task.cont       
    
    def processServerMessage(self, netDatagram):
        myIterator = PyDatagramIterator(netDatagram)
        print myIterator.getString()
        
Client = Client()
run()

just a heads up on the code working on Linux it works great the only issue that I see so far is that after about 3 client connections the server starts to respond slower. Other then that I had about 10 clients connected to the server and minus the initial delay it worked as intended.

this line is giving me problems Dont know if its the indentation or what but it throws an error about being undefined.

taskMgr.add(self.tskReaderPolling,"read the connection listener",-40)

and the error its generating:

 File "C:\Panda3D-1.7.1\DarkRift\src\darkriftmmo.py", line 49, in __init__
    taskMgr.add(self.tskReaderPolling,"read the connection listener",-40)
NameError: global name 'taskMgr' is not defined

taskMgr is one of the Panda builtins that is defined when you import DirectStart. So be sure to import DirectStart before you load this code.

David

YAY ! thanks for pointing that out I know some of my issues are simple fixes but Im prettymuch a python beginner. I have had a little experiance with it when the Torque mmo kit was still availible but most of the code for that was written for me and I was basicly expanding code, not writing NEW code of my own.

So if some of my problems seem a tad noob ish , its with good reason lol Thanks again