Threaded UDP Networking

I’ve been trying really hard to get my mind around networking, having not been able to sit down and apply it to my Panda project, I decided I would just post my sample classes, maybe some insight can be given into how to apply it, or fix it.

This is the Client code:

import socket
import sys
import threading

class UDPClient(threading.Thread):

    _server = None
    _msgs = []

    def __init__(self, host='localhost',port=9999,size=1024):
    
        self.host = host
        self.port = port
        self.size = size
        self.is_running = threading.Event()
        threading.Thread.__init__(self)

    def start_server(self):

        try:
            self._server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self._server.connect((self.host,self.port))
            self._server.setblocking(False)
        except socket.error, (value,message):
            if self._server:
                self._server.close()
            print "Could not connect socket: " + message
            sys.exit(1)

    def run(self):

        if not self._server:
            self.start_server()

        while not self.is_running.isSet():
            #Do stuff
            self.poll_server()
            if len(self._msgs) > 0:
                self.send_msg()

    def poll_server(self):

        try:
            data = self._server.recv(self.size)
            self.process_data(data)

        except socket.error:
            pass

    def process_data(self, data):
        print data

    def send_msg(self):

        for i in range(len(self._msgs)):
            msg = self._msgs.pop(i)
            self._server.send(msg)

    def add_message(self, data):
        self._msgs.append(data)

    def shutdown(self):
        self.is_running.set()
        self.join()
        self._server.close()

if __name__ == "__main__":

    c = UDPClient()
    c.start()
    print 'Started thread'
    going = True
    while going:
        line = raw_input()
        if line.startswith('quit'):
            going = False
        else:
            c.add_message(line)
            
    c.shutdown()

And the Server:
Note, I might have gone a little overboard with handling bad clients feel free to ignore it

import socket
import sys
import threading

class UDPClient(threading.Thread):

    _server = None
    _msgs = []

    def __init__(self, host='localhost',port=9999,size=1024):
    
        self.host = host
        self.port = port
        self.size = size
        self.is_running = threading.Event()
        threading.Thread.__init__(self)

    def start_server(self):

        try:
            self._server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self._server.connect((self.host,self.port))
            self._server.setblocking(False)
        except socket.error, (value,message):
            if self._server:
                self._server.close()
            print "Could not connect socket: " + message
            sys.exit(1)

    def run(self):

        if not self._server:
            self.start_server()

        while not self.is_running.isSet():
            #Do stuff
            self.poll_server()
            if len(self._msgs) > 0:
                self.send_msg()

    def poll_server(self):

        try:
            data = self._server.recv(self.size)
            self.process_data(data)

        except socket.error:
            pass

    def process_data(self, data):
        print data

    def send_msg(self):

        for i in range(len(self._msgs)):
            msg = self._msgs.pop(i)
            self._server.send(msg)

    def add_message(self, data):
        self._msgs.append(data)

    def shutdown(self):
        self.is_running.set()
        self.join()
        self._server.close()

if __name__ == "__main__":

    c = UDPClient()
    c.start()
    print 'Started thread'
    going = True
    while going:
        line = raw_input()
        if line.startswith('quit'):
            going = False
        else:
            c.add_message(line)
            
    c.shutdown()

Comments and suggestions appreciated

You do not want to use threads like that in panda3d. (IMHO threads are hard! And they sux in python).

Panda3d also provided cross-platform abstraction over the UDP and TCP socket - use it - its nice! I think what you want is to create a panda3d task that does the networking. There is plenty of networking examples on the forums.

Yeah, I realized that later on. Panda3D and Python threads do not get along at all.

Have you seen this page?
panda3d.org/manual/index.php/Threading

Why not use the built-in panda connection managers? They handle the threading for the connections. Here’s the code I’m using right now:

class Server(DirectObject):
    def __init__( self ):
        self.version = "0.01a"
        print "\n\nInitializing Server (" + str(self.version) + ")\n"
        
        self.port = 9099
        self.portStatus = "Closed"
        self.host = "localhost"
        self.backlog = 1000
        self.Connections = {}
        self.StartConnectionManager()
        # output a status box to the console which tells you the port
        # and any connections currently connected to your server

    ##################################################################
    #              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 "\n---------------\nSOMEBODY CONNECTED"
                    print "IP Address: " + str(newConnection.getAddress())
                    print "Connection ID: " + str(newConnection.this)
                    print "---------------"
                    # you can delete this, I've left it in for debugging
                    # purposes
        return Task.cont  

    def ConnectionManagerTASK_Listen_For_Datagrams(self, task):
        if self.cReader.dataAvailable():
            datagram=NetDatagram()  
            if self.cReader.getData(datagram):
                self.ProcessDatagram(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 "\n---------------\nSOMEBODY DISCONNECTED"
            print "IP Address: " + str(lostConnection.getAddress())
            print "ConnectionID: " + str(lostConnection.this)
            print "---------------"
            
            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

To make this work for UDP, you would just need to change:

        self.tcpSocket = self.cManager.openTCPServerRendezvous(self.port,self.backlog)

To this (I might not be entirely right on the syntax here):

        self.tcpSocket = self.cManager.openUDPServerRendezvous(self.port,self.backlog)

What this does is use the built-in panda connection manager system, which creates a thread for each client that connects to you. It will spit out a simple ‘SOMEBODY CONNECTED’ and ‘SOMEBODY DISCONNECTED’ message to the console with their connection information (useful for debugging).

It also stores all your connections in a dictionary for easy reference (the self.Connections variable).

Taking this exact code and changing the 1 line I mentioned might work (might require some debugging, I haven’t done UDP networking in panda but I’m pretty sure they are very similar in coding for the connection manager).

From here all you need to do is create a function called ProcessDatagram which actually handles your datagrams.

Also - are you sure you want to use UDP? It’s not guaranteed, which means packets can get lost in transmission.