TCP server: Robust, performant, ressourcenschonend

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
Rickson1982
User
Beiträge: 6
Registriert: Donnerstag 29. März 2012, 16:44

Hallo zusammen!

Ich möchte gerne mittels eines Python-Scripts einen TCP-Server implementieren.

Dessen Aufgabe soll die Folgende sein:
Er wird im ein- bis zweistelligen ms Bereich zyklisch von einem Remote-Client gepollt, liest dann bestimmte Daten auf seiner lokalen Workstation aus und schickt diese an den pollenden Client.

Von folgendem kann man ausgehen:
- Es verbindet sich immer nur ein Client mit dem Server über Ethernet
- Client und Server laufen auf einer Windows-Plattform
- Es wird Python 2.5.1 eingesetzt

Ziel:
- Der Server sollte möglichst performant sein bzgl. lesen, bearbeiten und zurücksenden der Clientanfrage
- Er soll seine lokale Workstation so wenig wie möglich belasten
- Er soll nicht zu Systeminstabilitäten führen

Da ich mich mit der Thematik nicht besonders gut auskennen, möchte ich hier gerne einmal diskutieren, wie ich meinen bestehenden (recht einfachen) Code optimieren kann, um die vorher genannten Anforderungen besser zu erfüllen.

Bisher habe ich folgendes:

Code: Alles auswählen

import socket
import sys

port_number = 12345
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server_address = (socket.gethostname(), port_number)
sock.bind(server_address)

sock.listen(1)
while True:
    connection, client_address = sock.accept()
    try:
        while True:
            data = connection.recv(4096)
            if data:
                connection.sendall(data) #hier werden später die angefragten Daten lokal gelesen, aufbereitet und an den Client gesendet
            else:
                break
    finally:
        connection.close()
Ich bedanke mich schon einmal herzlich für eure Kommentare!
Zuletzt geändert von Anonymous am Samstag 13. April 2013, 11:02, insgesamt 3-mal geändert.
Grund: Quelltext in Python-Code-Tags gesetzt.
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Also, das ist nicht trivial. Das habe ich vor einigen Jahren mal gemacht, um zu verstehen, wie so ein Server auf asynchroner Basis funktioniert. Das lief am Schluß auch gut, aber nicht besser als Twisted. Falls Dein Anliegen also nicht akademischer Natur ist, würde ich Dir eine Lösung wie z.B. Twisted empfehlen.
Rickson1982
User
Beiträge: 6
Registriert: Donnerstag 29. März 2012, 16:44

Vielen Dank für deine Anwort!

Leider habe ich nur Python 2.5.1 verügbar und kann auch nicht upgraden.
Twisted benötigt jedoch wenigstens Python 2.6.

Hast du vielleicht - basierend auf deinen eigenen Erfahrungen - noch ein paar Ideen, wie ich meinen Code optimieren kann?
BlackJack

@Rickson1982: Du hast in Deinem Code einen typischen Anfängerfehler bei der Socketprogrammierung, zumindest solange Dir nicht klar ist, dass das Lesen *so* nur bei so einem simplen Echo-Server funktioniert.

`recv()` liest sozusagen beliebige Teilstücke aus dem Datenstrom. Das muss weder alles sein was zu einer „Nachricht” gehört, noch muss alles was da gelesen wird, zu *einer* „Nachricht” gehören. Da hier nur alle Teilstücke 1:1 wieder zurück gesendet werden, ist das egal, aber wenn Du anfängst Nachrichten zu verarbeiten, musst Du sicherstellen, dass a) alles von einer Nachricht gelesen wurde, bevor sie weiterverarbeitet wird, und b) eventuelle Teile einer Folgenachricht nicht verloren gehen und beim Empfangen der nächsten Nachricht berücksichtigt werden.
Rickson1982
User
Beiträge: 6
Registriert: Donnerstag 29. März 2012, 16:44

Hallo BlackJack!

Danke für deinen Hinweis.

Ich habe das bisher so verstanden, dass connection.recv(4096) die ersten 4096 Bytes der vom Client gesendeten Nachricht empfängt.
Da die Strings, die ich vom Client erhalten werde, deutlich kleiner als 4096 Bytes sind, sollte ich eigentlich immer die ganze Nachricht mit dem ersten recv empfangen, oder?

Ich denke, man könnte die Grösse des Buffers vielleicht sogar noch verkleinern um allgemein die Performance zu erhöhen (falls das tatsächlich ins Gewicht fällt)?
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Rickson1982 hat geschrieben:Leider habe ich nur Python 2.5.1 verügbar und kann auch nicht upgraden.
Twisted benötigt jedoch wenigstens Python 2.6.
Ältere Versionen von Twisted laufen auch mit älteren Python-Versionen
Rickson1982 hat geschrieben:Hast du vielleicht - basierend auf deinen eigenen Erfahrungen - noch ein paar Ideen, wie ich meinen Code optimieren kann?
Das ist schon eine Weile her, aber

Code: Alles auswählen

while True:
            data = connection.recv(4096)
            if data:
                connection.sendall(data)
wird so nicht funktionieren. 'connection.recv(4096)' liest bis zu 4096 Zeichen, nicht notwendigerweise aber alle, die Du erwartest, auch wenn Du nur wenige Bytes erwartest. D.h. Du musst diese Funktion öfter aufrufen und die Daten aggregieren. Zudem blockiert diese Funktion. Das kann zu Problemen führen, wenn viele requests kommen. Das mag in Deinem speziellen Fall jetzt kein Problem sein, aber die Details eines Servers führen in eine Komplexität, die sich in einem kurzen Posting hier nicht abhandeln lassen.
BlackJack

@Rickson1982: Es gibt keine Nachrichten die gesendet werden sondern einen Bytestrom. Und ``recv(4096)`` liest davon 1 bis 4096 Bytes, wieviele genau liegt nicht unter Deiner Kontrolle. Das kann weniger sein als mit einem `send()`/`sendall()` auf der anderen Seite geschrieben wurde, das kann aber auch der Inhalt von mehr als einem `send()`/`sendall()`-Aufruf auf der anderen Seite sein. Wobei auch hier wieder nicht alles gelesen werden muss. Wenn man also so etwas wie Nachrichten auf diesen Bytestrom schreibt, dann muss man ein Protokoll entwerden bei dem man irgendwie erkennen kann was alles zu *einer* Nachricht gehört und dementsprechend solange `recv()` aufrufen bis man sicher ist eine komplette Nachricht zu haben — und nur *eine* Nachricht und nicht Teile der nächsten oder gar mehr.

So ein Protokoll hat entweder feste Nachrichtengrössen, oder kodiert die Grösse einer Nachricht mit einer festen Anzahl von Bytes am Anfang der Nachricht, oder es gibt ein spezielles Endkennzeichen was dann aber niemals *innerhalb* einer Nachricht auftauchen darf.

Das HTTP-Protokoll mischt zum Beispiel zwei dieser Techniken. Zum einen gibt es Textzeilen mit einer Leerzeile als Endkennzeichen des Nachrichtenheaders, und zum anderen kann es innerhalb des Headers eine Angabe zur Anzahl der Bytes geben, die nach dem Header noch folgen. Dadurch weiss der Empfänger in zwei Schritten was alles zu der Nachricht gehört. Erst liesst er Daten bis er zu einer Leerzeile kommt. Dann kann er in den Zeilen nach dem ``Content-Length``-Header suchen und dementsprechend viele weitere Bytes lesen.
Rickson1982
User
Beiträge: 6
Registriert: Donnerstag 29. März 2012, 16:44

@BlackJack:
Vielen Dank für deine gute Erklärung.

Falls ich es richtig verstehe, heisst das:
Wenn der Client z.B. folgendes schicken soll (erster Pollvorgang): %a=b,c=d% (eine 'Nachricht' befindet sich bei mir immer zwischen zwei Prozentzeichen), könnte der Server anfangs nur folgendes empfangen: %a=b,

In diesem Fall müsste der Server einen weiteren Lesevorgang durchführen, um die fehlenden Zeichen bis zum nächsten Prozent zu erhalten und die Nachricht entsprechend zusammensetzen. Eventuell 'zuviel' übertragene Zeichen (entweder noch vom ersten oder vom nächsten Pollvorgang) müsste der Server zwischenspeichern und mit dem nächsten vom Client gesendeten Bytestrom wieder derartig zusammenfügen, dass die nächste Nachricht verarbeitet werden kann (falls die 'zuviel' übertragenen Zeichen nicht schon wieder eine ganze Nachricht enthalten).

Stimmt das so? Oder kann der Client erst einen neuen Pollvorgang initiieren, wenn alle Daten vom ersten Pollvorgang vom Server ausgelesen wurde?
Antworten