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
-
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.
-
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.
-
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()