Skip to content

11. De netwerkfuncties van Python

We gaan nu in op de netwerkfuncties van Python waarmee we kunnen programmeren met TCP / IP (Transfer Control Protocol / Internet Protocol).

  

11.1. De naam of het adres IP van een computer op het internet opvragen


Programma (inet_01)


import sys, socket

#------------------------------------------------
def getIPandName(nomMachine):
    #nomMachine: naam van de machine waarvan we het adres willen weten IP
    # nomMachine-->adres IP
    try:
        ip=socket.gethostbyname(nomMachine)
        print "ip[%s]=%s" % (nomMachine,ip)
    except socket.error, erreur:
        print "ip[%s]=%s" % (nomMachine,erreur)
        return

    # adres IP --> nomMachine
    try:
        name=socket.gethostbyaddr(ip)
        print "name[%s]=%s" % (ip,name[0])
    except socket.error, erreur:
        print "name[%s]=%s" % (ip,erreur)
        return


# ---------------------------------------- hoofd

# constanten
HOTES=["istia.univ-angers.fr","www.univ-angers.fr","www.ibm.com","localhost","","xx"]

# adressen IP van de machines van HOTES
for i in range(len(HOTES)):
    getIPandName(HOTES[i])
# einde
sys.exit()

Opmerkingen:

  • regel 1: de netwerkfuncties van Python zijn ondergebracht in de module socket.

Resultaten

1
2
3
4
5
6
7
8
9
ip[istia.univ-angers.fr]=193.49.146.171
name[193.49.146.171]=istia.istia.univ-angers.fr
ip[www.univ-angers.fr]=193.49.144.40
name[193.49.144.40]=ametys-fo.univ-angers.fr
ip[www.ibm.com]=129.42.58.216
name[129.42.58.216]=[Errno 11004] host not found
ip[localhost]=127.0.0.1
name[127.0.0.1]=Gportpers3.ad.univ-angers.fr
ip[xx]=[Errno 11004] getaddrinfo failed

11.2. Een webclient

Een script waarmee de inhoud van de indexparagraaf van een website kan worden opgehaald.


Programma (inet_02)


import sys,socket

  #-----------------------------------------------------------------------
def getIndex(site):
    # leest de website URL en slaat deze op in het bestand site.html
  
    # in eerste instantie geen fout
    erreur=""
    # het bestand site.html wordt aangemaakt
    try:
        html=open("%s.html" % (site),"w")
    except IOError, erreur:
        pass
    # fout?
    if erreur:
        return "Erreur (%s) lors de la création du fichier %s.html" % (erreur,site)

    # verbinding tot stand gebracht op poort 80 van de website
    try:
        connexion=socket.create_connection((site,80))
    except socket.error, erreur:
        pass
    # terugkeren bij fout
    if erreur:
        return "Echec de la connexion au site (%s,80) : %s" % (site,erreur)

    # verbinding vormt een bidirectionele communicatiestroom
    # tussen de client (dit programma) en de benaderde webserver
    # dit kanaal wordt gebruikt voor de uitwisseling van opdrachten en informatie
    # het communicatieprotocol is HTTP

    # de client verstuurt het get-commando om de URL op te vragen /
    # 'get'-syntaxis: URL HTTP/1.0
    # de headers van het protocol HTTP moeten eindigen met een lege regel
    connexion.send("GET / HTTP/1.0\n\n")

    # de server zal nu reageren via het verbindingskanaal. Hij zal alle
    # zijn gegevens verzenden en vervolgens het kanaal sluiten. De client leest alles wat binnenkomt via het verbindingskanaal
    # totdat het kanaal wordt gesloten
    ligne=connexion.recv(1000)
    while(ligne):
        html.write(ligne)
        ligne=connexion.recv(1000)

    # sluit de client op zijn beurt de verbinding
    connexion.close()
    # het html-bestand wordt gesloten
    html.close()
    # terug
    return "Transfert reussi du paragraphe index du site %s" % (site)

# --------------------------------------------- main    

# de tekst HTML ophalen uit URL

# lijst met websites
SITES=("istia.univ-angers.fr","www.univ-angers.fr","www.ibm.com","xx")

# de indexpagina's van de websites uit de tabel SITES lezen
for i in range(len(SITES)):
      # de indexpagina van de website SITES[i] lezen
      resultat=getIndex(SITES[i])
      # weergave van het resultaat
      print resultat

# einde
sys.exit()

Opmerkingen:

  • regel 57: de lijst met URL's van de websites waarvan de indexpagina wordt opgevraagd. Deze lijst is opgeslagen in het tekstbestand [nomsite.html];
  • regel 62: de functie getIndex voert de taak uit;
  • regel 4: de functie getIndex;
  • regel 20: met de methode create_connection((site,port)) kan een verbinding worden gemaakt met een dienst TCP / IP die draait op poort port van de machine site;
  • regel 35: met de methode send kunnen gegevens worden verzonden via een verbinding TCP / IP. Hier wordt tekst verzonden. Deze tekst volgt het protocol HTTP (HyperText Transfer Protocol);
  • regel 40: met de methode recv kunnen gegevens worden ontvangen via een verbinding TCP / IP. Hier wordt het antwoord van de webserver in blokken van 1000 tekens gelezen en opgeslagen in het tekstbestand [nomsite.html].

Resultaten

Transfert reussi du paragraphe index du site istia.univ-angers.fr
Transfert reussi du paragraphe index du site www.univ-angers.fr
Transfert reussi du paragraphe index du site www.ibm.com
Echec de la connexion au site (xx,80) : [Errno 11001] getaddrinfo failed

Het ontvangen bestand voor de website [www.ibm.com]:

HTTP/1.1 302 Found
Date: Wed, 08 Jun 2011 15:43:56 GMT
Server: IBM_HTTP_Server
Content-Type: text/html
Expires: Fri, 01 Jan 1990 00:00:00 GMT
Pragma: no-cache
Cache-Control: no-cache, must-revalidate
Location: http://www.ibm.com/us/en/
Content-Length: 209
Kp-eeAlive: timeout=10, max=14
Connection: Keep-Alive

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1>
<p>The document has moved <a href="http://www.ibm.com/us/en/">here</a>.</p>
</body></html>
  • regels 1-11 zijn de HTTP-headers van het antwoord van de server;
  • regel 1: de server vraagt de client om door te sturen naar de in regel 8 aangegeven URL;
  • regel 2: datum en tijdstip van het antwoord;
  • regel 3: identiteit van de webserver;
  • regel 4: door de server verzonden inhoud. Hier een pagina HTML die begint op regel 13;
  • regel 12: de lege regel die de headers HTTP afsluit;
  • regels 13-19: de pagina HTML die door de webserver is verzonden.

11.3. Een SMTP-client

Van de protocollen TCP / IP is SMTP (SendMail Transfer Protocol) het communicatieprotocol voor de berichtverzendservice.


Programma (inet_03)


# -*- coding=utf-8 -*-

import sys,socket

#-----------------------------------------------------------------------
def getInfos(fichier):
    # geeft de informatie (smtp, afzender, ontvanger, bericht) weer die uit het tekstbestand [fichier] is gehaald
    # regel 1: smtp, afzender, ontvanger
    # volgende regels: de tekst van het bericht

    # openen van [fichier]
    erreur=""
    try:
        infos=open(fichier,"r")
    except IOError, erreur:
        return ("Le fichier %s n'a pu etre ouvert en lecture : %s" % (fichier, erreur))
  
    # lezen van de eerste regel
    ligne=infos.readline()
    # verwijdering van het einde-van-regel-teken
    ligne=cutNewLineChar(ligne)
    # ophalen van de velden smtp, afzender, ontvanger
    champs=ligne.split(",")
    # is het aantal velden correct?
    if len(champs)!=3 :
        return ("La ligne 1 du fichier %s (serveur smtp, expediteur, destinataire) a un nombre de champs incorrect" % (fichier))
    # "verwerking" van de opgehaalde informatie
    # uit elk van de 3 velden worden de "spaties" verwijderd die voor of na de nuttige informatie staan
    for i in range(3):
        champs[i]=champs[i].strip()
    # ophalen van de velden
    (smtpServer,expediteur,destinataire)=champs
    message=""
    # lezen van de rest van het bericht
    ligne=infos.readline()
    while ligne!='':
        message+=ligne
        ligne=infos.readline()
    infos.close()
    # terug
    return ("",smtpServer,expediteur,destinataire,message)

#-----------------------------------------------------------------------
def sendmail(smtpServer,expediteur,destinataire,message,verbose):
    # bericht verzenden naar de SMTP-server smtpserver namens de afzender
    # voor de ontvanger. Als verbose=True, wordt de communicatie tussen client en server bijgehouden

    # de naam van de client wordt opgehaald
    try:
        client=socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
    except socket.error, erreur:
        return "Erreur IP / Nom du client : %s" % (erreur)
    
    # een verbinding wordt geopend op poort 25 van smtpServer
    try:
        connexion=socket.create_connection((smtpServer,25))
    except socket.error, erreur:
        return "Echec de la connexion au site (%s,25) : %s" % (smtpServer,erreur)

    # de verbinding vormt een bidirectionele communicatiestroom
    # tussen de client (dit programma) en de benaderde SMTP-server
    # dit kanaal wordt gebruikt voor de uitwisseling van opdrachten en informatie

    # na het tot stand brengen van de verbinding stuurt de server een welkomstbericht dat wordt gelezen
    erreur=sendCommand(connexion,"",verbose,1)
    if(erreur) :
        connexion.close()
        return erreur
    # commando ehlo:
    erreur=sendCommand(connexion,"EHLO %s" % (client),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # commando mail from:
    erreur=sendCommand(connexion,"MAIL FROM: <%s>" % (expediteur),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # commando rcpt to:
    erreur=sendCommand(connexion,"RCPT TO: <%s>" % (destinataire),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # commando data
    erreur=sendCommand(connexion,"DATA",verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # voorbereiding van het te verzenden bericht
    # het moet de volgende regels bevatten
    # From: afzender
    # To: ontvanger
    # lege regel
    # Bericht
    # .
    data="From: %s\r\nTo: %s\r\n%s\r\n.\r\n" % (expediteur,destinataire,message)
    # bericht verzenden
    erreur=sendCommand(connexion,data,verbose,0)
    if erreur :
        connexion.close()
        return erreur
    # commando afsluiten
    erreur=sendCommand(connexion,"QUIT",verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # einde
    connexion.close()
    return "Message envoye"

# --------------------------------------------------------------------------
def sendCommand(connexion,commande,verbose,withRCLF):
    # commando verzenden naar het verbindingskanaal
    # uitgebreide modus als verbose=1
    # als withRCLF=1, voeg de reeks RCLF toe aan het commando

    # gegevens
    RCLF="\r\n" if withRCLF else ""
    # commando verzenden als commando niet leeg is
    if commande:
        connexion.send("%s%s" % (commande,RCLF))
        # eventuele echo
        if verbose:
            affiche(commande,1)
        # antwoord met minder dan 1000 tekens lezen
        reponse=connexion.recv(1000)
        # eventuele echo
        if verbose:
            affiche(reponse,2)
        # ophalen van foutcode
        codeErreur=reponse[0:3]
        # fout teruggestuurd door de server?
        if int(codeErreur) >=500:
            return reponse[4:]
        # terugkeer zonder fout
        return ""

# --------------------------------------------------------------------------
def affiche(echange,sens):
    # uitwisseling weergeven op het scherm?
    # als sens=1, toon -->uitwisseling
    # als sens=2, toon <-- uitwisseling zonder de laatste 2 tekens RCLF
    if sens==1:
        print "--> [%s]" % (echange)
        return
    elif sens==2:
        l=len(echange)
        print "<-- [%s]" % echange[0:l-2]
        return

      
# --------------------------------------------------------------------------
def cutNewLineChar(ligne):
    # verwijder het einde-van-regel-teken uit [ligne] indien aanwezig
    l=len(ligne)
    while(ligne[l-1]=="\n" or ligne[l-1]=="\r"):
        l-=1
    return(ligne[0:l])

# main ----------------------------------------------------------------
  
# client SMTP (SendMail Transfer Protocol) waarmee een bericht kan worden verzonden
# de gegevens worden uit een bestand gehaald INFOS met de volgende regels
# regel 1: smtp, afzender, ontvanger
# volgende regels: de tekst van het bericht

# afzender: e-mailadres van de afzender
# ontvanger: e-mailadres ontvanger
# smtp: naam van de te gebruiken smtp-server


# communicatieprotocol SMTP client-server
# -> client maakt verbinding via poort 25 van de SMTP-server
# <- de server stuurt hem een welkomstbericht
# -> de client stuurt het commando EHLO: de naam van zijn computer
# <- de server antwoordt met OK of niet
# -> de client verstuurt het commando mail from: <afzender>
# <- server antwoordt met OK of niet
# -> de client verstuurt het commando rcpt to: <ontvanger>
# <- server antwoordt met OK of niet
# -> de client verstuurt het commando data
# <- server antwoordt met OK of niet
# -> de client verstuurt alle regels van zijn bericht en sluit af met een regel die alleen het teken bevat.
# <- server antwoordt met OK of niet
# -> de client verstuurt het commando ‘quit’
# <- de server antwoordt met OK of niet

# de antwoorden van de server hebben de vorm xxx tekst, waarbij xxx een getal van 3 cijfers is. Elk getal xxx >=500
# duidt op een fout. Het antwoord kan uit meerdere regels bestaan die allemaal beginnen met xxx-, behalve de laatste
# in de vorm xxx(spatie)

# de uitgewisselde tekstregels moeten eindigen met de tekens RC(#13) en LF(#10)

# # de parameters voor het verzenden van de e-mail
MAIL="mail2.txt" 
# de e-mailinstellingen worden opgehaald
res=getInfos(MAIL)
# fout?
if res[0]:
      print "%s" % (erreur)
      sys.exit()
# e-mail verzenden in logboekmodus
(smtpServer,expediteur,destinataire,message)=res[1:]
print "Envoi du message [%s,%s,%s]" % (smtpServer,expediteur,destinataire)
resultat=sendmail(smtpServer,expediteur,destinataire,message,True)
print "Resultat de l'envoi : %s" % (resultat)
# einde
sys.exit()

Opmerkingen:

  • op een Windows-computer met antivirussoftware kan het zijn dat deze software het Python-script verhindert verbinding te maken met poort 25 van een SMTP-server. In dat geval moet de antivirussoftware worden uitgeschakeld. Voor McAfee kan dit bijvoorbeeld als volgt worden gedaan:
  • in [1] activeer je de console VirusScan
  • bij [2] wordt de service [Protection lors de l'accès] gestopt
  • bij [3] is deze gestopt

Resultaten

Het bestand infos.txt:

smtp.univ-angers.fr, serge.tahe@univ-angers.fr , serge.tahe@univ-angers.fr
Subject: test

ligne1
ligne2

ligne3

De schermresultaten:

Envoi du message [smtp.univ-angers.fr,serge.tahe@univ-angers.fr,serge.tahe@univ-angers.fr]
--> [EHLO Gportpers3.ad.univ-angers.fr]
<-- [220 smtp.univ-angers.fr ESMTP Postfix
250-smtp.univ-angers.fr
250-PIPELINING
250-SIZE 20480000
250-VRFY
250-ETRN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN]
--> [MAIL FROM: <serge.tahe@univ-angers.fr>]
<-- [250 2.1.0 Ok]
--> [RCPT TO: <serge.tahe@univ-angers.fr>]
<-- [250 2.1.5 Ok]
--> [DATA]
<-- [354 End data with <CR><LF>.<CR><LF>]
--> [From: serge.tahe@univ-angers.fr
To: serge.tahe@univ-angers.fr
Subject: test

ligne1
ligne2

ligne3

.
]
<-- [250 2.0.0 Ok: queued as 63412114203]
--> [QUIT]
<-- [221 2.0.0 Bye]
Resultat de l'envoi : Message envoye

Het bericht zoals weergegeven door de e-mailclient Thunderbird:

  

11.4. Een tweede SMTP-client

Dit tweede script doet hetzelfde als het vorige, maar maakt gebruik van de functionaliteiten van de module [smtplib].


Programma (inet_04)


# -*- coding=utf-8 -*-

import sys,socket, smtplib

#-----------------------------------------------------------------------
def getInfos(fichier):
    # geeft de informatie (smtp, afzender, ontvanger, bericht) uit het tekstbestand [fichier] weer
    # regel 1: smtp, afzender, ontvanger
    # volgende regels: de tekst van het bericht

    # het openen van [fichier]
    erreur=""
    try:
        infos=open(fichier,"r")
    except IOError, erreur:
        return ("Le fichier %s n'a pu etre ouvert en lecture : %s" % (fichier, erreur))
  
    # lezen van de eerste regel
    ligne=infos.readline()
    # verwijdering van het einde-van-regel-teken
    ligne=cutNewLineChar(ligne)
    # ophalen van de velden smtp, afzender, ontvanger
    champs=ligne.split(",")
    # hebben we het juiste aantal velden?
    if len(champs)!=3 :
        return ("La ligne 1 du fichier %s (serveur smtp, expediteur, destinataire) a un nombre de champs incorrect" % (fichier))
    # "verwerking" van de opgehaalde informatie – de "spaties" ervoor of erna worden verwijderd
    for i in range(3):
        champs[i]=champs[i].strip()
    # velden ophalen
    (smtpServer,expediteur,destinataire)=champs
    # het te verzenden bericht lezen
    message=""
    ligne=infos.readline()
    while ligne!='':
        message+=ligne
        ligne=infos.readline()
    infos.close()
    # terug
    return ("",smtpServer,expediteur,destinataire,message)

#-----------------------------------------------------------------------
def sendmail(smtpServer,expediteur,destinataire,message,verbose):
    # bericht verzenden naar de SMTP-server smtpserver namens de afzender
    # voor de ontvanger. Als verbose=True, wordt de communicatie tussen client en server bijgehouden

    # er wordt gebruikgemaakt van de smtplib-bibliotheek 
    try:
        server = smtplib.SMTP(smtpServer)
        if verbose:
            server.set_debuglevel(1)
            server.sendmail(expediteur, destinataire, message)
            server.quit()
    except Exception, erreur:
        return "Erreur envoi du message : %s" % (erreur)
    # einde
    return "Message envoye"
      
# --------------------------------------------------------------------------
def cutNewLineChar(ligne):
    # het einde-van-regel-teken van [ligne] wordt verwijderd, indien aanwezig
    l=len(ligne)
    while(ligne[l-1]=="\n" or ligne[l-1]=="\r"):
        l-=1
    return(ligne[0:l])

# main ----------------------------------------------------------------
  
# client SMTP (SendMail Transfer Protocol) waarmee een bericht kan worden verzonden
# de gegevens worden uit een bestand gehaald INFOS met de volgende regels
# regel 1: smtp, afzender, ontvanger
# volgende regels: de tekst van het bericht

# afzender: e-mailadres van de afzender
# ontvanger: e-mailadres ontvanger
# smtp: naam van de te gebruiken smtp-server


# communicatieprotocol SMTP client-server
# -> client maakt verbinding via poort 25 van de SMTP-server
# <- de server stuurt hem een welkomstbericht
# -> de client stuurt het commando EHLO: de naam van zijn computer
# <- de server antwoordt met OK of niet
# -> de client verstuurt het commando mail from: <afzender>
# <- server antwoordt met OK of niet
# -> de client verstuurt het commando rcpt to: <ontvanger>
# <- server antwoordt met OK of niet
# -> de client verstuurt het commando data
# <- server antwoordt met OK of niet
# -> de client verstuurt alle regels van zijn bericht en sluit af met een regel die alleen het teken bevat.
# <- server antwoordt met OK of niet
# -> de client verstuurt het commando ‘quit’
# <- de server antwoordt met OK of niet

# de antwoorden van de server hebben de vorm xxx tekst, waarbij xxx een getal van 3 cijfers is. Elk getal xxx >=500
# duidt op een fout. Het antwoord kan uit meerdere regels bestaan die allemaal beginnen met xxx-, behalve de laatste
# in de vorm xxx(spatie)

# de uitgewisselde tekstregels moeten eindigen met de tekens RC(#13) en LF(#10)

# # de parameters voor het verzenden van de e-mail
MAIL="mail2.txt" 
# de e-mailinstellingen worden opgehaald
res=getInfos(MAIL)
# fout?
if res[0]:
      print "%s" % (erreur)
      sys.exit()
# e-mail verzenden in logboekmodus
(smtpServer,expediteur,destinataire,message)=res[1:]
print "Envoi du message [%s,%s,%s]" % (smtpServer,expediteur,destinataire)
resultat=sendmail(smtpServer,expediteur,destinataire,message,True)
print "Resultat de l'envoi : %s" % (resultat)
# einde
sys.exit()

Opmerkingen:

  • dit script is identiek aan het vorige, op de functie sendmail na. Deze functie maakt nu gebruik van de functionaliteiten van de module [smtplib] (regel 3).

Resultaten

Het bestand mail2.txt:

smtp.univ-angers.fr, serge.tahe@univ-angers.fr , serge.tahe@univ-angers.fr
From: serge.tahe@univ-angers.fr
To: serge.tahe@univ-angers.fr
Subject: test

ligne1
ligne2

ligne3

De schermresultaten:

Envoi du message [smtp.univ-angers.fr,serge.tahe@univ-angers.fr,serge.tahe@univ-
angers.fr]
send: 'ehlo Gportpers3.ad.univ-angers.fr\r\n'
reply: '250-smtp.univ-angers.fr\r\n'
reply: '250-PIPELINING\r\n'
reply: '250-SIZE 20480000\r\n'
reply: '250-VRFY\r\n'
reply: '250-ETRN\r\n'
reply: '250-ENHANCEDSTATUSCODES\r\n'
reply: '250-8BITMIME\r\n'
reply: '250 DSN\r\n'
reply: retcode (250); Msg: smtp.univ-angers.fr
PIPELINING
SIZE 20480000
VRFY
ETRN
ENHANCEDSTATUSCODES
8BITMIME
DSN
send: 'mail FROM:<serge.tahe@univ-angers.fr> size=99\r\n'
reply: '250 2.1.0 Ok\r\n'
reply: retcode (250); Msg: 2.1.0 Ok
send: 'rcpt TO:<serge.tahe@univ-angers.fr>\r\n'
reply: '250 2.1.5 Ok\r\n'
reply: retcode (250); Msg: 2.1.5 Ok
send: 'data\r\n'
reply: '354 End data with <CR><LF>.<CR><LF>\r\n'
reply: retcode (354); Msg: End data with <CR><LF>.<CR><LF>
data: (354, 'End data with <CR><LF>.<CR><LF>')
send: 'From: serge.tahe@univ-angers.fr\r\nTo: serge.tahe@univ-angers.fr\r\nSubje
ct: test\r\n\r\nligne1\r\nligne2\r\n\r\nligne3\r\n.\r\n'
reply: '250 2.0.0 Ok: queued as D4977114358\r\n'
reply: retcode (250); Msg: 2.0.0 Ok: queued as D4977114358
data: (250, '2.0.0 Ok: queued as D4977114358')
send: 'quit\r\n'
reply: '221 2.0.0 Bye\r\n'
reply: retcode (221); Msg: 2.0.0 Bye
Resultat de l'envoi : Message envoye

11.5. Echo-client/server

We maken een echo-service aan. De server stuurt alle tekstregels die de client naar hem stuurt in hoofdletters terug. De service kan dankzij het gebruik van threads meerdere clients tegelijk bedienen.


Het serverprogramma (inet_08)


# -*- coding=utf-8 -*-

# generieke TCP-server op Windows

# headerbestanden laden
import re,sys,SocketServer,threading

# multithreaded TCP-server
class ThreadedTCPRequestHandler(SocketServer.StreamRequestHandler):

  def handle(self):
    # huidige thread
    cur_thread = threading.currentThread()
    # gegevens van de client
    self.data="on"
    # stoppen bij lege tekenreeks
    while self.data:
      # de tekstregels van de client worden gelezen met de readline-methode
      self.data =self.rfile.readline().strip()
      # console-uitvoer
      print "client %s : %s (%s)" % (self.client_address[0],self.data,cur_thread.getName())
      # antwoord naar de klant verzenden
      response = "%s: %s" % (cur_thread.getName(), self.data.upper())
      # self.wfile is de schrijfstroom naar de client
      self.wfile.write(response)                    

class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass

# ------------------------------------------------------ main        
# aanroepsyntaxis: argv[0] poort
# de server wordt gestart op de poort met de naam

# Gegevens
syntaxe="syntaxe : %s port" % (sys.argv[0])

#------------------------------------- de aanroep wordt gecontroleerd
# er moet één enkel argument zijn
nbArguments=len(sys.argv)
if nbArguments!=2:
    print syntaxe
    sys.exit(1)

# de poort moet een getal zijn
port=sys.argv[1]
modele=r"^\s*\d+\s*$"
if not re.match(modele,port):
    print "Le port %s n'est pas un nombre entier positif\n" % (port)
    sys.exit(2)

# de server wordt gestart – bedient elke client in een eigen thread
host="localhost"
server = ThreadedTCPServer((host,int(port)), ThreadedTCPRequestHandler)

# de server wordt in een thread gestart
# elke client wordt bediend in een extra thread
server_thread = threading.Thread(target=server.serve_forever)
# start de server – oneindige lus om op clients te wachten
server_thread.start()
# vervolg
print "Serveur d'echo a l'ecoute sur le port % s" % (port)

Opmerkingen:

  • regel 39: sys.argv staat voor de parameters van het script. Deze moeten hier de volgende vorm hebben: nom_du_script poort. Er moeten er dus twee zijn. sys.argv[0] wordt dan nom_du_script en sys.argv[1] wordt port ;
  • regel 53: de echo-server is een instantie van de klasse ThreadedTcpServer. De constructor van deze klasse verwacht twee parameters:
    • parameter 1: een tuple met twee elementen (host, port) die de machine en de luisterpoort van de server vastlegt;
    • parameter 2: de naam van de klasse die verantwoordelijk is voor het verwerken van verzoeken van een client.
  • regel 57: er wordt een thread aangemaakt (maar nog niet gestart). Deze thread voert de methode [serve_forever] van de server TCP uit. Deze methode is een lus die luistert naar verbindingen van klanten. Zodra een clientverbinding wordt gedetecteerd, wordt een instantie van de klasse ThreadedTCPRequestHandler aangemaakt. De methode handle van deze klasse is verantwoordelijk voor de communicatie met de client;
  • regel 59: de thread van de echo-service wordt gestart. Vanaf dat moment kunnen clients verbinding maken met de service;
  • regel 27: de klasse van de echo-server. Deze is afgeleid van twee klassen: SocketServer.ThreadingMixIn en SocketServer.TCPServer. Dit maakt het een multithreaded TCP-server: de server draait in één thread en elke klant wordt in een extra thread bediend;
  • regel 9: de klasse die de verzoeken van de clients verwerkt. Deze is afgeleid van de klasse SocketServer.StreamRequestHandler. Daardoor erft hij twee attributen:
    • rfile: dit is de leesstroom van de door de client verzonden gegevens – kan worden behandeld als een tekstbestand;
    • wfile: dit is de schrijfstroom waarmee gegevens naar de klant kunnen worden verzonden – kan worden behandeld als een tekstbestand.
  • regel 11: de methode handle verwerkt de verzoeken van de klanten;
  • regel 13: de thread die deze methode uitvoert;
  • regel 17: lus voor het verwerken van verzoeken van de klant. De lus eindigt wanneer de klant een lege regel verstuurt;
  • regel 19: het lezen van het verzoek van de klant;
  • regel 21: self.client_address[0] vertegenwoordigt het adres IP van de klant. cur_thread.getName() is de naam van de thread die de methode handle uitvoert;
  • regel 23: het antwoord aan de klant bestaat uit twee onderdelen: de naam van de thread die de klant bedient en het commando dat de klant heeft verzonden, weergegeven in hoofdletters.

Het programma van de klant (inet_06)


# -*- coding=utf-8 -*-
import re,sys,socket

# ------------------------------------------------------ main        
# generieke TCP-client
# aanroepsyntaxis: argv[0] host poort
# de client maakt verbinding met de echo-service (host, poort)
# de server stuurt de door de klant ingevoerde regels terug

# syntaxis
syntaxe="syntaxe : %s hote port" % (sys.argv[0])

#------------------------------------- de aanroep wordt gecontroleerd
# er moeten twee argumenten zijn
nbArguments=len(sys.argv)
if nbArguments!=3:
    print syntaxe
    sys.exit(1)

# de argumenten worden opgehaald
hote=sys.argv[1]
# de poort moet een getal zijn
port=sys.argv[2]
modele=r"^\s*\d+\s*$"
if not re.match(modele,port):
    print "Le port %s foit être un nombre entier positif" % (port)
    sys.exit(2)

try:
  # verbinding van de client met de server
  connexion=socket.create_connection((hote,int(port)))
except socket.error, erreur:
  print "Echec de la connexion au site (%s,%s) : %s" % (hote,port,erreur)
  sys.exit(3)
  
try:
    # invoerlus
    ligne=raw_input("Commande (rien pour arreter): ").strip()
    while ligne!="":
        # de regel wordt naar de server verzonden
        connexion.send("%s\n" % (ligne))
        # we wachten op het antwoord
        reponse=connexion.recv(1000)
        print "<-- %s" % (reponse)
        ligne=raw_input("Commande (rien pour arreter): ")
except socket.error, erreur:
    print "Echec de la connexion au site (%s,%s) : %s" % (hote,port,erreur)
    sys.exit(3)
finally:
    # de verbinding wordt verbroken
    connexion.close()

De resultaten

De server wordt gestart in een eerste opdrachtvenster:

cmd>%python% inet_08.py 100
Serveur d'echo a l'ecoute sur le port 100

Een eerste client wordt gestart in een tweede venster:

1
2
3
4
5
6
7
cmd>%python% inet_06.py localhost 100
Commande (rien pour arreter): cmde 1 du client 1
[cmde 1 du client 1]
<-- Thread-2: CMDE 1 DU CLIENT 1
Commande (rien pour arreter): cmde 2 du client 1
<-- Thread-2: CMDE 2 DU CLIENT 1
Commande (rien pour arreter):

De client ontvangt inderdaad als antwoord op het commando dat hij naar de server stuurt, datzelfde commando in hoofdletters. Een tweede client wordt gestart in een derde venster:

1
2
3
4
5
6
cmd>%python% inet_06.py localhost 100
Commande (rien pour arreter): cmde 1 du client 2
<-- Thread-3: CMDE 1 DU CLIENT 2
Commande (rien pour arreter): cmde 2 du client 2
<-- Thread-3: CMDE 2 DU CLIENT 2
Commande (rien pour arreter):

De serverconsole ziet er dan als volgt uit:

1
2
3
4
5
6
cmd>%python% inet_08.py 100
Serveur d'echo a l'ecoute sur le port 100
client 127.0.0.1 : cmde 1 du client 1 (Thread-2)
client 127.0.0.1 : cmde 2 du client 1 (Thread-2)
client 127.0.0.1 : cmde 1 du client 2 (Thread-3)
client 127.0.0.1 : cmde 2 du client 2 (Thread-3)

De clients worden inderdaad in verschillende threads bediend. Om een client te stoppen, volstaat het om een leeg commando in te voeren.

11.6. Generieke TCP-server

We stellen voor om een Python-script te schrijven dat

  • een TCP-server is die één client tegelijk kan bedienen,
  • die door de client verzonden tekstregels accepteert,
  • en tekstregels van het toetsenbord accepteert die als antwoord naar de client worden verzonden.

Zo fungeert de gebruiker achter het toetsenbord als server:

  • hij ziet op zijn console de door de client verzonden tekstregels;
  • hij antwoordt door het antwoord op het toetsenbord in te typen.

Zo kan hij zich aanpassen aan elk type client. Daarom noemen we hem een "generieke TCP-server". Het is een handig hulpmiddel om TCP-communicatieprotocollen te verkennen. In het volgende voorbeeld is de TCP-client een webbrowser, waardoor we het HTTP-protocol kunnen verkennen dat door webclients wordt gebruikt.


Het programma (inet_10)


# -*- coding=utf-8 -*-

# generieke TCP-server

# het laden van de headerbestanden
import re,sys,SocketServer,threading

# generieke TCP-server
class MyTCPHandler(SocketServer.StreamRequestHandler):
    def handle(self):
        # de client wordt weergegeven
        print "client %s" % (self.client_address[0])
        # er wordt een thread aangemaakt om de opdrachten van de client te lezen
        thread_lecture = threading.Thread(target=self.lecture)
        thread_lecture.start()
        # stoppen bij het commando 'bye'
        # lezen van het via het toetsenbord ingevoerde commando
        cmde=raw_input("--> ")
        while cmde!="bye":
            # commando naar de client verzenden. self.wfile is de schrijfstroom naar de client
            self.wfile.write("%s\n" % (cmde))
            # volgende opdracht lezen
            cmde=raw_input("--> ")

    def lecture(self):
        # alle door de client verzonden regels worden weergegeven totdat het commando 'bye' wordt ontvangen
        ligne=""
        while ligne!="bye":
            # de tekstregels van de client worden gelezen met de methode readline
            ligne = self.rfile.readline().strip()
            # console-log
            print "<--- %s : %s" % (self.client_address[0], ligne)
    
# ------------------------------------------------------ main        
# aanroepsyntaxis: argv[0] poort
# de server wordt gestart op de opgegeven poort

# Gegevens
syntaxe="syntaxe : %s port" % (sys.argv[0])

#------------------------------------- de aanroep wordt gecontroleerd
# er moet één enkel argument zijn
nbArguments=len(sys.argv)
if nbArguments!=2:
    print syntaxe
    sys.exit(1)

# de poort moet een getal zijn
port=sys.argv[1]
modele=r"^\s*\d+\s*$"
if not re.match(modele,port):
    print "Le port %s n'est pas un nombre entier positif\n" % (port)
    sys.exit(2)

# de server wordt gestart
host="localhost"
server = SocketServer.TCPServer((host, int(port)), MyTCPHandler)
print "Service tcp generique lance sur le port %s. Arret par Ctrl-C" % (port)
server.serve_forever()

Opmerkingen:

  • regel 57: de TCP-server is een instantie van de klasse SocketServer.TCPServer. De constructor hiervan accepteert twee parameters:
    • de eerste parameter is een tuple met twee elementen (host, poort), waarbij host de machine is waarop de service draait (meestal localhost) en port, de poort waarop de dienst verzoeken van clients verwacht (luistert);
    • de tweede parameter specificeert de serviceklasse van een client. Wanneer een client verbinding maakt, wordt een instantie van de serviceklasse aangemaakt en is het de methode handle die de verbinding met de client moet beheren.

De TCP-server SocketServer.TCPServer is niet multithreaded. Hij bedient één client tegelijk;

  • regel 59: de methode serve_forever van de TCP-server wordt uitgevoerd. Dit is een eindeloze lus waarin op clients wordt gewacht;
  • regel 9: de TCP-server is hier een klasse die is afgeleid van de klasse SocketServer.StreamRequestHandler. Hierdoor kunnen de gegevensstromen die met de client worden uitgewisseld, worden beschouwd als tekstbestanden. We zijn deze klasse al eerder tegengekomen. We beschikken over de methoden:
    • readline om een tekstregel van de client te lezen;
    • write om tekstregels naar de klant terug te sturen.
  • regel 12: self.client_address[0] is het IP-adres van de klant;
  • regel 14: de TCP-server communiceert met de client via twee threads
    • een thread voor het lezen van de regels van de klant;
    • een thread voor het schrijven van regels naar de client.
  • regel 14: de thread voor het lezen van de regels van de client wordt aangemaakt. De parameter target bepaalt de methode die door de thread wordt uitgevoerd. Dit is de methode die op regel 25 is gedefinieerd;
  • regel 25: de lees-thread wordt gestart;
  • regels 25-32: de methode die door de lees-thread wordt uitgevoerd;
  • regel 28: de leesthread leest alle door de client verzonden tekstregels totdat de regel "bye" wordt ontvangen;
  • regel 18: we bevinden ons hier in de schrijfthread naar de client. Het principe is om alle tekstregels die de gebruiker via het toetsenbord invoert naar de client te verzenden.

De resultaten

De server

dos>%python% inet_10.py 100
Service tcp generique lance sur le port 100. Arret par Ctrl-C

De browser van de client

 

Het verzoek dat door de server is ontvangen

dos>%python% inet_10.py 100
Service tcp generique lance sur le port 100. Arret par Ctrl-C
client 127.0.0.1
<--- 127.0.0.1 : GET / HTTP/1.1
<--> --- 127.0.0.1 : Host: localhost:100
<--- 127.0.0.1 : User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 GTB7.1 ( .NET CLR 3.5.30729)
<--- 127.0.0.1 : Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0,8
<--- 127.0.0.1 : Accept-Language: fr,fr-fr;q=0.8,en;q=0.5,en-us;q=0.3
<--- 127.0.0.1 : Accept-Encoding: gzip,deflate
<--- 127.0.0.1 : Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
<--- 127.0.0.1 : Keep-Alive: 115
<--- 127.0.0.1 : Connection: keep-alive
<--- 127.0.0.1 : Cache-Control: max-age=0
<--- 127.0.0.1 :

Het antwoord van de server, door de gebruiker via het toetsenbord ingevoerd (zonder het teken -->)

1
2
3
4
5
6
7
HTTP/1.1 200 OK
--> Server: serveur tcp generique
--> Connection: close
--> Content-Type: text/html
-->
--> <html><body><h2>Serveur tcp generique</h2></body></html>
--> bye
  • regels 1-5: HTTP-antwoord verzonden naar de client;
  • regel 6: de HTML-pagina die naar de klant is verzonden;
  • regel 7: einde van de dialoog met de klant. De klantenservice wordt beëindigd en de verbinding wordt verbroken, waardoor de thread die de door de klant verzonden tekstregels leest, abrupt wordt onderbroken;
  • de regels 1-5 van het HTTP-antwoord aan de klant hebben de volgende betekenis:
  • regel 1: de door de klant opgevraagde bron is gevonden;
  • regel 2: identificatie van de server;
  • regel 3: de server zal de verbinding verbreken na het verzenden van de bron;
  • regel 4: aard van de door de server verzonden bron: een HTML-document;
  • regel 5: een lege regel.

De pagina die door de browser [1] wordt weergegeven:

Als we de broncode weergeven die door de browser [2] is ontvangen, zien we dat de code HTML die door de webbrowser is ontvangen, inderdaad dezelfde is als die we naar de browser hadden verzonden.