Search code examples
pythonmultithreadingsocketsconnectionpython-multithreading

Can't stop socket server thread from within


I have the following situation:
I have a class called Server, which is, as the name tells, the class which starts the server and is supposed to stop it. Here's the code:

import socket
import threading


class Server:
    def __init__(self, server_manager, host, port, running):
        # Define the host and port to listen on
        self.host = host
        self.port = port

        self.server_manager = server_manager

        # setting stop event up
        self.running = running

        # Create a socket object
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Bind the socket to the host and port
        self.server.bind((self.host, self.port))

        # Start listening for up to 5 incoming connections
        self.backlog = 5
        self.server.listen(self.backlog)

    def run(self):
        while not self.running.is_set():
            try:
                # Accept a connection from a client
                client_socket, client_address = self.server.accept()

                client_handler = threading.Thread(target=self.handle_client, args=(client_socket,))
                client_handler.start()
            except socket.error:
                pass

    def handle_client(self, client_socket):
        while not self.running.is_set():
            try:
                client_data = client_socket.recv(1024)  # Receive data from the client
                if not client_data:
                    break

                # Process the received data here

                self.server_manager.show_data(client_data)

                # Send a response back to the client (optional)
                response = "Successfully sent data! Now responding!"
                client_socket.send(response.encode('utf-8'))

            except Exception as e:
                print(f"Error: {e}")
            finally:
                client_socket.close()

        client_socket.close()

    def close(self):
        # close
        pass

I also have a class called ServerManager, which processes the data coming from the server. Again the code:

import threading
from ConnectionProcessor.connection import Server


class ServerManager:
    def __init__(self, host="127.0.0.1", port=6969):
        self.host = host
        self.port = port

        self.running = threading.Event()

        self.server = Server(server_manager=self, host=self.host, port=self.port, running=self.running)
        self.server_thread = threading.Thread(target=self.server.run)
        self.server_thread.start()

    def close(self):
        self.running.set()

    def show_data(self, data: str):
        print(data)

It works well, I can print the data coming from the server. My only problem is, that I can't close the server from my main class:

from ConnectionProcessor import ServerManager
from time import sleep

if __name__ == "__main__":
    server_manager = ServerManager()
    sleep(4)
    server_manager.close()

The server is still up and running.

I tried multiple things, also I tried changing self.running from threading.Event to a simple bool, but it has the same effect, it doesn't work.

This is my first time working with threads and socket, maybe it's just a simple mistake.


Solution

  • The problem is socket.accept doesn't return until a client connects, so the while loop condition won't check the running event without a client connecting. Set a timeout on the server socket so accept periodically returns:

        def run(self):
            while not self.running.is_set():
                try:
                    # Accept a connection from a client
                    self.server.settimeout(1)
                    client_socket, client_address = self.server.accept()
                except TimeoutError as e:
                    continue
                client_handler = threading.Thread(target=self.handle_client, args=(client_socket,))
                client_handler.start()
    

    Another option is to use select.select with a timeout to see if there is a waiting connection on the server:

    import select
    
    ...
    
        def run(self):
            while not self.running.is_set():
                readable, _, _ = select.select([self.server], [], [], 1.0)
                if readable:
                    client_socket, client_address = self.server.accept()
                    client_handler = threading.Thread(target=self.handle_client, args=(client_socket,))
                    client_handler.start()