Tkinter und Socket-Server parallel... Threading?

Fragen zu Tkinter.
Antworten
foreverengineer
User
Beiträge: 2
Registriert: Donnerstag 14. Januar 2016, 19:15

Hallo zusammen,

ich bin dabei eine Anwendung zu schreiben, die neben einer Benutzeroberfläche mit Tkinter auch einen Socket-Server behinhaltet.
Grobe Funktion: Anwendung liest eine .csv-Datei ein, in der Aufgaben stehen. Die Tasks werden in der GUI dargestellt und können beabeitet und neu abgespeichert werden. Über den Socket verbinden sich Clients, welche vom Server gewisse Tasks zugeschickt bekommen sollen. Die Clients geben daraufhin Feedback.

Mit vielen Tutorials und Beispielcodes habe ich mich ans Werk gemacht und versucht es möglichst simpel umzusetzen (ohne OOP).

Alle Bestandteile für sich funktionieren soweit. Nun friert leider die GUI ein, sobald sich ein Client mit dem Server verbindet. Warum ist mir auch klar: der Socket blockiert die GUI solange, bis die Verbindungen geschlossen werden.
Abhilfe würde es also schaffen, die GUI in einem separatem Thread laufen zu lassen. Ich habe schon mehrere Möglichkeiten probiert, aber nichts hat funktioniert.

Ich habe mal versucht meinen Code um alles für dieses Problem überflüssige zu erleichtern. Dies ist der letzte funktionierende Stand (ohne meine vergeblichen Versuche die GUI in einen Thread zu verpacken):

Code: Alles auswählen

from Tkinter import *
import csv
import socket
import sys
import json
from thread import *
import threading

root = Tk()

#------------Server Config------

HOST = ''   
PORT = 8888 

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #test
print 'Socket created'

#Bind socket to local host and port
try:
    s.bind((HOST, PORT))
except socket.error as msg:
    print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message: ' + msg[1]
    sys.exit()

print 'Socket bind complete'

#Start listening on socket
s.listen(10)
print 'Socket now listening'

#------------Functions---------


# a lot of functions...       


def clientthread(conn):

    conn.send('CONNECTED \n')
    #conn.send(json.dumps(tasksA))
    station = ""


    while True:
        print "Entered loop" #

        databytes = conn.recv(1024)
        if not databytes: break

        data = databytes.decode("utf-8")
        print "Client: " + data


        if data =='STATION_A':
            station = "Station A"
            print "Connected to: " + station
            conn.send(json.dumps(tasksA))

        #some more if-clauses


    conn.close()

def runServer():
    while True:
        conn, addr = s.accept()
        #conn.setblocking(0)
        print 'Connected with ' + addr[0] + ':' + str(addr[1])

        #start new thread takes 1st argument as a function name to be run, second is the tuple of arguments to the function.
        start_new_thread(clientthread ,(conn,))
        root.after(100, runServer)
    s.close()

#------------GUI-----------------------

root.geometry("1024x768")
root.minsize(width="1024", height="768")
root.title("FTS v04")

#... left out most of the gui stuff... just too much code


thread = threading.Thread(target = runServer)
thread.deamon = True
thread.start()
root.mainloop()
Ich bin ein kompletter Python-Neuling und dies stellt mein erstes Projekt dar. Würde mich sehr über ein paar konkrete Hinweise freuen!

Viele Grüße
Oliver

P.S.: Es MUSS leider bei Python 2.7 bleiben, da ich auf ein zusätzliches Modul angewiesen bin, welches nur unter 2.7 läuft!
Zuletzt geändert von Anonymous am Donnerstag 14. Januar 2016, 19:46, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@foreverengineer: Dein Code bedarf dringender Überarbeitung. Ohne das Durcharbeiten eines Grundlagentutorials kommt beim Zusammenstückeln von gefundenen Teilen nichts sinnvolles heraus.

Vermeide *-Importe. Du hast keine Kontrolle was da alles in Deinen Namensraum geladen wird. Das thread-Modul ist veraltet. Verwende threading. Mische keinen aktiven Code mit Funktionsdefinitionen. Eigentlich sollte gar kein ausführbarer Code außerhalb von Funktionen stehen. Dein Socket-Code in clientthread ist kaputt. send garantiert nicht dass alles gesendet wurde, recv garantiert nur, dass mindestens 1 Byte gelesen wird. Ein sinnvolles Protokoll kann ich auch nicht erkennen. Du startest doch schon Threads. Da solltest Du nicht mit after auch noch auf einen neuen Client warten. Das blockiert nämlich dann.

Ein bißchen aufgeräumt sähe das dann vielleicht so aus:

Code: Alles auswählen

import Tkinter as tk
import socket
import threading


#------------Server Config------
HOST = ''   
PORT = 8888 

def clientthread(conn):
    input = conn.makefile('rb')
    conn.sendall('CONNECTED \n')
    #conn.send(json.dumps(tasksA))
    station = ""

    while True:
        print "Entered loop" #
        data = input.read(9)
        if data =='STATION_A':
            station = "Station A"
            print "Connected to: %s" % station
            conn.sendall(json.dumps(tasksA))

        #some more if-clauses
    conn.close()


def run_server():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #test
    print 'Socket created'
    s.bind((HOST, PORT))
    s.listen(10)
    while True:
        conn, addr = s.accept()
        print 'Connected with %s:%s' % addr
        
        #start new thread takes 1st argument as a function name to be run, second is the tuple of arguments to the function.
        threading.Thread(target=clientthread, args=(conn,))


def main():
    thread = threading.Thread(target = run_server)
    thread.deamon = True
    thread.start()

    root = tk.Tk()
    root.geometry("1024x768")
    root.minsize(width="1024", height="768")
    root.title("FTS v04")
    root.mainloop()
    
if __name__ == '__main__':
    main()
BlackJack

@foreverengineer: GUI-Programmierung möglichst simpel ist *mit* OOP. Ohne wirds komplizierter und vor allem auch schnell unübersichtlicher. OOP dient dazu die Komplexität zu strukturieren und nicht um etwas einfach nur anders aber komplexer zu schreiben. Was da alles modulglobal rumschwirrt, da bekommt man ja jetzt schon Kopfschmerzen, wie soll dass dann erst aussehen wenn da noch die Funktionalität drin steht.

Auf Modulebene gehört nur Code der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm gehört auch in eine Funktion. Werte (ausser Konstanten) betreten Funktionen und Methoden nur als Argumente und verlassen sie gegebenfalls als Rückgabewerte. Alles andere führt bei jedem nicht-trivialen Programm ganz schnell zu Code der nicht mehr les- und wartbar ist.

Die Behandlung von TCP ist fehlerhaft. Das ist ein Datenstrom und ein `recv()`-Aufruf kann irgendeinen Teil vom Anfang des Datenstroms liefern. Das kann im Extremfall jedes Byte einzeln sein. Es ist nicht garantiert das wenn der Sender mit einem Aufruf 'Hallo' auf den Weg schickt, dass der Empfänger das dann auch mit einem Aufruf von `recv()` komplett bekommt. Man muss solange `revc()` aufrufen bis man sicher ist die gesammten Daten ausgelesen zu haben.

Das `thread`-Modul sollte nicht mehr verwendet werden. Du verwendest *zusätzlich* auch noch `threading`? Und in `runServer()` das als Thread gestartet wird versuchst sich selbst mit `Widget.after()` nochmal auszuführen? Das ist ja total wirr. Der Server hat nichts mit der GUI zu tun, sollte also auch nichts mit GUI-Objekten machen.
foreverengineer
User
Beiträge: 2
Registriert: Donnerstag 14. Januar 2016, 19:15

Hallo Sirius3, hallo BlackJack,

danke für eure Hinweise...
Puh, mit solcher Kritik hab ich schon fast gerechnet. Ich hatte leider nicht die Möglichkeit mit den Basics anzufangen, sondern musste gleich ein richtiges Projekt starten. Mit C++ kann ich ganz gut mit Objekten und Funktionen umgehen, bei Python sehe ich dort leider nicht so recht durch bzgl. OOP. Das soll aber keine Entschuldigung sein, ich versuche mir das anzueignen.

Ich habe mir den Code von Sirius3 (vielen, vielen Dank!) genauer angeschaut und versucht mein restliches Programm dorthin zu integrieren.
Leider stoße ich auf eine Menge Fehler und weiß nicht so recht, wodran es liegt.

Der Reihe nach:
-alle Tkinter Widget fehlen leider (Label, Button, Canvas usw sind nicht definiert) habe testweise wieder "from Tkinter import *" verwendet, womit es klappt. Muss ich dann manuell alle benötigten module importieren? Also "from Tkinter import Label, Button, Canvas usw"?

-dann kriege ich den Fehler, dass alle meine Tkinter Objekte nicht definiert sind "global name 'rightFrame' is not defined". Diese Widget werden jetzt ja in der main-Funktion erstellt. In meinen Fall möchte ich zB ein vorhandenes Textfeld mit einer anderen Funktion (loadTasks) mit Daten aus einer .csv Datei füllen und per Schleife für jeden Datensatz drei Buttons einfüge. Ich denke, dass geht jetzt nicht mehr, da die Widgets nur innerhalb von main benutzt werden können? In meinem jugendlichen Leichtsinn habe ich einfach loadtasks() in der main() aufgerufen. Funktioniert natürlich nicht :K ...

Was hingegen wohl funktioniert ist das Threading, auch wenn ich nicht genau weiß warum. Jedenfalls kann ich mit einem Client connecten und die GUI stürzt nicht ab (es sind aber bisher auch nur statische Elemente vorhanden, das oben genannte Textfeld bleibt ja leer).

Ich poste noch einmal meinen etwas erweiterten Code, damit ihr seht, was ich meine:

Code: Alles auswählen

#import Tkinter as tk
from Tkinter import *
import socket
import threading
import csv
import sys
import json
 
 
#------------Server Config------
HOST = ''  
PORT = 8888
#-------------------------------

 
def clientthread(conn):
    input = conn.makefile('rb')
    conn.sendall('CONNECTED \n')
    #conn.send(json.dumps(tasksA))
    station = ""
 
    while True:
        print "Entered loop" #
        data = input.read(9)
        if data =='STATION_A':
            station = "Station A"
            print "Connected to: %s" % station
            conn.sendall(json.dumps(tasksA))
 
        #some more if-clauses
    conn.close()
 
 
def run_server():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #test
    print 'Socket created'
    s.bind((HOST, PORT))
    s.listen(10)
    while True:
        conn, addr = s.accept()
        print 'Connected with %s:%s' % addr
       
        #start new thread takes 1st argument as a function name to be run, second is the tuple of arguments to the function.
        threading.Thread(target=clientthread, args=(conn,))
 
    

def loadTasks():
    #taskList.config(state=NORMAL)  # ERROR: taskList not definied...
    global tasks
    tasks = csv_open()
    n = 0
    for i in range(len(tasks)):
        w=12
        w1=22
        Label(rightFrame, text="Auftrag: ", width=w, bd=0, anchor=W, bg="#dadee1").grid(row=n, column=0)
        Label(rightFrame, text=tasks[i][0], width=w1, bd=0, anchor=W, bg="#dadee1").grid(row=n, column=1)
        Button(rightFrame, text="PRIORITAET", width=10, anchor=W, bg="#84919a", fg="#ffffff", command=lambda i=i: moveTask(i)).grid(row=n, column=2)
        n = n+1
        Label(rightFrame, text="Empfaenger: ", width=w, bd=0, anchor=W, bg="#dadee1").grid(row=n, column=0)
        Label(rightFrame, text=tasks[i][1], width=w1, bd=0, anchor=W, bg="#dadee1").grid(row=n, column=1)
        Button(rightFrame, text="BEARBEITEN", command=lambda i=i: editTask(i), width=10, anchor=W, bg="#7dd5cd", fg="#ffffff").grid(row=n, column=2)
        n = n+1
        Label(rightFrame, text="Status: ", width=w, bd=0, anchor=W, bg="#dadee1").grid(row=n, column=0)
        Label(rightFrame, text=tasks[i][2], width=w1, bd=0, anchor=W, bg="#dadee1").grid(row=n, column=1)
        Button(rightFrame, text="LOESCHEN", command=lambda i=i: delTask(i), width=10, anchor=W, bg="#ff5751", fg="#ffffff").grid(row=n, column=2)
        n = n+1
        Label(rightFrame, text="Ware: ", width=w, bd=0, anchor=W, bg="#dadee1").grid(row=n, column=0)
        Label(rightFrame, text=tasks[i][3], width=w1, bd=0, anchor=W, bg="#dadee1").grid(row=n, column=1)
        n = n+1
        Label(rightFrame, text="", width=w, bd=0, anchor=W, bg="#dadee1").grid(row=n, column=0)
        Label(rightFrame, text="", width=w1, bd=0, anchor=W, bg="#dadee1").grid(row=n, column=1)
        n = n+1
        
    for i in range(len(tasks)):
        global tasksA, tasksB, tasksC
        if tasks[i][1] == "STATION A":
            temp = []
            temp.append(tasks[i][0])
            temp.append(tasks[i][1])
            temp.append(tasks[i][2])
            temp.append(tasks[i][3])
            tasksA.append(temp)
        if tasks[i][1] == "STATION B":
            temp = []
            temp.append(tasks[i][0])
            temp.append(tasks[i][1])
            temp.append(tasks[i][2])
            temp.append(tasks[i][3])
            tasksB.append(temp)
        if tasks[i][1] == "STATION C":
            temp = []
            temp.append(tasks[i][0])
            temp.append(tasks[i][1])
            temp.append(tasks[i][2])
            temp.append(tasks[i][3])
            tasksC.append(temp)
    print(tasks)
    anzTasks = len(tasks)-1
    #taskList.config(state=DISABLED)

def onFrameConfigure(canvas):
    '''Reset the scroll region to encompass the inner frame'''
    canvas.configure(scrollregion=canvas.bbox("all"))

def delTask(number):
    print "Delete Task No: " + str(number)
    tasks.pop(number)
    csv_save(tasks)
    loadTasks()



def moveTask(number):
    print "Move Task No: " + str(number)
    tasks.insert(0, tasks.pop(number))
    csv_save(tasks)
    loadTasks()



 
def main():

    global tasksA
    tasksA = []
    global tasksB
    tasksB = []
    global tasksC
    tasksC = []

    global newTasks
    newTasks = []

    thread = threading.Thread(target = run_server)
    thread.deamon = True
    thread.start()
    root = Tk()
    root.geometry("1024x768")
    root.minsize(width="1024", height="768")
    root.title("FTS Leitstand v04")

    head = Label(root, text="FAHRERLOSES TRANSPORTSYSTEM - LEITSTAND", bg="#2d383e", fg="white", width="1024", height="2")
    head.pack(fill=X)

    footer = Frame(root, bg="#84919a", width="1024", height="130")      #Info Bereich
    footer.pack(side=BOTTOM, expand=YES, fill=BOTH)

    left = Frame(root, bg="#35424b", width="660", height="600")         #Karte
    left.pack(side=LEFT, expand=NO, fill=X)

    right = Frame(root, bg="#dadee1", width="364", height="600")        #Task-Liste
    right.pack(side=LEFT, expand=YES, fill=X)

    canvas = Canvas(right, height=590, bd=0, background="#dadee1")
    rightFrame = Frame(canvas, height="600", background="#dadee1")
    vsb = Scrollbar(right, orient="vertical", command=canvas.yview)
    canvas.configure(yscrollcommand=vsb.set)

    rightFrame.pack(expand=YES, fill="both")
    vsb.pack(side="right", fill="y")
    canvas.pack(side="left", fill="both", expand=YES)
    canvas.create_window((4,4), window=rightFrame, anchor="nw")

    right.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))

    scrollbar = Scrollbar(right)
    taskList = Text(right, bg="#dadee1", height=39)

    karte = Canvas(left, width=660, height=600, bg="#35424b", bd="0", highlightthickness="0")
    karte.pack()

    
    loadTasks()
    
    root.mainloop()
   
if __name__ == '__main__':
    main()
Zuletzt geändert von Anonymous am Freitag 15. Januar 2016, 16:31, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@foreverengineer: natürlich fehlen die Namen, wenn man nicht mehr ALLES importiert. Der Ausweg besteht darin, einfach vor alles, was in Tkinter steht ein "tk." zu hängen, dann wird auch gleich klar, dass das ein Tk-Objekt ist (vor allem die Konstanten haben ja sehr allgemeine Namen).
Dass man auf bestimmte Objekte nicht zugreifen kann, löst man, wie BlackJack schon geschrieben hat, per OOP.
Antworten