Thursday 22 December 2011

Asynchronous Socket Programming in Python

How to transfer data from one client to another through a server? The solution is very easy using the select call supported in sockets.

To understand the benefit of select call in sockets, one should understand asynchronous socket programming. Assume a case where many clients connect to a server and send data for processing concurrently, then the server has to handle the clients asynchronously.

In synchronous socket programming, the server processes each client sequentially, in this case when it waits for a response/data from a client using the recv call, it blocks or in other words the recv call cannot return until there is some data received from the socket, in a real time scenario, this way of handling clients is inefficient in the sense that all other connected clients need to wait till the server completes processing the current one.

Therefore one needs a more elegant way to asynchronously handle client requests or the ability to read, write from multiple sockets whenever they are ready to be read or written, which is where the select call comes handy.

To explain the use of select system call, I will illustrate a TCP/IP chat Client server program, where the functionalities of the server and the client program were mentioned below.


TCP/IP Chat Server:

1. Accepts connection from multiple clients
2. Use select call to get the list of available sockets which are ready to be read.
3. Whenever a client connects, the server notifies all other connected clients of this new connection, in the same way the server notifies all when a client quits or that client connection is lost.
4. The server broadcasts data sent by a client to all other connected clients.

TCP/IP Chat Client:

1. Connects to the server and starts two threads, one to process received data and one for getting data input to be sent to other connected clients through the server.
2. When the client quits (using q or Q) or the server is suddenly down (handle the worst case scenario), the socket is closed and the process exits.
3. Whenever any thread closes the socket connection, it interrupts the main program using the thread.interrupt_main() call, then the main exits.

The syntax used for the select call is as follows

read_sockets,write_sockets,error_sockets = select.select(CONNECTION_LIST,[],[])

The select call returns three lists, the list of sockets which are ready to be read, written and those which caused an error, since we are interested only in the list of sockets which are ready to be read, we use only a single select input parameter.


The client and server Chat program were implemented in python for the ease of understanding. Try to see how the chat client/server works by launching multiple clients in different windows connected to the chat server.

Server.py
# The server accepts connection from multiple clients and
# broadcasts data sent by a client to all other clients
# which are online (connection active with server)

import socket
import select
import string

def broadcast_data (sock, message):
    """Send broadcast message to all clients other than the
       server socket and the client socket from which the data is received."""
    
    for socket in CONNECTION_LIST:
        if socket != server_socket and socket != sock:            
            socket.send(message)

if __name__ == "__main__":

    # List to keep track of socket descriptors
    CONNECTION_LIST=[]

    # Do basic steps for server like create, bind and listening on the socket
    
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(("127.0.0.1", 5000))
    server_socket.listen(10)

    # Add server socket to the list of readable connections
    CONNECTION_LIST.append(server_socket)

    print "TCP/IP Chat server process started."

    while 1:
        # Get the list sockets which are ready to be read through select
        read_sockets,write_sockets,error_sockets = select.select(CONNECTION_LIST,[],[])

        for sock in read_sockets:

            if sock == server_socket:
                # Handle the case in which there is a new connection recieved
                # through server_socket
                sockfd, addr = server_socket.accept()
                CONNECTION_LIST.append(sockfd)
                print "Client (%s, %s) connected" % addr
                broadcast_data(sockfd, "Client (%s, %s) connected" % addr)

            else:
                # Data recieved from client, process it
                try:
                    #In Windows, sometimes when a TCP program closes abruptly,
                    # a "Connection reset by peer" exception will be thrown
                    data = sock.recv(4096)
                except:
                    broadcast_data(sock, "Client (%s, %s) is offline" % addr)
                    print "Client (%s, %s) is offline" % addr
                    sock.close()
                    CONNECTION_LIST.remove(sock)
                    continue

                if data:
                    # The client sends some valid data, process it
                    if data == "q" or data == "Q":
                        broadcast_data(sock, "Client (%s, %s) quits" % addr)
                        print "Client (%s, %s) quits" % addr
                        sock.close()
                        CONNECTION_LIST.remove(sock)
                    else:
                        broadcast_data(sock, data)                       
                
    server_socket.close()    

client.py
# The client program connects to server and sends data to other connected 
# clients through the server
import socket
import thread
import sys

def recv_data():
    "Receive data from other clients connected to server"
    while 1:
        try:
            recv_data = client_socket.recv(4096)            
        except:
            #Handle the case when server process terminates
            print "Server closed connection, thread exiting."
            thread.interrupt_main()
            break
        if not recv_data:
                # Recv with no data, server closed connection
                print "Server closed connection, thread exiting."
                thread.interrupt_main()
                break
        else:
                print "Received data: ", recv_data

def send_data():
    "Send data from other clients connected to server"
    while 1:
        send_data = str(raw_input("Enter data to send (q or Q to quit):"))
        if send_data == "q" or send_data == "Q":
            client_socket.send(send_data)
            thread.interrupt_main()
            break
        else:
            client_socket.send(send_data)
        
if __name__ == "__main__":

    print "*******TCP/IP Chat client program********"
    print "Connecting to server at 127.0.0.1:5000"

    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect(('127.0.0.1', 5000))

    print "Connected to server at 127.0.0.1:5000"

    thread.start_new_thread(recv_data,())
    thread.start_new_thread(send_data,())

    try:
        while 1:
            continue
    except:
        print "Client program quits...."
        client_socket.close()       



2 comments:

  1. Select with blocking sockets? Really?

    ReplyDelete
  2. Thanks for the article.....it solved my problem..i will spread the word

    ReplyDelete