socketserver vom Client aus beenden

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
Benutzeravatar
Dirk1972
User
Beiträge: 5
Registriert: Mittwoch 29. April 2015, 09:44
Wohnort: Augsburg
Kontaktdaten:

Hallo,
erstmal ein herzliches Hallo an das Forum, denn ich bin neu hier.

Kurzer Hintergrund: An Python bin ich geraten da ich Skripte zur Hausautomation in meinen Webanwendungen verwende. Hierfür ist auch der Zugriff auf Datenbanken notwendig...

Mein Problem:
Ich habe zur schnelleren Anbindung an die mariadb auf meinem Synology-Server einen kleinen Socketserver programmiert, der die Abfragen aus den Webzugriffen über mysql.connector an die Datenbank weitergibt und die Ergebnisse an den Client zurückgibt. Das Funktioniert auch alles so wie ich mir das gewünscht habe. ABER wenn ich mit server.shutdown() den Server beende, können meine Clients zwar nicht mehr darauf zugreifen, aber ich kann den Server nicht mehr neu starten. Obwohl der Socketserver "beendet" ist, läuft der Prozess noch auf meinem Synology-Server! Somit kommt die Fehlermeldung, dass der Port bereits vergeben ist. Erst nachdem ich den Prozess mit "kill" herausschmeisse kann ich den Socketserver wieder neu starten.

Daher meine Frage. Wie beende ich einen Socketserver inkl. Beenden des Prozesses korrekt?

So sieht mein Server-Code aus:

Code: Alles auswählen

#!/usr/bin/python3

from time import time
from module._config import config
from module.log import *
import mysql.connector as mysql
import socketserver
import pickle

class MyUDPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        daten=self.request[0].strip()
        s= self.request[1]
        addr =self.client_address
        if daten==b".stop":
            s.sendto(b"Der DB-Server wird gestoppt, Datenbank wird geschlossen.:END",addr)
            db_log("Server wird gestoppt")
            warnung("Der Server und die Datenbank wurden geschlossen!","Server")
            server.shutdown()
            conn.close()
        else:
            data=pickle.loads(daten)
            if data[0][:6]=="SELECT":
                datasend=db_select(*data)
            elif data[0]=="log":
                datasend=db_log(*data[1])
            else:
                datasend=db_comm(*data)
            datasend=datasend+b":END"
            if len(datasend)<1025:
                s.sendto(datasend,addr)
            else:
                x=int(len(datasend)/1024)
                for y in range (x+1):
                    if y==x+1:
                        s.sendto(datasend[y*1024:],addr)
                    else:
                        s.sendto(datasend[y*1024:(y+1)*1024],addr)

def db_log (T,W=0,Z=time(),Wa="Server",B="Privat"):
    return(db_comm("INSERT INTO Ereignisse (Warnung,Zeit,Webarea,Bereich,Text) VALUES (%s,%s,%s,%s,%s)",(W,Z,Wa,B,T)))

def db_comm(comm,value):
    try:
        c=conn.cursor()
        c.execute(comm,value)
        conn.commit()
        c.close()
        return(pickle.dumps("OK"))
    except:
        return(pickle.dumps("Fehler"))

def db_select(comm,value):
    try:
        c=conn.cursor()
        c.execute(comm,value)
        data=c.fetchall()
        c.close()
        return(pickle.dumps(data))
    except:
        return(pickle.dumps("Fehler"))

if __name__=="__main__":
    with open(config("db.pw"), "r") as z:
        zugang=[line.strip() for line in z]
    db_log("Datenbank wird geöffnet")
    try:
        conn=mysql.connect(host=zugang[0],user=zugang[1],password=zugang[2], database=zugang[3])
        db_log("Server wird gestartet")
        server = socketserver.UDPServer(("127.0.0.1", 55072), MyUDPHandler)
        server.serve_forever()
    except:
        warnung("Fehler im Server oder Datenbank!","Server")
    finally:
        server.shutdown()
        server.close()
        warnung("Der Server und die Datenbank wurden geschlossen!","Server")
In Zeile 15-20 ist mein Wunsch programmiert: Wenn ich ".stop" sende, dann soll der Server inlkl. Prozess beendet werden.
Wenn ich also. ".stop" sende erscheinen alle richtigen Meldungen, der Server ist mit meinem Client auch nicht mehr erreichbar, ABER der Prozess ist noch aktiv.

Der Vollständigkeit wegen hier noch der Client-Code zum Beenden des Servers:

Code: Alles auswählen

#!/usr/bin/python3

import cgitb; cgitb.enable()

from module.html import *
import socket

ip="127.0.0.1"
port=55072

data=b""

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(1)
try:
    s.sendto(b".stop",(ip,port))
    while True:
        data+=(s.recv(1024))
        if b":END" in data:
            break
    x=data[:-4].decode()
except:
    x="Keine Verbindung zum Server"
finally:
    s.close()

html_start("white")
print(x)
html_end()
Vielen Dank im Voraus aus an Alle die sich dieses Themas annehmen...

Dirk
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@Dirk1972: Dir ist bewußt, dass UDP kein verlässliches Protokoll ist, Du Dich also selbst darum kümmern mußt, dass Commandos bestätigt werden und bei einem Timeout gegebenenfalls nochmal gesendet werden?

"module" ist ein ungewöhnlicher Name für ein Packet und auch sehr nichtssagend. Für Logging gibt es bereits das logging-Modul. Sternchenimporte sollte man generell vermeiden, weil sie unkontrolliert Namen in den eigenen Namensraum bringen.
Pickle sollte nicht für allgemeinen Datentransfer benutzt werden. Damit hast Du Dir eine riesige Sicherheitslücke geöffnet. Du benutzt globale Variablen, was das ganze Programm unübersichtlich macht, was wann passiert. Nackte excepts sollte man nicht benutzen, weil dadurch auch manche Programmierfehler abgefangen werden und das Debuggen unmöglich gemacht wird.
Ich kann bei all den Problemen und unübersichtlichem Code keinen Fehler entdecken. Mit shutdown sollte eigentlich server_forever und damit das Programm beendet werden.
BlackJack

Dirk1972: Also ich sehe da ja noch ganz andere Probleme, nämlich dass das alles andere als fehlerfrei ist.

Das Protokoll ist ziemlicher Murks und Daten bei UDP auf Pakete verteilen ist eigentlich ein Zeichen das man TCP verwenden möchte. Bei UDP ist weder garantiert *das* ein Paket überhaupt ankommt, noch das versendete Pakete in der gleichen Reihenfolge ankommen in der sie abgeschickt wurden. Das wird bei Dir im privaten Netz wahrscheinlich fast immer klappen, aber eben auch nur fast. Irgendwann fliegt einem das um die Ohren. Und bevor man anfängt das in einem Protokoll mit Sequenznummern in den Paketen und Bestätigungsnachrichten abzusichern, kann man auch gleich TCP verwenden, denn das macht das bereits alles. UDP ist für kleine Datenpakete bei denen weder die Reihenfolge noch ob sie überhaupt ankommen eine Rolle spielt.

':END' an die Daten anzuhängen ist nicht robust. Das fällt auf die Nase sobald das als Teilbytekette in den Daten davor vorkommt. Und bevor Du jetzt sagst das kommt nie vor: Das kann auch die ganze Zahl 1145980218 sein die das zum Problem werden lassen kann, oder die Zahlenfolge 17722, 17486. Oder die Gleitkommazahl 1.1167812434971056e+21 (und viele andere).

Das Aufteilen auf Pakete ist viel zu kompliziert geschrieben. Der ``else``-Teil funktioniert auch für Pakete die ≤1024 Bytes sind, also ist der Test schon mal unnötig. Und auch beim Aufteilen selber hätte der ``else``-Teil vollkommen ausgereicht. Und statt der Multiplikation hätte man das alles bereits `range()` erledigen lassen können: ``for i in range(0, len(data) + 1, 1024): sock.sendto(data[i:i + 1024], address)``.

Spezielle Fehlerrückgabewerte mit den eigentlichen Rückgabewerten zu vermischen ist keine gute Idee. Bei solchen APIs gibt man deshalb in der Regel immer zwei Werte zurück, den Rückgabwert und den Fehlerwert, oder nur eines davon, und ”automatisch” unterscheidbar, so dass das auf jeden Fall sichtbar auf die Nase fällt. Beispielsweise ein Wörterbuch mit 'result' *oder* 'error'-Schlüssel, so dass bei einem Fehler das Programm in einen `KeyError` läuft wenn jemand das 'result' verwenden möchte ohne geprüft zu haben ob und welcher Fehler vorliegt.

Ein nacktes ``except:`` ohne konkrete Ausnahmen und ohne eine allgemeine Ausnahme zu behandeln in dem sie wenigsten komplett *mit Traceback* in irgendeiner Weise protokolliert wird, nimmt Dir jede sinnvolle Möglichkeit Fehlerursachen zu finden. Dem Client könnte man wenigstens die Zeichenkettendarstellung der behandelten Ausnahme übermitteln. Wenn nicht gar den gesamten Traceback.

Der Code des Haupprogramms gehört in eine Funktion. Was dann dazu führt dass man die Datenbankverbindung irgendwie *sauber* an die beiden `db_*()`-Funktionen übergeben muss statt das einfach so aus dem Modulnamensraum zu holen. An der Stelle sei auch erwähnt das mit die Namen `comm` und `conn` in der gleichen Funktion etwas verwirrt haben. Bitte `connection` ausschreiben. Bei `comm` bin ich leider nicht darauf gekommen was das bedeuten soll. Namen sollten klar sein und nicht zum rätselraten animieren. Das Argument `value` bei beiden Funktionen sollte auch eher `values` heissen. Du hast tatsächlich ein Package `modul` genannt‽

Was ist mit dem `log`-Modul? Erfindest Du da das Rad neu? Es gibt in der Standardbibliothek ein `logging`-Modul.
Benutzeravatar
Dirk1972
User
Beiträge: 5
Registriert: Mittwoch 29. April 2015, 09:44
Wohnort: Augsburg
Kontaktdaten:

Erstmal vielen Dank für die Hinweise.
Wahrscheinliche denke ich noch zu wenig pythonisch, da ich erst seit kurzen dabei bin und ich eigentlich über die Datenbank- und html-Programmierung (Beides mach ich seit über 15 Jahren) an Python gekommen bin. Skripte mit Python zu erstellen ist "relativ" einfach.
Insbesondere wenn mann auf Datenbanken und mit telnet und ssh auf andere Prozesse auf Microcontroler zugreifen möchte. Ich versuche Mal zu erklären wir ich zu diesem "Murks" gekommen bin. :wink:

Meine WEB-Seiten haben Skripte zum Darstellen von Rezepten, zum Schalten von Schaltern, zum Auslesen von Sensoren etc.
Die jeweiligen Daten hierfür liegen in einer Datenbank. Jedes Skript benötigt seine Parameter. Kleine Datenmengen.

Ursprungsproblem:
Skript mit sqlite3 Datenbank. Alles läuft rund. Aber zeitgleicher Zugriff... geht ja nicht.
Umgestiegen auf mariadb. Jedes Skript meldet sich bei der Datenbank an. Der Seitenaufbau verzögerte sich um mehr als 5 sec!
Dann meine Idee: Die Anmeldung muss es sein: Daher socketserver mit angemeldeter Datenbank. Die Einzelskripte greifen schnell auf die Datenbank zu!
Und tatsächlich mit diesem Servierskript erreiche ich nicht ganz die zeitliche Performance von sqlite aber wesentlich besser als ohne.

UDB schneller als TCP sonst ja wieder Geschwindigkeitsverlust.
Sicherheit: Ich greife ausschließlich über localhost vom Webserver auf das Skript zu. Der Port ist nach aussen geschlossen=> Keine Sicherheitsrisiko.
Für die meisten abfragen (99% aller Abfragen) reichen 1024 bit um die Parameter eines Schalters abzufragen, oder einen Log in die Datenbank zu schreiben.
Nur für den Fall die Rezeptliste oder die Logdaten auszulesen sind es mehr Daten...

Eigenen log-Anwendung: Meine Logs schreiben in meine Datenbank und unterscheiden zwischen Warnung (mit automatischer Email an mich) und Ereignis zum reinen speichern in der log-Datenbank.

Also wenn es einen anderen Weg gibt um schnell auf Daten in mariadb zuzugreifen bin ich sehr dankbar.

An BlackJack: Sorry für die Anfängerfehler in der Schreibweise: comm steht für die Datenbankbefehle z.B. "SELECT * FROM .... WHERE ....
"module" als Name für mein Package stammt aus einem Beispiel aus einem Pythontutorial. Es funktioniert und ist immer so geblieben...

Code aus dem Hauptprogramm in eine Funktion zu schreiben sollte natürlich gemacht werden. Aber ich probiere erst mir neue Programmierschritte aus, bevor ich Sie in Funktionen umschreibe. Aber hier funktioniert noch nicht alles...
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@Dirk1972: Dein letztes Argument ist ein ganz schwaches. Du hast Dein ganzes Programm um dieses Hauptprogramm herumgeschrieben und greifst deshalb überall auf globale Variablen zu. Hättest Du alles gleich in Funktionen geschrieben, wären solche Fehler erst gar nicht passiert. Der Geschwindigkeitsunterschied zwischen UDP und TCP ist in Deinem Fall nicht meßbar.

Dein ganzes Programm beruht auf einigen falschen Annahmen, die Du gemacht hast. Solange die SQLite-Datei auf einer lokalen Platte liegt, ist Parallelzugriff überhaupt kein Problem. Es gibt bestimmt etliche MySQL-Proxies, die Connection-Pooling unterstützen für einen schnelleren Zugriff. Das eigentliche Problem ist aber viel grundlegender: Python ist anders als PHP. Man startet nicht für jeden Request über CGI einen neuen Prozess, sondern es gibt einen Python-Prozess der über wsgi an den Web-Server angebunden ist. Dadurch erspart man sich das ständige Neuverbinden mit irgendwelchen Datenbankservern. Dafür bietet Python etliche Frameworks an, die einem die meiste Arbeit abnehmen, wie Flask, Bottle oder Django.
Benutzeravatar
Dirk1972
User
Beiträge: 5
Registriert: Mittwoch 29. April 2015, 09:44
Wohnort: Augsburg
Kontaktdaten:

Hmm. Ich glaube ich werde mal ein paar grundlegende Gedanken, die Euch Dank Euch erhalten haben weiter verfolgen.

Ich dachte mir fast, dass ich auf Grund von meinem Weg aus html heraus einen grundlegenden Gedankenfehler hatte...

Welches Framework hat welche Vor- und Nachteile?!
Antworten