Serwer chatu na wątkach - wyłączenie wątku

0

Postanowiłem napisać prostą aplikację serwer-klient jednak jestem dość "świeży" w temacie wątków przez co nie wiem jak poprawnie wyłączyć serwer (bez używania Ctrl-C z klawiatury).

 #!/usr/bin/env python3

import socket
import sys
import threading
import select
from funkcje import to_bytes, to_strings

host = 'localhost'

class ManagerThread(threading.Thread):

    def __init__(self, servsocket):
        threading.Thread.__init__(self)
        self.servsocket = servsocket
        self.is_running = True

    def run(self):
        while self.is_running:
            try:
                (read, write, error) = select.select([self.servsocket] + klienci, [], [])

                for connection in read:

                    if connection == self.servsocket:
                        (connection, address) = self.servsocket.accept()
                        klienci.append(connection)

                    else:
                        message = to_strings(connection.recv(1024))
                        method = message[0]
                        username = message[1]

                        if method == "JOIN":
                            print("Połączono z użytkownikiem", username, "pod adresem",  connection.getsockname())
                            polaczenia[username] = connection.getsockname()
                            send_all(to_bytes(username, 1), connection)

                        elif method == "MSG":
                            msg = message[2]
                            send_all(to_bytes(username, 0, msg), connection)

                        elif method == "QUIT":
                            print("Użytkownik", username, "rozłączył się")
                            klienci.remove(connection)
                            connection.close()
                            polaczenia.pop(username)
                            send_all(to_bytes(username, 2))


            except socket.error as blad:
                print("Błąd przy odbieraniu połączenia!")
                print("Opis: ", str(blad))
                sys.exit()

    def stop(self):
        self.is_running = False
        self.servsocket.close()
        self.join()

def main():

    global klienci, port, polaczenia
    polaczenia = {}
    klienci = []

    if len(sys.argv) < 2:
        port = 6789
    else:
        port = int(sys.argv[1])

    # Tworzenie gniazda TCP / IPv4
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    except socket.error as blad:
        print("Błąd przy próbie uruchomienia serwera!")
        print("Opis: ", str(blad))
        sys.exit()

    # Dowiązanie do portu
    try:
        s.bind((host,port))
    except socket.error as blad:
        print("Błąd: nie udało się dowiązać gniazda do portu %d" % port)
        print("Opis: ", str(blad))
        sys.exit()

    print("Serwer uruchomiony")
    print("Gniazdo nasłuchujące przypisane do portu %d" % port)

    try:
        s.listen(10)
    except socket.error as blad:
        print("Błąd przy nasłuchu gniazda!")
        print("Opis: ", str(blad))
        sys.exit()

    serwer = ManagerThread(s)
    serwer.start()

    while True:
        menu= 20 * "-" + "\nKomendy:\n\n" + "! - wyłącz serwer\n" + "? - status\n" + 20 * "-" + "\n"
        command = input(menu)

        if command == "!":
            serwer.stop()
            s.close()
            sys.exit()
        elif command == "?":
            for user, conn in polaczenia.items():
                print(user, conn)

def send_all(message, sender=None):

    for klient in klienci:
        if klient == sender:
            continue
        klient.send(message)

if __name__ == "__main__":
    main()

Jak widać próbowałem użyć w wątku metody stop, jednak niestety nie robi ona nic. Jedyny sposób wyłączenia aplikacji to podwójne ctrl-c w okienku konsoli co generuje poniższy traceback:

 Traceback (most recent call last):
  File "serwer.py", line 121, in <module>
    main()
  File "serwer.py", line 106, in main
    serwer.stop()
  File "serwer.py", line 59, in stop
    self.join()
  File "/usr/lib/python3.5/threading.py", line 1054, in join
    self._wait_for_tstate_lock()
  File "/usr/lib/python3.5/threading.py", line 1070, in _wait_for_tstate_lock
    elif lock.acquire(block, timeout):
KeyboardInterrupt
^CException ignored in: <module 'threading' from '/usr/lib/python3.5/threading.py'>
Traceback (most recent call last):
  File "/usr/lib/python3.5/threading.py", line 1288, in _shutdown
    t.join()
  File "/usr/lib/python3.5/threading.py", line 1054, in join
    self._wait_for_tstate_lock()
  File "/usr/lib/python3.5/threading.py", line 1070, in _wait_for_tstate_lock
    elif lock.acquire(block, timeout):
KeyboardInterrupt
1

Jest nieźle :)

Poprawiłbym dwie rzeczy:

  1. W wątku ManagerThread w select.select dodaj timeout (czwarty parametr) i ustaw go np. na 0.1 - wtedy co 0.1 sekundy select będzie przerywać (chyba jakiś exception wtedy leci - rzuć okiem w dokumentacji), dzięki czemu warunek z początku pętli (self.is_running) będzie sprawdzany.
    W innym wypadku warunek jest sprawdzany jedynie przy okazji jakichś zdarzeń na socketach.

  2. Z metody stop przesunąłbym self.servsocket.close() do metody run na sam koniec (tak, żeby wykonało się po zakończeniu pętli while). Pozwoli to uniknąć dziwnych rzeczy typu zamknięcie socketa w jednym wątku podczas gdy inny wątek ma go w select.
    Szczerze mówiąc, nie wiem czy to coś zmienia na systemie którego używasz, natomiast w takich wypadkach (tj. w wypadkach niewiedzy ;>) lepiej jest grać ostrożnie i unikać sytuacji tego typu (co też robi zaproponowana zmiana).

Dodam, że osobiście lubię wątku włączać w trybie demona (przed serwer.start() robisz serwer.daemon = True) - powoduje to tyle, że dany wątek nie blokuje zamknięcia procesu przy Ctrl-C. Natomiast to co robisz (czyste zakończenie wszystkich wątków i serwera) jest oczywiście lepszym (bardziej eleganckim) rozwiązaniem.

1 użytkowników online, w tym zalogowanych: 0, gości: 1