Search code examples
qtsocketspyqt5threadpoolpython-multithreading

QT5, Threading, Sockets : Issue with GUI and QRunnable in Python


I'm looking for help for my project with threading and sockets. I have to make a communication between a server and few clients, and I have to do a GUI. The issue is that when I use QRunnable, the GUI (line edit, buttons) does not do anything. For example, my function sender in the AcceptThread does not run at all, same isse with update_reply. (so my SenderThread does not launch) I did the same script for my server with QThread and that works perfectly, but only for one user.

So I want my GUI update with the clients message, and that I can send messages with my Sever too. Thank you for your help.

import sys
import time
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import socket


flag = False
arret = False  

# Création d'une classe qui hérite de QThread pour gérer la réception des messages
class ReceiverThread(QThread):
    # Signal émis lorsque des messages sont reçus
    message_received = pyqtSignal(str)
    def __init__(self, connexion, server_socket):
        super().__init__()
        self.conn = connexion
        self.server_socket = server_socket

    # La méthode run est appelée lorsque le thread démarre
    def run(self):
        print("ReceiverThread Up")
        global flag, arret
        try:
            while not flag:
                recep = self.conn.recv(1024).decode()
                
                if recep == "arret" or recep == "bye":
                    print("Un client se déconnecte")
                    flag = True
                    if recep == "arret":
                        print("Arrêt du serveur")
                        arret = True

                elif not recep:
                    self.conn.close()
                    flag = False

                else:
                    print(f'User : {recep}\n')
                    # Émission du signal avec le message reçu
                    self.message_received.emit(recep)
            
        except Exception as err:
            print(err)

        print("ReceiverThread ends\n")
            
    def quitter(self):
        QCoreApplication.instance().quit()


class SenderThread(QThread):
    def __init__(self, reply, connexion):
        super().__init__()
        self.reply = reply
        self.conn = connexion

    def run(self):
        print("SenderThread Up")
        print(self.reply)
        try:
            try:
                self.conn.send(self.reply.encode())

            except ConnectionRefusedError as error:
                print(error)

            except ConnectionResetError as error:
                print(error)
        except Exception as err:
            print(err)
        
        print("SenderThread ends")

    def quitter(self):
        QCoreApplication.quit()


class AcceptThread(QRunnable):
    def __init__(self, log, send, connect, server_socket, host, i):
        super().__init__()
        self.log = log
        self.send = send
        self.connect = connect
        self.server_socket = server_socket
        self.host = host
        self.i = i
    
    
    def run(self):
        print(f"AcceptThread Up {self.i}")
        global flag, arret
        try:
            while not arret:
                print("En attente d'une nouvelle connexion")
                self.conn, self.address = self.server_socket.accept()
                self.connect.connect(self.sender)
                print(f"Nouvelle connexion de {self.host} !")

                self.receiver_thread = ReceiverThread(self.conn, self.server_socket)
                self.receiver_thread.message_received.connect(self.update_reply)      
                self.receiver_thread.start()
                self.receiver_thread.wait()
                #on attend la fin du thread receive avant de close la connexion
                self.conn.close()

                flag = False 
                #on remet la variable globale flag en False pour que la boucle du ReceiverThread fonctionne
                
                #nécessaire sinon à chaque itération de la boucle il y a une nouvelle connexion du bouton à la fonction sender
            self.server_socket.close()
            print("Socket closed")
            self.quitter()


        except Exception as err:
            print(err)
        
        print(f"AcceptThread ends {self.i}")
    
    # Méthode appelée pour mettre à jour l'interface utilisateur avec le message reçu
    def update_reply(self, message):
        self.log.setText(message)
    
    def listen(self):
        self.server_socket.listen(100)
    
    def sender(self):
        reply = self.send.text()
        self.sender_thread = SenderThread(reply, self.conn)
        self.sender_thread.start()
        self.sender_thread.wait()
    
    def quitter(self):
        QCoreApplication.instance().quit()

# Classe de la fenêtre principale
class Window(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi()

    def setupUi(self):
        self.setWindowTitle("Serveur")
        self.resize(250, 150)
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        # Création et connexion des widgets
        self.label = QLabel("Logs")
        self.label2 = QLabel("Message Serveur")
        self.label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.label2.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)

        self.line_edit = QLineEdit()
        self.line_edit2= QLineEdit()

        self.countBtn = QPushButton("Envoyer")
        self.btn_quit = QPushButton("Quitter")
        self.dialog = QPushButton("?")

        self.line_edit.setEnabled(False)
        self.dialog.clicked.connect(self.button_clicked)
        self.btn_quit.clicked.connect(self.quitter)

        # Configuration du layout
        layout = QGridLayout()
        layout.addWidget(self.label, 0, 0)
        layout.addWidget(self.label2, 2, 0)
        layout.addWidget(self.line_edit, 1, 0)
        layout.addWidget(self.line_edit2, 3, 0)
        layout.addWidget(self.countBtn, 4, 0)
        layout.addWidget(self.btn_quit, 5, 0)
        layout.addWidget(self.dialog, 5, 1)

        self.centralWidget.setLayout(layout)

        self.label.setText(f"Log du serveur")
        self.line_edit.setText(f"")

        self.main_thread()

    def main_thread(self):
        log = self.line_edit
        send = self.line_edit2
        connect = self.countBtn.clicked

        host, port = ('0.0.0.0', 11111)
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server_socket.bind((host, port))
        self.host = socket.gethostname()
        self.listen()

        #threadCount = QThreadPool.globalInstance().maxThreadCount()
        threadCount = 2
        pool = QThreadPool.globalInstance()
        
        for i in range(threadCount):
            runnable = AcceptThread(log, send, connect, self.server_socket, self.host, i)
            pool.start(runnable)


    def button_clicked(self, s):
        dlg = QMessageBox(self)
        dlg.setWindowTitle("Aide")
        dlg.setText("Centrale du Serveur.")
        dlg.setStandardButtons(QMessageBox.Ok)
        dlg.setIcon(QMessageBox.Question)
        dlg.exec()

    # Méthode appelée lorsqu'on clique sur le bouton Quitter
    def quitter(self):
        global flag, arret
        flag = True
        arret = True
        QCoreApplication.instance().quit()

    def listen(self):
        self.server_socket.listen(100)


# Configuration de l'application PyQt
app = QApplication(sys.argv)
window = Window()
window.show()

sys.exit(app.exec())

Client

import socket 
import threading
import time, sys

flag = False
arret = False

def envoi(client_socket):
    global flag, arret
    while flag == False:
        try:
            message = str(input(">"))
       
            client_socket.send(message.encode())

            if message == "arret" or message == "bye":
                print("Arret du serveur")
                flag = True
                arret = True

        except ConnectionRefusedError as error:
            print(error)
            main()

        except ConnectionResetError as error:
            print(error)
            main()
    print("Arret de la Thread envoi")
                

def reception(client_socket):
    global flag, arret
    while not flag:
        try:
            reply = client_socket.recv(1024).decode("utf-8")

            if not reply:
                print("Le serveur n'est plus accessible...")
                flag = True
                arret = True
    
            else:
                print(f'Serveur : {reply}')
                
        except ConnectionRefusedError as error:
            print(error)
            main()

        except ConnectionResetError as error:
            print(error)
            main()

        except BrokenPipeError as error:
            print(f'{error} : Wait a few seconds')
            main()
    print("Arret de la Thread reception")


def main():
    try :
        host, port = ('127.0.0.1', 11111)
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client_socket.connect((host,port))

        while not arret:
            t1 = threading.Thread (target=reception, args=[client_socket])
            t2 = threading.Thread (target=envoi, args=[client_socket])
            t1.start()
            t2.start()

            t1.join()
            t2.join()

            client_socket.close()

    except ConnectionRefusedError:
        print("Impossible de se connecter")
        time.sleep(5)
        main()


if __name__ == '__main__':
    main()

Solution

  • I forgot QRunnable thing and just did a list "all_thread" for the client connections. Then I just append new connections in the list or remove when they disconnect. I used QThread instead of QRunnable and do not try to make a Thread for each connections now.

    import sys
    import time
    from PyQt5.QtCore import *
    from PyQt5.QtWidgets import *
    import socket
    
    
    flag = False
    arret = False  
    
    # Création d'une classe qui hérite de QThread pour gérer la réception des messages
    class ReceiverThread(QThread):
        # Signal émis lorsque des messages sont reçus
        message_received = pyqtSignal(str)
        def __init__(self, connexion, server_socket, all_threads):
            super().__init__()
            self.conn = connexion
            self.server_socket = server_socket
            self.all_threads = all_threads
    
        # La méthode run est appelée lorsque le thread démarre
        def run(self):
            print("ReceiverThread Up")
            global flag, arret
            try:
                while not flag:
                    recep = self.conn.recv(1024).decode()
    
                    **if recep == "arret" or recep == "bye":
                        print("Un client se déconnecte")
                        for conn in self.all_threads:
                            if conn != self.conn:
                                continue
                            else:
                                # Fermer uniquement la connexion qui a dit "bye"
                                conn.close()
                                self.all_threads.remove(conn)**
    
                        if recep == "arret":
                            print("Arrêt du serveur")
                            arret = True
                            self.quitter()
    
                    elif not recep:
                        for conn in self.all_threads:
                            if conn != self.conn:
                                continue
                            else:
                                # Fermer uniquement la connexion qui a dit "bye"
                                conn.close()
                                self.all_threads.remove(conn)
    
                    else:
                        print(f'User : {recep}\n')
                        # Émission du signal avec le message reçu
                        self.message_received.emit(recep)
                
            except Exception as err:
                print(f"{err}")
    
            print("ReceiverThread ends\n")
                
        def quitter(self):
            for conn in self.all_threads:
                conn.close()
            self.server_socket.close()
            QCoreApplication.instance().quit()
    
    
    class SenderThread(QThread):
        def __init__(self, reply, all_threads):
            super().__init__()
            self.reply = reply
            self.all_threads = all_threads
    
        def run(self):
            print("SenderThread Up")
            print(self.reply)
            try:
                try:
                    **for conn in self.all_threads:
                        conn.send(self.reply.encode())**
                except ConnectionRefusedError as error:
                    print(error)
    
                except ConnectionResetError as error:
                    print(error)
            except Exception as err:
                print(err)
            
            print("SenderThread ends")
    
        def quitter(self):
            QCoreApplication.instance().quit()
    
    class AcceptThread(QThread):
        def __init__(self, server_socket, log, send, connect):
            super().__init__()
            self.server_socket = server_socket
            self.log = log
            self.send = send
            self.connect = connect
        
        def run(self):
            print("AcceptThread Up")
            global flag, arret
            **self.all_threads = []**
            try:
                host, port = ('0.0.0.0', 11111)
                self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                self.server_socket.bind((host, port))
                self.host = socket.gethostname()
                self.listen()
                self.connect.connect(self.sender)
                while not arret:
                    print("En attente d'une nouvelle connexion")
                    self.conn, self.address = self.server_socket.accept()
                    print(f"Nouvelle connexion de {self.host} !")
    
                    self.receiver_thread = ReceiverThread(self.conn, self.server_socket, self.all_threads)
                    self.receiver_thread.message_received.connect(self.update_reply)
                    self.receiver_thread.message_received.connect(self.send_everyone)      
                    self.receiver_thread.start()
                    **self.all_threads.append(self.conn)**
       
                    #nécessaire sinon à chaque itération de la boucle il y a une nouvelle connexion du bouton à la fonction sender
                
                else:
                    for conn in self.all_threads:
                        conn.close()
    
                    self.server_socket.close()
                    print("Socket closed")
                    self.quitter()
    
    
            except Exception as err:
                print(err)
            
            print("AcceptThread ends")
        
        # Méthode appelée pour mettre à jour l'interface utilisateur avec le message reçu
        def update_reply(self, message):
            self.log.append(f'{self.host}: {message}') 
        
        def listen(self):
            self.server_socket.listen(100)
        
        def sender(self):
            reply = self.send.text()
            self.sender_thread = SenderThread(f'Serveur : {reply}', self.all_threads)
            self.sender_thread.start()
            self.sender_thread.wait()
    
        def send_everyone(self, message):
            print("message")
            self.sender_thread = SenderThread(message, self.all_threads)
            self.sender_thread.start()
            self.sender_thread.wait()
        
        def quitter(self):
            QCoreApplication.instance().quit()
    
    # Classe de la fenêtre principale
    class Window(QMainWindow):
        def __init__(self, parent=None):
            super().__init__(parent)
            self.setupUi()
    
        def setupUi(self):
            self.setWindowTitle("Serveur")
            self.resize(250, 150)
            self.centralWidget = QWidget()
            self.setCentralWidget(self.centralWidget)
            # Création et connexion des widgets
            self.label = QLabel("Logs")
            self.label2 = QLabel("Message Serveur")
            self.label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
            self.label2.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
    
            self.text_edit = QTextEdit()
            self.text_edit.setReadOnly(True)
            self.line_edit2= QLineEdit()
    
            self.countBtn = QPushButton("Envoyer")
            self.btn_quit = QPushButton("Quitter")
            self.dialog = QPushButton("?")
    
            self.dialog.clicked.connect(self.button_clicked)
            self.btn_quit.clicked.connect(self.quitter)
    
            # Configuration du layout
            layout = QGridLayout()
            layout.addWidget(self.label, 0, 0)
            layout.addWidget(self.label2, 2, 0)
            layout.addWidget(self.text_edit, 1, 0, 1, 2) 
            layout.addWidget(self.line_edit2, 3, 0)
            layout.addWidget(self.countBtn, 4, 0)
            layout.addWidget(self.btn_quit, 5, 0)
            layout.addWidget(self.dialog, 5, 1)
    
            self.centralWidget.setLayout(layout)
    
            self.label.setText(f"Log du serveur")
            self.text_edit.setText(f"")
    
            self.main_thread()
    
        def main_thread(self):
            log = self.text_edit
            send = self.line_edit2
            connect = self.countBtn.clicked
    
            self.accept_thread = AcceptThread(self, log, send, connect)
            self.accept_thread.start()
    
    
        def button_clicked(self, s):
            dlg = QMessageBox(self)
            dlg.setWindowTitle("Aide")
            dlg.setText("Centrale du Serveur.")
            dlg.setStandardButtons(QMessageBox.Ok)
            dlg.setIcon(QMessageBox.Question)
            dlg.exec()
    
        # Méthode appelée lorsqu'on clique sur le bouton Quitter
        def quitter(self):
            global flag, arret
            flag = True
            arret = True
            QCoreApplication.instance().quit()
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        window = Window()
        window.show()
        sys.exit(app.exec())