Mise en place d'une application de Tchat en ligne en python

Résolu
alka93 Messages postés 3 Date d'inscription mercredi 24 mai 2023 Statut Membre Dernière intervention 24 mai 2023 - 24 mai 2023 à 02:34
alka93 Messages postés 3 Date d'inscription mercredi 24 mai 2023 Statut Membre Dernière intervention 24 mai 2023 - 24 mai 2023 à 23:41

Bonjour,

Dans le cadre de mon projet qui consiste à la mise en place d’une application de Tchat sous python
L’objectif est de proposer un système client-serveur permettant à plusieurs utilisateurs de se connecter sur le serveur et de pouvoir se communiquer  avec les autres utilisateurs via ce dernier.
Vous devez être en mesure de permettre à ces utilisateurs de se connecter sur l’application de chat via une interface de connexion. Il doit être déjà inscrit.
Une fois connecté il  est directement redirigé vers l’interface de Tchat.
Si l’utilisateur envoie un message il sera diffusé à tous les autres utilisateurs sauf celui qui a envoyé ce message.
L’utilisateur s’il le désire  aura la possibilité d’envoyer des messages à un seul utilisateur parmi ceux qui sont connectés.
Bon pour le moment la partie consacrée à l'envoi des messages par diffusion multicast est déjà achevée et il me reste la partie unicast réservée à l'envoi de messages ciblés privées unicast dont je n'arrive pas à gérer . Du coup je compte sur un appui de quelqu'un à travers cette plateforme d'échange et de partage pour pouvoir terminer l'application.
voici le code coté serveur :
 import tkinter as tk
from socket import *
from threading import Thread

class ChatServerGUI(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Serveur de Chat")
        self.geometry("400x300")

        self.text_area = tk.Text(self, background="white")
        self.text_area.place(x=20, y=20, width=360, height=200)

        self.button_shutdown = tk.Button(self, text="Arrêter", fg="black", font=("Times New Roman", 12), command=self.stop_server)
        self.button_shutdown.place(x=280, y=240, width=100, height=30)

        self.server_socket = None
        self.clients = set()
        self.port_entry = tk.Entry(self)
        self.port_entry.place(x=100, y=240, width=100, height=30)
        self.start_button = tk.Button(self, text="Démarrer", fg="black", font=("Times New Roman", 12), command=self.start_server)
        self.start_button.place(x=200, y=240, width=70, height=30)

    def start_server(self):
        port = self.port_entry.get()
        if not port:
            tk.messagebox.showerror("Erreur", "Veuillez saisir le numéro de port.")
            return

        try:
            port = int(port)
            self.server_socket = socket(AF_INET, SOCK_STREAM)
            self.server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
            self.server_socket.bind(("localhost", port))
            self.server_socket.listen(5)

            self.text_area.insert(tk.END, "Le serveur écoute sur le port {}\n".format(port))
            self.text_area.insert(tk.END, "Serveur démarré sur le port {}\n".format(port))

            self.start_button.config(state=tk.DISABLED)
            self.port_entry.config(state=tk.DISABLED)

            # Démarrer le serveur dans un thread séparé
            server_thread = Thread(target=self.accept_connections)
            server_thread.start()

        except ValueError:
            tk.messagebox.showerror("Erreur", "Le numéro de port doit être un entier.")

    def accept_connections(self):
        while True:
            client_socket, client_address = self.server_socket.accept()
            self.clients.add(client_socket)
            self.text_area.insert(tk.END, "Connexion établie avec: {}\n".format(client_address[0] + ":" + str(client_address[1])))
            thread = Thread(target=self.client_thread, args=(client_socket, client_address))
            thread.start()

    def client_thread(self, client_socket, client_address):
        while True:
            try:
                message = client_socket.recv(1024).decode("utf-8")
                self.text_area.insert(tk.END, "{}: {}\n".format(client_address[0] + ":" + str(client_address[1]), message))
                self.broadcast_message("{} says: {}".format(client_address[0] + ":" + str(client_address[1]), message), client_socket)
                if not message:
                    self.clients.remove(client_socket)
                    self.text_area.insert(tk.END, "{} disconnected\n".format(client_address[0] + ":" + str(client_address[1])))
                    break
            except ConnectionResetError:
                self.clients.remove(client_socket)
                self.text_area.insert(tk.END, "{} disconnected\n".format(client_address[0] + ":" + str(client_address[1])))
                break

        client_socket.close()

    def broadcast_message(self, message, sender_socket):
        for client in self.clients:
            if client is not sender_socket:
                client.send(message.encode("utf-8"))

    def unicast_message(self, recipient_address, message):
        recipient_socket = None
        for client_socket in self.clients:
            client_address = client_socket.getpeername()
            if client_address[0] == recipient_address[0] and str(client_address[1]) == recipient_address[1]:
                recipient_socket = client_socket
                break

        if recipient_socket is not None:
            recipient_socket.send(message.encode("utf-8"))
            self.text_area.insert(tk.END, "Message unicast envoyé à {}: {}\n".format(recipient_address, message))
        else:
            self.text_area.insert(tk.END, "Impossible d'envoyer le message unicast à {}: Client non trouvé\n".format(recipient_address))

    def stop_server(self):
        for client in self.clients:
            client.close()
        self.server_socket.close()
        self.text_area.insert(tk.END, "Serveur arrêté\n")
        self.quit()

if __name__ == "__main__":
    server = ChatServerGUI()
    server.mainloop()
voici le code coté client: 
import tkinter as tk
from tkinter import messagebox
from tkinter import ttk
import socket
from threading import Thread
import datetime

class ChatWindow(tk.Tk):
    def __init__(self, client_socket, username, disconnect_callback, connected_users_callback):
        super().__init__()
        self.title("Chat")
        self.geometry("600x400")

        self.username = username

        self.welcome_panel = tk.Frame(self, bg="gray")
        self.welcome_panel.pack(fill=tk.X)

        self.username_label = tk.Label(self.welcome_panel, text="Utilisateur: {}".format(username), font=("Arial", 12), bg="gray", fg="green")
        self.username_label.pack(pady=5)

        self.welcome_label = tk.Label(self.welcome_panel, text="Bienvenue dans le Chat", font=("Arial", 16), bg="gray")
        self.welcome_label.pack(pady=5)

        current_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.date_label = tk.Label(self.welcome_panel, text="Date actuelle: {}".format(current_date), bg="gray")
        self.date_label.pack()

        self.text_area = tk.Text(self, background="white")
        self.text_area.pack(fill=tk.BOTH, expand=True)

        self.input_frame = tk.Frame(self)
        self.input_frame.pack(pady=10)

        self.destination_label = tk.Label(self.input_frame, text="Destination:")
        self.destination_label.pack(side=tk.LEFT)

        self.destination_field = tk.Entry(self.input_frame, width=50)
        self.destination_field.pack(side=tk.LEFT, padx=5)

        self.input_frame2 = tk.Frame(self)
        self.input_frame2.pack(pady=10)

        self.input_label = tk.Label(self.input_frame2, text="Message:")
        self.input_label.pack(side=tk.LEFT)

        self.input_field = tk.Entry(self.input_frame2, width=50)
        self.input_field.pack(side=tk.LEFT, padx=5)

        self.send_button = tk.Button(self.input_frame2, text="Envoyer", command=self.send_message)
        self.send_button.pack(side=tk.LEFT)

        self.private_message_button = tk.Button(self.input_frame2, text="Message privé", command=self.send_private_message)
        self.private_message_button.pack(side=tk.LEFT)

        self.client_socket = client_socket
        self.username = username
        self.disconnect_callback = disconnect_callback
        self.connected_users_callback = connected_users_callback

        self.private_conversations = {}  # Dictionnaire pour stocker les conversations privées

        self.receive_thread = Thread(target=self.receive_messages)
        self.receive_thread.start()

    def send_message(self):
        message = self.input_field.get()
        if message:
            full_message = "[{}]: {}".format(self.username, message)
            self.client_socket.sendall(full_message.encode())
            self.input_field.delete(0, tk.END)

    def send_private_message(self):
        recipient = self.destination_field.get()
        message = self.input_field.get()
        if message and recipient:
            full_message = "[PRIVATE][{}][{}]: {}".format(self.username, recipient, message)
            self.client_socket.sendall(full_message.encode())
            self.input_field.delete(0, tk.END)

            # Vérifier si la conversation privée existe déjà
            if recipient in self.private_conversations:
                private_chat = self.private_conversations[recipient]
                private_chat.append("[Vous][{}]: {}".format(recipient, message))
            else:
                # Créer une nouvelle conversation privée
                private_chat = ["[Vous][{}]: {}".format(recipient, message)]
                self.private_conversations[recipient] = private_chat

    def receive_messages(self):
        try:
            while True:
                message = self.client_socket.recv(1024).decode()

                if message.startswith("[CONNECTED_USERS]"):
                    connected_users = message[17:].split(',')
                    self.connected_users_callback(connected_users)
                elif message.startswith("[PRIVATE]"):
                    self.handle_private_message(message)
                else:
                    self.text_area.insert(tk.END, message + "\n")
        except ConnectionError:
            messagebox.showinfo("Information", "La connexion au serveur a été interrompue.")
            self.disconnect_callback()

    def handle_private_message(self, message):
        sender = message[9:].split(']')[0]
        content = message.split(']: ')[1]

        if sender == self.username:
            recipient = message.split('][')[1]
            if recipient in self.private_conversations:
                private_chat = self.private_conversations[recipient]
                private_chat.append(message)
            else:
                private_chat = [message]
                self.private_conversations[recipient] = private_chat
        else:
            if sender in self.private_conversations:
                private_chat = self.private_conversations[sender]
                private_chat.append(message)
            else:
                private_chat = [message]
                self.private_conversations[sender] = private_chat

        self.text_area.insert(tk.END, message + "\n", "private_message")

class ChatUserGUI(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Interface Utilisateur")
        self.geometry("400x300")

        self.welcome_panel = tk.Frame(self, bg="gray")
        self.welcome_panel.pack(fill=tk.X)

        self.welcome_label = tk.Label(self.welcome_panel, text="Bienvenue dans notre Tchat", font=("Arial", 14), bg="gray")
        self.welcome_label.pack(pady=10)

        current_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.date_label = tk.Label(self.welcome_panel, text="Date actuelle: {}".format(current_date), bg="gray")
        self.date_label.pack()

        self.port_label = tk.Label(self, text="Port:")
        self.port_label.pack(pady=5)
        self.port_entry = tk.Entry(self)
        self.port_entry.pack(pady=5)

        self.ip_label = tk.Label(self, text="Adresse IP:")
        self.ip_label.pack(pady=5)
        self.ip_entry = tk.Entry(self)
        self.ip_entry.pack(pady=5)

        self.username_label = tk.Label(self, text="Identifiant:")
        self.username_label.pack(pady=5)
        self.username_entry = tk.Entry(self)
        self.username_entry.pack(pady=5)

        self.connect_button = tk.Button(self, text="Connexion", command=self.connect_to_server)
        self.connect_button.pack(pady=10)

    def connect_to_server(self):
        port = self.port_entry.get()
        ip = self.ip_entry.get()
        username = self.username_entry.get()

        if not port or not ip or not username:
            messagebox.showerror("Erreur", "Veuillez saisir toutes les informations requises.")
            return

        try:
            port = int(port)
            client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            client_socket.connect((ip, port))

            chat_window = ChatWindow(client_socket, username, self.disconnect_from_server, self.connected_users_callback)
            self.destroy()
            chat_window.mainloop()

        except ValueError:
            messagebox.showerror("Erreur", "Le port doit être un nombre entier.")
        except ConnectionRefusedError:
            messagebox.showerror("Erreur", "La connexion au serveur a été refusée.")

    def disconnect_from_server(self):
        self.destroy()

    def connected_users_callback(self, connected_users):
        self.update_connected_users(connected_users)

    def update_connected_users(self, connected_users):
        self.connected_users_combobox['values'] = connected_users
        self.connected_users_combobox.current(0)

if __name__ == "__main__":
    chat_user = ChatUserGUI()
    chat_user.mainloop()


Windows / Chrome 113.0.0.0

4 réponses

Ton code est plutôt long, je l'ai lu en diagonale.
Je me demande toujours pourquoi on fait du multithreading avec les socket ...
Je suppose que tous tes utilisateurs ont un pseudonyme.
Dans ce système, on ne peut pas entrer de commande, donc tu ne sais pas comment envoyer un message personnel? Est-ce là le seul problème?
Je pourrais suggérer ceci:
avant le broadcast, tu vérifies si le message commence par un caractère spécial, disons le '@'
si le message commence par '@', tu supposes que ce qui suit est un pseudonyme (jusqu'au premier espace).
Tu vérifies si le pseudonyme est connecté.
Si oui, tu initialises un échange personnel, sinon tu envoies un message d'erreur à l'envoyeur.
En fait, je n'ai pas compris si tu passais dans un autre mode ou si les messages personnels sont ponctuels et si le récepteur doit répondre de la même façon.
Dans ce cas, le message suivrait le pseudonyme.
exemple: tu m'envoies un message:
@PierrotLeFou pourquoi m'as-tu envoyé ce message que tous le monde reçoit?

je te répond:

@alka93 Je ne savais pas comment t'envoyer un message personnel.

Ça peut ouvrir la porte à d'autres applications.

Si je veux avoir la liste de toutes les personnes connectées:

@@list

je veux savoir si un usager est connecté:

@@connected Einstein

1
alka93 Messages postés 3 Date d'inscription mercredi 24 mai 2023 Statut Membre Dernière intervention 24 mai 2023
24 mai 2023 à 23:41

voici le résultat de mes test par envoi broadcast et unicast .

pour ce test c'est obrian qui envoi un message privé à badman et ce dernier reçoit ceci 

127.0.0.1:56567 says: [obrian]: salut
127.0.0.1:56567 says: [PRIVATE][obrian][@badman]: bonsoir

le problème majeur se trouve ici pour spyderman qui n'est pas censé etre le destinataire du message privé et qui l'a reçu en privé .


127.0.0.1:56567 says: [PRIVATE][obrian][@badman]: bonsoir.

je compte votre aide la dessus .

0
alka93 Messages postés 3 Date d'inscription mercredi 24 mai 2023 Statut Membre Dernière intervention 24 mai 2023
24 mai 2023 à 04:36

Merci pour votre suggestion .

je pense selon votre analyse j'ai déjà fait de tout ce qui est envoi par broadcast à tous les utilisateurs connectés au niveau du serveur mais mon problème majeur se trouve au niveau de l'envoi unicast c'est à dire l'envoi de messages privés à u  tiers pour expliciter davantage quant trois utilisateurs se connectent avec les pseudos respectifs jean , Paul et Antoine . si Jean veut envoyer un message privé à Paul il choisira comme destinataire le pseudo de Paul sur le champ de saisi destinataire ce dernier à son tour recevra le message envoyé par jean en privé excepté de Antoine qui n'aura pas aces à ce message au niveau de sa zone de tchat . 

grosso modo dans un premier temps il s'agit de faire un envoi diffusé à tous les user et dans un second un envoi ciblé et privé .

Bien sûr que j'ai utilisé un séparateur pour ca le code fonctionne bien le problème est que au niveau de cet envoi unicast comme je l'ai démontré ci dessous l'utilisateur Antoine  a reçu ce message qui était destiné à Paul et non lui .c'est ce qui m'étonne 

0
PierrotLeFou
24 mai 2023 à 05:34

Si je considère la ligne suivante:
            if client_address[0] == recipient_address[0] and str(client_address[1]) == recipient_address[1]:
Pourquoi convertis-tu le premier [1] en str et pas l'autre?

0

Salut.

Il faudrait déjà que ton code fasse que ta méthode unicast_message soit utilisée, ce n'est pas le cas.

Il y a aussi un problème avec le server qui ne se ferme pas (lock_acquire fail), obligé de le kill avec le terminal, aussi mettre le foreground du text, car chez moi (avec mon thème graphique)  c'est blanc sur blanc, donc invisible, les fenêtres clients ne font pas apparaître les champs de saisies et boutons si on ne met en plein écran.

Ton code devrait aussi séparer les applications serveur, client des interfaces graphiques, ce serait bien plus lisible, et plus modulable, c-à-d pouvoir l'utiliser dans un autre type d'environnement, simple console, autres GUI, etc.

Sinon, complètement d'accord avec PierrotLeFou concernant le @ qui est devenu un standard dans les chats.

0