Asynchronous Socket Programming using Non Blocking Sockets

Sunday, August 31, 2008

In my last article related socket programming, we have seen how to use select call to process multiple clients in an asynchronous way with a multi client server TCP/IP Chat program, now we will see how to implement a multi client server program in an asynchronous way using non blocking sockets. The main motivation for me in writing this one is till date I never found a complete implementation of a TCP Server handling multiple clients (accepting new connections and processing existing connections concurrently) using non blocking sockets. This code will work exactly like the way I implemented multiple client server communication using select, but it will use non blocking sockets, so there will be subtle difference in the way the server part is implemented.

Just to give a customary introduction of non blocking sockets, by default a socket is in a blocking mode, in other words when using recv call to read data from a socket, it will return only after some data is recieved, or it blocks till some data is is available in the client socket. In a typical client server scenario where many clients connect to the server, each client has to wait till the previous client socket is processed, resulting in inefficient use of system resources due to this block (idle time), therefore we need a solution where calls like recv, accept should not block, there comes non blocking sockets.

When a socket is set to non blocking mode, it will not block on calls like recv, when there is no data to be read on a socket, an exception will be thrown, the server can just ignore this socket exception and continue processing the next client and so on, thus multiple clients can be processed without any delay due to blocking.

The implementation language used is python, due to the simplicity in using the socket calls, I have used two threads, one to process existing TCP connections and another to accept new connections (for a basic introduction on using threads in python, read my article on thread programming here), the server socket is to non blocking mode to ensure that the accept call which is used to accept new connections will not block, as and when a client socket connects to the server, the client socket will also be set to non blocking mode, to ensure that the server will not block on recv call and thus can handle multiple clients without waiting for data to be read on a given client socket.

I have used locks to ensure that the threads used for accepting and processing client connections run into completion without context switch, though I have observed that without locks also the code would work similar, we will stick to the better programming approach. The only difference you will note here compared to my multi client server chat program using select call is that there would be a minor delay when a client connects to the server because the new connection would be accepted only when the thread which processes an existing connection will run to completion.

So here we go.

TCP/IP Chat Server:

1. Accepts connection from multiple clients
2. Use non blocking sockets to accept and process client connections.
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.

TCP/IP Non blocking Chat Server: tcpchatserver_nonblocking.py
#
# tcpchatserver_nonblocking.py
# TCP/IP Chat server
# Author: S.Prasanna
# 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
import thread
import sys, time
import traceback

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."""

global CONNECTION_LIST

for socket in CONNECTION_LIST:
if socket != sock:
socket.send(message)

def accept_connection():

global CONNECTION_LIST, RECV_BUFFER

try:

while 1:

threadlock.acquire()

try:

#print "waiting for accept"
sockfd, addr = server_socket.accept()
# Set socket to non-blocking mode
sockfd.setblocking(0)
CONNECTION_LIST.append(sockfd)
print "Client (%s, %s) connected" % addr
broadcast_data(sockfd, "Client (%s, %s) connected" % addr)

except:
pass

threadlock.release()

except:
#Handle the case when client program is terminated with Ctrl-C
#catch the exception and exit
pass

def process_connection():

global CONNECTION_LIST, RECV_BUFFER

try:

while 1:

#print "waiting for packet"
for sock in CONNECTION_LIST:

threadlock.acquire()

try:

data = sock.recv(RECV_BUFFER)

if data:

# The client sends some valid data, process it

if data == "q" or data == "Q":

broadcast_data(sock, "Client (%s, %s) quits" % sock.getpeername())
print "Client (%s, %s) quits" % sock.getpeername()
sock.close()
CONNECTION_LIST.remove(sock)

else:

broadcast_data(sock, data)

except:

#Exception thrown, get the error code and do cleanup actions
socket_errorcode = sys.exc_value[0]

if socket_errorcode == 10054:

# Connection reset by peer exception
# In Windows, sometimes when a TCP client program closes abruptly,
# or when you press Ctrl-C a "Connection reset by peer" exception will be thrown

broadcast_data(sock, "Client (%s, %s) quits" % sock.getpeername())
print "Client (%s, %s) quits" % sock.getpeername()
sock.close()
CONNECTION_LIST.remove(sock)

else:
# The socket is not ready for reading, which results in an exception,
# ignore this and pass on with the next client socket (without blocking)
# The exception you will see here is
# "The socket operation could not complete without blocking"
pass

threadlock.release()

except:
#Handle the case when server program is terminated with Ctrl-C
#catch the exception and exit
pass

if __name__ == "__main__":

# List to keep track of socket descriptors
CONNECTION_LIST=[]
RECV_BUFFER=4096 # Advisable to keep it as an exponent of 2

# 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)

# Set the server socket in non blocking mode, so that the server can accept connections
# without blocking
server_socket.setblocking(0)

threadlock = thread.allocate_lock()

print "TCP/IP Chat server process started."

#Invoke two threads to accept connections and handle eisting connections (client data)
thread.start_new_thread(accept_connection, ())
thread.start_new_thread(process_connection, ())

try:
while 1:
pass
except:
server_socket.close()

TCP/IP Chat Client: tcpchatclient_nonblocking.py
#
# tcpchatclient_nonblocking.py
# TCP/IP Chat client
# Author: S.Prasanna
# The client program connects to server and sends data to other connected
# clients through the server
#

import socket
import thread
import sys

HOST = '127.0.0.1' # The remote host
PORT = 5000 # The same port as used by the server
RECV_BUFFER=4096

def recv_data():
"Receive data from other clients connected to server"
while 1:
try:
recv_data = client_socket.recv(RECV_BUFFER)
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((HOST, PORT))

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

6 comments:

Alan Kennedy said...

Hi Prasanna,

Nice tutorial.

Have you tried your asynchronous code on jython?

Jython's socket module can do asynchronous now, and we have a select module as well ...

Jython socket module

Jython select module

Regards,

Alan Kennedy.

Prasanna Seshadri said...

Hi Alan,

Thanks for the pointers, these socket modules sounds interesting, seems the effort to catch up with CPython intensifies and good to see activity in jython development.

I will write more about jython in future, do look out (especially jython 2.5)

Prasanna Seshadri said...

Hi Alan,

I read those articles on Select module and NewSocketModule in jython, it was really interesting, keep up your efforts to be friendly with both CPython and Java (The design was very logical and upto the point).

muthu said...

Hi Prasana,

Is it possible for you to post the Non-Blocking Server - Client example in C language program, It will be helpful for me to understand..

Pytte said...

This pretty much makes the CPU run at 100% on 1 core or god forbid if you have a single core machine the whole CPU.

Is there anywhere you can put a sleep without breaking the code?

Thành Nhân Phạm said...

thanks a lot ^^


Copyright © 2008 Prasanna Seshadri, www.prasannatech.net, All Rights Reserved.
No part of the content or this site may be reproduced without prior written permission of the author.