Messenger Programme

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Hallo Leute,
ich möchte euch gerne zwei Projekte vorstellen. Diese behandeln das gleiche Thema und sind im gleichen Respository auf Github zu finden (siehe: https://github.com/Domroon/MessengerOne). Ich werde den Quellcode beider Dateien (chat.py (Projekt 1) und messenger.py (Projekt 2)) aber auch am Ende dieses Beitrags reinstellen.

ACHTUNG: Diese beiden Projekte sollen keinesfalls guten Programmierstyl präsentieren und sind meiner Meinung nach ein manchen Stellen etwa "vermurckst". Soll bedeuten, dass alles unübersichtlich und schwer zu verstehen sein kann. Tut mir leid an dieser Stelle. Diese Projekte sollten dafür dienen, um das TCP-Protokoll, Sockets und Verbindungen zwischen zwei Rechnern zu verstehen. Macht euch bitte deshalb keine Mühe den Programmcode zu verbessern. Ich werde diese beiden Programme sowieso nicht weiter verbessern. Ich werde mich stattdessen zwei weiteren Projekten (3 und 4) widmen:

Projekt 3: Ein "ordentliches" Chatprogramm, welches sauberen, gut lesbaren und "pythonischen" Programmcode enthält. Ich werde hier weniger mit "nackten Sockets" arbeiten. Vielmehr werde ich mich dem Modul "socketserver" widmen, welches mir viele Aufgaben abnimmt die ich Versucht habe zu programmieren. Die Klasse "ThreadingMixIn" sieht hier sehr vielversprechend aus. Ich möchte gerne eine eigene Klasse schreiben welche von dieser erbt. (An dieser Stelle möchte ich meine Begeisterung für Vererbung aussprechen. Hiermit ist es möglich den Gedankengang von dem Programmierer von "socketserver" zu verstehen und in den eigenen Programmcode zu implementieren - das ist auch der Grund warum ich neue Projekte anfangen werde, anstatt meine Programme zu verbessern)

Projekt 4: Ein Programm aufbauend auf dem Chatprogramm, welches Nachrichten verschlüsselt übertragen kann. Vielversprechend hört sich hier das ssl-Modul an welches ja als "TLS/SSL wrapper for socket objects" "beworben" wird. Da ich mich bestimmt erstmal in das Thema Verschlüsselung einfinden muss, ist dies der Grund das dieses Chatprogramm ein weiteres Projekt wird.

Aber nun erstmal zurück zu meinen beiden Projekten (1 und 2), welche schon existieren:

chat.py (Projekt 1):
Ich möchte hier grob beschreiben, was mein Programm tut.
Generell muss "Wait Client" auf einem beliebigem Rechner ausgeführt werden. Auf dem gleichen Rechner oder einem anderen Rechner im lokalen Netzwerk muss das Programm nochmals als "Search Client" gestartet werden.

A) Das macht der Wait-Client:
1. In einem Thread wird ein socket-Objekt erzeugt, welche auf einem vorgegeben Port "lauscht".
2. Nachdem dieses Socket-Objekt eine Verbindungsanfrage vom Saerch-Client bekommen hat, akzeptiert es diese Verbindung, speichert IP und Port vom Verbindungs-Socket ab, öffnet mit diesen Daten ein weiteren Kommunikationsocket und gibt das "OK", dass nun ein weiterer Thread (siehe Schritt 3) gestartet werden kann. Anschließend wartet dieser Kommunikationsocket ausschließlich auf Nachrichten vom Search-Client und gibt diese in der Kommandozeile aus. Dieser "blockiert" mit dieser Aufgabe den kompletten Thread.
3. Nachdem also nun Nachrichten empfangen werden können, wird nun ein weiterer Thread gestartet, welcher sich wie in Schritt 2 mithilfe eines Verbindungs-Sockets auf einem weiteren Port mit dem Search-Client verbindet und dann ein weiteres Kommunikations-Socket-Objekt erstellt.

B) Das macht der Search-Client:
Dieser macht die Schritte in A im Prinzip genau gegenteilig. Tut also das was in A vom "Search-Client" erwartet wird.

Ich wollte in diesem Projekt erreichen, dass sich nur zwei Clients miteinander verbinden können. Ich habe zwei Verbindungssockets benutzt, da es nicht möglich ist mit einem Socket gleichzeitig zu "lauschen" und zu senden. Wenn doch, dann lass ich mich gerne eines besseren belehren.

messenger.py (Projekt 2):
In diesem Projekt lag der Fokus darauf, dass sich mehrere Clients mit einem Server verbinden können und zu diesem Nachrichten schicken können. Im nächsten Schritt hätte ich alle Nachrichten und IPs der Clients gespeichert und dann mithilfe dieser Daten an alle Clients die mit dem Server verbunden sind zurück geschickt. Da mir die Programmstruktur zu unübersichtlich geworden ist und mir das Design generell nicht gefällt, habe ich wie gesagt erstmal mit diesem Projekt abgeschlossen und möchte nun stattdessen mit "socketserve" in Projekt 3 und 4 beginnen. Der Programmablauf von diesem Projekt ist ähnlich dem von Projekt , wobei hier jeweils kein zweites Socket erzeugt wird, welcher dann ausschließlich zum senden an den Client benutzt werden würde.

Meine Fragen:

a) Wenn ihr euch Projekt 1 und 2 anschaut und meinen Text dazu liest, kann man dann daraus schließen, dass ich Sockets und das TCP-Protokoll grundsätzlich verstanden habe, oder bin ich ganz oder teilweise auf dem Holzweg?

b) Ist meine Vorgehensweise sinnvoll meine beiden Projekte 1 und 2 fallen zu lassen um mit "socketserver" besser durchdachte Chatprogramme zu schreiben?

Anmerkungen zur Ausführung der Programme:
-> Beide sind getestet und funktionieren. Natürlich gibt es Fehler welche nicht abgefangen werden, tut mir leid dafür. Wenn man im lokalen Netzwerk kommunizieren möchte, dann muss man für den python-Interpreter eine Ausnahme in der Firewall der Computer eingerichtet werden mit denen man kommunizieren möchte. (Bis ich das herausbekommen habe vergingen Welten :D)

"chat.py" (Projekt 1):
- > wait_client auf beliebigem Rechner starten
-> search_client auf dem selben oder beliebigen Rechner im lokalen Rechner starten und IP-Adresse von wait_client eingeben (Ausnahme für Firewall auf beiden Rechnern nicht vergessen)
- > um sauber zu beenden müssen wait_client und search_client jeweils 'exit' senden

"messenger.py" (Projekt 2):
-> host auf beliebigen Rechner starten
-> beliebig viele Clients auf dem selben Rechner oder auf Rechnern im lokalen Netzwerk starten und IP-Adresse vom Host angeben (auch hier Firewall nicht vergessen ;))
-> die clients können mit der Nachricht 'exit' einen "disconnect" vom server durchführen

Hier also die beiden Quellcodes (wie gesagt auch auf Github zu finden)

chat.py (nicht wundern, das time-Modul habe ich nur zu testzwecken genutzt und vergessen den import zu löschen):

Code: Alles auswählen

import socket 
import threading
import time

HEADER = 64
PORT = 5050
PORT_2 = 5051
SERVER = socket.gethostbyname(socket.gethostname())
ADDR = (SERVER, PORT)
FORMAT = 'utf-8'
DISCONNECT_MESSAGE = "!DISCONNECT"
CLIENT_NAME = "HOST"

class Chat:
    def __init__(self):
        self.message = ""
        self.target_ip = 0
        self.receive_target_connected = False
        self.transreceive_target_connected = False

    def receive_message(self, conn):
        msg_length = conn.recv(HEADER).decode(FORMAT)
        if msg_length:
            msg_length = int(msg_length)
            msg = conn.recv(msg_length).decode(FORMAT)
            return msg

    def send_message(self, conn, msg):
        message = msg.encode(FORMAT)
        msg_length = len(message)
        send_length = str(msg_length).encode(FORMAT)
        send_length += b' ' * (HEADER - len(send_length))
        conn.send(send_length)
        conn.send(message)

    def receive(self, port):
        receive_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        receive_socket.bind((SERVER, port))
        print("[STARTING] Receiver is starting...")
        
        receive_socket.listen()
        print(f"[LISTENING] Receiver is listening on {SERVER}:{port}")

        conn, addr = receive_socket.accept()
        self.target_ip, conn_port = addr # save ip for transceive socket
        self.receive_target_connected = True # set connection status for transceive socket
        print(f"[CONNECTED] Receiver connected to {self.target_ip}:{port}")

        while True:
            self.message = self.receive_message(conn)
            print(self.message)
            if self.message == DISCONNECT_MESSAGE:
                conn.close()
                break

    def transreceive(self, ip_adress, port, name):
        transreceive_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        print("[STARTING] Transceiver ist starting...")

        transreceive_socket.connect((ip_adress, port))
        self.transreceive_target_connected = True #set connection status for receive socket
        print(f"[CONNECTED] Transceiver connected to {ip_adress}:{port}")

        while True:
            user_msg = input()
            msg = f"\n{name}: {user_msg}"
            self.send_message(transreceive_socket, msg)
            if user_msg == "exit":
                self.send_message(transreceive_socket, DISCONNECT_MESSAGE)
                break

    def wait_client(self):
        name = input("Enter your Nickname: ")
        receive_thread = threading.Thread(target=self.receive, args=([PORT]))
        receive_thread.start()

        while True:
            if self.receive_target_connected:
                transceiver_thread = threading.Thread(target=self.transreceive, args=([str(self.target_ip), PORT_2, name]))
                transceiver_thread.start()
                break

    def search_client(self):
        name = input("Enter your Nickname: ")
        ip_adress = input("IP Address: ")
        transceive_thread = threading.Thread(target=self.transreceive, args=([str(ip_adress), PORT, name]))
        transceive_thread.start()

        while True:
            if self.transreceive_target_connected:
                receive_thread = threading.Thread(target=self.receive, args=([PORT_2]))
                receive_thread.start()
                break


def main():
    chat = Chat()
    
    print("1 - Wait Client\n2 - Search Client")
    choice = input("Input: ")
    if choice == '1':
        chat.wait_client()
    elif choice == '2':
        chat.search_client()
    else:
        print("Wrong Input")
    

if __name__ == '__main__':
    main()
messenger.py: (nicht wundern, das subprocess-Modul habe ich nur zu testzwecken genutzt und vergessen den import zu löschen):

Code: Alles auswählen


import socket
import threading
import subprocess

class Host:
    def __init__(self, ip_address, port):
        self.ip_address = ip_address
        self.port = port
        self.header = 64
        self.format = 'utf-8'
        self.disconnect_message = "!DISCONNECT"
        self.client_nickname = None
        self.messages = []
        self.connections = []

    def start_server(self):
        print("[STARTING] Server is starting...")
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.bind((self.ip_address, self.port))

        print(f"[LISTENING] Server is listening on {self.ip_address}:{self.port}")
        server.listen()
        while True:
            communication_socket, client_ip_port = server.accept()
            thread = threading.Thread(target=self.handle_client, args=(communication_socket, client_ip_port))
            thread.start()
            print(f"[ACTIVE CONNECTIONS] {threading.active_count() - 1}")

    def handle_client(self, communication_socket, client_ip_port):
        client_ip, client_port = client_ip_port
        print(f"[NEW CONNECTION] Communication - Socket from client at {client_ip}:{client_port}")
        self.connections.append(client_ip_port)

        #only for testing: show connections
        print(self.connections)

        # first: client sending an empty header that have the length 'header - message_length'
        # communication_socket receive it
        message_header = communication_socket.recv(self.header).decode(self.format)

        # second: client will sending the message; communication socket receive it
        # first message is always the nickname
        if message_header: # if not None
                message_length = int(message_header)
                self.client_nickname = communication_socket.recv(message_length).decode(self.format)

        # Host can now receive messages from this client
        while True:
            # first: receive header
            message_header = communication_socket.recv(self.header).decode(self.format) 

            # second: receive message
            if message_header: # if not None
                message_length = int(message_header)
                message = communication_socket.recv(message_length).decode(self.format)
                if message == self.disconnect_message:
                    print(f"[DISCONNECT] '{self.client_nickname}' ({client_ip}:{client_port}) disconnected")
                    self.connections.remove(client_ip_port)

                    #only for testing: show connections list
                    print(self.connections)

                    break

                print(f"[RECEIVE] '{self.client_nickname}' ({client_ip}:{client_port}) send a message:")
                print(f"{message}")

                # HERE: save message in an list an send it to all clients!
                send_message = f"{self.client_nickname}: {message}"
                self.messages.append(send_message)
                # only for testing: show message list
                print(f"[MESSAGES LIST]")
                print(self.messages)

                # for testing: send message back to client
        
        communication_socket.close()

    def sending_messages():
        #this should be a new Thread that sends every message in the messages list to every ip_address in connections list
        pass


class Client:
    def __init__(self, host_ip_address, host_port, nickname):
        self.host_ip_address = host_ip_address
        self.host_port = host_port
        self.format = 'utf-8'
        self.header = '64'
        self.disconnect_message = "!DISCONNECT"
        self.nickname = nickname

    def start_client(self):
        print("[STARTING] Client is starting...")
        communication_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # try accept block around this should make sense
        # for timeout error(no computer on the other end) or ConnectionRefusedError(no listener on port)
        communication_socket.connect((self.host_ip_address, self.host_port))

        print(f"[CONNECTED] Connected with host ({self.host_ip_address}:{self.host_port})")

        # first message is always the nickname
        self.send_message(self.nickname, communication_socket)

        # now the client can send messages
        while True:
            message = input()
            if message == 'exit':
                self.send_message(self.disconnect_message, communication_socket)
                break

            self.send_message(message, communication_socket)

    def send_message(self, msg, communication_socket):
        # first: client sending an empty header that have the length 'header - message_length'
        message = msg.encode(self.format)

        message_length = len(message)
        send_length = str(message_length).encode(self.format)
        send_length += b' ' * (int(self.header) - len(send_length))
        communication_socket.send(send_length)

        # second: client will sending the message
        communication_socket.send(message)


def main():
    print("1 - HOST\n2 - CLIENT")
    user_input = input()

    if user_input == '1':
        host_ip_address = socket.gethostbyname(socket.gethostname())
        host_port = 50500
        host = Host(host_ip_address, host_port)
        host.start_server()
    elif user_input == '2':
        nickname = input("Enter a nickname: ")
        host_ip_address = input("Host IP: ")
        host_port = 50500
        client = Client(host_ip_address, host_port, nickname)
        client.start_client()
    else:
        print("Wrong Input.")


if __name__ == '__main__':
    main()
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du hast die ueblichen Dinge bei sockets nicht verstanden, und noch ein bisschen was obendrauf.

Die ueblichen Dinge ist die Tatsache, dass sockets *streams* sind. Da pladdern einfach Bytes raus, und es gibt keine Garantie, dass du mit recv genau die Menge an Bytes in einem Aufruf geliefert bekommst, die du auf der anderen Seite reingestopft hast. Womit wir zum zweiten ueblichen Missverstaendnis kommen; socket.send kann, muss aber nicht alle uebergebenen Bytes abschicken. Darum liefert es zurueck, wieviele es *wirklich* gewesen sind. Und man muss dann den Rest so lange weiter verschicken, bis alles weg ist. Weil das nervig & trivial zu machen ist, gibt es gleich eine Methode sendall. Die musst du also benutzen.

Das erste Problem ist schwieriger zu loesen. Dazu benoetigt man ein Protkokoll. ZB kann man newline-basiert arbeiten. Man vereinbart also, dass in keiner Nachricht ein newline-zeichen vorkommen darf, sondern das aussschliesslich dazu verwandt wird, das Ende *einer* Nachricht zu markieren.

Und dann kann man von einem eingehende socket die Bytes aneinder reihen, bis man ein newline gefunden hat, und dann hat man eine vollstaendige Nachricht.

Andere Protokolle haben immer einen definierten Header, in dem steht, wie lang der Rest ist. Etc pp.

Zu guter Letzt: sockets sind bi-direktional, man kann mit einem Socket Daten schicken und versenden. Nichts anderes macht dein Browser und der HTTP-Server mit dem der redet - eine Anfrage schicken, und eine Antwort bekommen.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Wie 99.9999% aller Socket-Beispiele sind auch Deine fehlerhaft. Warum, steht in jedem Beitrag hier im Forum, der sich im Socket-Programmierung dreht.
64 Zeichen für eine Länge sind auch etwas viel. 20 Bytes sind völlig ausreichend.
Dann stückelt man Strings nicht per + zusammen, sondern benutzt Formatierungsstrings.

Code: Alles auswählen

header = b'%20d' % message_length
Diese Heizschleifen in wait_client und search_client sollten nicht sein.
Im zweiten Beispiel sind viel Dinge im Code doppelt.
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Hallo ihr beiden, erstmal vielen Dank für eure Rückmeldungen ;)
__deets__ hat geschrieben: Freitag 7. Mai 2021, 14:28 Die ueblichen Dinge ist die Tatsache, dass sockets *streams* sind. Da pladdern einfach Bytes raus, und es gibt keine Garantie, dass du mit recv genau die Menge an Bytes in einem Aufruf geliefert bekommst, die du auf der anderen Seite reingestopft hast.
Wieso gibt keine Garantie, dass ich mit recv genau die Menge an Bytes in einem Aufruf geliefert bekomme?
Dazu möchte ich gerne folgenden Teil vom Projekt 1 betrachten:

Code: Alles auswählen

def receive_message(self, conn):
    msg_length = conn.recv(HEADER).decode(FORMAT)
    if msg_length:
        msg_length = int(msg_length)
        msg = conn.recv(msg_length).decode(FORMAT)
        return msg 
def send_message(self, conn, msg):
    message = msg.encode(FORMAT)
    msg_length = len(message)
    send_length = str(msg_length).encode(FORMAT)
    send_length += b' ' * (HEADER - len(send_length))
    conn.send(send_length)
    conn.send(message)
Eine Nachricht ist immer 64 bytes lang. Das stelle ich sicher indem ich mit "send_length += b' ' * (HEADER - len(send_length))" fehlende bytes vor meiner eigentlichen nachricht mit "conn.send(send_length)" sende. Würde ich zum Beispiel 'a' senden wollen so wäre "send_length" 63 bytes groß und besteht ausschließlich aus leeren Zeichen. Das letzte byte (das 'a') wird direkt danach mit "conn.send(message)" hinterher geschickt. Somit stelle ich doch sicher das der Socket-Buffer immer leer ist oder nicht? (siehe: https://de.wikipedia.org/wiki/Transmiss ... _daten.svg)

Der Empfänger erwartet grundsätzlich zunächst das byte-Objekt mit leeren Zeichen und erwartet anschließend die eigentliche Nachricht und gibt sie nach dem decoden entsprechend aus. So stelle ich doch sicher, dass ich mit revc genau die Menge an Bytes geliefert bekomme die ich auf der anderen Seite reingestopft habe oder?
Wo ist mein Gedankenfehler?
__deets__ hat geschrieben: Freitag 7. Mai 2021, 14:28 Zu guter Letzt: sockets sind bi-direktional, man kann mit einem Socket Daten schicken und versenden.
Das habe ich verstanden. Aber kann ich mit dem gleichen Socket gleichzeitig senden und empfangen? Ich will ja schließlich nicht erst eine Nachricht versenden können nur wenn der andere mir zuerst eine Nachricht gesendet hat, und das nur weil ich auf recv warten muss.
Ich hatte schon den Gedanken das ich das connection-socket auf zwei Threads aufteile. Im einen Thread lasse ich es ausschließlich nachrichten empfangen und ausgeben. In dem anderen lasse ich es ausschließlich nachrichten senden welche ich mit input() eingebe. Wobei ich das ganze jeweils mit 'send_message' und 'receive_message' umschließen ("wrappen") muss. Aber kann ich ein einziges connection-socket gleichzeitig senden und empfangen lassen? Ich kanns mir nicht vorstellen...

Wenn ich socket-objekt.recv ausführe dann blockiert mir das doch die weitere Ausführung bis die Nachricht tatsächlich kommt oder nicht? Genauso socket.object.send(). Die Ausführung dieser Zeile blockiert doch auch den weiteren Programmfluss oder sehe ich das falsch?
Sirius3 hat geschrieben: Freitag 7. Mai 2021, 14:29 Diese Heizschleifen in wait_client und search_client sollten nicht sein.
Da gebe ich Dir recht :D Muss mal gucken wie ich diese "ausbremse". 'time.sleep(1)' wäre wohl etwas komisch. Ich sollte das wohl irgendwie anders lösen ;)
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Domroon hat geschrieben: Freitag 7. Mai 2021, 16:38 Wieso gibt keine Garantie, dass ich mit recv genau die Menge an Bytes in einem Aufruf geliefert bekomme?
Bildlich kannst Du Dir eine Socket-Verbindung als einen Wasserschlauch vorstellen, aus dem mit variablem Druck mal mehr oder weniger viel Wasser pro Zeiteinheit in Deinen darunterstehenden Eimer fließt. Wenn der Eimer voll ist, hast Du eine Nachricht empfangen. Wann der Eimer voll ist und Du den nächsten drunterstellen kannst, musst Du selbst erkennen. So ist es auch bei 'recv': Es wird maximal die übergebene Menge an Bytes geliefert, möglicherweise aber auch weniger. 'recv' gibt Dir Daten zurück, sobald welche da sind – wieviele auch immer (aber eben nicht mehr als angegeben).
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Hallo Leute,
ich habe versucht die Tipps von __deets__ und Sirius3 umzusetzen:
- ich habe nun ein Protokoll geschrieben, welches bytes empfängt und solange an einen String hängt bis ein newline-Zeichen ("\n") erkannt wird
- ich habe den buffer von revc auf 20 bytes reduziert, dass einzige was mich etwas irriert, dass in der python-Dokumentation folgendes steht "For best match with hardware and network realities, the value of bufsize should be a relatively small power of 2, for example, 4096." (siehe hier unter "socket.recv(bufsize[, flags])": https://docs.python.org/3/library/socke ... et-objects)

Könntet ihr noch einmal über mein Programm schauen ob ich nun das Thema "Sockets" verstanden habe? Ich wäre euch sehr dankbar ;)

Hier mein neues Programm:

Code: Alles auswählen

import socket
import threading 

class Server:
    def __init__(self):
        self.connection_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.ip_address = socket.gethostbyname(socket.gethostname())
        self.port = 50500
        # self.connections = []
        # self.messages = []

    def receive_message(self, communication_socket):
        message = ""
        receive_message = True
        while receive_message:
            #receive 20 bytes
            bytes = communication_socket.recv(20)
            # decode the bytes to utf-8 and search for newline-sign
            bytes_decoded = bytes.decode("utf-8")
            for sign in bytes_decoded:
                if sign == "\n":
                    bytes_decoded = bytes_decoded[:-1]
                    receive_message = False
            message += bytes_decoded
        return message

    def send_message(self, message_decoded, communication_socket):
        message = (message_decoded + "\n").encode()
        communication_socket.sendall(message)

    def start(self):
        print("[STARTING] Server is starting ... ")
        self.connection_socket.bind((self.ip_address, self.port))

        self.connection_socket.listen()
        print(f"[LISTENING] Server is listening on {self.ip_address}:{self.port}")

        # waiting for new connections
        # each connection will have its own communication_socket in a seperate Thread
        while True:
            communication_socket, address = self.connection_socket.accept()
            print(f"[NEW CONNECTION] Connected with {address}")
            thread = threading.Thread(target=self.handle_client, args=(communication_socket, address))
            thread.start()

    def handle_client(self, communication_socket, address):
        # Server sending the welcome message
        self.send_message("[SERVER] Welcome to the Server!", communication_socket)

        # Server receive messages from this client
        while True:
            client_message = self.receive_message(communication_socket)
            if client_message == "!DISCONNECT":
                print(f"[DISCONNECT] Client {address} disconnected")
                self.send_message("[SERVER] Bye! We hope to see you again soon :)", communication_socket)
                break
            print(f"[RECEIVE] from {address}: {client_message}")


class Client:
    def __init__(self):
        self.communication_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         
    def receive_message(self):
        message = ""
        receive_message = True
        while receive_message:
            #receive 2 bytes
            bytes = self.communication_socket.recv(2)
            # decode the bytes to utf-8 and search for newline-sign
            bytes_decoded = bytes.decode("utf-8")
            for sign in bytes_decoded:
                if sign == "\n":
                    bytes_decoded = bytes_decoded[:-1]
                    receive_message = False
            message += bytes_decoded
        return message

    def send_message(self, message_decoded):
        message = (message_decoded + "\n").encode()
        self.communication_socket.sendall(message)

    def start(self):
        print("[STARTING] Client is starting ... ")

        ip_address = input("IP Address: ")
        port = int(input("Port: "))

        self.communication_socket.connect((ip_address, port))
        # Receiving Welcome Message
        print(self.receive_message())

        print(f"[CONNECTED] Connected with server ('{ip_address}':{port})")

        # User can send messages
        while True:
            user_message = input()
            self.send_message(user_message)
            if user_message == 'q':
                self.send_message("!DISCONNECT")
                print(self.receive_message())
                break
        
        self.communication_socket.close()

def main():
    print("1 - SERVER \n2 - CLIENT \n")
    user_input = input()
    if user_input == '1':
        server = Server()
        server.start()
    if user_input == '2':
        client = Client()
        client.start()


if __name__ == '__main__':
    main()
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

kbr hat geschrieben: Freitag 7. Mai 2021, 17:46
Domroon hat geschrieben: Freitag 7. Mai 2021, 16:38 Wieso gibt keine Garantie, dass ich mit recv genau die Menge an Bytes in einem Aufruf geliefert bekomme?
Bildlich kannst Du Dir eine Socket-Verbindung als einen Wasserschlauch vorstellen, aus dem mit variablem Druck mal mehr oder weniger viel Wasser pro Zeiteinheit in Deinen darunterstehenden Eimer fließt. Wenn der Eimer voll ist, hast Du eine Nachricht empfangen. Wann der Eimer voll ist und Du den nächsten drunterstellen kannst, musst Du selbst erkennen. So ist es auch bei 'recv': Es wird maximal die übergebene Menge an Bytes geliefert, möglicherweise aber auch weniger. 'recv' gibt Dir Daten zurück, sobald welche da sind – wieviele auch immer (aber eben nicht mehr als angegeben).
Ich wollte Dir nicht glauben und habe daher mit "revc" und "sendall" etwas herumexperimentiert und habe festgestellt, dass Du recht hast :D
Danke Dir für diese Anmerkung ;) Ich hätte es sonst nicht verstanden ;)
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Der Code ist immer noch genauso fehlerhaft. Niemand hat gesagt, dass man maximal 20 Zeichen auf einmal lesen darf. Man muß halt berücksichtigen, dass zwischen 1 und der angegebenen Zahl an Bytes zurückkommen kann, und dass in einer Rückgabe mehrere "Nachrichten" stecken könnten.
Also in den 20 Bytes könnte sowohl ein \n vorkommen, als auch schon der Anfang der nächsten Nachricht. Was Du in Deinem Code jetzt einfach ignorierst.
Aus

Code: Alles auswählen

erste Nachricht\nzwei
wird

Code: Alles auswählen

erste Nachricht\nzwe
Du brauchst jetzt also entweder einen Speicher, wo unverarbeitete Bytes zwischengespeichert werden können, oder Du ließt nur ein Byte nach dem anderen, was sehr ineffizient ist.

Nächster Fehler: ein utf8-Zeichen kann aus mehreren Bytes bestehen, Da Du aber 20 Bytes liest, kann das mitten in einem Zeichen getrennt werden, so dass decode fehlschlägt.
Zusätzlich hast Du jetzt das Problem, dass \n nicht in der Nachricht vorkommen kann.

Viel besser war also Dein erster Ansatz, als Du noch die länge der Nachricht in Bytes übertragen hattest.
Da die Funktionen zum Senden und Emfangen self nicht brauchen, haben die nichts in den Klassen verloren und können natürlich für Client und Server benutzt werden, ohne dass man Code kopieren muß:

Code: Alles auswählen

def send_message(communication_socket, message):
    message_encoded = message.encode("utf-8")
    communication_socket.sendall(b"%20d" % len(message_encoded))
    communication_socket.sendall(message_encoded)

def recv_all(socket, length):
    result = []
    while length > 0:
        data = socket.recv(min(4096, length))
        result.append(data)
        length -= len(data)
    return b"".join(data)

def receive_message(communication_socket):
    length = int(recv_all(socket, 20))
    return recv_all(socket, length).decode("utf-8")
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Sirius3 hat geschrieben: Samstag 8. Mai 2021, 20:10 Also in den 20 Bytes könnte sowohl ein \n vorkommen, als auch schon der Anfang der nächsten Nachricht. Was Du in Deinem Code jetzt einfach ignorierst.
Okay, ich habe lange gebraucht und zu verstehen was du meinst :D Ich habe es aber nun verstanden. Beim experimentieren mit client und server im lokalen Netzwerk ist es sogar einmal passiert, dass bei einer Nachricht der letzte Buchstabe nicht ankam. So wurde aus dem Befehl "!DISCONNECT" vom Client an den Server einfach mal "!DISCONNEC" und schon wurde die Verbindung nicht sauber getrennt :D Ich habe mich sehr gewundert was da passiert ist. Jetzt was ich ja was los war ;) Ich habe fehlende Bytes nicht beachtet und wenn diese fehlenden mit der nächsten Nachricht ankamen dann habe ich sie einfach "weggeschmissen" bzw. überschrieben :D

Dein Protokoll ist super! Ich habe es in mein Programm integriert. Du hast einen kleinen Fehler gemacht.
Du wolltest in "recv_all" nicht "data" anhängen

Code: Alles auswählen

def recv_all(socket, length):
    result = []
    while length > 0:
        data = socket.recv(min(4096, length))
        result.append(data)
        length -= len(data)
    return b"".join(data)
sondern "result"

Code: Alles auswählen

def recv_all(socket, length):
    result = []
    while length > 0:
        data = socket.recv(min(4096, length))
        result.append(data)
        length -= len(data)
    return b"".join(result)
Ich will Dein Protokoll für mich nochmal durchgehen:
- ich nehme an, dass der Client "asdfghjklö" senden will (der String hat eine Länge von 10)

- durch das encodieren in utf-8 erhalte ich ein byte-objekt welches 11byte groß ist
-> die Zeichen "asdfghjkl" (also ohne das "ö") sind alle jeweils ein byte groß, weil diese unter die ersten 128-Zeichen von UTF-8 fallen und desalb Deckungsgleich mit den ASCII Zeichen sind. Ein ASCII-Zeichen ist immer ein byte groß
-> das "ö" ist eine "Deutsche Eigenart" und ist nicht in den ersten 128 UTF-8-Zeichen enthalten. Es benötigt deshalb ein weiteres byte
-> mein byte Objekt sieht also nun so aus: b'asdfghjkl\xc3\xb6' (\xc3\xb6 steht für die zwei bytes die das "ö" repräsentieren)

- mit "length = len(b'asdfghjkl\xc3\xb6')" erhalte ich nun also für 'length' 11

- mit b"%20d" % len(message_encoded) erhalte ich dann ein byte-objekt welches 20byte groß ist, weil es durch "%20d" 20 Integer-Ziffern erhält. Durch %11 bzw. len(message_encoded) schreibe ich in diese 20 Ziffern die Zahl 11 hinein

- damit sieht das zu versendene byte-Objekt nun so aus:

Code: Alles auswählen

b"                  11"
(18 leere Ziffern und 2 Einsen)

- der Empfänger empfängt durch "while length > 0" solange bytes bis alle erwarteten 20 bytes angekommen sind und hängt fehlende bytes immer wieder an die liste "result" an

- diese liste aus byte objekten wird mithilfe von "b"".join(result)" zu einem einzigen 20byte langen byte-objekt zusammengefügt
-> somit ist sichergestellt, dass keine bytes verloren gehen
-> folgendes könnte nämlich passieren: anstatt 20 bytes kommen nur 19 an also:

Code: Alles auswählen

b"                  1"
(die zweite Ziffer der 11 fehlt!)
-> dadurch das aber weiter empfangen wird und durch das zusammenfügen durch join() ist sichergestellt das die funktion "recv_all" auf jedenfall

Code: Alles auswählen

b"                  11"
zurückliefert

Da ich das Programm ständig verbessere und neues integriere findet ihr immer den aktuellsten und vollständigen Code hier: https://github.com/Domroon/MessengerOne ... ngerTwo.py

hier der entsprechende Codeauschnitt von Sirius3 welchen ich mit Kommentaren in mein Programm integriert habe. Funktioniert prima! :) Vielen Dank nochmal an Sirius3 für Deine Mühe ;)

Code: Alles auswählen

def recv_all(communication_socket, length):
    result = []
    while length > 0:
        data = communication_socket.recv(min(4096, length))
        result.append(data)
        length -= len(data)
    return b"".join(result)


def receive_message(communication_socket, address):
    # first: receive message-length
    length = int(recv_all(communication_socket, 20))

    # second: receive the message
    return recv_all(communication_socket, length).decode("utf-8")


def send_message(message_decoded, communication_socket):
    # convert message into a byte-object
    message_encoded = message_decoded.encode("utf-8")

    # send 20 byte 
    # this byte object contains 20 integer digits
    # this in turn contains a number that indicates the length of the following message
    # eg. b"                  12"
    # the example is 20 bytes long because each digit is one byte in size
    # digits 1 to 9 are ASCII - characters
    # UTF-8 is congruent with ASCII in the first 128 characters (indices 0–127)
    # 128 = 2^7 corresponds to 8 bit (1 byte)
    communication_socket.sendall(b"%20d" % len(message_encoded))
    communication_socket.sendall(message_encoded)
Antworten