Python: Sockets Programming - Multi-Threaded Server


In a previous article, we built a simple socket server that uses only one thread to service client connections, allowing it to service only one client at a time. New clients were able to connect but could not receive or send data to/from the socket server. In this article, we look at how to create a multi-threaded socket server to serve an unlimited number of clients.

We use the same code base as the single threaded server and extend it to create multiple threads. The parent socket sets up a listening socket as before and when new clients connect a new child socket object is created. Unlike the single threaded server, we pass the child socket object to a new thread class which then handles all communication with the client in its own thread. Using a while loop the parent socket then goes back to listen for more connections and creates new threads for new connections passing the reference of the child sockets which are created.The code that executes in it own thread and communication with clients remains the same as for the single threaded class.

The following is the code for a multi-threaded socket server. As for the client, the socket.socket() call returns a socket object (tcpParentSock). The extra calls of tcpParentSock.bind() and tcpParentSock.listen(1) are used to bind the socket to a specific address (host/ipaddress and port combination) and start accepting client connections, respectively. The parameter to the tcpParentSock.listen() call is 1 which will allow one connection to connect and wait in the queue to be processed. However, since we process the new connection immediately new clients will not have to wait and the queue will be more or less empty. Setting a value of 1 for number of queued connections give some room in case the server becomes slow. As shown in another article on multi-threading a new class deriving from threading.Thread is used to process new connections in separate threads. Using telnet we then show how this socket server is able to serve multiple clients.

Inline comments in the code highlight the purpose of each important line of code.

import socket
import threading

# Set the buffer size to receive data
# It is recommended that this be a power of 4
SIZE_BUFFER = 4096

class tcpServerParentSocketClass():
    def __init__(self, host, port):
        self.host = host
        self.port = port

    def start(self):
        # Get a socket object
        tcpParentSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Bind socket to local host and port
        try:
            tcpParentSock.bind((self.host, self.port))
        except:
            print('Could not bind to socket. Make sure the host/socket combination is not in use,')
            return #exit program

        # Start listening on socket
        # The parameter value being passed determines the backlog
        # which is the number of connections that are allowed to be held
        # in queue without being refused. Since we create a new thread for each
        # client we can service any new connection almost immediately. Hence,
        # this value can be kept small allowing refusal of new clients only in
        # situations where resources are low and new threads cannot be created.
        tcpParentSock.listen(1)

        # Wait to accept a connection
        # This call will block till a client connects
        # Once a client connects we will get a new socket object 'newChildSock' which
        # will be used to send an receive data
        # We delegate the communication with the new client to a new thread
        # which is spawned for each new client connection. This way we can service
        # and unlimited number of clients and will be only limited by the resources
        # available to service the connections
        count = 0
        try:
            while(1):
                # This while loop with continue indefinitely till the process
                # is terminated or an error is encountered. One can implement some
                # logic to exit the while loop based on keyboard input on the server
                count+=1 # increment to keep count of client connections
                newChildSock, addr = tcpParentSock.accept()
                print('Connected with ' + addr[0] + ':' + str(addr[1]))
                # Apart from child socket passing some other information for display
                # purposes
                socketThread = tcpServerChildSocketClass(newChildSock,
                                                         self.host,
                                                         self.port, count)
                print("Starting socket thread")
                socketThread.start()
        except:
                print("Error in main parent thread")

        # Once out of the while loop close the socket
        try:
            # It is good practise to explicitly shutdown the socket before closing it
            # with the SHUT_RDRW option any further ends to this socket are disallowed
            # other options are SHUT_WR and SHUT_RD
            tcpParentSock.shutdown(socket.SHUT_RDWR)
            # Now close the socket for proper cleanup
            tcpParentSock.close()
            print("Successfully closed parent socket")
        except:
            print("Could not close parent socket")


class tcpServerChildSocketClass(threading.Thread):
    def __init__(self, newChildSock,host,port,count):
        threading.Thread.__init__(self)
        self.newChildSock = newChildSock
        self.host = host
        self.port = port
        self.count = count

    def run(self):
        print("--------- In Child Socket Thread")
        # Send some data to the client using the new Socket
        dataTX = bytes('Welcome ! You have connected to ' + self.host + ' on port ' + str(self.port) + '\r\n', 'utf-8')
        self.newChildSock.sendall(dataTX)
        dataTX = bytes('You are client #' + str(self.count) +
                       ' to connect to this server !\r\n', 'utf-8')
        self.newChildSock.sendall(dataTX)
        dataTX = bytes('Please press any key in your console, after that this server socket will close\r\n', 'utf-8')
        self.newChildSock.sendall(dataTX)
        # The following call will block till 1 byte of data comes in
        dataRX = self.newChildSock.recv(SIZE_BUFFER)
        dataRXStr = dataRX.decode("utf-8")
        print('Data received from client: ' + dataRXStr)
        print('Closing the child socket now')
        try:
            # It is good practise to explicitly shutdown the socket before closing it
            # with the SHUT_RDRW option any further ends to this socket are disallowed
            # other options are SHUT_WR and SHUT_RD
            self.newChildSock.shutdown(socket.SHUT_RDWR)
            # Now close the socket for proper cleanup
            self.newChildSock.close()
            print("Successfully closed child socket")
        except:
            print("Could not close child socket")


# Class to instantiate the threading class and run the thread
class demoSocketServerMTClass:
    def startSocketServer(self):
        print("----------- startSocketServer")
        # Specify the hostname and port for the server to run
        # Host name of blank "" means the server will run on all available
        # IP addresses of the current machine. We can also give a specific ip address
        # to run only in that port
        socketServer = tcpServerParentSocketClass("", 8001)
        print("Starting Socket Server ...")
        socketServer.start()
        print("Started Socket Server")

# main() function which contain the high level routines
def main():
    dsc = demoSocketServerMTClass()
    dsc.startSocketServer()

# Call the main() function
main()

We will make a connection to this server using the telnet command. If we run telnet localhost 8001 it will connect to this server. The output in telnet and on the Server side is given below:

Telnet:


Output on server side:

----------- startSocketServer
Starting Socket Server ...
Connected with 127.0.0.1:60267
Starting socket thread
--------- In Child Socket Thread
Data received from client: 

Closing the child socket now
Successfully closed child socket

Since this is a multi-threaded server it can service multiple clients which we now demonstrate using 4 telnet windows, just like we did in the previous article. As you can see all 4 telnet clients are able to connect unlike the single threaded case where only one client was able to connect. Output on the server side is also given.

Telnet:

Output on server side:

----------- startSocketServer
Starting Socket Server ...
Connected with 127.0.0.1:59823
Starting socket thread
--------- In Child Socket Thread
Connected with 127.0.0.1:59843
Starting socket thread
--------- In Child Socket Thread
Connected with 127.0.0.1:59851
Starting socket thread
--------- In Child Socket Thread
Connected with 127.0.0.1:59889
Starting socket thread
--------- In Child Socket Thread
Data received from client: 
Closing the child socket now
Successfully closed child socket
Data received from client: 
Closing the child socket now
Successfully closed child socket
Data received from client: 
Closing the child socket now
Successfully closed child socket
Data received from client: l
Closing the child socket now
Successfully closed child socket

Process finished with exit code -1

Comments

Popular posts from this blog

Part III: Backpropagation mechanics for a Convolutional Neural Network

Introducing Convolution Neural Networks with a simple architecture

Deriving Pythagoras' theorem using Machine Learning