Doppeltes Paket

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
s4fti
User
Beiträge: 5
Registriert: Donnerstag 2. Oktober 2014, 02:20

Hi Leute,

ich muss sagen ich bin komplett neu bei python, und habe mir diese Sprache ausgesucht um eine kleine Aufgabe mit Sockets zu bewältigen. Ich möchte einen kleinen Kommandozeile-gesteuerten Server schreiben, der Simple Strings speichern kann. Jetzt habe ich mit Hilfe von Wireshark gemerkt, dass ich jedes mal, wenn ich eine Antwort schicke, vorher noch ein leeres Paket mit schicke.
Das würde ich gerne verhindern, da unnötiger Netzwerkverkehr zu vermeiden ist. Ich hoffe jemand sieht, was ich hier falsch gemacht habe, da ich einfach nicht drauf komme wo das eine Paket herkommen soll.
Anbei der Code... Es ist noch nicht so fertig, wie ich das möchte, da ich bisher an diesem Problem hing. Die eingegebene Zahl, die der Länge des zu speichernden/auszugebenen Strings entsprechen soll, ist bisher noch irrelevant.

Danke schonmal im vorraus - alle Anregungen (evtl. auch Off-Topic sind hilfreich)!

Code: Alles auswählen

import socket
import sys
import re
from thread import *

HOST = '' # symbolic name meaning all available interfaces
PORT = 31337 

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# print 'socket created'

try:
	s.bind((HOST, PORT))
except socket.error, msg:
	print 'binding failed, code: ' + str(msg[0]) + ' message: ' + msg[1]
	sys.exit()

# print 'socket bind complete'

s.listen(10)
print 'socket listening'

def clientthread(conn):

	memory = dict()
	key = ''

	# infinite loop
	dataToSave = ''
	waitingForData = False

	conn.sendall('000\n')

	while True:
		# receive from client
		data = conn.recv(1024)
		# split up received data
		result = data.split()
		count = len(result)

		print('Client: ' + data)


		if waitingForData:
			# TODO ask just for NEWLINE PERIOD NEWLINE
			if '.' in data:
				dataToSave = dataToSave + data
				dataToSave = dataToSave.replace(' ', '')[:-3]
				memory[key] = dataToSave
				print('PUT: Stored: ' + dataToSave + ' under ' + key)
				dataToSave = ''
				waitingForData = False
				reply = '000\n'	
			else:
				reply = '001\n'
				print('PUT: Receiving more data')
				dataToSave = dataToSave + data
		else:
			if count > 0:	
				if result[0] == 'PUT' or result[0] == 'GET':
					
					if count == 3:
						length = result[1]
						key = result[2]

						if result[0] == 'PUT':
							# PUT
							if key in memory:
								print('PUT: Key is already in system')
								reply = '102\n'
							else:
								print('PUT: Awaiting data')
								reply = '001\n'
								waitingForData = True
						else:
							# GET
							if key in memory:
								# TODO send data
								print('GET: Client demands ' + key)
								print(memory[key])
								reply = memory[key] + '\n'
								reply = reply + '000\n'
							else:
								print('GET: Key already in system')
								reply = '102\n'
					else: 
						# less than 2 different keywords
						print('Not exactly 3 words received')
						reply = '100\n'

				elif result[0] == 'CLEAR':
					if count == 2:

						key = result[1]

						if key in memory:
							print('CLEAR: Key accepted, deleting data')
							reply = 'deleting data\n'
							# TODO delete data
					else: 
						print('CLEAR: nNot exactly 2 words received')
						reply = '100\n'
				elif result[0] == 'QUIT':
					# quit
					print('QUIT: Client demands quit, quitting')
					conn.close()
				else:
					print('Unknown Command')
					reply = '100\n'
			else:
				print('No words received')
				reply = '100\n'

		if not data:
			break


		conn.sendall(reply)

		
	conn.close()


# keep talking with client
while 1:

	#wait to accept a connection - blocking call
	conn, addr = s.accept()

	# display client information
	print 'Connected with: ' + addr[0] + ' : ' + str(addr[1])

	# start new thread 1st argument is function to be run second is tuple of arguments for function
	start_new_thread(clientthread, (conn,))

s.close()
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@s4fti: das thread-Modul ist veraltet, Du solltest threading statt dessen nehmen. Allen Code auf Modul-Ebene solltest Du in Funktionen stecken, und nur eine Main-Funktion auf oberster Ebene aufrufen, am besten mit

Code: Alles auswählen

if __name__ == '__main__':
    main()
Die clientthread-Funktion ist viel zu lang und damit viel zu verschachtelt. Teile sie in mehrere Funktionen auf.
Dein Protokoll ist kaputt. recv kann bei Dir 1 bis 1024 bytes liefern, im Extremfall in 1-byte Häppchen. Du mußt in einer Schleife so lange recv aufrufen, bis Du einen kompletten Befehl eingelesen hast, den Du dann verarbeitet kannst. Das heißt, Du brauchst irgendeine Kennung, wann ein Befehl vollständig ist.
Um herauszufinden, warum etwas gesendet wird, was Du nicht erwartest, solltest Du Dein Programm auf ein Minimum reduzieren und dann testen.
BlackJack

@s4fti: Ergänzende Anmerkungen: Das `re`-Modul wird importiert aber nicht verwendet.

Sternchenimporte sollte man vermeiden weil man dann a) nicht mehr sieht *was* man sich da alles an Namen in das aktuelle Modul holt, und b) schnell die Übersicht verlieren kann welcher Name aus welchem Modul kommt.

Einiges am Quelltext weicht vom Style Guide for Python Code ab, zum Beispiel die Einrücktiefe und Namensschreibweisen. Wenn man die Einrücktiefe den Konventionen anpasst, dann erfüllt man trotz der tiefen Verschachtelung auch gleich noch die Empfehlung keine Zeilen länger als 80 Zeichen zu haben.

Bei den Namen ist `s` ein bisschen kurz für einen Bezeichner der dem Benutzer ja vermitteln soll um was für einen Wert es sich handelt, also was der im Kontext des Programms bedeutet.

Werte und Zeichenketten mit ``+`` und `str()` verbinden ist eher BASIC als Python. In Python gibt es dafür Zeichenkettenformatierung mit der `format()`-Methode auf Zeichenketten oder dem ``%``-Operator.

Wenn man `sys.exit()` verwendet um ein Programm in Fehlerfall abzubrechen, erwartet der Aufrufer eigentlich einen Rückgabecode ungleich 0 als Zeichen das etwas schief gelaufen ist.

Den Code in Funktionen stecken wurde ja schon erwähnt. Damit würde man auch das Problem beseitigen das jetzt nicht nur viel auf Modulebebe steht, sondern auch das dort Hauptprogramm und Funktionsdefinition abwechselnd stehen, was es dem Leser schwer macht das Hauptprogramm am Stück zu lesen.

In Python 2 ist ``print`` noch eine Anweisung und keine Funktion, dementsprechend haben Klammern um die ”Argumente” nichts zu suchen. Alternativ kann man mit einem `__future__`-Import die Anweisung tatsächlich zu einer Funktion machen. Man sollte sich aber entscheiden welches von beidem es sein soll.

Python hat einen dedizierten Typ für Wahrheitswerte, da sollte man keine Zahlen für missbrauchen, also beispielsweise ``while True:`` statt ``while 1:`` für eine Endlosschleife verwenden.

Funktionen (und Methoden) haben üblicherweise eine Tätigkeit als Namen weil sie etwas tun und um sie von ”passiven” Werten besser unterscheiden zu können. `clientthread()` hiesse also zum Beispiel besser `serve_client()`.

Auch sollte man Abkürzungen vermeiden solange die nicht allgemein bekannt sind. Dann muss ein Leser weniger rätseln was mit einem Namen gemeint ist. Auch bei so Sachen wie `conn`, die einem beim Schreiben sonnenklar sind, kann jemand anderes vielleicht eine Weile brauchen bis er die Verbindung zu `connection` erkennt, was dieser Name ausgeschrieben bedeutet.

Kommentare die keinen Mehrwert über den Code den sie kommentieren bieten, sind überflüssig. Also insbesondere Kommentare die das offensichtliche noch einmal beschreiben. Als Faustregel: Kommentare sollten beschreiben *warum* der Code das tut was er tut, nicht *was* der Code tut. Wenn letzteres nicht aus dem Code selber ersichtlich wird, dann sollte man darüber nachdenken ihn so umzuschreiben das er verständlicher ist.

Die numerischen Rückgabecodes könnte man als Konstanten definieren. Das macht das Programm verständlicher und die tatsächlichen Codes leichter änderbar.

Das Abbrechen der Server-Schleife wenn die Verbindung von der Gegenseite geschlossen wurde, würde ich direkt nach dem empfangen machen also vor der dann sinnlosen Verarbeitung der nicht empfangenen Daten.

Das Aufteilen der empfangenen Daten wird nur im ``else``-Teil des folgenden ``if`` benötigt, sollte also auch dort stehen.

Dass das Protokoll kaputt ist, hat Sirius3 ja schon erwähnt. Der Code dazu könnte auch deutlicher sein, und das Format einer Nachricht wäre dann mal etwas für einen Kommentar denn das puzzlet man sich nicht so leicht aus dem Quelltext zusammen, insbesondere Annahmen die über die Daten gemacht werden, kann man aus dem Quelltext nur schwert ersehen. Insbesondere solche die gar nicht überprüft werden.

Der '.' wird scheinbar schon als Ende einer PUT-Nachricht verwendet, geprüft wird aber nicht ob der am Ende steht sondern irgendwo in den Daten. Dann werden drei Bytes am Ende der Empfangenen Daten entfernt — welche sind das denn? Sollte man nicht prüfen ob dort tatsächlich das steht was man erwartet? Insbesondere könnte da schon ein Teil der nächsten Nachricht stehen falls man nicht Halbduplex garantieren kann, was dann irgendwie dokumentiert sein dürfte.

Der Name `result` wird für etwas verwendet was eher kein Ergebnis ist.

Ich lande dann ungefähr bei so etwas (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python
from __future__ import absolute_import, division, print_function
import sys
from contextlib import closing
from socket import AF_INET, error as SocketError, socket as Socket, SOCK_STREAM
from threading import Thread

HOST = ''  # symbolic name meaning all available interfaces
PORT = 31337

OK_CODE = '000\n'
WAITING_FOR_DATA_CODE = '001\n'
KEY_ERROR_CODE = '002\n'
COMMAND_ERROR_CODE = '100\n'


def serve_client(connection):

    with closing(connection):
        memory = dict()
        key = None

        data_to_save = ''
        waiting_for_data = False

        connection.sendall(OK_CODE)

        while True:
            data = connection.recv(1024)
            print('Client: ' + data)
            if not data:
                break

            reply = None
            if waiting_for_data:
                # TODO ask just for NEWLINE PERIOD NEWLINE
                if '.' in data:
                    assert key is not None
                    data_to_save += data.replace(' ', '')[:-3]
                    memory[key] = data_to_save
                    print(
                        'PUT: Stored: {0!r} under {1!r}'.format(
                            data_to_save, key
                        )
                    )
                    data_to_save = ''
                    waiting_for_data = False
                    key = None
                    reply = OK_CODE
                else:
                    reply = WAITING_FOR_DATA_CODE
                    print('PUT: Receiving more data')
                    data_to_save += data
            else:
                request = data.split()
                count = len(request)
                if count > 0:
                    if request[0] == 'PUT' or request[0] == 'GET':

                        if count == 3:
                            length = request[1]
                            key = request[2]

                            if request[0] == 'PUT':
                                # PUT
                                if key in memory:
                                    print('PUT: Key is already in system')
                                    reply = KEY_ERROR_CODE
                                else:
                                    print('PUT: Awaiting data')
                                    reply = WAITING_FOR_DATA_CODE
                                    waiting_for_data = True
                            else:
                                # GET
                                if key in memory:
                                    # TODO send data
                                    print('GET: Client demands ' + key)
                                    print(memory[key])
                                    reply = memory[key] + '\n'
                                    reply = reply + OK_CODE
                                else:
                                    print('GET: Key already in system')
                                    reply = KEY_ERROR_CODE
                        else:
                            # less than 2 different keywords
                            print('Not exactly 3 words received')
                            reply = COMMAND_ERROR_CODE

                    elif request[0] == 'CLEAR':
                        if count == 2:

                            key = request[1]

                            if key in memory:
                                print('CLEAR: Key accepted, deleting data')
                                reply = 'deleting data\n'
                                # TODO delete data
                        else:
                            print('CLEAR: Not exactly 2 words received')
                            reply = COMMAND_ERROR_CODE
                    elif request[0] == 'QUIT':
                        # quit
                        print('QUIT: Client demands quit, quitting')
                        connection.close()
                    else:
                        print('Unknown Command')
                        reply = COMMAND_ERROR_CODE
                else:
                    print('No words received')
                    reply = COMMAND_ERROR_CODE

            assert reply is not None
            connection.sendall(reply)


def main():
    with closing(Socket(AF_INET, SOCK_STREAM)) as server_socket:

        try:
            server_socket.bind((HOST, PORT))
        except SocketError as error:
            print('binding failed, code: {0[0]} message: {0[1]}'.format(error))
            sys.exit(1)

        server_socket.listen(10)
        print('socket listening')

        while True:
            connection, address = server_socket.accept()
            print('Connected with: {0[0]} : {0[1]}'.format(address))

            thread = Thread(target=serve_client, args=(connection,))
            thread.start()


if __name__ == '__main__':
    main()
Die Verarbeitung einer Anfrage ist viel zu umfangreich und zu verwoben. Da habe ich jetzt nichts weiter dran geändert. Da könnte man am Anfang das erste Wort, das Kommando, vom Rest trennen, und dann für jedes Kommando eine eigene Funktion schreiben. Das könnte man Beispielsweise durch ein Wörterbuch das den jeweiligen Kommandonamen auf die Anzahl der erwarteten Argumente und eine Funktion zur Abarbeitung abbildet recht kurz und generisch, und damit auch einfach erweiterbar gestalten.
s4fti
User
Beiträge: 5
Registriert: Donnerstag 2. Oktober 2014, 02:20

Wow, erst einmal möchte ich euch beiden ganz gewaltig danken, dass ihr euch die Zeit genommen habt so ausführlich auf die Fragestellung einzugehen. Ich entschuldige mich schon im Vorraus, falls die Fragen die ich Stelle arg naiv und blöd klingen mögen. Wie gesagt, ich bin, obwohl nicht ganz unerfahren, was Programmieren im Allgemeinen angeht, total neu bei Python. Allerdings gefällt mir die Sprache bisher außerordentlich gut.

@ Sirius3:

Danke für den Tipp mit der Main. Was genau meinst du mit Modul-Ebene? Ist das die Ebene, in der ich bisher meinen ganzen Code ausgeführt hatte? (Mir fehlte ja bisher eine Main-Methode.) Wenn ja, warum ist das architektonisch schöner?

Ja, die Verschachtelung ist echt problematisch und ich arbeite dran, da Sachen auszulagern.

Das mit dem kaputten recv habe ich glaube ich noch nicht ganz Verstanden. Ich habe jetzt eine vereinfachte Version erstellt, auf Basis dessen, was BlackJack geschrieben hat. (Danke, an dieser Stelle für die genommene Zeit und Mühe. Ich weiß das wirklich zu schätzen!)

Hier der Code:

Code: Alles auswählen

#!/usr/bin/env python
from __future__ import absolute_import, division, print_function
import sys
from contextlib import closing
from socket import AF_INET, error as SocketError, socket as Socket, SOCK_STREAM
from threading import Thread
 
HOST = ''  # symbolic name meaning all available interfaces
PORT = 31337

def willAcceptMoreData(data):
    if not '.' in data:
        return True
    else:
        return false

def serve_client(connection):
 
    with closing(connection):

        connection.sendall('000\n')

        while True:
            data = connection.recv(1024)
            if willAcceptMoreData(data):
                reply = None
            
                reply = data
                print('client: ' + data)

                assert reply is not None
                connection.sendall(reply)
            else:
                break
                
            
 
 
def main():
    with closing(Socket(AF_INET, SOCK_STREAM)) as server_socket:
 
        try:
            server_socket.bind((HOST, PORT))
        except SocketError as error:
            print('binding failed, code: {0[0]} message: {0[1]}'.format(error))
            sys.exit(1)
 
        server_socket.listen(10)
        print('socket listening')
 
        while True:
            connection, address = server_socket.accept()
            print('Connected with: {0[0]} : {0[1]}'.format(address))
 
            thread = Thread(target=serve_client, args=(connection,))
            thread.start()
 
 
if __name__ == '__main__':
    main()
Die Kommunikation läuft jetzt folgendermaßen ab:
3-Way-Handshake, Eröffnung des Sockets:
Client: [SYN]
Server: [SYN, ACK]
Client: [ACK]
Mein Code:
Server: 000\n
Client: some command
Server: leeres Paket (auch als [ACK] = Acknowledgement geflagt)
Server: some command

Also, das Problem, nach dem ich fragte, ist damit leider bis hier noch nicht gelöst. :/


So nun @BlackJack:

Danke für den Tipp mit den Sternchenimporten - macht total Sinn sich nur das zu holen was man braucht.
Den Style Guide für Python werde ich mir, leider erst nach abschicken des Posts, zu Gemüte führen, danke dafür. Den Namen server_socket habe ich von dir übernommen. Ich bin teilweise sehr faul/gleichgültig was die Benennung von Variablen angeht, ist eine schlechte Angewohnheit.

Die + Schreibweise bin ich aus C++ gewohnt. Oft probiere ich bisher einfach aus, was ich schon von C++, Java, Objective C oder so kenne und freue mich wenn es klappt.

Auch sonst, vielen Dank für die Tipps zu Codekonventionen und unschönen Sachen, wie den Kommentaren. Die Faustregel zum Kommentar-Schreiben habe ich so noch nicht gehört, werde aber versuchen das so anzuwenden, die Erläuterung macht Sinn!

Ich glaube dem Abbrechen der Server-Schleife entspreche ich mit dem vereinfachten Code.

Eigentlich ist beabsichtigt, als Abbruchkriterium '000\n.000\n' zu verwenden. Allerdings funktionierte das bisher nicht, wenn ich es im Code an der Stelle des Punktes einsetze.

Abschließend muss ich leider gestehen, das ich nicht ganz offen war und möchte das jetzt berichtigen. Ihr helft mir aktuell bei einer Hausaufgabe für einen Kurs names "Application-Security". Ich mache im Moment ein Auslandssemester in New York and der NYU. Der Kurs hat eigentlich Graduate Standing und ist für Computer Science Absolventen. Ich bin in meinem 5ten Semester Medieninformatik in Deutschland, also nicht ganz so umfassend in Informatik ausgebildet, wie die anderen Kursteilnehmer, daher tu ich mich wahrscheinlich etwas schwerer als meine Kommilitonen. Der Professor gab uns folgendes Protokoll, dass wir, in welcher Sprache auch immer, implementieren sollten. Ich habe mich für Python entschieden, da dies eine Sprache war, die ich schon länger als interessant entdeckt hatte und ich hörte, dass gerade Socket Programming dort eine einfache Sache ist.

Ich hoffe mir sei das Vorenthalten des Protokolls verziehen, doch ich befürchtete, dass ich mit dem Argument, dass ich meine Hausaufgaben doch alleine lösen sollte, oder mich an die Schule wenden muss, abgewiesen worden wäre. (Wahrscheinlich Irrsinn?!)

Anyway, hier das vorenthaltene Protokoll:

Code: Alles auswählen

CS 9163 Working Group                                                   M. Bentz
Matt Declared Protocol: 0001                                                 NYU
                                                                        Sep 2014

                     Arbitrary Storage Protocol (ASP) v1



Status of this Memo

This document specifies a protocol for CS9163 students to implement.  Please
refer to the author for the most current edition of this document.  Distribution
is unlimited.


1. Overview

It is often nice to be able to store arbitrary data on a remote host.  This
statement is exceedingly vague and based on unquestioned assumptions.
Fortunately, the protocol herein described has no real purpose in the world
other than to provide a teachable experience to students in CS9163.


2. Basic Functionality

The protocol supports the storage and retrieval of arbitrary data for the
duration of a network session.  Information exchange between the client and
server are handled in a one-to-one fashion, i.e. each request by the client is
met with a response from the server.  These requests are handled by presenting
access methods to the server.  The server will respond with a response code
which indicates that the client should proceed, the method was a success, or an
error occurred.

The two basic methods GET and PUT store and retrieve those data associated with
a given key.  A type may be associated with each key.  There is no guarantee
that data will survive close of the connection.  The CLEAR method clears a key.
The QUIT method closes the session.


3. Methods


3.1 The PUT Method

The PUT method is the means by which a client requests data of an indicated
length (indicated in decimal integer bytes) be stored by the server under the
supplied key:

   "PUT" <space> <length> <space> <key> <newline>

The server will respond with one of the following codes:

   Proceed, Unknown Error, Length Error, Key Error

Upon receipt of the Proceed code, the client will send its data.  The completion
of this sent data will be indicated by <newline> <period> <newline>, i.e. a
period on its own line.  The server will associate as many bytes of this data as
specified in the method's length.  Superfluous bytes are ignored.  An
under-supply of data will cause a Length Error to result.  A successful store
will result in a Ready code.


3.2 The GET Method

The GET method is the means by which a client requests data associated with the
supplied key to be returned:

   "GET" <space> <length> <space> <key> <newline>

The server will respond with either the requested data and a Ready code or an
error code.  On success, the server immediately begins sending the data
associated with the supplied key up to the given length in bytes.  Upon
completion, it appends a <newline> to the data and sends a Ready code.

In the event of an error, the server may respond with one of the following
codes: 

   Unknown Error, Length Error, Key Error


3.3 The CLEAR Method

The CLEAR method removes a key and its associated data from the server's
consideration.  It is specified as follows:

   "CLEAR" <space> <key> <newline>

And may return one of the following codes:

   Ready, Unknown Error, Key Error


3.4 The QUIT Method

The QUIT method requests that the connection to the server be terminated.  It
results in no response code.


4. Response Codes

Response codes are numeric indicators of direction, information, success, and
failure.  All codes are sent with an immediately following newline.  They are as
follows:


4.1. Ready ("000")

The server is ready to accept the next request.


4.2. Proceed ("001")

The server is ready to receive your information.


4.3. Unknown Error ("100")

An unknown error has occurred.


4.4. Length Error ("101")

The method length is incorrect.  For a PUT method, the server may be unable to
store that amount of data.  For A GET method, the client may have requested more
bytes than were stored with that key.


4.5. Key Error ("102")

There is a problem with the key.  For a PUT method, the key is already present
and thereby in collision.  For a GET or CLEAR method, the key does not exist.


5. Example Sequence

The following is an example session between client and server.  Server sent
information is prepended by S: and client send information by C:

   S: 000
   C: PUT 13 foo
   S: 001
   C: Hello, world!
   C: .
   S: 000
   C: PUT 8 bar
   S: 001
   C: Rutabaga!
   C: .
   S: 000
   C: FEED dog
   S: 100
   C: GET 8 bar
   S: Rutabaga
   S: 000
   C: CLEAR foo
   S: 000
   C: CLEAR foo
   S: 102
   C: PUT 8 foo
   S: 001
   C: I am new data in foo!
   C: .
   S: 000
   C: GET 12 foo
   S: 101
   C: QUIT


6. ABNF Definitions

   <space>   = %x20 ; sp
   <newline> = %x0a ; nl
   <period>  = %x2e ; .
   <key>     = ALPHA / DIGIT
   <length>  = DIGIT


7. References

D. Crocker, "Augmented BNF for Syntax Specifications: ABNF", STD 68, RFC 5234,
Brandenburg InternetWorking, January 2008
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@s4fti: Das mit kein Code auf Modulebene hat den Vorteil, dass das Programm tatsächlich auch als Modul verwendet werden kann, weil beim Importieren kein Code ausgeführt wird. Du kannst einzelne Funktionen testen. Und das Programm ist einfach schöner strukturiert.
Das zusammensetzen und teilen von Paketen übernimmt TCP selbständig. Es kann also auch sein, dass irgendeine Vermittlungsstation im Internet der Meinung ist, das aktuelle Paket sei zu groß und teilt es in 2 oder sammelt mehrere kleine, um sie am Stück zu verschicken.
Um es an einem Beispiel zu sagen. Du schickst zwei Pakete "PUT 13 foo\n" und "Hello, world!\n.\n" und was beim Server ankommt sind zwei Pakete "PUT 1" und "3 foo\nHello, world!\n.\n" die jeweils die Rückgabewerte eines recv-Aufrufs sind. Das Protokoll sagt deshalb auch, ein Befehl wird mit einem Newline-Zeichen abgeschlossen.
BlackJack

@s4fti: Code auf Modulebene wird beim importieren des Moduls ausgeführt. Wenn dort das Hauptprogramm steht, dann kann man das Modul nicht importieren ohne dass das dann auch los läuft. Wenn man auf Modulebene nur Konstanten, Funktionen und Klassen definiert und das Hauptprogramm in einer Funktion stehen hat die mit dem ``if __name__ == '__main__':``-Idiom ”geschützt” ist, dann kann man das Modul sowohl als Programm ausführen (Hauptprogramm läuft ab), als auch als Modul importieren (Hauptprogramm läuft nur ab wenn man die Funktion explizit aufruft).

Das macht es möglich einzelne Funktionen oder Klassen aus dem Modul zu testen — manuell in einer Python-Shell oder automatisiert als Unit-Tests. Oder Funktionen oder Klassen in andere Module zu importieren und dort zu verwenden. Einige Programme wie Dokumentationswerkzeuge erwarten auch das man Module ”effektfrei” importieren kann. Dazu gehört zum Beispiel auch die `autodoc`-Erweiterung von Sphinx. Sphinx ist das Werkzeug mit dem die Python-Dokumentation erstellt wird, und das auch viele andere Python-Projekte verwenden.

`recv(n)` liefert einen Ausschnitt aus dem Datenstrom der über die TCP-Verbindung herein kommt, und zwar mindestens ein Byte und maximal n Bytes. Dabei muss dieser Ausschnitt in keinster Weise dem Ausschnitt entsprechen der auf der Gegenseite mit *einem* `send()` oder `sendall()` auf die Reise geschickt wurde. Man muss also wenn da mehrere Nachrichten über die selbe Verbindung kommen, eventuell mehr als einen `recv()`-Aufruf machen um die komplette Nachricht zu empfangen, und auf der anderen Seite grundsätzlich auch damit rechnen das man mit einem Aufruf mehr als eine Nachricht bekommt, oder zumindest Teile von mehr als einer Nachricht. Mit anderen Worten: man muss über das Protokoll dafür sorgen dass man Code schreiben kann, der die Nachrichten sauber voneinander trennen kann.

Bei einem Halbduplexprotokoll kann man das mit den mehreren Nachrichten ausschliessen, weil die Gegenseite ja erst die nächste Nachricht sendet wenn man die letzte in irgendeiner Weise bestätigt hat. Aber beim Empgangen muss man immer noch damit rechnen, dass ein gesendetes 'CLEAR foo\n' nicht in einem `recv()` gelesen wird, sondern beliebig auf mehrere Aufrufe aufgeteilt sein kann. Robuster Code muss an der Stelle im Extremfall damit klar kommen das jedes Byte einzeln gelesen werden muss bis das Kommando vollständig ist. Warnung: Das Protokoll was Du da umsetzen musst ist leider nicht wirklich vollständig Halbduplex, Du musst Dich mit diesem Problem also tatsächlich auseinandersetzen.

Die `willAcceptMoreData()`-Funktion ist sehr umständlich geschrieben. Die Bedingung selbst wird doch schon zu `True` oder `False` ausgewertet. *Das* ist doch im Grunde schon der Rückgabewert:

Code: Alles auswählen

def willAcceptMoreData(data):
    return '.' not in data
Wobei das noch nicht dem Protokoll entspricht.

Zum eigentlichen ”Problem”: Da wird auf TCP-Ebene vom Server das Kommando vom Client bestätigt. Das ist unterhalb der Abstraktionsebene die Du in Deinem Programm verwendest. Du hast einen Datenstrom in Form einer TCP-Verbindung. Was der TCP/IP-Stack des Systems damit macht geht Dich nichts an. Wenn der Lust hat kann der ständig leere IP-Pakete verschicken, oder Daten die Du sendest mit einem Paket pro Byte auf die Reise schicken. Das bekommt Dein Programm weder mit, noch kann es daran etwas ändern. Du hast da also gar kein Problem was gelöst werden müsste. :-)

'000\n.000\n' als Abbruchkriterium sehe ich jetzt gerade nicht in der Protokollbeschreibung‽

„Application Security” klingt als müsste man bei der Umsetzung des Protokolls auch auch so Sachen wie „denial of service”-Angriffe achten. :-)

Socketprogrammierung ist in Python nicht einfacher oder schwerer als in C, oder jeder anderen Programmiersprache die im wesentlichen die BSD-Socket-API 1:1 anbietet wie es das `socket`-Modul von Python macht. Einfacher würde es nur, wenn man etwas verwendet was darauf aufsetzt und einem Arbeit abnimmt und hilft typische Fehler zu vermeiden. `twisted` wäre vielleicht etwas in die Richtung.

Bei Hausaufgaben solltest Du halt nicht auf Komplettlösungen hoffen. Die meisten sind dann etwas zurückhaltender mit Quelltext und versuchen mehr mit Hinweisen und Nachfragen in die richtige Richtung zur Lösung zu weisen.

Gut dass Du das Protokoll gezeigt hast, denn sonst hättest Du wahrscheinlich endlos Nachfragen/Hinweise bekommen dass das Grütze ist. So ist wenigsten klar dass Du das tatsächlich so umsetzen musst, und mit dem Titel der Lehrveranstaltung auch *warum* das so schlecht ist. ;-)

Bei dem Protokoll sollt ihr euch sicher auch Gedanken machen was daran nicht stimmt. Gedankenanstösse zum Protokoll: Gibt es Daten die man nicht zu einem Schlüssel speichern kann? Falls ja, wie sehen die aus? Gibt es Daten die man zwar speichern, aber nicht wieder auslesen kann? Falls ja, wie sehen die aus? Falls es mit GET und/oder PUT derartige Probleme gibt, kann dann ein Angreifer Daten konstruieren die gezielt Kommandos ausführen, oder dem Client bei der Abfrage in der Folge falsche/untergeschobene Antworten liefern?
s4fti
User
Beiträge: 5
Registriert: Donnerstag 2. Oktober 2014, 02:20

Arg. Fast den Post fertig geschrieben gehabt, falsche Tastenkombination gedrückt und nun den ganzen Post neu verfassen. Ich sollte mir angewöhnen, jeden Post in einer stabileren Umgebung als meinem Webbrowser vor zu schreiben. Verzeiht, wenn er nicht ganz so ausführlich wird wie die ursprüngliche Version.


@Sirius3:
Okay, danke für die ausführliche Erklärung, macht Sinn, habe halt bisher nur den Use Case gehabt, das ich mein Programm laufen sollte. Von daher bin ich noch nicht auf die Problematik gestoßen aber um Module zu importieren ist das mehr als sinnvoll.

@BlackJack:
Auch dir danke für die ausführliche Erklärung des Sachverhalts, sowie den hervorragend kurzen Code. Ich dachte doch, dass es einfacher geht.

Die Abbruchbedingung ist in der Beschreibung der GET-Methode definiert. Ist versteckt im Text, habe es beim ersten Mal auch überlesen. Du hast total recht, es ist einfach nur \n.\n ohne die 0’en.
The completion
of this sent data will be indicated by <newline> <period> <newline>, i.e. a
period on its own line.
Ja, “Application Security” bedeutet genau das. Dies war die erste Aufgabe, die ich halt wegen des zusätzlichen Pakets nicht zu 100% lösen konnte. Einen klassischen Denial of Service wird er uns wohl nicht antun. Aber dafür wird dieser Service bald eine Menge weiterer diverser Tests durchlaufen. Wir werden dann daraufhin bewertet, wie unserer Server mit diesen Fällen umgeht. Also alle vorstellbaren Varianten interessanten Randfällen, die auch mit Absicht so vage definiert sind.

Beispielsweise könnte es passieren, dass der Dozent versucht 10 byte zu speichern, allerdings 10^9 bytes sendet. Da sollte dann der Server irgendwann abbrechen und nicht weiter zu hören. Verschiedene Versionen von Buffer-Overflow oder ähnliches. Ich bin gespannt.

Ich habe nie auf Komplettlösungen gehofft. Ich hoffte, und meine Hoffnungen wurden gut erfüllt, das ich hier Tipps und Hilfe bekomme, anhand derer ich selber weiter arbeiten kann und der Lösung ein Stück näher komme.

Die nächste Hausaufgabe ist es eine neue Version des Protokolls zu erstellen und eventuelle Ungenauigkeiten auszubügeln - um nicht zu sagen auszumerzen.

Danke für Die Denkanstöße dafür.

@Beide und zu der Problematik mit recv(n):

Ich habe versucht das mit der Schleife zu umgehen, scheint das wie der richtige Weg der Architektur? Wisst ihr, wie ich <newline>, <period>, <newline> abfangen kann?

Habe versucht den Code weiter zu kapseln und lande dann bei:

Code: Alles auswählen

#!/usr/bin/env python
from __future__ import absolute_import, division, print_function
import sys
from contextlib import closing
from socket import AF_INET, error as SocketError, socket as Socket, SOCK_STREAM
from threading import Thread
 
HOST = ''  # symbolic name meaning all available interfaces
PORT = 31337
 
OK_CODE = '000\n'
WAITING_FOR_DATA_CODE = '001\n'
KEY_ERROR_CODE = '002\n'
COMMAND_ERROR_CODE = '100\n'
waiting_for_data = False
memory = dict()
data_to_save = ''
key = None



def get(memory, length, key):
    if key in memory:
        # TODO send data
        print('GET: Client demands ' + key)
        print(memory[key])
        return memory[key] + '\n' + OK_CODE
    else:
        print('GET: Key already in system')
        return KEY_ERROR_CODE

def put(memory, length, key):
    global waiting_for_data
    if key in memory:
        print('PUT: Key is already in system')
        return KEY_ERROR_CODE
    else:
        print('PUT: Awaiting data')
        waiting_for_data = True

        return WAITING_FOR_DATA_CODE

def process_incomming_data(data):
    global waiting_for_data
    global memory
    global data_to_save
    global key
    if waiting_for_data:
        # TODO ask just for NEWLINE PERIOD NEWLINE
        if '.\n' in data:
            assert key is not None
            data_to_save += data.replace(' ', '')[:-3]
            memory[key] = data_to_save
            print(
                'PUT: Stored: {0!r} under {1!r}'.format(
                    data_to_save, key
                )
            )
            data_to_save = ''
            waiting_for_data = False
            key = None
            return OK_CODE
        else:
            print('PUT: Receiving more data')
            data_to_save += data
            return WAITING_FOR_DATA_CODE

    else:
        request = data.split()
        count = len(request)
        if count > 0:
            if request[0] == 'PUT' or request[0] == 'GET':

                if count == 3:
                    length = request[1]
                    key = request[2]

                    if request[0] == 'PUT':
                        return put(memory, length, key)
                    else:
                        return get(memory, length, key)
                else:
                    # less than 2 different keywords
                    print('Not exactly 3 words received')
                    return COMMAND_ERROR_CODE

            elif request[0] == 'CLEAR':
                if count == 2:

                    key = request[1]

                    if key in memory:
                        print('CLEAR: Key accepted, deleting data')
                        return 'deleting data\n'
                        # TODO delete data
                else:
                    print('CLEAR: Not exactly 2 words received')
                    return COMMAND_ERROR_CODE
            elif request[0] == 'QUIT':
                # quit
                print('QUIT: Client demands quit, quitting')
                return 'quitting'
            else:
                print('Unknown Command')
                return COMMAND_ERROR_CODE
        else:
            print('No words received')
            return COMMAND_ERROR_CODE



def serve_client(connection):
 
    with closing(connection):
 
        connection.sendall(OK_CODE)
 
        while True:
            data = connection.recv(1024)
            print('Client: ' + data)
            if not data:
                break
 
            reply = None
            reply = process_incomming_data(data)
            if reply == 'quitting':
                connection.closing()
 
            assert reply is not None
            connection.sendall(reply)
 
 
def main():
    with closing(Socket(AF_INET, SOCK_STREAM)) as server_socket:
 
        try:
            server_socket.bind((HOST, PORT))
        except SocketError as error:
            print('binding failed, code: {0[0]} message: {0[1]}'.format(error))
            sys.exit(1)
 
        server_socket.listen(10)
        print('socket listening')
 
        while True:
            connection, address = server_socket.accept()
            print('Connected with: {0[0]} : {0[1]}'.format(address))
 
            thread = Thread(target=serve_client, args=(connection,))
            thread.start()
 
 
if __name__ == '__main__':
    main()
Zu dem eigentlichen “Problem”:
Ärgerlich! Das das Probleme eine Ebene tiefer zu suchen ist, erklärt, warum ich mir nicht erklären konnte, wie in diesem Code ein leeres Paket mehr produziert wird. Vielleicht hängt es ja damit zusammen, dass ich die Anwendung im Terminal auf meinem Mac laufen lasse? Anscheinend werde ich mich doch wohl mit externen Server-Servicen beschäftigen müssen und es dort einmal ausprobieren müssen.
Andere Möglichkeit wäre ein Test mit UDP? Allerdings scheint mir TCP hier doch eigentlich das Protokoll der Wahl, oder?
Zuletzt geändert von Anonymous am Freitag 3. Oktober 2014, 19:02, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Code-Tags gesetzt.
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@s4fi: vergiss ganz schnell, dass es "global" überhaupt gibt. Damit kannst Du schon Deine erste Lektion in “Application Security” lernen. Wenn zwei Nutzer gleichzeitig den Dienst benutzen werden sie sich zwangsläufig in die Quere kommen. Du mußt sauber den Zustand der Verbindung zwischen den Funktionen hin und her reichen.
Nochmal zum grundsätzlichen Gerüst, wie Du einen Befehl verarbeiten solltest:
1. so lange vom Socket lesen, bis genau eine Zeile empfangen wurde.
2. Zeile in Befehl und Parameter aufteilen
3. Funktion zum Befehl suchen und aufrufen
4. Innerhalb der Funktion Parameter prüfen und Fehler erkennen.
BlackJack

@s4fti: Falls die falsche Tastenkombination nicht den Browser geschlossen hat, hilft in der Regel die „Zurück”-Schaltfläche, beziehungweise wenn man diese Funktion aus versehen erwischt hat die „Weiter”-Schaltfläche.

Ich schreibe Beiträge meistens im Texteditor. Für Firefox gibt es da das praktische Plugin „It's All Text!”. Da stellt man seinen Lieblingstextedior ein und dann hat jedes Texteingabefeld im Browser einen kleinen Button mit dem man den Inhalt in dem Editor öffnen kann. Das geht über eine temporäre Datei die vom Plugin überwacht wird, dass heisst jedes mal wenn man im Editor speichert, wird der Inhalt der Datei im Browser neu in das Texteingabefeld geladen.

Ich verstehe nicht was Dich an diesem zusätzlichen Paket stört? Ob das da ist oder nicht spielt doch überhaupt gar keine Rolle, Du benutzt doch gar kein paketbasiertes Protokoll, sondern einen Datenstrom. Was da in der Schicht drunter passiert ist doch völlig irrelevant. Und die TCP ist die richtige Wahl, sonst wird es schwierig so etwas wie Sitzungen und die Dauer einer Sitzung zu erkennen. Man müsste sich dann letztendlich so etwas wie TCP selber basteln. Davon steht aber nichts in der Protokollbeschreibung, das müsste also irgendwie ausserhalb des Protokolls passieren in einer zusätzlichen Schicht darunter.

Deutlich zu viele Daten zu senden *ist* ein klassischer „denial of service”-Angriff. Der zielt darauf ab den Server in die Knie zu zwingen und entweder total zu überlasten oder zum Absturz zu bringen. Was bei einer einfachen und offensichtlichen Implementierung auch klappen würde, denn normalerweise würde man ja erstmal solange Daten in den Speicher lesen bis die Endmarkierung der Nachricht gelesen wurde. Also so viel bis der Server wegen Speichermangel abstürzt, nach dem er vorher unsagbar langsam war weil der Rechner vorher nur noch mit „swappen” beschäftigt war.

„Buffer overflow” wird in Python schwierig. Ist halt eine Sprache die den Speicher „automagisch” verwaltet und Grenzen prüft. Das bietet Schutz gegen so etwas ohne das man etwas tun müsste. :-) Ebenso hat man keine Probleme mit dem parsen von Zahlen weil Python's ganzahliger Datentyp ”unendlich” gross werden kann.

``global`` solltest Du ganz schnell wieder vergessen. Da bekommt man dann ganz schnell sehr unübersichtliche Programme wo alles irgendwie von allem anderen abhängt. Da lassen sich auch schlechter Aussagen über die Sicherheit machen als wenn Änderungen an Daten auf bestimmte Funktionen/Methoden eingegrenzt werden kann ohne dass man sich das gesamte Programm anschauen muss. Die Funktionen sind auch nicht besonders gut aufgeteilt. Eine pro Kommando wäre besser, die jeweils alle benötigten Daten als Argumente übergeben bekommt und einen Rückgabewert hat, statt über globale Namen zu kommunizieren. Wenn all diese Funktionen die gleiche API haben, also die gleichen Argumente erwarten und der Rückgabewert bei allen das gleiche bedeutet, kann man ein Wörterbuch (`dict`) benutzen um den Kommandonamen auf eine Funktion abzubilden. So kann man für die Verarbeitung sehr allgemeinen Code schreiben der nichts über die konkreten Kommandos wissen muss, und auch einfach neue Kommandos implementieren in dem man eine geeignete Funktion schreibt und die im Wörterbuch einträgt.

Was ich letztentlich trennen würde vom Rest ist das lesen einer Nachricht. Da kann dann die Erkennung der Endmarkierung mit rein und auch das Puffern von mehr als einer Nachricht falls mehr als Eine, oder zumindest Teile der nächsten Nachricht, schon mit ankommen. Das wird sehr wahrscheinlich ein Testszenario sein, zu sehen was passiert wenn man eine Textdatei mit mehreren Kommandos per ``netcat`` auf den Server loslässt. Auch wenn das Protokoll eigentlich (doch komplett) Halbduplex ist (ich hatte die „Proceed”-Rückmeldung übersehen), sollte ein Server IMHO auch mit einem Client klar kommen, der sich nicht für die Antworten interessiert und einfach alle Befehle ”auf einmal” abfeuert. Da man dafür Zustand, nämlich den Puffer, über Funktionsaufrufe hinweg merken muss, kommt man an der Stelle um eine Klasse IMHO nicht herum. Wenn verschiedene Nachrichten nicht verschiedene Endkennzeichen verwenden würden, wäre noch eine Generatorfunktion gegangen, aber man muss auch in die Gegenrichtigung kommunizieren und zwar jeweils *nachdem* man eine Nachricht aus dem Datenstrom gewonnen hat.

Wo ist denn das löschen von Leerzeichen aus den Daten im Protokoll beschrieben?

Wann ist denn der Abgabetermin? Ich habe hier eine Implementierung die ich wohl besser erst danach hier zeige, damit Du keine Probleme bekommen kannst falls Ähnlichkeiten zu Deinem Quelltext bestehen. :-)
s4fti
User
Beiträge: 5
Registriert: Donnerstag 2. Oktober 2014, 02:20

Hey, okay, werde heute Abend mal versuchen das ganze anständig ohne globale Variablen aufzuziehen. Bin im Moment unterwegs und deswegen reicht die Zeit nur zum knappen Antworten.

Mein Problem war einfach, dass diese Ein-Paket-Einschränkung eine Vorgabe des Bewerte-Skripts war. Die hat unser Dozent aufgestellt, da man ansonsten einfach die Antworten auf das Tastatur eintippen könnte und mit Enter abschicken. Der automatisierte Grader hat die Beispielsequenz auf dem Protokoll geprüft. Ab einer gewissen Stelle (nach der Anfrage: get 8 bar) erwartet er nur ein Paket. Die relevanten Daten sind bei mir in einem Paket, allerdings bewirkt dieses unerwünschte Paket, dass ich nicht weiter komme.
Die Frist ist lange vorbei (vorletzte Woche). Es gab für das Öffnen eines Sockets 70% und für 12 weitere Anfragen des Clientens jeweils weitere 2.5%. Ich bin deswegen leider nicht über 85% gekommen. Ehrlich gesagt wäre mir ansonsten das zusätzliche Paket gar nicht aufgefallen und ich hätte wahrscheinlich auch kein Debugging mit Wireshark betrieben.

Zu DoS: Ich hatte wohl DoS mit DDoS verwechselt. Ja, das von dir beschriebene Szenario wird garantiert vorkommen und ich weiß, mein Server ist bisher dagegen nicht gefeit. Ich hatte mir vorgenommen ersteinmal die Basis Funktionen zum Laufen zu kriegen, bevor ich mich auf die Feinheiten stürze.

@Syrius3: Danke für die Tipps, werde es später ausprobieren.
@BlackJack: Ich habe auf eine meiner Favoritenseiten mittels Shortcut gewechselt. Der Text war leider weg, trotz Zurück-Button. Jetzt bin ich gewarnt.

@Beide: Danke nochmal für das Mentoring. Ich weiß das wirklich zu schätzen. Habt ihr beide beruflich mit Python zu tun oder ist das mehr Hobby?
BlackJack

@s4fti: Ich würde mich da ja beschweren, denn wenn das tatsächlich nur im automatisierten Grader geprüft wird, beziehungsweise der auf einer Schicht prüft auf die man eigentlich gar keinen Einfluss hat/die mit dem Protokoll nichts zu tun hat, dann ist das ein Problem des Graders und nicht der Protokollimplementierung.

Das kann eigentlich nur angehen wenn ihr auch TCP und Sockets auf einer tieferen Ebene als Thema hattet und man da vielleicht mit Socket-Optionen irgend etwas fummeln kann um dieses IP-Paket weg zu bekommen. Und dann sollte das auch als Anforderung in der Aufgabe erwähnt werden. Selbst dann halte ich eine Bewertung der Kommunikation auf IP-Paket-Ebene für fragwürdig, denn in der Realität hat man darüber keine so detaillierte Kontrolle. Will man ja auch gar nicht haben, sonst würde man keine höhere Schicht benutzen.

Ich benutze Python auf Arbeit wo ich kann und wo es Sinn macht. Ist aber auch Hobby. Habe sozusagen das Hobby zum Beruf gemacht.

Wenn der Abgabetermin schon war, dann hier meine ”naive” Umsetzung des Protokolls auf Serverseite:

Code: Alles auswählen

#!/usr/bin/env python
from __future__ import absolute_import, division, print_function
from collections import namedtuple
from contextlib import closing
from socket import (
    AF_INET, SOCK_STREAM, socket as Socket, SO_REUSEADDR, SOL_SOCKET
)
from threading import Thread


RECV_BUFFER_SIZE = 32 * 1024

READY_CODE = b'000\n'
PROCEED_CODE = b'001\n'
UNKNOWN_ERROR_CODE = b'100\n'
LENGTH_ERROR_CODE = b'101\n'
KEY_ERROR_CODE = b'102\n'


Session = namedtuple('Session', 'connection memory')


class Connection(object):

    def __init__(self, socket, recv_buffer_size=RECV_BUFFER_SIZE):
        self.socket = socket
        self.buffer = ''
        self.recv_buffer_size = recv_buffer_size

    def __iter__(self):
        return self

    __enter__ = __iter__

    def __exit__(self, *args):
        self.close()

    def read(self, end_marker=b'\n'):
        while True:
            try:
                end_index = self.buffer.index(end_marker)
            except ValueError:
                data = self.socket.recv(self.recv_buffer_size)
                if not data:
                    raise StopIteration
                self.buffer += data
            else:
                message = self.buffer[:end_index]
                self.buffer = self.buffer[end_index + len(end_marker):]
                return message

    next = read

    def write(self, data):
        self.socket.sendall(data)

    def close(self):
        self.socket.close()


def parse_length(string):
    result = int(string)
    if result < 0:
        raise ValueError
    return result


def do_put(session, (length, key)):
    try:
        length = parse_length(length)
    except ValueError:
        return UNKNOWN_ERROR_CODE

    if key in session.memory:
        return KEY_ERROR_CODE

    session.connection.write(PROCEED_CODE)
    data = session.connection.read(b'\n.\n')

    if len(data) < length:
        return LENGTH_ERROR_CODE

    session.memory[key] = data[:length]
    return READY_CODE


def do_get(session, (length, key)):
    try:
        length = parse_length(length)
    except ValueError:
        return UNKNOWN_ERROR_CODE

    try:
        data = session.memory[key]
    except KeyError:
        return KEY_ERROR_CODE

    if length > len(data):
        return LENGTH_ERROR_CODE

    session.connection.write(data[:length] + b'\n')
    return READY_CODE


def do_clear(session, (key,)):
    try:
        del session.memory[key]
    except KeyError:
        return KEY_ERROR_CODE
    else:
        return READY_CODE


COMMAND_TO_FUNCTION = {
    b'PUT': (do_put, 2),
    b'GET': (do_get, 2),
    b'CLEAR': (do_clear, 1),
    b'QUIT': (lambda *_: None, 0),
}


def split_arguments(string, count):
    if count != 0:
        result = string.split(' ', count - 1)
        if len(result) != count:
            raise ValueError('less than expected arguments')
    else:
        if string:
            raise ValueError('more than expected arguments')
        result = list()
    return result


def execute_line(session, line):
    command, _, remainder = line.partition(' ')
    do_command, argument_count = COMMAND_TO_FUNCTION.get(
        command, (None, None)
    )

    if not do_command:
        return UNKNOWN_ERROR_CODE

    try:
        arguments = split_arguments(remainder, argument_count)
    except ValueError:
        return UNKNOWN_ERROR_CODE
    else:
        return do_command(session, arguments)


def serve_client(socket):
    with Connection(socket) as connection:
        session = Session(connection, dict())
        session.connection.write(READY_CODE)
        for line in connection:
            response_code = execute_line(session, line)
            if response_code is None:
                break
            session.connection.write(response_code)


def main():
    address = ('localhost', 31337)
    with closing(Socket(AF_INET, SOCK_STREAM)) as server_socket:
        server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        server_socket.bind(address)
        server_socket.listen(10)
        print('Listening on {0[0]}:{0[1]}...'.format(address))
        try:
            while True:
                socket, _ = server_socket.accept()
                thread = Thread(target=serve_client, args=(socket,))
                thread.daemon = True
                thread.start()
        except KeyboardInterrupt:
            print('Bye...')


if __name__ == '__main__':
    main()
Ich denke die Implementierung ist halbwegs sicher, jetzt ausser acht lassend dass das Protokoll selber nicht so toll ist. Den Versuch den Server mit zu vielen Daten in die Knie zu zwingen die *nicht* gespeichert werden sollen, also Daten bei PUT die über das Längenargument hinaus gehen, könnte man zwar recht einfach vereiteln, aber da das Protokoll weder die Länge einer Kommandozeile noch die Länge für tatsächlich zu speichernde Daten einschränkt, könnte man den Server darüber trotzdem zum Absturz bringen. Das lohnt also nicht solange man das Protokoll selbst dagegen nicht sicherer macht.

Edit: Code noch mal überarbeitet weil ich den Randfall der leeren Eingabe nicht berücksichtigt hatte. Dafür dürfen jetzt auch Leerzeichen im Schlüssel sein! :-)

Code: Alles auswählen

$ nc localhost 31337
000

100
PUT 5 foo bar
001
hallo
.
000
GET 5 foo bar
hallo
000
QUIT
$
s4fti
User
Beiträge: 5
Registriert: Donnerstag 2. Oktober 2014, 02:20

Hey, sorry das ich so lange für eine Antwort gebraucht habe. Ich war schwer beschäftigt mit anderen Hausaufgaben und Projekten und ich konnte mich nicht daher nicht wie erwartet mit diesem Thema befassen.
Leider funktioniert bei mir der Code nicht wie gewünscht. Ich scheiter an dem ersten Kommando.
"PUT 13 foo" gibt als Antwort "001". Soweit so gut.
Anschließend sollte bei deiner Sequenz eigentlich ein 001 folgen. Das ist jedoch gar nicht so entscheidend, da bei mir auch deine Sequenz, wie du sie gepostet hast, nicht reproduzieren lässt.
Auf "." folgt bei mir keine Antwort.
Anschließend liefert der Server aann keine Antworten mehr, unabhängig von der Eingabe.

Mit netcat klappt es. Wäre interessant für mich herauszufinden, warum es mit netcat klappt und mit telnet nicht.
Außerdem folgt sobald ich den telnet-Klienten schließe im Server eine Exception. Bei netcat is alles i.O..

Allerdings scheint die GET Methode bei meinem GET etwas abgeschnitten zu haben.
Bild

Vielen Dank auf jeden Fall für die ordentliche Struktur und die gezeigten Kniffe aber ich habe da noch einige Fragen zum Code:

Was macht das "b" vor den Strings?
Warum werden Methoden __name__ benannt?
Was macht die __iter__ methode? Und warum ist der Parameter self und auch der Rückgabewert?
Wieso überhaupt self, jedes Mal als Parameter?

Was genau macht die Zeile "next = read" ?
Fehlen da nicht zwei Parameter für die Methode read(self, end_marker=b'\n')?

Vielen Dank nochmal an euch beide für die Hilfe, ihr habt euch für eine Postkarte aus den Staaten qualifiziert. :D Wer möchte, bekommt eine. Dafür bräuchte ich die Adresse per PM. Kann verstehen, wenn das aus Datenschutzgründen nicht erwünscht ist.
BlackJack

@s4fti: Das klappt mit telnet nicht weil das am Zeilenende kein '\n' sendet sondern '\r\n'. Beim 'PUT 13 foo\r\n' ist das kein Problem, da wird das '\r' halt Teil des Schlüssels, aber man kann so unmöglich '\n.\n' eingeben denn das wird zu '\r\n.\r\n' und darauf wartet der Server nun mal nicht.

Die Ausnahme tritt bei netcat auch auf wenn Du die Verbindung auf Client-Seite abbrichst während `PUT` nach dem '001' auf die Daten wartet. Könnte man an der Stelle abfangen und den Thread beenden, aber das passiert durch die Ausnahme ja sowieso schon. Der Server läuft für alle anderen Clients ja trotzdem normal weiter.

Bei der GET-Methode wird nichts abgeschnitten, die liefert genau die 13 angeforderten Bytes. Und das ist auch alles was gespeichert ist, denn Du hast beim PUT ja gesagt es sollen 13 Bytes gespeichert werden. Und das Protokoll sagt alles zwischen den 13 Bytes und dem '\n.\n' soll vom Server ingoriert werden.

Das b kennzeichnet die Zeichenketten als Bytestrings. Ist in Python 2 nicht notwendig, dient aber als Dokumentation das hier keine Zeichen sondern Bytes gemeint sind. In Python 3 wird das dann notwendig. Und diese Kennzeichnung kann Werkzeugen wie 2to3.py beim halbautomatischen portieren von Quelltext helfen.

Was meinst Du bei der Frage nach Methoden mit `__name__`? Das `__name__` ist ja keine Methode. Das ist ein Name auf Modulebene der den Namen des Moduls enthält. Oder falls das Modul nicht importiert sondern als Programm ausgeführt wurde den Wert '__main__'. Woran man dann erkennen kann ob das Modul nur importiert wurde oder als Programm ausgeführt werden soll.

Die `__iter__`-Methode findest Du in der Dokumentation. Oben rechts ist auf den meisten Seiten ein Link zum Index. Darüber kommt man dann zum Abschnitt über Iteratortypen: Iterator Types.

Warum `self` als Argument: Darum. Ist halt so. Irgendwie muss das Objekt auf dem eine Methode aufgerufen wird ja an diese Methode übergeben werden. Bei anderen Sprachen ist dann auf magische Weise ein `self` oder `this` vorhanden, in Python wird es halt explizit als Argument übergeben wie jedes andere Argument auch. Der Name `self` ist eine Konvention, von der sollte man aber nicht abweichen.

Die Zeile ``next = read`` weist `next` den Wert von `read` zu. Wer hätte das gedacht. ;-) Danach sind beide Namen an den selben Wert gebunden. In diesem Fall also eine ungebundene Methode. Man braucht für einen Iterator halt zwingend eine `next()`-Methode die man ohne Argument aufrufen kann, was die Methode macht hat aber IMHO eher den Namen `read()` verdient. ``session.connection.read(b'\n.\n')`` liest sich IMHO besser/verständlicher als ``session.connection.next(b'\n.\n')``. Wobei wenn ich so darüber nachdenke ein Name wie `read_up_to()` vielleicht noch treffender wäre.
Antworten