Nisha Sharon
Nisha Sharon

Reputation: 91

Python: Multithreaded socket server runs endlessly when client stops unexpectedly

I have created a multithreaded socket server to connect many clients to the server using python. If a client stops unexpectedly due to an exception, server runs nonstop. Is there a way to kill that particular thread alone in the server and the rest running

Server:

class ClientThread(Thread):  
def __init__(self,ip,port): 
    Thread.__init__(self) 
    self.ip = ip 
    self.port = port 
    print("New server socket thread started for " + ip + ":" + str(port))

def run(self): 
    while True :
        try: 
            message = conn.recv(2048) 
            dataInfo = message.decode('ascii')
            print("recv:::::"+str(dataInfo)+"::")                         
        except:
            print("Unexpected error:", sys.exc_info()[0])
            Thread._stop(self)

tcpServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
tcpServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
tcpServer.bind((TCP_IP, 0))  
tcpServer.listen(10)
print("Port:"+ str(tcpServer.getsockname()[1]))
threads = []

while True: 
print( "Waiting for connections from clients..." )
(conn, (ip,port)) = tcpServer.accept() 
newthread = ClientThread(ip,port) 
newthread.start() 
threads.append(newthread) 

for t in threads: 
t.join() 

Client:

def Main():    
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((host,int(port)))
while True:
    try:
        message = input("Enter Command")
        s.send(message.encode('ascii'))
    except Exception as ex:
        logging.exception("Unexpected error:")
        break
        s.close()

Upvotes: 1

Views: 1198

Answers (1)

Hannu
Hannu

Reputation: 12205

Sorry about a very, very long answer but here goes.

There are quite a many issues with your code. First of all, your client does not actually close the socket, as s.close() will never get executed. Your loop is interrupted at break and anything that follows it will be ignored. So change the order of these statements for the sake of good programming but it has nothing to do with your problem.

Your server code is wrong in quite a many ways. As it is currently written, it never exits. Your threads also do not work right. I have fixed your code so that it is a working, multithreaded server, but it still does not exit as I have no idea what would be the trigger to make it exit. But let us start from the main loop:

while True: 
    print( "Waiting for connections from clients..." )
    (conn, (ip,port)) = tcpServer.accept() 
    newthread = ClientThread(conn, ip,port) 
    newthread.daemon = True
    newthread.start() 
    threads.append(newthread)   # Do we need this?

for t in threads: 
    t.join() 

I have added passing of conn to your client thread, the reason of which becomes apparent in a moment. However, your while True loop never breaks, so you will never enter the for loop where you join your threads. If your server is meant to be run indefinitely, this is not a problem at all. Just remove the for loop and this part is fine. You do not need to join threads just for the sake of joining them. Joining threads only allows your program to block until a thread has finished executing.

Another addition is newthread.daemon = True. This sets your threads to daemonic, which means they will exit as soon as your main thread exits. Now your server responds to control + c even when there are active connections.

If your server is meant to be never ending, there is also no need to store threads in your main loop to threads list. This list just keeps growing as a new entry will be added every time a client connects and disconnects, and this leaks memory as you are not using the threads list for anything. I have kept it as it was there, but there still is no mechanism to exit the infinite loop.

Then let us move on to your thread. If you want to simplify the code, you can replace the run part with a function. There is no need to subclass Thread in this case, but this works so I have kept your structure:

class ClientThread(Thread):  
    def __init__(self,conn, ip,port): 
        Thread.__init__(self) 
        self.ip = ip 
        self.port = port
        self.conn = conn
        print("New server socket thread started for " + ip + ":" + str(port))

    def run(self): 
        while True :
            try: 
                message = self.conn.recv(2048)
                if not message:
                    print("closed")
                    try:
                        self.conn.close()
                    except:
                        pass
                    return
                try:
                    dataInfo = message.decode('ascii')
                    print("recv:::::"+str(dataInfo)+"::")
                except UnicodeDecodeError:
                    print("non-ascii data")
                    continue                         
            except socket.error:
                print("Unexpected error:", sys.exc_info()[0])
                try:
                    self.conn.close()
                except:
                    pass
                return

First of all, we store conn to self.conn. Your version used a global version of conn variable. This caused unexpected results when you had more than one connection to the server. conn is actually a new socket created for the client connection at accept, and this is unique to each thread. This is how servers differentiate between client connections. They listen to a known port, but when the server accepts the connection, accept creates another port for that particular connection and returns it. This is why we need to pass this to the thread and then read from self.conn instead of global conn.

Your server "hung" upon client connetion errors as there was no mechanism to detect this in your loop. If the client closes connection, socket.recv() does not raise an exception but returns nothing. This is the condition you need to detect. I am fairly sure you do not even need try/except here but it does not hurt - but you need to add the exception you are expecting here. In this case catching everything with undeclared except is just wrong. You have also another statement there potentially raising exceptions. If your client sends something that cannot be decoded with ascii codec, you would get UnicodeDecodeError (try this without error handling here, telnet to your server port and copypaste some Hebrew or Japanese into the connection and see what happens). If you just caught everything and treated as socket errors, you would now enter the thread ending part of the code just because you could not parse a message. Typically we just ignore "illegal" messages and carry on. I have added this. If you want to shut down the connection upon receiving a "bad" message, just add self.conn.close() and return to this exception handler as well.

Then when you really are encountering a socket error - or the client has closed the connection, you will need to close the socket and exit the thread. You will call close() on the socket - encapsulating it in try/except as you do not really care if it fails for not being there anymore.

And when you want to exit your thread, you just return from your run() loop. When you do this, your thread exits orderly. As simple as that.

Then there is yet another potential problem, if you are not only printing the messages but are parsing them and doing something with the data you receive. This I do not fix but leave this to you.

TCP sockets transmit data, not messages. When you build a communication protocol, you must not assume that when your recv returns, it will return a single message. When your recv() returns something, it can mean one of five things:

  1. The client has closed the connection and nothing is returned
  2. There is exactly one full message and you receive that
  3. There is only a partial message. Either because you read the socket before the client had transmitted all data, or because the client sent more than 2048 bytes (even if your client never sends over 2048 bytes, a malicious client would definitely try this)
  4. There are more than one messages waiting and you received them all
  5. As 4, but the last message is partial.

Most socket programming mistakes are related to this. The programmer expects 2 to happen (as you do now) but they do not cater for 3-5. You should instead analyse what was received and act accordingly. If there seems to be less data than a full message, store it somewhere and wait for more data to appear. When more data appears, concatenate these and see if you now have a full message. And when you have parsed a full message from this buffer, inspect the buffer to see if there is more data there - the first part of the next message or even more full messages if your client is fast and server is slow. If you process a message and then wipe the buffer, you might have wiped also bytes from your next message.

Upvotes: 1

Related Questions