Skip to content

11. Funkcje sieciowe w języku Python

Przechodzimy teraz do funkcji sieciowych języka Python, które pozwalają nam programować w protokołach TCP / IP (Transfer Control Protocol / Internet Protocol).

  

11.1. Pobieranie nazwy lub adresu IP komputera w Internecie


Program (inet_01)


import sys, socket

#------------------------------------------------
def getIPandName(nomMachine):
    #nomMachine: nazwa komputera, którego adres chcemy uzyskać 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


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

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

# adresy IP maszyn z HOTES
for i in range(len(HOTES)):
    getIPandName(HOTES[i])
# koniec
sys.exit()

Uwagi:

  • wiersz 1: funkcje sieciowe języka Python są zawarte w module socket.

Wyniki

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. Klient internetowy

Skrypt umożliwiający pobranie treści akapitu indeksowego ze strony internetowej.


Program (inet_02)


import sys,socket

  #-----------------------------------------------------------------------
def getIndex(site):
    # odczytuje stronę URL i zapisuje ją w pliku site.html
  
    # początkowo bez błędów
    erreur=""
    # utworzenie pliku site.html
    try:
        html=open("%s.html" % (site),"w")
    except IOError, erreur:
        pass
    # błąd?
    if erreur:
        return "Erreur (%s) lors de la création du fichier %s.html" % (erreur,site)

    # nawiązanie połączenia na porcie 80 serwisu
    try:
        connexion=socket.create_connection((site,80))
    except socket.error, erreur:
        pass
    # powrót w przypadku błędu
    if erreur:
        return "Echec de la connexion au site (%s,80) : %s" % (site,erreur)

    # Połączenie stanowi dwukierunkowy strumień komunikacji
    # między klientem (tym programem) a serwerem WWW, z którym nawiązano połączenie
    # kanał ten służy do wymiany poleceń i informacji
    # protokołem komunikacji jest HTTP

    # klient wysyła polecenie GET, aby zażądać pliku URL /
    # składnia polecenia „get”: URL HTTP/1.0
    # nagłówki (headers) protokołu HTTP muszą kończyć się pustym wierszem
    connexion.send("GET / HTTP/1.0\n\n")

    # serwer odpowie teraz na kanale połączenia. Wyśle wszystkie
    # swoje dane, a następnie zamknie kanał. Klient odczytuje wszystko, co przychodzi z połączenia
    # aż do zamknięcia kanału
    ligne=connexion.recv(1000)
    while(ligne):
        html.write(ligne)
        ligne=connexion.recv(1000)

    # klient z kolei zamyka połączenie
    connexion.close()
    # zamknięcie pliku HTML
    html.close()
    # powrót
    return "Transfert reussi du paragraphe index du site %s" % (site)

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

# pobieranie tekstu HTML z URL

# lista stron internetowych
SITES=("istia.univ-angers.fr","www.univ-angers.fr","www.ibm.com","xx")

# odczyt stron indeksowych witryn z tabeli SITES
for i in range(len(SITES)):
      # odczyt strony indeksowej serwisu SITES[i]
      resultat=getIndex(SITES[i])
      # wyświetlanie wyniku
      print resultat

# koniec
sys.exit()

Uwagi:

  • wiersz 57: lista adresów URL stron internetowych, z których chcemy pobrać stronę główną. Lista ta jest zapisana w pliku tekstowym [nomsite.html];
  • wiersz 62: zadanie to realizuje funkcja getIndex;
  • wiersz 4: funkcja getIndex;
  • wiersz 20: metoda create_connection((site,port)) pozwala na nawiązanie połączenia z usługą TCP / IP działającą na porcie port na komputerze site;
  • wiersz 35: metoda send umożliwia wysyłanie danych poprzez połączenie TCP / IP. W tym przypadku wysyłany jest tekst. Tekst ten jest zgodny z protokołem HTTP (HyperText Transfer Protocol);
  • wiersz 40: metoda recv umożliwia odbieranie danych poprzez połączenie TCP / IP. W tym przypadku odpowiedź serwera WWW jest odczytywana w blokach po 1000 znaków i zapisywana w pliku tekstowym [nomsite.html].

Wyniki

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

Plik otrzymany dla strony [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>
  • wiersze 1–11 to nagłówki HTTP z odpowiedzi serwera;
  • wiersz 1: serwer prosi klienta o przekierowanie do adresu URL podanego w wierszu 8;
  • wiersz 2: data i godzina odpowiedzi;
  • wiersz 3: identyfikator serwera WWW;
  • wiersz 4: treść wysłana przez serwer. W tym przypadku jest to strona HTML, która zaczyna się w wierszu 13;
  • wiersz 12: pusty wiersz kończący nagłówki HTTP;
  • wiersze 13–19: strona HTML wysłana przez serwer WWW.

11.3. Klient SMTP

Spośród protokołów TCP / IP, SMTP (SendMail Transfer Protocol) jest protokołem komunikacyjnym usługi wysyłania wiadomości.


Program (inet_03)


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

import sys,socket

#-----------------------------------------------------------------------
def getInfos(fichier):
    # zwraca informacje (SMTP, nadawca, odbiorca, treść wiadomości) pobrane z pliku tekstowego [fichier]
    # wiersz 1: SMTP, nadawca, odbiorca
    # kolejne wiersze: treść wiadomości

    # otwarcie pliku [fichier]
    erreur=""
    try:
        infos=open(fichier,"r")
    except IOError, erreur:
        return ("Le fichier %s n'a pu etre ouvert en lecture : %s" % (fichier, erreur))
  
    # odczyt pierwszego wiersza
    ligne=infos.readline()
    # usunięcie znaku końca wiersza
    ligne=cutNewLineChar(ligne)
    # pobieranie pól smtp, nadawca, odbiorca
    champs=ligne.split(",")
    # czy liczba pól jest prawidłowa?
    if len(champs)!=3 :
        return ("La ligne 1 du fichier %s (serveur smtp, expediteur, destinataire) a un nombre de champs incorrect" % (fichier))
    # „przetwarzanie” pobranych informacji
    # usuwanie „spacji” poprzedzających lub następujących po informacji użytkowej z każdego z trzech pól
    for i in range(3):
        champs[i]=champs[i].strip()
    # pobieranie pól
    (smtpServer,expediteur,destinataire)=champs
    message=""
    # odczyt pozostałej części wiadomości
    ligne=infos.readline()
    while ligne!='':
        message+=ligne
        ligne=infos.readline()
    infos.close()
    # powrót
    return ("",smtpServer,expediteur,destinataire,message)

#-----------------------------------------------------------------------
def sendmail(smtpServer,expediteur,destinataire,message,verbose):
    # wysyła wiadomość na serwer SMTP smtpserver w imieniu nadawcy
    # dla odbiorcy. Jeśli verbose=True, śledzi wymianę danych między klientem a serwerem

    # pobierana jest nazwa klienta
    try:
        client=socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
    except socket.error, erreur:
        return "Erreur IP / Nom du client : %s" % (erreur)
    
    # otwarcie połączenia na porcie 25 serwera smtpServer
    try:
        connexion=socket.create_connection((smtpServer,25))
    except socket.error, erreur:
        return "Echec de la connexion au site (%s,25) : %s" % (smtpServer,erreur)

    # Połączenie stanowi dwukierunkowy strumień komunikacji
    # między klientem (tym programem) a serwerem SMTP, z którym nawiązano połączenie
    # kanał ten służy do wymiany poleceń i informacji

    # po nawiązaniu połączenia serwer wysyła wiadomość powitalną, którą odczytujemy
    erreur=sendCommand(connexion,"",verbose,1)
    if(erreur) :
        connexion.close()
        return erreur
    # polecenie „ehlo”:
    erreur=sendCommand(connexion,"EHLO %s" % (client),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # polecenie mail from:
    erreur=sendCommand(connexion,"MAIL FROM: <%s>" % (expediteur),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # polecenie rcpt to:
    erreur=sendCommand(connexion,"RCPT TO: <%s>" % (destinataire),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # polecenie data
    erreur=sendCommand(connexion,"DATA",verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # przygotowanie wiadomości do wysłania
    # musi zawierać następujące wiersze
    # From: nadawca
    # Do: odbiorca
    # pusta linia
    # Wiadomość
    # .
    data="From: %s\r\nTo: %s\r\n%s\r\n.\r\n" % (expediteur,destinataire,message)
    # wysłanie wiadomości
    erreur=sendCommand(connexion,data,verbose,0)
    if erreur :
        connexion.close()
        return erreur
    # polecenie quit
    erreur=sendCommand(connexion,"QUIT",verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # koniec
    connexion.close()
    return "Message envoye"

# --------------------------------------------------------------------------
def sendCommand(connexion,commande,verbose,withRCLF):
    # wysyła polecenie na kanale połączenia
    # tryb szczegółowy, jeśli verbose=1
    # jeśli withRCLF=1, dodaj sekwencję RCLF do polecenia

    # dane
    RCLF="\r\n" if withRCLF else ""
    # wysyłanie polecenia, jeśli polecenie nie jest puste
    if commande:
        connexion.send("%s%s" % (commande,RCLF))
        # ewentualne echo
        if verbose:
            affiche(commande,1)
        # odczyt odpowiedzi o długości mniejszej niż 1000 znaków
        reponse=connexion.recv(1000)
        # ewentualne echo
        if verbose:
            affiche(reponse,2)
        # pobieranie kodu błędu
        codeErreur=reponse[0:3]
        # błąd zwrócony przez serwer?
        if int(codeErreur) >=500:
            return reponse[4:]
        # powrót bez błędu
        return ""

# --------------------------------------------------------------------------
def affiche(echange,sens):
    # wyświetla wymianę na ekranie?
    # jeśli sens=1, wyświetla -->wymiana
    # jeśli sens=2, wyświetla <-- wymiana bez dwóch ostatnich znaków RCLF
    if sens==1:
        print "--> [%s]" % (echange)
        return
    elif sens==2:
        l=len(echange)
        print "<-- [%s]" % echange[0:l-2]
        return

      
# --------------------------------------------------------------------------
def cutNewLineChar(ligne):
    # usuwa się znak końca linii z [ligne], jeśli istnieje
    l=len(ligne)
    while(ligne[l-1]=="\n" or ligne[l-1]=="\r"):
        l-=1
    return(ligne[0:l])

# main ----------------------------------------------------------------
  
# klient SMTP (protokół transferu SendMail) umożliwiający wysłanie wiadomości
# informacje są pobierane z pliku INFOS zawierającego następujące wiersze
# wiersz 1: smtp, nadawca, odbiorca
# kolejne wiersze: treść wiadomości

# nadawca: adres e-mail nadawcy
# odbiorca: adres e-mail odbiorcy
# SMTP: nazwa serwera SMTP, z którego należy korzystać


# protokół komunikacji SMTP klient-serwer
# -> klient łączy się z serwerem SMTP na porcie 25
# <- serwer wysyła mu wiadomość powitalną
# -> klient wysyła polecenie EHLO: nazwa swojego komputera
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie mail from: <nadawca>
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie rcpt to: <odbiorca>
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie data
# <- serwer odpowiada OK lub nie
# -> klient wysyła wszystkie wiersze swojej wiadomości i kończy wierszem zawierającym tylko jeden znak .
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie quit
# <- serwer odpowiada OK lub nie

# odpowiedzi serwera mają postać xxx tekst, gdzie xxx to trzycyfrowa liczba. Każda liczba xxx >=500
# oznacza błąd. Odpowiedź może składać się z kilku wierszy, z których wszystkie oprócz ostatniego zaczynają się od xxx-
# w postaci xxx (spacja)

# wymieniane wiersze tekstu muszą kończyć się znakami RC(#13) i LF(#10)

# # parametry wysyłania wiadomości
MAIL="mail2.txt" 
# pobieramy parametry wiadomości
res=getInfos(MAIL)
# błąd?
if res[0]:
      print "%s" % (erreur)
      sys.exit()
# wysyłanie wiadomości w trybie szczegółowym
(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)
# koniec
sys.exit()

Uwagi:

  • na komputerze z systemem Windows wyposażonym w program antywirusowy, program ten może uniemożliwić skryptowi w języku Python połączenie się z portem 25 serwera SMTP. W takim przypadku należy wyłączyć program antywirusowy. Na przykład w przypadku McAfee można postępować w następujący sposób:
  • w [1] należy włączyć konsolę VirusScan
  • w przypadku [2] należy zatrzymać usługę [Protection lors de l'accès]
  • w przypadku [3] usługa jest zatrzymana

Wyniki

Plik infos.txt:

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

ligne1
ligne2

ligne3

Wyniki na ekranie:

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

Wiadomość wyświetlana w kliencie poczty Thunderbird:

  

11.4. Drugi klient SMTP

Ten drugi skrypt działa tak samo jak poprzedni, ale wykorzystuje funkcje modułu [smtplib].


Program (inet_04)


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

import sys,socket, smtplib

#-----------------------------------------------------------------------
def getInfos(fichier):
    # zwraca informacje (SMTP, nadawca, odbiorca, treść wiadomości) pobrane z pliku tekstowego [fichier]
    # wiersz 1: SMTP, nadawca, odbiorca
    # kolejne wiersze: treść wiadomości

    # otwarcie pliku [fichier]
    erreur=""
    try:
        infos=open(fichier,"r")
    except IOError, erreur:
        return ("Le fichier %s n'a pu etre ouvert en lecture : %s" % (fichier, erreur))
  
    # odczyt pierwszego wiersza
    ligne=infos.readline()
    # usunięcie znaku końca linii
    ligne=cutNewLineChar(ligne)
    # pobieranie pól smtp, nadawca, odbiorca
    champs=ligne.split(",")
    # czy liczba pól jest prawidłowa?
    if len(champs)!=3 :
        return ("La ligne 1 du fichier %s (serveur smtp, expediteur, destinataire) a un nombre de champs incorrect" % (fichier))
    # „przetwarzanie” pobranych informacji – usuwanie spacji poprzedzających lub następujących po nich
    for i in range(3):
        champs[i]=champs[i].strip()
    # pobieranie pól
    (smtpServer,expediteur,destinataire)=champs
    # odczyt wiadomości do wysłania
    message=""
    ligne=infos.readline()
    while ligne!='':
        message+=ligne
        ligne=infos.readline()
    infos.close()
    # powrót
    return ("",smtpServer,expediteur,destinataire,message)

#-----------------------------------------------------------------------
def sendmail(smtpServer,expediteur,destinataire,message,verbose):
    # wysyła wiadomość na serwer SMTP smtpserver w imieniu nadawcy
    # dla odbiorcy. Jeśli verbose=True, śledzi wymianę danych między klientem a serwerem

    # wykorzystuje się bibliotekę smtplib 
    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)
    # koniec
    return "Message envoye"
      
# --------------------------------------------------------------------------
def cutNewLineChar(ligne):
    # usuwa się znak końca linii z [ligne], jeśli istnieje
    l=len(ligne)
    while(ligne[l-1]=="\n" or ligne[l-1]=="\r"):
        l-=1
    return(ligne[0:l])

# main ----------------------------------------------------------------
  
# klient SMTP (protokół transferu SendMail) umożliwiający wysłanie wiadomości
# informacje są pobierane z pliku INFOS zawierającego następujące wiersze
# wiersz 1: smtp, nadawca, odbiorca
# kolejne wiersze: treść wiadomości

# nadawca: adres e-mail nadawcy
# odbiorca: adres e-mail odbiorcy
# SMTP: nazwa serwera SMTP, z którego należy korzystać


# protokół komunikacji klient-serwer SMTP
# -> klient łączy się z serwerem SMTP na porcie 25
# <- serwer wysyła mu wiadomość powitalną
# -> klient wysyła polecenie EHLO: nazwa swojego komputera
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie mail from: <nadawca>
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie rcpt to: <odbiorca>
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie data
# <- serwer odpowiada OK lub nie
# -> klient wysyła wszystkie wiersze swojej wiadomości i kończy wierszem zawierającym tylko jeden znak .
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie quit
# <- serwer odpowiada OK lub nie

# odpowiedzi serwera mają postać xxx tekst, gdzie xxx to trzycyfrowa liczba. Każda liczba xxx >=500
# oznacza błąd. Odpowiedź może składać się z kilku wierszy, z których wszystkie oprócz ostatniego zaczynają się od xxx-
# w postaci xxx (spacja)

# wymieniane wiersze tekstu muszą kończyć się znakami RC(#13) i LF(#10)

# # parametry wysyłania wiadomości
MAIL="mail2.txt" 
# pobieramy parametry wiadomości
res=getInfos(MAIL)
# błąd?
if res[0]:
      print "%s" % (erreur)
      sys.exit()
# wysyłanie wiadomości w trybie szczegółowym
(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)
# koniec
sys.exit()

Uwagi:

  • skrypt ten jest identyczny z poprzednim, z wyjątkiem funkcji sendmail. Funkcja ta korzysta teraz z możliwości modułu [smtplib] (wiersz 3).

Wyniki

Plik 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

Wyniki na ekranie:

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. Klient / serwer echa

Tworzymy usługę echo. Serwer zwraca wszystkie wiersze tekstu przesłane przez klienta, zamieniając je na wielkie litery. Usługa może obsługiwać wielu klientów jednocześnie dzięki wykorzystaniu wątków.


Program serwera (inet_08)


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

# ogólny serwer TCP w systemie Windows

# ładowanie plików nagłówkowych
import re,sys,SocketServer,threading

# wielowątkowy serwer TCP
class ThreadedTCPRequestHandler(SocketServer.StreamRequestHandler):

  def handle(self):
    # bieżący wątek
    cur_thread = threading.currentThread()
    # dane klienta
    self.data="on"
    # zatrzymanie na pustym łańcuchu
    while self.data:
      # wiersze tekstu od klienta są odczytywane za pomocą metody readline
      self.data =self.rfile.readline().strip()
      # monitorowanie konsoli
      print "client %s : %s (%s)" % (self.client_address[0],self.data,cur_thread.getName())
      # wysyłanie odpowiedzi do klienta
      response = "%s: %s" % (cur_thread.getName(), self.data.upper())
      # self.wfile to strumień zapisu do klienta
      self.wfile.write(response)                    

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

# ------------------------------------------------------ main        
# składnia wywołania: argv[0] port
# serwer uruchomiono na porcie o nazwie

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

#------------------------------------- sprawdzane jest wywołanie
# musi być jeden i tylko jeden argument
nbArguments=len(sys.argv)
if nbArguments!=2:
    print syntaxe
    sys.exit(1)

# port musi być liczbą
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)

# uruchomienie serwera – obsługuje każdego klienta w osobnym wątku
host="localhost"
server = ThreadedTCPServer((host,int(port)), ThreadedTCPRequestHandler)

# serwer jest uruchamiany w wątku
# każdy klient będzie obsługiwany w osobnym wątku
server_thread = threading.Thread(target=server.serve_forever)
# uruchamia serwer – nieskończona pętla oczekiwania na klientów
server_thread.start()
# monitorowanie
print "Serveur d'echo a l'ecoute sur le port % s" % (port)

Uwagi:

  • wiersz 39: sys.argv oznacza parametry skryptu. Muszą one mieć tutaj postać: nom_du_script port. Muszą więc występować w dwóch egzemplarzach. sys.argv[0] stanie się wówczas nom_du_script, a sys.argv[1] stanie się port ;
  • wiersz 53: serwer echo jest instancją klasy ThreadedTcpServer. Konstruktor tej klasy oczekuje dwóch parametrów:
    • parametr 1: krotka składająca się z dwóch elementów (host, port), która określa komputer i port nasłuchowy serwera;
    • parametr 2: nazwa klasy odpowiedzialnej za przetwarzanie żądań klienta.
  • wiersz 57: tworzony jest wątek (ale jeszcze nie uruchomiony). Wątek ten wykonuje metodę [serve_forever] serwera TCP. Metoda ta stanowi pętlę nasłuchującą na połączenia klienckie. Gdy tylko wykryte zostanie połączenie klienckie, utworzona zostanie instancja klasy ThreadedTCPRequestHandler. Jej metoda handle odpowiada za komunikację z klientem;
  • wiersz 59: uruchamiany jest wątek usługi echa. Od tego momentu klienci mogą łączyć się z usługą;
  • wiersz 27: klasa serwera echo. Wywodzi się ona z dwóch klas: SocketServer.ThreadingMixIn oraz SocketServer.TCPServer. To sprawia, że jest to serwer wielowątkowy typu TCP: serwer działa w jednym wątku, a każdy klient jest obsługiwany w dodatkowym wątku;
  • wiersz 9: klasa przetwarzająca żądania klientów. Wywodzi się z klasy SocketServer.StreamRequestHandler. Dziedziczy zatem dwa atrybuty:
    • rfile: strumień odczytu danych wysyłanych przez klienta – można go traktować jak plik tekstowy;
    • wfile: strumień zapisu umożliwiający wysyłanie danych do klienta – można go traktować jako plik tekstowy.
  • wiersz 11: metoda handle przetwarza żądania klientów;
  • wiersz 13: wątek, który wykonuje tę metodę;
  • wiersz 17: pętla przetwarzania żądań klienta. Pętla kończy się, gdy klient wyśle pusty wiersz;
  • wiersz 19: odczyt żądania klienta;
  • wiersz 21: self.client_address[0] oznacza adres klienta IP. cur_thread.getName() to nazwa wątku, który wykonuje metodę handle;
  • wiersz 23: odpowiedź dla klienta składa się z dwóch elementów – nazwy wątku obsługującego klienta oraz wysłanego przez niego polecenia, podanego wielkimi literami.

Program klienta (inet_06)


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

# ------------------------------------------------------ main        
# ogólny klient TCP
# składnia wywołania: argv[0] host port
# klient łączy się z usługą echo (host, port)
# serwer zwraca wiersze wpisane przez klienta

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

#------------------------------------- sprawdzanie wywołania
# muszą być dwa argumenty
nbArguments=len(sys.argv)
if nbArguments!=3:
    print syntaxe
    sys.exit(1)

# pobierane są argumenty
hote=sys.argv[1]
# port musi być liczbą
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:
  # połączenie klienta z serwerem
  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:
    # pętla wprowadzania danych
    ligne=raw_input("Commande (rien pour arreter): ").strip()
    while ligne!="":
        # wysyłanie wiersza do serwera
        connexion.send("%s\n" % (ligne))
        # oczekiwanie na odpowiedź
        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:
    # zamykanie połączenia
    connexion.close()

Wyniki

Serwer uruchamia się w pierwszym oknie poleceń:

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

Pierwszy klient uruchamia się w drugim oknie:

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):

Klient otrzymuje w odpowiedzi na polecenie wysłane do serwera to samo polecenie zapisane wielkimi literami. Drugi klient zostaje uruchomiony w trzecim oknie:

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):

Konsola serwera wygląda wówczas następująco:

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)

Klienci są obsługiwani w różnych wątkach. Aby zatrzymać klienta, wystarczy wpisać puste polecenie.

11.6. Ogólny serwer TCP

Proponujemy napisanie skryptu w języku Python, który

  • będzie serwerem TCP obsługującym jednego klienta na raz,
  • przyjmujący wiersze tekstowe wysyłane przez klienta,
  • przyjmował linie tekstu z klawiatury i wysyłał je w odpowiedzi do klienta.

W ten sposób to użytkownik korzystający z klawiatury pełni rolę serwera:

  • widzi na swojej konsoli wiersze tekstowe wysyłane przez klienta;
  • odpowiada mu, wpisując odpowiedź na klawiaturze.

W ten sposób może dostosować się do każdego rodzaju klienta. Dlatego nazwiemy go „ogólnym serwerem TCP”. Jest to praktyczne narzędzie do poznawania protokołów komunikacyjnych TCP. W poniższym przykładzie klientem TCP będzie przeglądarka internetowa, co pozwoli nam poznać protokół HTTP używany przez klientów internetowych.


Program (inet_10)


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

# ogólny serwer TCP

# ładowanie plików nagłówkowych
import re,sys,SocketServer,threading

# ogólny serwer TCP
class MyTCPHandler(SocketServer.StreamRequestHandler):
    def handle(self):
        # wyświetlanie klienta
        print "client %s" % (self.client_address[0])
        # tworzenie wątku do odczytu poleceń klienta
        thread_lecture = threading.Thread(target=self.lecture)
        thread_lecture.start()
        # zatrzymanie po komendzie „bye”
        # odczyt polecenia wpisanego z klawiatury
        cmde=raw_input("--> ")
        while cmde!="bye":
            # wysyłanie polecenia do klienta. self.wfile to strumień zapisu do klienta
            self.wfile.write("%s\n" % (cmde))
            # odczyt kolejnego polecenia
            cmde=raw_input("--> ")

    def lecture(self):
        # wyświetlane są wszystkie wiersze wysłane przez klienta do momentu otrzymania polecenia „bye”
        ligne=""
        while ligne!="bye":
            # wiersze tekstowe od klienta są odczytywane za pomocą metody readline
            ligne = self.rfile.readline().strip()
            # monitorowanie konsoli
            print "<--- %s : %s" % (self.client_address[0], ligne)
    
# ------------------------------------------------------ main        
# składnia wywołania: argv[0] port
# serwer uruchamia się na porcie o nazwie

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

#------------------------------------- sprawdzane jest wywołanie
# musi być jeden i tylko jeden argument
nbArguments=len(sys.argv)
if nbArguments!=2:
    print syntaxe
    sys.exit(1)

# port musi być liczbą
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)

# uruchomienie serwera
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()

Uwagi:

  • wiersz 57: serwer TCP będzie instancją klasy SocketServer.TCPServer. Jej konstruktor przyjmuje dwa parametry:
    • pierwszy parametr to krotka składająca się z dwóch elementów (host, port), gdzie host to komputer, na którym działa usługa (zazwyczaj localhost), a port to port, na którym usługa oczekuje (nasłuchuje) na żądania klientów;
    • drugi parametr określa klasę usługi klienta. Gdy klient nawiązuje połączenie, tworzona jest instancja klasy usługi i to jej metoda handle musi zarządzać połączeniem z klientem.

Serwer TCP SocketServer.TCPServer nie jest wielowątkowy. Obsługuje tylko jednego klienta na raz;

  • wiersz 59: wykonywana jest metoda serve_forever serwera TCP. Jest to pętla bez końca, w której serwer oczekuje na klientów;
  • wiersz 9: serwer TCP jest tutaj klasą wywodzącą się z klasy SocketServer.StreamRequestHandler. Pozwala to traktować strumienie danych wymienianych z klientem jako pliki tekstowe. Z tą klasą mieliśmy już do czynienia. Dysponujemy następującymi metodami:
    • readline do odczytu linii tekstu od klienta;
    • write do wysyłania mu w zamian wierszy tekstu.
  • wiersz 12: self.client_address[0] to adres IP klienta;
  • wiersz 14: serwer TCP będzie komunikował się z klientem za pomocą dwóch wątków
    • jednego wątku do odczytu wierszy od klienta;
    • wątku do zapisywania wierszy dla klienta.
  • wiersz 14: tworzony jest wątek odczytu wierszy od klienta. Jego parametr target określa metodę wykonywaną przez ten wątek. Jest to metoda zdefiniowana w wierszu 25;
  • wiersz 25: uruchamiany jest wątek odczytu;
  • wiersze 25–32: metoda wykonywana przez wątek odczytu;
  • wiersz 28: wątek odczytu odczytuje wszystkie wiersze tekstu wysłane przez klienta aż do momentu odebrania wiersza „bye”;
  • wiersz 18: znajdujemy się tutaj w wątku pisania do klienta. Zasadą jest wysyłanie do klienta wszystkich wierszy tekstu wpisanych przez użytkownika na klawiaturze.

Wyniki

Serwer

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

Przeglądarka klienta

 

Żądanie otrzymane przez serwer

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 :

Odpowiedź serwera wpisana przez użytkownika na klawiaturze (bez znaku -->)

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
  • wiersze 1–5: odpowiedź HTTP wysłana do klienta;
  • wiersz 6: strona HTML wysłana do klienta;
  • wiersz 7: koniec dialogu z klientem. Obsługa klienta zostanie zakończona, a połączenie zamknięte, co spowoduje nagłe przerwanie wątku odczytu wierszy tekstu wysłanych przez klienta;
  • wiersze 1–5 odpowiedzi HTTP wysłanej do klienta mają następujące znaczenie:
  • wiersz 1: zasób żądany przez klienta został znaleziony;
  • wiersz 2: identyfikacja serwera;
  • wiersz 3: serwer zamknie połączenie po wysłaniu zasobu;
  • wiersz 4: rodzaj zasobu wysłanego przez serwer: dokument HTML;
  • wiersz 5: pusty wiersz.

Strona wyświetlana przez przeglądarkę [1]:

Jeśli wyświetlimy kod źródłowy odebrany przez przeglądarkę [2], zauważymy, że kod HTML odebrany przez przeglądarkę internetową jest rzeczywiście tym, który został do niej wysłany.