Panda3D network example

While trying to figure out the network stuff, I wound up with some nice example code. Since there have been alot of ppl asking about this, I’ll just post the links here.

Client code
Server code

I cant guarantee that this is the most efficient way, but its a good way to see what does what.

Good luck!

ZOMG nice! :slight_smile:

TBH I understood only half of it, but I will see if I can get the essence of it :slight_smile:

Otherwise I will steal your work and tell the ppl it was my idea :wink:
Nah… but I will do some surgeries… lets see if I can adept the mechanism if I am allowed to :smiley:

Regards, Bigfoot29

MKay… after Yellow has had to spend a couple of hours telling me how his code works, I don’t want to that knowledge to be forgotten…

So here is my version of Yellows Code (did no changes at the code itself) with much much more declaration…

Client.py

from pandac.PandaModules import *
import direct.directbase.DirectStart
from direct.distributed.PyDatagram import PyDatagram
from direct.distributed.PyDatagramIterator import PyDatagramIterator

from direct.gui.DirectGui import *
import sys


######################################3
##
## Config
##

IP = '127.0.0.1'
PORT = 9099
USERNAME = "yellow"
PASSWORD = "mypass"


######################################3
##
## Defines
##
## Quote Yellow: This are server opcodes. It tells the server
## or client what pkt it is receiving. Ie if the pkt starts
## with 3, the server knows he has to deal with a chat msg

MSG_NONE            = 0
CMSG_AUTH           = 1
SMSG_AUTH_RESPONSE  = 2
CMSG_CHAT           = 3
SMSG_CHAT           = 4
CMSG_DISCONNECT_REQ = 5
SMSG_DISCONNECT_ACK = 6

class Client(DirectObject):
    def __init__(self):
        self.accept("escape", self.sendMsgDisconnectReq)
        
        # Create network layer objects
	## This is madatory code. Don't ask for now, just use it ;)
	## If something is unclear, just ask.
	
 	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(IP, PORT,1)
        self.cReader.addConnection(self.Connection)

        # Start tasks
        taskMgr.add(self.readTask, "serverReaderPollTask", -39)

        # Send login msg to the server
	## required to get the whole thing running.
        self.sendMsgAuth()

    ########################################
    ##
    ## Addition:
    ## If in doubt, don't change the following. Its working.
    ## Here are the basic networking code pieces.
    ## If you have questions, ask...
    ##
    
    def readTask(self, task):
        while 1:
            (datagram, data, msgID) = self.nonBlockingRead(self.cReader)
            if msgID is MSG_NONE:
                break
            else:
                self.handleDatagram(data, msgID)
                
        return Task.cont

    def nonBlockingRead(self,qcr):
        """
        Return a datagram iterator and type if data is available on the
        queued connection reader
        """
        if self.cReader.dataAvailable():
            datagram = NetDatagram()
            if self.cReader.getData(datagram):
                data = PyDatagramIterator(datagram)
                msgID = data.getUint16()
            else:
                data = None
                msgID = MSG_NONE
        else:
            datagram = None
            data = None
            msgID = MSG_NONE
        # Note, return datagram to keep a handle on the data
        return (datagram, data, msgID)

    def handleDatagram(self, data, msgID):
        """
        Check if there's a handler assigned for this msgID.
        Since we dont have case statements in python,
        we're using a dictionary to avoid endless elif statements.
        """
	
	########################################################
	##
	## Of course you can use as an alternative smth like this:
	## if msgID == CMSG_AUTH: self.msgAuth(msgID, data, client)
	## elif...
	
        if msgID in Handlers.keys():
            Handlers[msgID](msgID,data)
        else:
            print "Unknown msgID: %d" % msgID
            print data
        return        

    def sendMsgAuth(self):
        
	#########################################################
	##
	## This handles the sending of the auth request.
	##
	
	## 1st. We need to create a buffer
	pkg = PyDatagram()
        
	## 2nd. We put a UInt16 type Number in it. Here its CMSG_AUTH
	## what means that the corresponding Value is "1"
	pkg.addUint16(CMSG_AUTH)
	
	## 3rd. We add the username to the buffer after the UInt.
        pkg.addString(USERNAME)
	
	## 4th. We add the password for the username after the username
        pkg.addString(PASSWORD)
	
	## Now that we have a Buffer consisting of a Number and 2 Strings
	## we can send it.
        self.send(pkg)

    def sendMsgDisconnectReq(self):
        #####################################################
	##
	## This is not used right now, but can be used to tell the 
	## server that the client is disconnecting cleanly.
	##
	pkg = PyDatagram()
        
	## Will be a short paket... we are just sending
	## the Code for disconnecting. The server doesn't
	## need more information anyways...
	pkg.addUint16(CMSG_DISCONNECT_REQ)
        self.send(pkg)

    def msgAuthResponse(self, msgID, data):
        
    ##################################################
    ##
    ## Here we are going to compare the auth response
    ## we got from the server. Yellow kept it short, but
    ## if the server sends a 0 here, it means, the User
    ## doesn't exist. 1 means: user/pwd combination
    ## successfull. If the server sends a 2: Wrong PWD.
    ## Note that its a security risk to do so. That way
    ## you can easily spy for existing users and then
    ## check for their passwords, but its a good example
    ## to show, how what is working.
	
	flag = data.getUint32()
        if flag == 0:
            print "Unknown user"
       
        if flag == 2:
            print "Wrong pass, please try again..."

	if flag == 1:
            print "Authentication Successfull"
            
	    ######################################################
	    ##
	    ## Now that we are known and trusted by the server, lets
	    ## send some text!
	    ##
	    
	    ## creating the buffer again
	    pkg = PyDatagram()
	    
	    ## Putting the Op-Code into the buffer saying that this is
	    ## going to be a chat message. (if you read the buffer you
	    ## would see then a 3 there. Why? Because CMSG_CHAT=3
            pkg.addUint16(CMSG_CHAT)
	    
	    ## Next we are going to add our desired Chat message
	    ## to the buffer. Don't get confused about the %s
	    ## its a useable way to use variables in C++
	    ## you can write also:
	    ## pkg.addString('Hey, ',USERNAME,' is calling!')
            pkg.addString("%s is calling in and is glad to be here" % USERNAME)
            
	    ## Now lets send the whole thing...
	    self.send(pkg)
            
 
    def msgChat(self, msgID, data):
        
	##########################################################
	##
	## Here comes the interaction with the data sent from the server...
	## Due to the fact that the server does not send any data the
	## client could display, its only here to show you how it COULD
	## be used. Of course you can do anything with "data". The
	## raw print to console should only show a example.
	## 
	
	print data.getString()

    def msgDisconnectAck(self, msgID, data):
        
	###########################################################
	##
	## If the server sends a "close" command to the client, this
	## would be handled here. Due to the fact that he doesn't do
	## that, its just another example that does show what would
	## be an example about how to do it. I would be careful with
	## the example given here... In that case everything a potential
	## unfriendly person needs to do is sending you a paket with a
	## 6 in it coming from the server (not sure if it even needs to 
	## be from the server) the application will close... You might
	## want to do a double checking with the server again to ensure
	## that he sent you the paket... But thats just a advice ;)
	##
	
	## telling the Manager to close the connection    
	self.cManager.closeConnection(self.Connection)
        
	## saying good bye
	sys.exit()
	

    def send(self, pkg):
        self.cWriter.send(pkg, self.Connection)

    def quit(self):
        self.cManager.closeConnection(self.Connection)
        sys.exit()
        
######################################################################
##
## OK! After all of this preparation lets create a Instance of the 
## Client Class created above. Call it as you wish. Make sure that you
## use the right Instance name in the dictionary "Handlers" as well...
##

aClient = Client()

######################################################################
##
## That is the second piece of code from the
## def handleDatagram(self, data, msgID): - Method. If you have
## trouble understanding this, please ask.
##

Handlers = {
    SMSG_AUTH_RESPONSE  : aClient.msgAuthResponse,
    SMSG_CHAT           : aClient.msgChat,
    SMSG_DISCONNECT_ACK : aClient.msgDisconnectAck,
    }

#######################################################################
##
## As Examples for other Instance names:
##
## justAExample = Client()
##
## Handlers = {
##    SMSG_AUTH_RESPONSE  : justAExample.msgAuthResponse,
##    SMSG_CHAT           : justAExample.msgChat,
##    SMSG_DISCONNECT_ACK : justAExample.msgDisconnectAck,
##    }

    
########################################################################
##
## We need that loop. Otherwise it would run once and then quit.
##    
run()

Server.py:

from pandac.PandaModules import *
import direct.directbase.DirectStart
from direct.distributed.PyDatagram import PyDatagram
from direct.distributed.PyDatagramIterator import PyDatagramIterator

from direct.gui.DirectGui import *
import sys

######################################3
##
## Config
##

PORT = 9099

######################################3
##
## Defines
##
## Quote Yellow: This are server opcodes. It tells the server
## or client what pkt it is receiving. Ie if the pkt starts
## with 3, the server knows he has to deal with a chat msg


MSG_NONE            = 0
CMSG_AUTH           = 1
SMSG_AUTH_RESPONSE  = 2
CMSG_CHAT           = 3
SMSG_CHAT           = 4
CMSG_DISCONNECT_REQ = 5
SMSG_DISCONNECT_ACK = 6

##################################################################
##
## This is just a demo how to implement Users. Of course you can
## use any other method of doing it... reading a plaintext file,
## decrypting it, using your systems users, asking a Database...
##

USERS = {
    'yellow'    : 'mypass',
    'tester'    : 'anotherpass'
    }

###################################################################
##    
## Creating a dictionary for the clients. Thats how we can adress
## them later on. For now they are adressed by their IP, but that
## isn't the best solution. What if 2 clients have the same IP?
## You can find of course any other working solution that will avoid
## this. Feel free to distribute it! As for the Demo its OK as is.
##

CLIENTS = {}

class Server(DirectObject):

    def __init__(self):
        
    	## If you press Escape @ the server window, the server will quit.
        self.accept("escape", self.quit)
        self.lastConnection = None
        
        # Create network layer objects

        # Deals with the basic network stuff
        self.cManager = QueuedConnectionManager()
        
        # Listens for new connections and queue's them
        self.cListener = QueuedConnectionListener(self.cManager, 0)

        # Reads data send to the server
        self.cReader = QueuedConnectionReader(self.cManager, 0)

        # Writes / sends data to the client
        self.cWriter = ConnectionWriter(self.cManager,0)

        # open a server socket on the given port. Args: (port,timeout)
        self.tcpSocket = self.cManager.openTCPServerRendezvous(PORT, 1)

        # Tell the listener to listen for new connections on this socket
        self.cListener.addConnection(self.tcpSocket)

        # Start Listener task 
        taskMgr.add(self.listenTask, "serverListenTask",-40)

        # Start Read task
        taskMgr.add(self.readTask, "serverReadTask", -39)

    def listenTask(self, task):
        """
        Accept new incoming connections from the client
        """
        # Run this task after the dataLoop
        
        # If there's a new connection Handle it
        if self.cListener.newConnectionAvailable():
            rendezvous = PointerToConnection()
            netAddress = NetAddress()
            newConnection = PointerToConnection()
            
            if self.cListener.getNewConnection(rendezvous,netAddress,newConnection):
                newConnection = newConnection.p()
                # tell the Reader that there's a new connection to read from
                self.cReader.addConnection(newConnection)
                CLIENTS[newConnection] = netAddress.getIpString()
                self.lastConnection = newConnection
                print "Got a connection!"
            else:
                print "getNewConnection returned false"
        return Task.cont

    def readTask(self, task):
        """
        If there's any data received from the clients,
        get it and send it to the handlers.
        """
        while 1:
            (datagram, data, msgID) = self.nonBlockingRead(self.cReader)
            if msgID is MSG_NONE:
                # got nothing todo
                break
            else:
                # Got a datagram, handle it
                self.handleDatagram(data, msgID,datagram.getConnection())
                
        return Task.cont

    def nonBlockingRead(self,qcr):
        """
        Return a datagram iterator and type if data is available on the
        queued connection reader
        """
        if self.cReader.dataAvailable():
            datagram = NetDatagram()
            if self.cReader.getData(datagram):
                data = PyDatagramIterator(datagram)
                msgID = data.getUint16()
            else:
                data = None
                msgID = MSG_NONE
        else:
            datagram = None
            data = None
            msgID = MSG_NONE
        # Note, return datagram to keep a handle on the data
        return (datagram, data, msgID)

    def handleDatagram(self, data, msgID, client):
        """
        Check if there's a handler assigned for this msgID.
        Since we dont have case statements in python,
        I'm using a dictionary to avoid endless elif statements.
        """
	
	########################################################
	##
	## Of course you can use as an alternative smth like this:
	## if msgID == CMSG_AUTH: self.msgAuth(msgID, data, client)
	## elif...
	
        if msgID in Handlers.keys():
            Handlers[msgID](msgID,data,client)
        else:
            print "Unknown msgID: %d" % msgID
            print data
        return

    def msgAuth(self, msgID, data, client):
    
    #########################################################
    ##
    ## Okay... The client sent us some Data. We need to extract
    ## the data the same way it was placed into the buffer.
    ## Its like "first in, first out"
    ##
    
        username = data.getString()
        password = data.getString()
        
	## Now that we have the username and pwd, we need to
	## determine if the client has the right user/pwd-combination.
	## this variable will be sent later to the client, so lets
	## create/define it here.
	flag = None
        
        if not username in USERS.keys():
            # unknown user
	    ## That 0 is going to be sent later on. The client knows with
	    ## that 0 that the username was not allowed.
            flag = 0
        elif USERS[username] == password:
            # authenticated, come on in
            flag = 1
            CLIENTS[username] = 1
            print "User: %s, logged in with pass: %s" % (username,password)
        else:
            # Wrong password, try again or bugger off
            flag = 2
	    
	## again... If you have read the client.py first, you know what
	## I want to say. Do not use this type in a productive system.
	## If you want to use it, just define 0 and 1.
	## 1 -> Auth OK
	## 0 -> Username/Password combination not correct.
	## Otherwise its far too easy to get into the system...
        
        ## Creating a buffer to hold the data that is going to be sent.
	pkg = PyDatagram()
	
	## The first Bytes we send to the client in that paket will be
	## the ones that classify them as what they are. Here they mean
	## "Hi Client! I am an Auth Response from the server."
	## How does the client know that? Well, because both have a
	## definition saying "SMSG_AUTH_RESPONSE  = 2"
	## Due to shorter Network Pakets Yellow used Numbers instead
	## of the whole Name. So you will see a 2 in the paket if you
	## catch it somewhere...
        pkg.addUint16(SMSG_AUTH_RESPONSE)
        
	## Now we are sending, if the auth was
	## successfull ("1") or not ("0" or "2")
	pkg.addUint32(flag)

	## Now lets send the whole story...
        self.cWriter.send(pkg,client)

    def msgChat(self, msgID, data, client):
        
    	#########################################################
	##
	## This is again only an example showing you what you CAN
	## do with the received code... Example: Sending it back.
	## pkg = PyDatagram()
	## pkg.addUint16(SMSG_CHAT)
	## chatMsg=data.getString()
	## pkg.addString(chatMsg)
	## self.cWriter.send(pkg,client)
	## print 'ChatMsg: ',chatMsg
	
	## If you have trouble with the print command:
	## print 'ChatMsg: ',data.GetString() does the same.
	## Attention! The (partial) content of "data" is lost after the
	## first getString()!!!
        ## If you want to test the above example, comment the
        ## next line out...
	print "ChatMsg: %s" % data.getString()

    def msgDisconnectReq(self, msgID, data, client):
        pkg = PyDatagram()
        pkg.addUint16(SMSG_DISCONNECT_ACK)
        self.cWriter.send(pkg,client)
        del CLIENTS[client]
        self.cReader.removeConnection(client)

    def quit(self):
        self.cManager.closeConnection(self.tcpSocket)
        sys.exit()

        
# create a server object on port 9099
serverHandler = Server()

#install msg handlers
## For detailed information see def handleDatagram(self, data, msgID, client):
Handlers = {
    CMSG_AUTH           : serverHandler.msgAuth,
    CMSG_CHAT           : serverHandler.msgChat,
    CMSG_DISCONNECT_REQ : serverHandler.msgDisconnectReq,
    }

## The loop again... otherwise the program would run once and thats it ;)
run()

Hope, it helped in some ways… Additions and changes are welcome… Maybe we can get here a fully working example that is fully documented (as said, in a way that also ppl with slower minds (like me) can get it)…

Big Thanks to Yellow for writing the demo code. And also big thanks for answering all my annoying questions :wink:

BTW… commenting the text I found some more… Yellow?!? :smiley:

Regards, Bigfoot29

Edit: Here the files:

bios.kicks-ass.org/downloads … dClient.py
bios.kicks-ass.org/downloads … dServer.py

Hmmm… I am messing around with the server/client combination, but after I did some work with it, I wanted to see, how it looks over a real network…

I fired Ethereal up and everything looks fine… except the “[CHECKSUM INCORRECT]” that is written in each of my sent messages from client and server. I am getting an ACK (to each paket) that is correct, but my pakets seem to have a big problem… how can I create a correct checksum before sending the Paket? I think, generating a checksum is the last thing that is done by the OS before sending the paket in the end?

Regards, Bigfoot29

If you look at other TCP traffic you should see similar results. My understanding is that it’s due to the checksum being calculated after the point where ethereal captures it.

dont worry about that, thats all handled through the TCP protocol.

Yep, but I do worry because the Checksum error is something that shouldn’t happen - and does not happen with all other packets transmitted and received within the past hour…

Seems to be a problem with the implementation you/we used…

Regards, Bigfoot29

…now I am getting sad…

After some weeks work of “pimping my server” I wanted to test it “in the wild” - means: put the server @ a dedicated root and let the client try to connect to it…

Here at the local network 192.168.x.x everything is fine… but only as long as all clients/server are in the same subnet or/and there is no gateway between them…

What is the problem? Well, The server works as suspected. He uses the IP given and he accepts incomming transmissions (tried telneting it and the server told me “got new connection”). Now I let the Client connect to the server, but the client cowardly refuses to set up the connection (with everything set up properly). I did a tcpdump at the server and got the following result:

13:45:59.961455 IP 38-203-116-85.32845 > 213-239-209-253.9099: S 4261624326:4261624326(0) win 5840 <mss 1452,sackOK,timestamp 2130652 0,nop,wscale 0>
13:45:59.961619 IP 213-239-209-253.9099 > 38-203-116-85.32845: S 2229722743:2229722743(0) ack 4261624327 win 5792 <mss 1460,sackOK,timestamp 4600066 2130652,nop,wscale 2>
13:46:00.011241 IP 38-203-116-85.32845 > 213-239-209-253..9099: R 4261624327:4261624327(0) win 0

(I removed the DNS part, otherwise the lines would get far too long…)

Can anybody of you handle this? It seems as if the windows (win 5840 and win 5792) won’t match and thus the server/client can’t connect to each other…

But I am wondering why this is working in a local net - or even throught the internet… I guess my problem is, that the client thinks it has IP 192.168.x.x instead of 38.203.116.85 (here)…

How to solve that issue? :frowning:

Help very much appreciated :cry:

Regards, Bigfoot29

Edit: Uh… I left the core of the server the way Yellow made it… so it connects using the method Yellow used here… but at the server I don’t even get the message that he got a new connection (what is normal because he was not able to establish the “desired” one from the client) :frowning:

Could there be a firewall blocking traffic somewhere? For instance, Windows Firewall, or a firewall in your gateway box?

David

Nope, tried that also… but Yellow found the problem…

Instead of

                self.Connection = self.cManager.openTCPClientConnection(IP, PORT,1)

it should be

                self.Connection = self.cManager.openTCPClientConnection(IP, PORT,1000)

Thats a 1 milisecond connection timeout… no wonder, that the client got no reply within 1 msecs in a Environment with a DSL connection :smiley:

But of course, your idea was one possible solution. Thanks for your time trying to figure out, drwr.
Same to you, Yellow… was a great help :smiley:

Regards, Bigfoot29

Edit: The work is done! The server/client system is far from completion, but I wanted to have the system as “basic” as possible before adding very game specific sections to it…

  • See the showcase :slight_smile:

As for the Checksum error, I’ve seen the same issue on some python TCP networking code I wrote (unrelated to Panda3D, used the Twisted framework), and it also showed the incorrect checksum in Ethereal. I wouldn’t worry about it, however, as that code always worked fine for delivering the data, so I’d say it’s probably more an issue with Ethereal.

When I tried either the server.py or client.py code, it always conplained the following error:

Traceback (most recent call last):
  File "server.py", line 56, in ?
    class Server(DirectObject):
TypeError: Error when calling the metaclass bases
    module.__init__() takes at most 2 arguments (3 given)

Any ideas?

Looks like an import error due some changes in Panda3D structure… with what sort of Panda3D release did these errors occur? This software was written / tested using Panda 1.0.5 when I remember correctly… maybe you want to check that first - in case its working there, its an import problem like we do have them from time to time with a new release. :slight_smile:

If its not - well, dunno… - hit me :smiley:

Regards, Bigfoot29

Actually, that looks like a change in the way DirectObject should be imported. Make sure you are doing:

from direct.showbase.DirectObject import DirectObject

and not something like:

from direct.showbase import DirectObject

David

Or that way :slight_smile:
Question is: Was that a mistake in the server.py code I did when working with the stuff or were that changes to Panda3D made later on? wonders :slight_smile:

Regards, Bigfoot29

David,

Even if I modified (actually I added it since it didn’t exist) the line about DirectObject, it still complained about the same message.

These are the import statement written in server.py:

from pandac.PandaModules import *
import direct.directbase.DirectStart
from direct.showbase.DirectObject import *
from direct.distributed.PyDatagram import PyDatagram
from direct.distributed.PyDatagramIterator import PyDatagramIterator

Be sure you put the DirectObject import statement after the line that imports DirectGui. In fact, make sure it’s the very last import in the file. In versions of Panda prior to 1.3.0, importing DirectGui would inadvertently (and incorrectly) import DirectObject as a module.

David

Amazing! It’s working now… :laughing: :laughing: :laughing:

Following a trail of clues scattered around this forum I updated the Feature-Tutorials–Networking from the 1.0.5 release to work with the 1.3.2 release and even fixed a little bug :smiley: . This is the latest thread from which I got some info, so I’ll post my result here.

You can get the updated Tutorial here:
phys.uu.nl/~keek/panda/Featu … ing.tar.gz

Hey, I hope this topic isn’t too old to dredge up.

I was running through this thread as a means of trying to teach myself the Panda3D networking and followed all the steps thus far (even all the corrections) and still cannot get the server to run.

I keep getting the following error when running the server:

F:\dev\s-c_testing>python trial_server.py
DirectStart: Starting the game.
Warning: DirectNotify: category 'Interval' already exists
Known pipe types:
  wglGraphicsPipe
(all display modules loaded.)
:util(warning): Adjusting global clock's real time by 2.23452e-006 seconds.
:net(error): Unable to open TCP connection to server 127.0.0.1 on port 9099
Traceback (most recent call last):
  File "trial_server.py", line 242, in <module>
    aClient = Client()
  File "trial_server.py", line 51, in __init__
    self.cReader.addConnection(self.Connection)
TypeError: ConnectionReader.addConnection() argument 1 must be Connection, not NoneType

I looked up the culprit line:
self.Connection = self.cManager.openTCPClientConnection(IP, PORT,1000)
self.cReader.addConnection(self.Connection)

They seem fine to me though.
Looking through the Panda3D API I found the method call:

openTCPClientConnection PointerTo< Connection > ConnectionManager::open_TCP_client_connection(NetAddress const &address, int timeout_ms);

Which is exactly as it is in this code sample.

So I’m strugling to find the issue. :stuck_out_tongue: