CSK nicht stabil

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
Benutzeravatar
Finux
User
Beiträge: 9
Registriert: Mittwoch 18. September 2019, 13:59

Donnerstag 19. September 2019, 16:39

Hallo,
ich bin neu hier und erhoffe mir etwas Rat von euch (ebenso ein Beginner was programming angeht).

Ich möchte eine stabile Client-Server-Kommunikation mit Python3 via Sockets und Multithreading aufbauen. Diese ist SSL-verschlüsselt. Soweit funktioniert auch alles, versuche alle möglichen Exceptions/Tracebacks abzufangen (zumindest bisher im Client-Script), aber wenn ich eine Clientverbindung beende, dreht der Server völlig durch und produziert ungewollte Ausgaben mit dem Fehler "Pipe broken". Das Serverscript stürzt dann auch irgendwann ab, und ich möchte, dass es sich quasi selbst wieder neu startet, wenn es abgestürzt ist oder nicht mehr stable läuft...

Die Kommunikation läuft auf Debian, und für die Erstellung eines eigenen openssl Zertifikats ist folgendes zu beachten:
openssl req -new -days 999 -newkey rsa:4096bits -sha512 -x509 -nodes -out server.crt -keyout server.key
Note: CN (Common Name) has to be the server adress (e.g. IP)

Don't forget to change rights for server.crt and server.key (chmod -v 777 server.crt and chmod -v 777 server.crt)
Vielen Dank schonmal für eure Hilfe! :ugeek:
LG, Finux

Hier mein bisheriger unbeschnittener Code:

server.py:

Code: Alles auswählen

#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::#
#::::::::::::::::::        MULTITHREAD + SSL       ::::::::::::::::::#
#::::::::::::::::::::::::::: [ server.py ] ::::::::::::::::::::::::::#
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::#
#TODO: Checkausgabe für Socketbinding, Try-Excepts, Funktionen (z.B. Uhrzeit), OOP

import socket
import ssl
from _thread import *

host = '127.0.0.1'
port = 4100
max_connections = 2

context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certfile="/home/finux/server.crt", keyfile="/home/fin/server.key")

socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket.bind((host, port))

socket.listen(max_connections)


def i_manage_client(connstream, client_socket, addr):  # Function to manage clients
    connstream.send(b'Server accepted the connection!')

    while True:
        data = connstream.recv(4096)
        data = data.decode()
        #print(data.encode('utf-8').hex())
        print("Client {}: ".format(addr), data)
        if data == "exit":
            print("+++++ SOCKET CLOSED ++++")
            socket.close()
        try:
            connstream.send(b'Servertestmessage')
        except BrokenPipeError as e:
            print(e)
            print("MESSAGE COULD NOT BE SEND!")
            socket.close()


try:
    while True:
        client_socket, addr = socket.accept()
        connstream = context.wrap_socket(client_socket, server_side=True)
        start_new_thread(i_manage_client, (connstream, client_socket, addr))
except (KeyboardInterrupt, OSError) as e:
    print(e)
    socket.close()

Hier dann die client.py:

Code: Alles auswählen

#::::::::::::::::::::::::::::::::::#
#::::     MULTITHREAD + SSL    ::::#
#:::::::::: [ client.py] ::::::::::#
#::::::::::::::::::::::::::::::::::#
#TODO: "Funktionen", OOP

import socket, ssl, pprint, sys
from _thread import *
import time

global msg
msg = ''

ip = '127.0.0.1'
port = 4100

context = ssl.SSLContext()
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True

try:
    context.load_verify_locations("/home/finux/server.crt")
    print("***CHECK- Certificate loading successful")
except (FileExistsError, FileNotFoundError) as e:
    print(e)
    print("::::: Program will be closed now! :::::")
    sys.exit()

try:
    # with socket.create_connection((ip, port)) as s:
    conn = context.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM), server_hostname="127.0.0.1")
    print("***CHECK- Socket only supports ssl connection successful")
    conn.connect((ip, port))
    print("***CHECK- Connection to server successful")
    conn.send(b"Thanks for accepting the connection!")
    print("***CHECK- Bytestring sending successful")
except ssl.CertificateError as e:
    print("Hostname doesn't match.")
    print("::::: Program will be closed now! :::::")
    sys.exit()
except ConnectionError as e:
    print(e)
    print("::::: Program will be closed now! :::::")
    sys.exit()


def msg_receive(conn, ip):
    try:
        while True:
            server_answer = conn.recv(4096)
            msg = server_answer.decode()
            if msg != '':
                print("\n[{}]: {}".format(ip, msg))
            time.sleep(0.000001)
    except OSError as e:
        return 0




def msg_print_send(conn, ip):
    while True:
        message = input("\nNachricht: ")
        if message != '':
            conn.send(message.encode())
        if message == 'exit':
            print("+++++ SOCKET CLOSED ++++")
            conn.close()
            sys.exit()
            break
    return 0


start_new_thread(msg_receive, (conn, ip))
start_new_thread(msg_print_send, (conn, ip))

try:
    while 1:	# wieso muss die while-Schleife hier hin, damit das Script wie gewollt funktioniert???
        pass
except KeyboardInterrupt as e:
    print(e)
Vielleicht kann mir jemand den Umstand des letzten Kommentars von client.py erklären? :?:
Benutzeravatar
__blackjack__
User
Beiträge: 4192
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Donnerstag 19. September 2019, 18:52

@Finux: Als erstes solltest Du mal `_thread` loswerden. Das Modul ist nicht für die Öffentlichkeit gedacht. Threads macht man in Python mit dem `threading`-Modul. Sternchen-Importe sollte man auch nicht machen. Jedenfalls nicht bei Modulen die nicht explizit dafür geschrieben wurden.

Dann ist TCP ein Datenstrom. Was auf der einen Seite mit einem Methodenaufruf auf den Weg geschickt wurde kommt am anderen Ende nicht mit einem `recv()`-Aufruf an. Das kann auf mehrere Aufrufen verteilt sein — im extrem Fall bekommst Du jedes Byte einzeln. Anders herum: Wenn beim Sender zwei Aufrufe zum verschicken verwendet wurden, kann das auf der anderen Seite beides zusammen mit einem `recv()`-Aufruf ankommen. Dann noch was: `send()` sendet nicht garantiert alles. Die Methode hat einen Rückgabewert der besagt wie viele Bytes tatsächlich gesendet wurden. Falls das nicht alle sind, muss man das mit dem Rest wiederholen. Oder `send_all()` verwenden.

Man muss, wenn man Nachrichten senden und empfangen will, also selbst dafür sorgen das a) entsprechend Daten gesammelt werden bis mindestens eine Nachricht komplett übertragen ist, und b) dafür sorgen das Daten die zur nächsten Nachricht gehören nicht verloren gehen. Dafür braucht man so etwas wie ein Protokoll.

Du solltest beim en- und dekodieren eine konkrete Kodierung angeben. Wenn die beiden Programme auf unterschiedlichen Systemen laufen, müssen die Default-Kodierungen auf beiden Seiten ja nicht gleich sein. Am besten nimmt man UTF-8. Damit lässt sich alles kodieren und es ist, zumindest für die Texte die man in unseren Breitengraden verschickt, speichersparender als die anderen UTF-Kodierungen.

Die Aufräumarbeiten sollte man mit ``try``/``finally`` oder ``with`` (ggf. mit `contextlib.closing()`) absichern. Da sind mir zu viele `close()`-Aufrufe bei denen davor noch etwas schief laufen könnte und die dann nicht ausgeführt werden. Bei so etwas sollte man defensiv programmieren.

Auf Modulebene sollte nur Code stehen der Konstanten (die schreibt man IN_GROSSBUCHSTABEN), Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

``global`` hat auf Modulebene genau gar keine Wirkung. Das gehört aber auch nirgendwoanders hin, weil Variablen auf Modulebene wie gesagt nichts verloren haben. Alles was Funktionen/Methoden ausser Konstanten benötigen sollte als Argument(e) übergeben werden. Umgekehrt sollte man nichts übergeben was dann gar nicht verwendet wird.

Nach einem `sys.exit()` wird kein Code mehr ausgeführt — geh davon aus, dass das äquivalent zu einem ``raise SystemExit`` ist. `sys.exit()` sollte man auch nur verwenden wenn man tatsächlich einen Rückgabecode an den Aufrufer liefern will, und nicht als billigen Ausweg den Programmverlauf sinnvoll zu strukturieren.

Zur Frage beim Client: Wenn der Hauptthread endet, dann werden in der Regel auch alle anderen Threads einfach beendet — teilweise ohne Rücksicht auf Verluste. Erster Schritt die sinnlose CPU-Heizung mit der ``while True:``-Schleife die nix macht zu beseitigen ist einfach nur *einen* zusätzlichen Thread starten und die Aufgabe des anderen im Hauptthread zu erledigen. Und dann sollte man den anderen Thread sauber beenden und auf sein Ende warten.

Die ``return 0`` in mindestens zwei Funktionen sind sinnfrei. Warum 0? Da macht ja keiner etwas mit, mit dem Wert.

`max_connections` ist als Name ein bisschen irreführend, weil der Server beliebig viele Verbindungen entgegen nimmt. Den Wert selbst zu setzen ist in der Regel auch nicht sinnvoll.
“Give a man a fire and he's warm for a day, but set fire to him and he's warm for the rest of his life.”
— Terry Pratchett, Jingo
Benutzeravatar
__blackjack__
User
Beiträge: 4192
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Freitag 20. September 2019, 09:54

@Finux: Weitere Anmerkungen: Du verwendest den Namen `socket` sowohl für das Modul als auch für ein `socket`-Objekt — im gleichen Namensraum. Das ist verwirrend und fehleranfällig weil man nach dem erstellen des `socket`-Objekts nicht mehr an Attribute des `socket`-Moduls heran kommt. Man kann zum Beispiel `socket.error` nicht mehr in einem ``except`` verwenden. Abhilfe wäre das umbennenen vom `socket`-Modul beim importieren.

Ausnahmen sollte man möglichst nicht durch ein einfaches `print()` vom Ausnahmeobjekt ersetzen, das lässt dann wichtige Informationen unter den Tisch fallen, die man zur Fehlersuche benötigt.

Faustregel zu Kommentaren: Die beschreiben nicht was gemacht wird, denn das steht da ja bereits als Code, sondern warum etwas gemacht wird — sofern das nicht offensichtlich ist. Ein Kommentar ``# Function to manage clients`` zu einer Funktion `i_manage_clients()` ist überflüssig. Wobei im Grunde sogar noch falsch, denn die besagte Funktion verwaltet nur *einen* Client.

Was soll der `i_`-Präfix bei der Funktion eigentlich bedeuten? → Keine kryptischen Abkürzungen oder Prä-/Suffixe bei Namen verwenden.

Im Server schliesst Du nie irgendwelche Client-Sockets. Das ist mindestens mal unsauber.

Wenn die Verbindung zu *einem* Client wegbricht, wird der Server-Socket geschlossen. Soll das so? Was ist der Gedanke dahinter? Und wenn die Verbindung abgebrochen ist, sollte man die Schleife die den Client behandelt vielleicht beenden. Die repariert sich ja nicht auf magische Weise wieder, die bleibt „broken“.

Clients können den Server-Socket mit "exit" schliessen? Wirklich auch so gewollt?

Daraus ergibt sich folgender Zwischenstand für den Server (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::#
#::::::::::::::::::        MULTITHREAD + SSL       ::::::::::::::::::#
#::::::::::::::::::::::::::: [ server.py ] ::::::::::::::::::::::::::#
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::#
# TODO: Checkausgabe für Socketbinding, Try-Excepts, Funktionen (z.B. Uhrzeit),
#   OOP

import socket as socketlib
import ssl
from threading import Thread

HOST = "127.0.0.1"
PORT = 4100


def manage_client(client_socket, server_socket, addr):
    with client_socket:
        client_socket.sendall(b"Server accepted the connection!\n")
        while True:
            #
            # FIXME TCP is a data stream, we need a protocol here to separate
            #   messages from the stream.
            #
            data = client_socket.recv(4096)
            print("Client {}: {}".format(addr, data))
            if data.rstrip() == "exit":
                print("+++++ SOCKET CLOSED ++++")
                server_socket.close()  # TODO Wrong socket?
            try:
                client_socket.sendall(b"Servertestmessage\n")
            except BrokenPipeError as error:
                print(error)
                print("MESSAGE COULD NOT BE SEND!")
                server_socket.close()  # TODO Wrong socket?
                break


def main():
    context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    context.load_cert_chain("/home/finux/server.crt", "/home/fin/server.key")
    server_socket = socketlib.socket(socketlib.AF_INET, socketlib.SOCK_STREAM)
    with server_socket:
        server_socket.bind((HOST, PORT))
        server_socket.listen()
        try:
            while True:
                client_socket, addr = server_socket.accept()
                client_socket = context.wrap_socket(client_socket, True)
                Thread(
                    target=manage_client,
                    args=(client_socket, server_socket, addr),
                    daemon=True,
                ).start()
        except KeyboardInterrupt:
            print("Bye...")


if __name__ == "__main__":
    main()
Ist natürlich immer noch kaputt, weil man ein Protokoll implementieren muss um die Nachrichten voneinander trennen zu können.

Und beim beenden des Servers möchte man vielleicht auch nicht einfach so abbrechen, sondern allen Verbundenen Clients noch Tschüss sagen. Sonst haben die nämlich einfach so kommentarlos abbrechende Verbindungen.
“Give a man a fire and he's warm for a day, but set fire to him and he's warm for the rest of his life.”
— Terry Pratchett, Jingo
Benutzeravatar
__blackjack__
User
Beiträge: 4192
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Dienstag 24. September 2019, 09:34

@Finux: Noch was zum Client:

Beim laden des SSL-Zertifikats kann ein `FileExistsError` auftreten?

Wenn ein `ssl.CertificateError` auftritt gibst Du aus das der Hostname nicht passt — es kann aber auch andere Gründe geben warum diese Ausnahme auftritt. Beispielsweise wenn das Zertifikat abgelaufen ist oder es von einer Stelle ausgestellt wurde der nicht vertraut wird.

Ungetesteter Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
#::::::::::::::::::::::::::::::::::#
#::::     MULTITHREAD + SSL    ::::#
#:::::::::: [ client.py] ::::::::::#
#::::::::::::::::::::::::::::::::::#
# TODO: "Funktionen", OOP

import socket as socketlib
import ssl
from threading import Thread

IP = "127.0.0.1"
PORT = 4100


def receive_messages(socket, ip):
    with socket:
        try:
            while True:
                #
                # FIXME TCP is a data stream, we need a protocol here to
                #   separate messages from the stream.
                #
                message = socket.recv(4096).rstrip("\n")
                if not message:
                    break
                print("\n[{}]: {}".format(ip, message))
        except OSError:
            pass


def send_messages(socket):
    with socket:
        while True:
            message = input("\nNachricht: ")
            if message:
                socket.sendall((message + "\n").encode("utf-8"))
            if message == "exit":
                break
    print("+++++ SOCKET CLOSED ++++")


def main():
    context = ssl.SSLContext()
    context.verify_mode = ssl.CERT_REQUIRED
    context.check_hostname = True
    try:
        context.load_verify_locations("/home/finux/server.crt")
        print("***CHECK- Certificate loading successful")
    except FileNotFoundError:
        print("Certificate file could not be found.")
        print("::::: Program will be closed now! :::::")
    else:
        try:
            socket = context.wrap_socket(
                socketlib.socket(socketlib.AF_INET, socketlib.SOCK_STREAM),
                server_hostname="127.0.0.1",
            )
            print("***CHECK- Socket only supports ssl connection successful")
            socket.connect((IP, PORT))
            print("***CHECK- Connection to server successful")
            socket.sendall(b"Thanks for accepting the connection!\n")
            print("***CHECK- Bytestring sending successful")
        except (ssl.CertificateError, ConnectionError) as error:
            print(error)
            print("::::: Program will be closed now! :::::")
        else:
            Thread(
                target=receive_messages, args=(socket, IP), daemon=True
            ).start()
            try:
                send_messages(socket)
            except KeyboardInterrupt:
                print("Bye...")


if __name__ == "__main__":
    main()
Die einfachste Möglichkeit um das mit dem Protokoll zu lösen ist ein zeilenbasiertes Protokoll zu verwenden, also eine Zeile = eine Nachricht. Dafür braucht man dann nämlich nicht viel selbst programmieren zum trennen der Nachrichten wenn man einfach die `makefile()`-Methode auf Socketobjekten verwendet um ein Dateiobjekt zu dem Socket zu bekommen. Da kann man dann in einer ``for``-Schleife über die empfangenen Zeilen gehen.
“Give a man a fire and he's warm for a day, but set fire to him and he's warm for the rest of his life.”
— Terry Pratchett, Jingo
Benutzeravatar
Finux
User
Beiträge: 9
Registriert: Mittwoch 18. September 2019, 13:59

Mittwoch 25. September 2019, 12:47

Hallo _blackjack_,
zunächst allerbesten Dank für die reichlichen Informationen! Ich bin gerade dabei alles (eins nach dem anderen) durchzuarbeiten, wollte aber im Vorfeld schon mal Danke sagen :)
Ich melde mich nochmal dazu, denn ich habe einiges auf Anhieb nicht verstehen können und grabe mich da jetzt mal etwas hinein.

LG,
Fin

P.S.: Mein To-Do war nicht an das Forum direkt gerichtet, ich hoffe das wurde nicht falsch verstanden. Habe lediglich den Code original gepasted.
Antworten