Python: Netzwerk
Willemers Informatik-Ecke
Die Netzwerkprogrammierung im Internetprotokoll TCP/IP erfolgt über Sockets. Der Umgang mit Sockets ähnelt denen von Dateien. Ein Socket wird eröffnet, es wird ein Schreib- oder Leseaufruf abgesetzt und der Socket wieder geschlossen. Sie können über die Sockets jede beliebige Netzwerkkommunikation durchführen.

Python bietet darüber hinausgehende Bibliotheken an, um Standardzugriffe zu vereinfachen. So gibt es Module, um beispielsweise mit Webservern oder Mailservern Kontakt aufzunehmen. Das erspart dem Programmierer das Zusammensetzen der Befehlspakete und das Zerpflücken der Antwortpakete und macht so auch die Programmierung deutlich übersichtlicher.

Grundlegend: Socketprogrammierung

Eine Netzwerkkommunikation findet typischerweise immer zwischen einem Prozess, der bereitsteht, und einem Prozess, der Anfragen stellt, statt. Der wartende Prozess wird Server genannt, der Anfrager Client.

Damit sich beide finden, müssen der Client zunächst wissen, auf welchem Computer sein Partner ist. Dazu muss er eine beim Start eine IP-Nummer oder einen Hostname angeben.

Kennt der Client den Computer des Servers, muss er den Prozess kennen, mit dem er sprechen will. Dazu wird ein sogenannter Port festgelegt, an dem der Server auf ihn wartet. Weil dieser Port dem Client vor der Kontaktaufnahme bekannt ist, spricht man von einem well known port. Die Ports eines Computers sind durchnummeriert. Ein normaler Webserver verwendet beispielsweise den Port 80.

Die Kontaktpunkte werden Socket genannt. Der Server bindet seinen Socket an den well known port und wartet auf Verbindungen. Der Client verbindet seinen Socket mit dem Port des Servers. Anschließend werden über read() und write() Daten getauscht wie über Dateien.

Der Server
Der Server soll seine Dienste über Port 50000 anbieten. Das ist reichlich hoch, sollte aber genau darum auch in der Regel frei sein. Es kann pro Computer nämlich nur einen geben, der auf einem bestimmten Port seine Dienste anbietet.

Anschließend geht der Server in eine Endlosschleife. In dieser wird zunächst recv() aufgerufen. Dies blockiert den Prozess solange, bis ein Client eine Verbindung aufnimmt und Daten schickt.

Sobald dies passiert, wird der Server die Daten verarbeiten, die Antwort zurücksenden und auf neue Anfragen warten. In unserem Beispiel werden die Clientdaten einfach auf dem Bildschirm ausgegeben und dem Client wird einen Gruß zugesandt.

import socket
MeinSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    MeinSocket.bind(("", 50000))
    MeinSocket.listen(1)
    while True:
        PartnerSocket, addr = MeinSocket.accept()
        while True:
            daten = PartnerSocket.recv(1024)
            if not daten:
                PartnerSocket.close()
                break
            print("[%s] %s" % (addr[0], daten))
            PartnerSocket.send("Guten Tag")
finally:
    MeinSocket.close()
Der Client
Der Client durchläuft keine Schleife, sondern verbindet sich über die Funktion connect() mit dem Server. Der Parameter von connect() ist ein Tupel, das aus Rechneradresse und Port besteht. Darum sind an dieser Stelle auch doppelte Klammern erforderlich. Die Adresse 127.0.0.1 ist immer der localhost, also der eigene Rechner. Wenn Sie die Verbindung unbedingt über das Netzwerk austesten wollen, müssen Sie die Adresse des Server-Computers eintragen. Die Portnummer muss natürlich die sein, unter der der Serverprozess gebunden ist.

Nun sendet der Client seine Daten und ruft gleich anschließend die Funktion recv() auf, die den Client blockiert, bis der Server eine Antwort sendet. Diese Antwort gibt er auf dem Bildschirm aus und endet.

import socket
MeinSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    MeinSocket.connect(("127.0.0.1", 50000))
    MeinSocket.send("Hallo du da")
    Daten = MeinSocket.recv(1024)
    print("%s" % (Daten))
finally:
    MeinSocket.close()
Prinzipiell können Sie auf dieser Basis eine eigene Netzwerkkommunikation aufbauen. Bei parallelen Zugriffen sollten Sie das Kapitel~thread zurate ziehen. Sie können mit dem Client-Grundgerüst auch durchaus Web-, Mail- und andere Server kontaktieren. In diesen Fällen ist es aber einfacher und sicherer, wenn Sie die Module verwenden, die Ihnen Python anbietet.

Versenden von Mails: SMTP

Es kommt immer häufiger vor, dass Programme Mails versenden. Das betrifft natürlich auch Spam, also ungefragte und damit unerlaubte Werbebotschaften. Aber auch ganz legale Anwendungen versenden Rechnungen, Angebote oder Anfragen direkt per Mail.

Um eine Mail zu versenden, wendet man sich an einen SMTP-Server. SMTP steht für Simple Mail Transfer Protocol. Für das Versenden von Mails bietet Python as Modul smtplib an.

Die grundlegende Abfolge ist zunächst recht simpel. Es wird ein Server-Objekt über den Konstruktor smtplib.SMTP() erzeugt, der den Servernamen als Parameter übernimmt. Über dieses Objekt werden die folgenden Funktionen nacheinander aufgerufen.

In der Praxis steht dem aber entgegen, dass fast alle Anbieter mit einer Verschlüsselung auf dem Transportweg mit TLS arbeiten. Aus diesem Grund muss vor dem Login die Methode starttls() aufgerufen werden. Die Methode ehlo() bewirkt eine saubere Anmeldung.
Problemfeld Nachricht
Der dritte Parameter von sendmail() ist zwar eine reine Zeichenkette. Sie müsste aber normgerecht ausgeführt sein, damit alle Beteiligten der Sendungskette den Inhalt korrekt verstehen. Diese enthält nämlich beispielsweise auch den Betreff und weitere Einträge.

Um dies zu vereinfachen, stellt Python das Modul email.message zur Verfügung. Die darin enthaltene Klasse Message ermöglicht den Eintrag der Felder Subject (Betreff), From (Absender) und To (Empfänger).

Die letzten beiden Felder werden zwar schon von sendmail() als Parameter erwartet. Dort sind es aber die nackten E-Mail-Adressen, hier die Einträge, die Sie typischerweise in Ihrem Mail-Programm sehen.

Die Methode set_payload() übernimmt nun den tatsächlichen Nachrichteninhalt an die Mail. Und der dritte Parameter der Funktion sendmail() kann erstellt werden, in dem Sie die Methode as_string() aufrufen.

Das folgende kleine Programm erfüllt nun all die oben genannten Bedingungen.

import smtplib
from email.message import Message

absender = "absender@provider.de"
password = "dasistjasogeheim"
empfaenger = "emfaenger@sonstwo.de"

try:
    server = smtplib.SMTP('smtp.1und1.de')
    server.starttls()   # verschluesselt senden
    server.ehlo()       # Hallo sagen!
    server.login(absender, password)
    # Nachricht erstellen
    mail = Message()
    mail.set_payload("Dies ist der Nachrichteninhalt.")
    mail["Subject"] = "Der Betreff"
    mail["From"]    = "mir "
    mail["To"]      = "dir "

    server.sendmail(absender, empfaenger, mail.as_string())
    server.quit()
except SMTPException:
    print("SMTP-Fehler")
Ein Anhang namens MIME
Das Versenden einer Textnachricht ist oft nicht ausreichend. Rechnungen werden beispielsweise gern in den Anhang gestellt. Manche Behörden gehen teilweise so weit, dass sie gar nichts mehr in den Mailtext schreiben, nur damit ihre großzügigen PDF-Anhänge so richtig zur Geltung kommen.

Ursprünglich war es bei Mails nicht vorgesehen, irgendwelche binären Daten zu versenden. Darum sind nicht alle Bytekombinationen zulässig. Damit man dennoch Bilder senden kann, wurde MIME erfunden. Damit werden Anhänge möglich und durch eine geschickte Kodierung kann man sogar binäre Daten senden. Allerdings muss man sie vor dem Senden um etwa ein Drittel aufpumpen.

Zunächst benötigen Sie eine ganze Reihe von Importen, wie Sie unten im Listing sehen können. Der Vorgang des eigentlichen Sendens hat sich nicht verändert, sondern lediglich das Mail-Inhaltsobjekt.

Statt einer einfachen Message() wird nun eine MIMEMultipart() erzeugt. Die Felder für Betreff, Empfänger und Absender haben sich nicht geändert. Das Textfeld, das zuvor mit set_payload() belegt wurde, wird nun mit mail.attach(MIMEText()) angehängt. Dann wird für jede Datei ein neuer MIME-Part geschaffen und mit dem Encoder kodiert. Das Listing ist größer geworden. Die Mail aber auch.

import smtplib
from email.message import Message
from email.mime.MIMEMultipart import MIMEMultipart
from email.mime.MIMEBase import MIMEBase
from email.mime.MIMEText import MIMEText
from email import encoders

absender = "absender@provider.de"
password = "dasistjasogeheim"
empfaenger = "emfaenger@sonstwo.de"

server = smtplib.SMTP('smtp.1und1.de')
server.starttls()   # verschluesselt senden
server.ehlo()       # Hallo sagen!
server.login(absender, password)
# mail als MIME erstellen
mail = MIMEMultipart()

mail["Subject"] = "Der Betreff"
mail["From"]    = "mir <absender@provider.de>"
mail["To"]      = "dir <emfaenger@sonstwo.de>"
# Den Nachrichteninhalt muss man nun anhaengen
mail.attach( MIMEText("Dies ist der neue Nachrichteninhalt") )
# fuer jeden Anhang wiederholen
dateiname = "AW.jpg"
part = MIMEBase('application', "octet-stream")
part.set_payload( open(dateiname,"rb").read() )
encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="%s"' % dateiname)
mail.attach(part)

server.sendmail(absender, empfaenger, mail.as_string())
server.quit()
Wenn Sie ein paar Experimente mit SMTP machen, werden Sie vielleicht aus Versehen lernen, wie leicht es ist, einen E-Mail-Absender zu fälschen und darum in Zukunft etwas vorsichtiger mit Mails umgehen, die einen scheinbar vertrauenswürdigen Absender haben.

Auslesen von Mails per POP3

Das Auslesen von E-Mails erfolgt nicht über einen SMTP-Server, sondern über die Protokolle IMAP oder POP3. IMAP ist das gängige Protokoll für E-Mail-Clients mit einer Flatrate, weil dann die Mails auf dem Server verbleiben und dort bearbeitet werden. Dies ist allerdings für Programme normalerweise weniger interessant. Stattdessen wird man eher POP3 verwenden, da die Handhabung aus Programmsicht einfacher ist und die Zugriffe normalerweise nur kurzzeitig erfolgen sollen.

Für den Umgang mit POP3 stellt Python die Bibliothek poplib zur Verfügung. Der Aufruf von POP3() erwartet den Servernamen als Parater. Optional können Sie als zweiten Parameter den Port angeben, falls der Server sich nicht an die Standard-Ports hält.

Heutzutage ist es fast bei allen Servern üblich, den Datenverkehr beim Abholen der Mails zu verschlüsseln und dazu TLS anzubieten. Der Vorgänger von TLS hieß SSL und darum heißt der Konstruktor auch POP3_SSL().

Sie erhalten ein Objekt, über das Sie die Funktionen des POP3-Servers ansprechen können. Diese Funktionen entsprechen den Protokollspezifikationen der RFC 1725, die das POP3-Protokoll beschreibt.

Die folgende Aufstellung geht davon aus, dass das Ergebnis des Aufrufs von POP3() respektive POP3_SSL() in der Variablen pop abgelegt wurde.

Das folgende Programm holt die Liste der Mails vom POP3-Server und zeigt die letzten fünf Mails an.
#!/usr/bin/python
import poplib

#pop3Server = poplib.POP3('pop.gmx.de')
pop3Server = poplib.POP3_SSL('pop.1und1.de')
pop3Server.user('absender@provider.de')
pop3Server.pass_('sowasvongeheim')
# Hole die Liste aller Mails.
mailliste = pop3Server.list()
# Hole nur die letzten 5 Mails
anzahl = len(mailliste[1])
start = anzahl-5
if start<0:
    start = 0
# Ausgabe der Mails
for i in range(start, anzahl):
    mailnr = mailliste[1][i]
    # lines = pop3Server.top(i+1, 10)[1]
    lines = pop3Server.retr(i+1)[1]
    for line in lines:
        print(line)
pop3Server.quit()

Griff ins WWW

Den Zugriff auf Websites erleichtert die Bibliothek urllib. Sie bietet mit urlopen eine Funktion, die eine Website öffnet wie eine Datei. Weitere Funktionen haben sogar die gleichen Namen wie ihre Gegenstücke beim Umgang mit Dateien, so dass der Programmierer sich gleich zu Hause fühlt.
import urllib
try:
    website = urllib.urlopen("http://www.willemer.de")
    Datei = open("index.htm", "w")
    Zeile = "irgendwas"
    while Zeile:
        Zeile = website.readline()
        Datei.write(Zeile)
except:
    print("Datei Schreibfehler")
website.close()
Datei.close()