Python: Sockets Programming - Non-blocking Client


Sockets are an operation system construct allowing Inter-Process Communication (IPC) over a network. Sockets is how you are able to browse the internet, watch movies or download files from your computer.

In a previous article we built a sockets client using blocking calls. In this article, we modify the code of the blocking socket client to make the data receive and send non-blocking. The socket mode for blocking/non-blocking can be set using the setblocking(value) or settimeout(value) functions of the socket class. Another important function is select which is a blocking/non-blocking (depending on mode) and is used to determine if the socket has data to read. Without select if we try to read data and there is none, we will get an exception which will have to be handled. Using select is a more cleaner way (compared to a try-except block) of doing non-blocking calls so that exceptions are used only for capturing genuine errors.

We also need a while loop to execute any non-blocking call so that we continuously check if the socket has data to read. If your program has to perform another action (like updating the UI), while data in socket is awaited that can be done in the while loop. As we are doing a single read, once data arrives in the socket we can exit the while loop and then read the socket.

In the following code we show how we write a basic non-blocking socket client. This socket client connects to a website (http://www.dasmic.com) and sends a 'GET' HTTP request. This socket client has similar functionality to the popular program 'telnet' and the following image shows the output from telnet (in Windows 10) when the same request to port 80 is made, by entering the command 'telnet www.dasmic.com 80' in the command prompt.


Communication through sockets is an I/O operation and as we learned in a previous article, it is good practice to run I/O operation in a separate thread. In the code the tcpSocketThreadClassNonBlocking inherits from the threading.thread class and is run in a separate thread as we learned in this article. The demoSocketNonBlockingClass class instantiates tcpSocketThreadClassNonBlocking and starts the threads, while main() is used as the entry code that instantiates demoSocketNonBlockingClass. Inline comments in the code further explain the purpose of important functions.


import threading
import socket
import select

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

# The run() function in this class will be executed in its own thread
class tcpSocketThreadClassNonBlocking(threading.Thread):
    def __init__(self, host, port):
        threading.Thread.__init__(self)
        self.host = host
        self.port = port

    def run(self):
        print("--------- in socket thread.run()")
        dataTX = bytes('GET /\n', 'utf-8')

        # None is equivalent to null
        # dataRX will be a bytes list
        dataRX = None
        tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Set the socket to non-blocking/blocking
        # Non-blocking: tcpSock.settimeout(1)
        # Blocking: tcpSock.settimeout(0)
        # During connect set the server to Blocking, as using a non-blocking call
        # does not help as you can't do anything with the socket till its connected
        tcpSock.setblocking(1)
        # Also it is a good idea to set a timeout to define how long the socket should
        # block (for the connection)
        # NOTE: Setting a time-out to a non-zero value also has the impact of making the socket
        # blocking. Hence though the previous tcpSock.setblocking(1) call is not required
        # we still use it to make it explicit that the socket is blocking.
        tcpSock.settimeout(5)

        try:
            print("Connecting to server via Socket")
            tcpSock.connect((self.host, self.port))
            print("Connected to socket ")
        except:
            print("Could not connect to server")
            # Socket not connected, no point in going further so exit
            return

        # Since we have a connection now, set the socket to non-blocking
        tcpSock.setblocking(0)

        # sendall() will send all data in the buffer
        # internally, it uses the send() function
        tcpSock.sendall(dataTX)
        readableSockets = [tcpSock]
        # Now receive data up to a maximum of BUFFER_SIZE at a time
        # Note: This is a non-blocking call and if there is no data in the socket
        # there will be an error
        flag = False

        # Since the socket is non-block we will have to run a loop to get the data
        while True:
            # The select call is important. Since the mode is blocking, it will be a
            # non-blocking call that returns a list of # sockets
            # (from a list of potential candidate sockets passed as parameters)
            # that have data to read, are writable or have errors.
            # In this case we only pass our socket among the list of potential candidates
            readable, writable, error = select.select(readableSockets, [], [], 0)
            # Once we have the list of sockets which are readable
            # iterate through the readable socket(s). We only passed a single potential
            # socket so there will only be one socket in readable
            # If there is not no data in out socket, readable will be blank and no
            # data will be read from the socket. The while loop ensures that we can
            # continuously call select so that when data arrives it can be read successfully
            for rs in readable:
                # No need to check if rs is tcpSock as readable will only have one socket
                # Set flag to true to mark that socket has data and
                # while loop can be exited since we are doing only one read
                # of the data
                    flag = True
                # Do any other action here while you wait for socket data to arrive
            if flag is True:
                break  # exit while loop

        # At this point socket has data so we can read the socket
        try:
            dataRX = rs.recv(SIZE_BUFFER)
        except BlockingIOError:
            print('Error in reading data from socket')

        if len(dataRX) != 0:
            # Convert bytes list into a string
            dataRXStr = dataRX.decode("utf-8")
            # Print the string
            print("Data received from server:", dataRXStr)
        else:
            print("Remote server closes connection and did:", dataRX)

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

# Class to instantiate the threading class and run the thread
class demoSocketNonBlockingClass:
    def runSocketThread(self):
        print("----------- runSocketThread")
        # Connect to the HTTP (port 80) of server www.dasmic.com
        # You can also specify an IP address here
        socketThread = tcpSocketThreadClassNonBlocking("www.dasmic.com", 80)
        print("Starting socket thread")
        socketThread.start()
        print("Socket thread started")

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

# Call the main() function
main()

The output by calling the main() function is given below. Note how the printed value of the dataRXStr is similar to the output from telnet shown above. This is because we are connected to the same web server and send the same 'GET /' request.

----------- runSocketThread
Starting socket thread
--------- in socket thread.run()
Socket thread started
Connecting to server via Socket
Connected to socket 
Data received from server: 



403 - Forbidden: Access is denied.



403 - Forbidden: Access is denied.

You do not have permission to view this directory or page using the credentials that you supplied.

Successfully closed socket Process finished with exit code 0


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