Einfacher Webserver zum Abruf von .txt & .jpg Datei

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
pythonanfänger1
User
Beiträge: 5
Registriert: Dienstag 29. April 2014, 12:35

Ich bin absoluter Python-Anfänger und brauche Hilfe bei der Erstellung eines kleinen TCP-Webservers.
Der Server soll zwei Dateien (eine .txt und eine .jpg Datei) aus dem Root-Verzeichnis zur Verfügung stellen und im Falle, dass die Datei fehlt eine 404 Response geben (auf localhost).
Ich stehe absolut auf dem Schlauch, wie ich das angehen soll. Bisher habe ich folgendes:

Code: Alles auswählen

import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(("127.0.0.1", 8080))
sock.sendall("GET /Text.txt HTTP/1.1\r\n\r\n".encode("UTF-8"))
Wie gehe ich weiter vor?
Kann mir jemand von euch helfen?

Danke im Voraus!
BlackJack

@pythonanfänger1: Das sieht mir eher nach einen Client statt einem Server aus.

Für einen einfachen Webserver schau Dir mal das `SimpleHTTPServer`-Modul aus der Standardbibliothek an.
pythonanfänger1
User
Beiträge: 5
Registriert: Dienstag 29. April 2014, 12:35

Danke erst einmal für den Tipp ;)

Eine .txt-Datei kann ich schon abrufen, jedoch stehe ich jetzt vor zwei weiteren Problemen:

1. die Textdatei habe ich einfach enkodieren können mit

Code: Alles auswählen

textFile = open("Datei.txt").read().encode("UTF-8")
, jedoch weiß ich nicht, wie ich ein .jpg Bild enkodieren kann

2. mit dem Header habe ich auch noch ein Problem. Mein bisheriger Code:

Code: Alles auswählen

    
#date = 'Date: ' + time.asctime() + '\r\n'
statusLine = 'HTTP/1.1 404 Not Found'
headerLines = "Server: NewType\r\n"
headerLines += 'Content-Type: text/html\r\n\r\n'
message404 = statusLine + headerLines    
message404String = bytes(message404, encoding="UTF-8")
Das funktioniert gut, sobald ich aber das Datum mit aufnehme, wird mir der Header aber als Text im Browser dargestellt, was nicht der Fall sein sollte; also:

Code: Alles auswählen

date = 'Date: ' + time.asctime() + '\r\n'
statusLine = 'HTTP/1.1 404 Not Found'
headerLines = "Server: NewType\r\n"
headerLines += 'Content-Type: text/html\r\n\r\n'
message404 = date + statusLine + headerLines    
message404String = bytes(message404, encoding="UTF-8")
Habt ihr eventuell Ideen, wie ich die Probleme in den Griff bekomme?
BlackJack

@pythonanfänger1: Du solltest weder den Text noch die Bilddaten *kodieren*. Das musst Du ja nur machen weil sie vorher *dekodiert* werden weil Du beides im Textmodus öffnest. Webserver liefern aber Binärdaten aus. Also öffne die Datei im Binärmodus und sende einfach die gelesenen Bytes.

Die Datei so zu öffnen und dann gleich zu lesen, nimmt Dir die Möglichkeit die Datei am Ende auch wieder explizit zu schliessen. Das sollte man aber immer tun, denn Dateihandles sind eine Ressource die nicht unbegrenzt zur Verfügung steht, und wenn Du Dich auf die automatische Speicherverwaltung verlässt, ist nicht garantiert *wann* die Datei geschlossen wird, oder ob sie vor Programmende *überhaupt* geschlossen wird. Wenn man die Datei zusammen mit der ``with``-Anweisung öffnet, ist man sicher das sie auf jeden Fall geschlossen wird, wenn der ``with``-Block verlassen wird, egal aus welchen Gründen.

Die Statuszeile muss die *erste* Zeile in der Antwort sein. Ausserdem ist die bei Dir nicht durch die entsprechenden Zeilenende-Bytes von den Headern getrennt.

Ich würde die Header zudem nicht UTF-8 kodieren, denn das ist ziemlich sicher nicht die korrekte Kodierung. Würde mich nicht wundern wenn da sogar nur ASCII erlaubt wäre. Schau mal in die ensprechende HTTP-Spezifikation.
pythonanfänger1
User
Beiträge: 5
Registriert: Dienstag 29. April 2014, 12:35

Ich schaffe es einfach nicht, die jpg-Datei zu übertragen.
Hierbei bekomme ich diese Fehlermeldung:

Code: Alles auswählen

picture = open('LKN.jpg').read()
TypeError: 'str' does not support the buffer interface

Code: Alles auswählen

picture = open('LKN.jpg',mode='wb')
Und hiermit diesen Fehler.
TypeError: '_io.BufferedWriter' does not support the buffer interface

Mode 'rb', 'r' usw. habe ich auch schon durchprobiert.
Ich werde auch durch googlen nicht schlauer... Wie öffne ich diese Bilddatei?
BlackJack

@pythonanfänger1: Also erst einmal solltest Du die Bilddatei zum *lesen* öffnen und nicht zum *schreiben*. Und dann musst Du an der richtigen Stelle suchen, denn die Fehlermeldung wird nicht von der `open()`-Funktion kommen, sondern wo anders ihre Ursache haben. Der Traceback sollte da helfen. Und würde uns auch helfen, also zeig den am besten immer Komplett wenn Du hier eine Fehlermeldung in einen Beitrag kopierst.
pythonanfänger1
User
Beiträge: 5
Registriert: Dienstag 29. April 2014, 12:35

Danke für die Antwort.

Habe den Code etwas gekürzt, aber so in etwa habe ich gehofft, die Bilddatei über den Browser abzurufen.

Code: Alles auswählen

def parseMessage(clientMessageString):
    if "GET /picture.jpg" in clientMessageString:
        #strHeader = response200_OK()
        jpgFile = readAndReturnJPG()
        answer = jpgFile
        return answer
    else:
        ErrorMessage = '404 Error'.encode("UTF-8")
        return ErrorMessage

def readAndReturnJPG():
    picture = open('picture.jpg',mode='r')
    return picture

import socket
HOST = "127.0.0.1"
PORT = 8080
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
def main():
    while 1:
        s.listen(1)
        conn, addr = s.accept()
        print('Connected by', addr)
        clientMessage = conn.recv(1024)
        clientMessageString = str(clientMessage)
        print(clientMessageString)
        toClient = parseMessage(clientMessageString)
        conn.sendall(toClient)

if __name__ == "__main__":
    print("Webserver is main process.")
    main()
BlackJack

@pythonanfänger1: Da gibt es einige Probleme mit Namen und auch technische Probleme.

Die Namensschreibweise folgt nicht dem Style Guide for Python Code. Und oft sind sie unpassend. `parseMessage()` macht deutlich mehr als nur die Nachricht vom Client zu parsen. Konkrete Datentypen sollten nicht bestandteil des Namens sein, weil man den konkreten Typ dann nicht mehr ändern kann, ohne überall den Namen anpassen zu müssen, oder Quelltext mit irreführenden Namen zu haben.

Der ``in``-Operator testet ob das erste Argument im zweiten *enthalten* ist; das ist aber nicht das was man in `parseMessage()` wissen möchte, denn eine Anfrage wie 'something GET /picture.jpg' wäre falsch, würde aber trotzdem von dem Test als richtig erkannt.

Es macht auch nicht so viel Sinn das *so* spezifisch zu machen. Denn das müsste man für die Textdatei auch noch mal machen. Hier würde sich eine allgemeine Funktion die GET-Anfragen beantwortet und die zur Verfügung stehenden Dateien als Argument erhält oder aus einer Konstante auf Modulebene, oder noch besser aus einem Unterverzeichnis wo die Dateien liegen. Also wie ein normaler Webserver. Im letzten Fall darf man nicht vergessen sicherzustellen, dass der Benutzer nicht über relative Pfadangaben in übergeordnete Verzeichnisse gelangen kann.

Man muss nicht jeden Wert an einen Namen binden, insbesondere wenn die nächste Aktion ein einfaches ``return`` mit diesem Namen ist. Und schon gar nicht muss man einen Wert an einen Namen binden, in der nächsten Zeile noch mal an einen *anderen* Namen, nur um den dann in der nächsten Zeile mit einem ``return`` zu verwenden.

Die Fehlerbehandlung in der Funktion ist nicht gut, denn der Aufrufer kann nicht zwischen einem regulären Dateiinhalt und einem Fehler unterscheiden.

`readAndReturnJPG()` ist überspezifisch. So etwas wie `AndReturn` gehört nicht in einen Namen. Wenn die Funktion `read_jpg()` heisst, ist normalerweise auch so klar was sie zurück gibt. Aber auch das `jpg` hat da nichts zu suchen, denn was das für ein Dateityp ist, sowie der fest kodierte Name *in* der Funktion sind schlecht entworfen. Einem Webserver ist an der Stelle völlig egal welche Daten in der Datei sind, die Bytes müssen einfach nur eingelesen werden. (Bei einem echten Webserver wird man das allerdings so nicht machen, denn es gibt ja auch richtig grosse Dateien, die man nicht komplett in den Speicher lesen möchte.)

In der Funktion ist auch der Fehler, denn entgegen dem Namen wird da nichts gelesen.

Dateien die man öffnet, sollte man auch wieder schliessen. Falls möglich bietet sich das öffnen zusammen mit der ``with``-Anweisung dafür an.

Importe gehören an den Anfang eines Moduls, damit man die Abhängigkeiten leichter sehen kann, und auch nicht das selbe Modul unnötig mehrfach importiert weil man übersehen hat, dass es irgendwo anders im Modul schon mal importiert wird.

Das gleiche gilt für die Definition von Konstanten.

Erstellen und Binden des Sockets gehört nicht auf Modulebene sondern zum Hauptprogramm.

Man sollte literale Zahlen nicht als Wahrheitswerte missbrauchen. ``while True:`` ist lesbarer als ``while 1:``.

Der `listen()`-Aufruf gehört nicht in die Schleife.

Das Lesen der Anfrage ist fehlerhaft. TCP ist ein Datenstrom und ein `recv()`-Aufruf kann einen beliebigen Teilausschnitt vom Anfang des Datenstroms liefern. Im Extremfall muss korrekter Socket-Code damit klar kommen, dass jedes Byte einzeln ankommt. Für einen einfachen Webserver muss man also mindestens solange `recv()` aufrufen bis man die erste Zeile der Anfrage empfangen hat, diese vom eventuell vorhandenen Rest abtrennen und dann verarbeiten.

Genau wie bei Dateien sollte man TCP-Verbindungen explizit schliessen. Damit die Gegenseite weiss, dass da keine Daten mehr kommen. Denn einen Size-Header sendest Du ja nicht, also kann der Client nur an der geschlossenen Verbindung erkennen, dass die Daten komplett sind. (Also eigentlich kann er daran nur erkennen, dass da keine weiteren Daten mehr folgen können.)
pythonanfänger1
User
Beiträge: 5
Registriert: Dienstag 29. April 2014, 12:35

@BlackJack:
Danke für die ausführliche Antwort. Der Server ist eine Hausaufgabe, in welcher das Grundgerüst vorgegeben und auch genutzt werden muss. Ich darf die Namen der Funktion nicht ändern. Das Teil soll dem Browser bei Eingabe von 127.0.0.1:8080/picture.txt einfach nur das Bild übergeben. Muss nicht etrem sauber geschrieben sein, sondern einfach nur funktionieren.

Leider bin ich immer noch nicht schlauer, wie ich das Bild verschicken kann... Wo liegt denn der Fehler mit der Bilddatei?
DaftWullie
User
Beiträge: 37
Registriert: Donnerstag 17. Mai 2012, 21:28

@Pythonanfänger: Das hat doch BlackJack geschrieben: Du liest das file nicht ein, da fehlt dann wohl ein read ...
BlackJack

@pythonanfänger1: Ob Du die Namen tatsächlich nicht ändern darfst, lasse ich mal so stehen, man *kann* es auf jeden Fall und man kann es auch begründen.

Andererseits kannst Du aber mindestens den Inhalt der Funktionen an den Namen anpassen. Eine `parse…()`-Funktion sollte tatsächlich nur etwas parsen und nicht noch zusätzlich andere Dinge machen. Es ist *wichtig*, das Namen von Funktionen und Werten auch zu dem passen was da passiert oder was die Werte im Programm bedeuten. Das ist ein wesentlicher Bestandteil vom Programmieren. Bei einer Hausaufgabe zeigt das dem Lehrer auch das man verstanden hat was man da tut, und nicht nur wild rumprobiert hat bis es irgendwie funktioniert.

Edit: Und einiges sind tatsächliche Programmfehler, die dafür sorgen können das sich das Programm nicht so verhält wie es soll, oder auf die Nase fällt, auch wenn es scheinbar ”funktioniert”. Dazu zählt das (nahezu) beliebiger Datenmüll vor dem GET in der Anfrage ignoriert wird, das Dateien und die TCP-Verbindung nicht geschlossen werden, und dass das `recv()` nicht ausreichend ist. Selbst auf dem lokalen Rechner habe ich bei entsprechend Netzwerkverkehr schon tatsächlich erlebt, dass Anfragen zerstückelt wurden und nicht in einem `recv()` gelesen wurden. Also auch wenn das bei Deinen Tests scheinbar immer funktioniert, ist das an der Stelle schlicht kaputt.
Antworten