Skip to content

11. Las funciones de red de Python

Ahora abordaremos las funciones de red de Python que nos permiten programar TCP / IP (Protocolo de control de transferencia / Protocolo de Internet).

  

11.1. Obtener el nombre o la dirección IP de una máquina en Internet


Programa (inet_01)


import sys, socket

#------------------------------------------------
def getIPandName(nomMachine):
    #nomMachine: nombre de la máquina cuya dirección se desea obtener IP
    # nomMachine-->dirección IP
    try:
        ip=socket.gethostbyname(nomMachine)
        print "ip[%s]=%s" % (nomMachine,ip)
    except socket.error, erreur:
        print "ip[%s]=%s" % (nomMachine,erreur)
        return

    # dirección 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


# ---------------------------------------- mano

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

# direcciones IP de las máquinas de HOTES
for i in range(len(HOTES)):
    getIPandName(HOTES[i])
# fin
sys.exit()

Notas:

  • línea 1: las funciones de red de Python están encapsuladas en el módulo socket.

Resultados

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. Un cliente web

Un script que permite obtener el contenido del párrafo «index» de un sitio web.


Programa (inet_02)


import sys,socket

  #-----------------------------------------------------------------------
def getIndex(site):
    # lee el sitio URL y lo guarda en el archivo site.html
  
    # al principio no hay error
    erreur=""
    # creación del archivo site.html
    try:
        html=open("%s.html" % (site),"w")
    except IOError, erreur:
        pass
    # ¿Error?
    if erreur:
        return "Erreur (%s) lors de la création du fichier %s.html" % (erreur,site)

    # apertura de una conexión en el puerto 80 del sitio
    try:
        connexion=socket.create_connection((site,80))
    except socket.error, erreur:
        pass
    # retorno en caso de error
    if erreur:
        return "Echec de la connexion au site (%s,80) : %s" % (site,erreur)

    # la conexión representa un flujo de comunicación bidireccional
    # entre el cliente (este programa) y el servidor web contactado
    # este canal se utiliza para el intercambio de comandos e información
    # el protocolo de comunicación es HTTP

    # el cliente envía el comando get para solicitar el URL /
    # sintaxis get URL HTTP/1.0
    # los encabezados (headers) del protocolo HTTP deben terminar con una línea en blanco
    connexion.send("GET / HTTP/1.0\n\n")

    # el servidor responderá ahora en el canal de conexión. Enviará todos
    # sus datos y luego cerrará el canal. El cliente lee todo lo que llega por el canal de conexión
    # hasta que se cierre el canal
    ligne=connexion.recv(1000)
    while(ligne):
        html.write(ligne)
        ligne=connexion.recv(1000)

    # el cliente cierra la conexión a su vez
    connexion.close()
    # cierre del archivo HTML
    html.close()
    # regreso
    return "Transfert reussi du paragraphe index du site %s" % (site)

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

# obtener el texto HTML de URL

# lista de sitios web
SITES=("istia.univ-angers.fr","www.univ-angers.fr","www.ibm.com","xx")

# lectura de las páginas de índice de los sitios de la tabla SITES
for i in range(len(SITES)):
      # lectura de la página de índice del sitio SITES[i]
      resultat=getIndex(SITES[i])
      # visualización del resultado
      print resultat

# fin
sys.exit()

Notas:

  • línea 57: la lista de las URL de los sitios web de los que se desea obtener la página de inicio. Esta lista se almacena en el archivo de texto [nomsite.html];
  • línea 62: la función getIndex realiza la tarea;
  • línea 4: la función getIndex;
  • línea 20: el método create_connection((sitio, puerto)) permite establecer una conexión con un servicio TCP / IP que opera en el puerto port de la máquina «sitio»;
  • línea 35: el método send permite enviar datos a través de una conexión TCP / IP. En este caso, lo que se envía es texto. Este texto sigue el protocolo HTTP (Protocolo de transferencia HyperText);
  • línea 40: el método recv permite recibir datos a través de una conexión TCP / IP. Aquí, la respuesta del servidor web se lee en bloques de 1000 caracteres y se guarda en el archivo de texto [nomsite.html].

Resultados

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

El archivo recibido para el sitio [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>
  • las líneas 1 a 11 son los encabezados HTTP de la respuesta del servidor;
  • línea 1: el servidor le pide al cliente que se redirija al URL indicado en la línea 8;
  • línea 2: fecha y hora de la respuesta;
  • línea 3: identidad del servidor web;
  • línea 4: contenido enviado por el servidor. En este caso, una página HTML que comienza en la línea 13;
  • línea 12: la línea vacía que cierra los encabezados HTTP;
  • líneas 13-19: la página HTML enviada por el servidor web.

11.3. Un cliente SMTP

Entre los protocolos TCP / IP, SMTP (Protocolo de transferencia SendMail) es el protocolo de comunicación del servicio de envío de mensajes.


Programa (inet_03)


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

import sys,socket

#-----------------------------------------------------------------------
def getInfos(fichier):
    # devuelve la información (SMTP, remitente, destinatario, mensaje) extraída del archivo de texto [fichier]
    # línea 1: smtp, remitente, destinatario
    # líneas siguientes: el texto del mensaje

    # apertura de [fichier]
    erreur=""
    try:
        infos=open(fichier,"r")
    except IOError, erreur:
        return ("Le fichier %s n'a pu etre ouvert en lecture : %s" % (fichier, erreur))
  
    # lectura de la primera línea
    ligne=infos.readline()
    # eliminación del carácter de fin de línea
    ligne=cutNewLineChar(ligne)
    # recuperación de los campos smtp, remitente, destinatario
    champs=ligne.split(",")
    # ¿Tenemos el número correcto de campos?
    if len(champs)!=3 :
        return ("La ligne 1 du fichier %s (serveur smtp, expediteur, destinataire) a un nombre de champs incorrect" % (fichier))
    # «procesamiento» de la información recuperada
    # Se eliminan de cada uno de los 3 campos los «espacios en blanco» que preceden o siguen a la información útil
    for i in range(3):
        champs[i]=champs[i].strip()
    # Recuperación de los campos
    (smtpServer,expediteur,destinataire)=champs
    message=""
    # lectura del resto del mensaje
    ligne=infos.readline()
    while ligne!='':
        message+=ligne
        ligne=infos.readline()
    infos.close()
    # retorno
    return ("",smtpServer,expediteur,destinataire,message)

#-----------------------------------------------------------------------
def sendmail(smtpServer,expediteur,destinataire,message,verbose):
    # envía el mensaje al servidor SMTP smtpserver en nombre del remitente
    # para el destinatario. Si verbose=True, realiza un seguimiento de los intercambios entre el cliente y el servidor

    # se recupera el nombre del cliente
    try:
        client=socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
    except socket.error, erreur:
        return "Erreur IP / Nom du client : %s" % (erreur)
    
    # se abre una conexión en el puerto 25 de smtpServer
    try:
        connexion=socket.create_connection((smtpServer,25))
    except socket.error, erreur:
        return "Echec de la connexion au site (%s,25) : %s" % (smtpServer,erreur)

    # la conexión representa un flujo de comunicación bidireccional
    # entre el cliente (este programa) y el servidor SMTP contactado
    # Este canal se utiliza para el intercambio de comandos e información

    # tras la conexión, el servidor envía un mensaje de bienvenida que se lee
    erreur=sendCommand(connexion,"",verbose,1)
    if(erreur) :
        connexion.close()
        return erreur
    # comando «ehlo»:
    erreur=sendCommand(connexion,"EHLO %s" % (client),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # comando mail from:
    erreur=sendCommand(connexion,"MAIL FROM: <%s>" % (expediteur),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # comando rcpt to:
    erreur=sendCommand(connexion,"RCPT TO: <%s>" % (destinataire),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # comando «data»
    erreur=sendCommand(connexion,"DATA",verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # preparación del mensaje para enviar
    # debe contener las líneas
    # De: remitente
    # Para: destinatario
    # línea en blanco
    # Mensaje
    # .
    data="From: %s\r\nTo: %s\r\n%s\r\n.\r\n" % (expediteur,destinataire,message)
    # envío del mensaje
    erreur=sendCommand(connexion,data,verbose,0)
    if erreur :
        connexion.close()
        return erreur
    # comando de salida
    erreur=sendCommand(connexion,"QUIT",verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # fin
    connexion.close()
    return "Message envoye"

# --------------------------------------------------------------------------
def sendCommand(connexion,commande,verbose,withRCLF):
    # envía comando al canal de conexión
    # modo detallado si verbose=1
    # si withRCLF=1, agrega la secuencia RCLF al comando

    # datos
    RCLF="\r\n" if withRCLF else ""
    # envía comando si el comando no está vacío
    if commande:
        connexion.send("%s%s" % (commande,RCLF))
        # posible respuesta
        if verbose:
            affiche(commande,1)
        # lectura de respuesta de menos de 1000 caracteres
        reponse=connexion.recv(1000)
        # eco eventual
        if verbose:
            affiche(reponse,2)
        # recuperación del código de error
        codeErreur=reponse[0:3]
        # ¿Error devuelto por el servidor?
        if int(codeErreur) >=500:
            return reponse[4:]
        # respuesta sin error
        return ""

# --------------------------------------------------------------------------
def affiche(echange,sens):
    # ¿Muestra el intercambio en pantalla?
    # si sens=1 muestra -->intercambio
    # si sentido=2 muestra <-- intercambio sin los dos últimos caracteres RCLF
    if sens==1:
        print "--> [%s]" % (echange)
        return
    elif sens==2:
        l=len(echange)
        print "<-- [%s]" % echange[0:l-2]
        return

      
# --------------------------------------------------------------------------
def cutNewLineChar(ligne):
    # se elimina el marcador de fin de línea de [ligne] si existe
    l=len(ligne)
    while(ligne[l-1]=="\n" or ligne[l-1]=="\r"):
        l-=1
    return(ligne[0:l])

# main ----------------------------------------------------------------
  
# cliente SMTP (Protocolo de transferencia SendMail) que permite enviar un mensaje
# la información se toma de un archivo INFOS que contiene las siguientes líneas
# línea 1: smtp, remitente, destinatario
# líneas siguientes: el texto del mensaje

# remitente: correo electrónico del remitente
# destinatario: correo electrónico del destinatario
# SMTP: nombre del servidor SMTP que se utilizará


# protocolo de comunicación SMTP cliente-servidor
# -> el cliente se conecta al puerto 25 del servidor SMTP
# <- el servidor le envía un mensaje de bienvenida
# -> el cliente envía el comando EHLO: nombre de su máquina
# <- el servidor responde OK o no
# -> el cliente envía el comando «mail from: <remitente>»
# <- el servidor responde OK o no
# -> el cliente envía el comando rcpt to: <destinatario>
# <- el servidor responde con OK o no
# -> el cliente envía el comando data
# <- el servidor responde OK o no
# -> el cliente envía todas las líneas de su mensaje y termina con una línea que contiene un solo carácter.
# <- el servidor responde OK o no
# -> el cliente envía el comando «quit»
# <- el servidor responde OK o no

# las respuestas del servidor tienen el formato xxx texto, donde xxx es un número de 3 dígitos. Cualquier número xxx >=500
# indica un error. La respuesta puede contener varias líneas, todas comenzando con xxx-, excepto la última
# con el formato xxx (espacio)

#: las líneas de texto intercambiadas deben terminar con los caracteres RC(#13) y LF(#10)

# # los parámetros para el envío del correo
MAIL="mail2.txt" 
# se recuperan los parámetros del correo
res=getInfos(MAIL)
# ¿Error?
if res[0]:
      print "%s" % (erreur)
      sys.exit()
# envío de correo en modo detallado
(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)
# fin
sys.exit()

Notas:

  • en una computadora con Windows que cuente con un antivirus, es posible que este impida que el script de Python se conecte al puerto 25 de un servidor SMTP. En ese caso, es necesario desactivar el antivirus. Para McAfee, por ejemplo, se puede proceder de la siguiente manera:
  • en [1], se activa la consola VirusScan
  • en [2], se detiene el servicio [Protection lors de l'accès]
  • en [3], ya está detenido

Resultados

El archivo infos.txt:

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

ligne1
ligne2

ligne3

Resultados en pantalla:

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

El mensaje leído por el lector de correo Thunderbird:

  

11.4. Un segundo cliente SMTP

Este segundo script hace lo mismo que el anterior, pero utilizando las funcionalidades del módulo [smtplib].


Programa (inet_04)


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

import sys,socket, smtplib

#-----------------------------------------------------------------------
def getInfos(fichier):
    # devuelve la información (SMTP, remitente, destinatario, mensaje) extraída del archivo de texto [fichier]
    # línea 1: smtp, remitente, destinatario
    # líneas siguientes: el texto del mensaje

    # apertura de [fichier]
    erreur=""
    try:
        infos=open(fichier,"r")
    except IOError, erreur:
        return ("Le fichier %s n'a pu etre ouvert en lecture : %s" % (fichier, erreur))
  
    # lectura de la primera línea
    ligne=infos.readline()
    # eliminación del carácter de fin de línea
    ligne=cutNewLineChar(ligne)
    # recuperación de los campos smtp, remitente, destinatario
    champs=ligne.split(",")
    # ¿Tenemos el número correcto de campos?
    if len(champs)!=3 :
        return ("La ligne 1 du fichier %s (serveur smtp, expediteur, destinataire) a un nombre de champs incorrect" % (fichier))
    # «procesamiento» de la información recuperada: se eliminan los «espacios en blanco» que la preceden o la siguen
    for i in range(3):
        champs[i]=champs[i].strip()
    # Recuperación de los campos
    (smtpServer,expediteur,destinataire)=champs
    # lectura del mensaje a enviar
    message=""
    ligne=infos.readline()
    while ligne!='':
        message+=ligne
        ligne=infos.readline()
    infos.close()
    # retorno
    return ("",smtpServer,expediteur,destinataire,message)

#-----------------------------------------------------------------------
def sendmail(smtpServer,expediteur,destinataire,message,verbose):
    # envía el mensaje al servidor SMTP smtpserver en nombre del remitente
    # para el destinatario. Si verbose=True, realiza un seguimiento de los intercambios entre el cliente y el servidor

    # se utiliza la biblioteca 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)
    # fin
    return "Message envoye"
      
# --------------------------------------------------------------------------
def cutNewLineChar(ligne):
    # se elimina el marcador de fin de línea de [ligne] si existe
    l=len(ligne)
    while(ligne[l-1]=="\n" or ligne[l-1]=="\r"):
        l-=1
    return(ligne[0:l])

# main ----------------------------------------------------------------
  
# cliente SMTP (Protocolo de transferencia SendMail) que permite enviar un mensaje
# la información se toma de un archivo INFOS que contiene las siguientes líneas
# línea 1: smtp, remitente, destinatario
# líneas siguientes: el texto del mensaje

# remitente: correo electrónico del remitente
# destinatario: correo electrónico del destinatario
# SMTP: nombre del servidor SMTP que se utilizará


# protocolo de comunicación SMTP cliente-servidor
# -> el cliente se conecta al puerto 25 del servidor SMTP
# <- el servidor le envía un mensaje de bienvenida
# -> el cliente envía el comando EHLO: nombre de su máquina
# <- el servidor responde OK o no
# -> el cliente envía el comando mail from: <remitente>
# <- el servidor responde OK o no
# -> el cliente envía el comando rcpt to: <destinatario>
# <- el servidor responde con OK o no
# -> el cliente envía el comando data
# <- el servidor responde OK o no
# -> el cliente envía todas las líneas de su mensaje y termina con una línea que contiene un solo carácter.
# <- el servidor responde OK o no
# -> el cliente envía el comando «quit»
# <- el servidor responde OK o no

# las respuestas del servidor tienen el formato xxx texto, donde xxx es un número de 3 dígitos. Cualquier número xxx >=500
# indica un error. La respuesta puede contener varias líneas, todas comenzando con xxx-, excepto la última
# con el formato xxx (espacio)

# las líneas de texto intercambiadas deben terminar con los caracteres RC(#13) y LF(#10)

# # los parámetros para el envío del correo
MAIL="mail2.txt" 
# se recuperan los parámetros del correo
res=getInfos(MAIL)
# ¿Error?
if res[0]:
      print "%s" % (erreur)
      sys.exit()
# envío del correo en modo detallado
(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)
# fin
sys.exit()

Notas:

  • este script es idéntico al anterior, salvo por la función sendmail. Esta función ahora utiliza las funcionalidades del módulo [smtplib] (línea 3).

Resultados

El archivo 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

Resultados en pantalla:

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. Cliente/servidor de eco

Se crea un servicio de eco. El servidor devuelve en mayúsculas todas las líneas de texto que le envía el cliente. El servicio puede atender a varios clientes al mismo tiempo gracias al uso de subprocesos.


El programa del servidor (inet_08)


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

# servidor TCP genérico en Windows

# carga de los archivos de encabezado
import re,sys,SocketServer,threading

# servidor TCP multihilo
class ThreadedTCPRequestHandler(SocketServer.StreamRequestHandler):

  def handle(self):
    # hilo actual
    cur_thread = threading.currentThread()
    # datos del cliente
    self.data="on"
    # detener en cadena vacía
    while self.data:
      # las líneas de texto del cliente se leen con el método readline
      self.data =self.rfile.readline().strip()
      # seguimiento de la consola
      print "client %s : %s (%s)" % (self.client_address[0],self.data,cur_thread.getName())
      # envío de respuesta al cliente
      response = "%s: %s" % (cur_thread.getName(), self.data.upper())
      # self.wfile es el flujo de escritura hacia el cliente
      self.wfile.write(response)                    

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

# ------------------------------------------------------ main        
# sintaxis de llamada: argv[0] puerto
# el servidor se inicia en el puerto denominado

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

#------------------------------------- se verifica la llamada
# debe haber un argumento y solo uno
nbArguments=len(sys.argv)
if nbArguments!=2:
    print syntaxe
    sys.exit(1)

# el puerto debe ser un número
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)

# Inicio del servidor: atiende a cada cliente en un hilo
host="localhost"
server = ThreadedTCPServer((host,int(port)), ThreadedTCPRequestHandler)

# el servidor se inicia en un hilo
# cada cliente será atendido en un hilo adicional
server_thread = threading.Thread(target=server.serve_forever)
# inicia el servidor: bucle infinito de espera de clientes
server_thread.start()
# seguimiento
print "Serveur d'echo a l'ecoute sur le port % s" % (port)

Notas:

  • línea 39: sys.argv representa los parámetros del script. Aquí deben tener el formato: nom_du_script puerto. Por lo tanto, deben ser dos. sys.argv[0] será entonces nom_du_script y sys.argv[1] será port ;
  • línea 53: el servidor de eco es una instancia de la clase ThreadedTcpServer. El constructor de esta clase espera dos parámetros:
    • parámetro 1: una tupla de dos elementos (host, puerto) que establece la máquina y el puerto de escucha del servidor;
    • parámetro 2: el nombre de la clase encargada de procesar las solicitudes de un cliente.
  • línea 57: se crea un hilo (aunque aún no se ha iniciado). Este hilo ejecuta el método [serve_forever] del servidor TCP. Este método es un bucle que escucha las conexiones de los clientes. Tan pronto como se detecte una conexión de cliente, se creará una instancia de la clase ThreadedTCPRequestHandler. Su método handle se encarga del diálogo con el cliente;
  • línea 59: se inicia el hilo del servicio de eco. A partir de este momento, los clientes pueden conectarse al servicio;
  • línea 27: la clase del servidor de eco. Deriva de dos clases: SocketServer.ThreadingMixIn y SocketServer.TCPServer. Esto lo convierte en un servidor TCP multihilo: el servidor se ejecuta en un hilo y cada cliente es atendido en un hilo adicional;
  • línea 9: la clase que procesa las solicitudes de los clientes. Deriva de la clase SocketServer.StreamRequestHandler. Por lo tanto, hereda dos atributos:
    • rfile: que es el flujo de lectura de los datos enviados por el cliente; puede tratarse como un archivo de texto;
    • wfile: que es el flujo de escritura que permite enviar datos al cliente; puede tratarse como un archivo de texto.
  • línea 11: el método handle procesa las solicitudes de los clientes;
  • línea 13: el hilo que ejecuta este método;
  • línea 17: bucle de procesamiento de las solicitudes del cliente. El bucle termina cuando el cliente envía una línea vacía;
  • línea 19: lectura de la solicitud del cliente;
  • línea 21: self.client_address[0] representa la dirección IP del cliente. cur_thread.getName() es el nombre del hilo que ejecuta el método handle;
  • línea 23: la respuesta al cliente tiene dos componentes: el nombre del hilo que atiende al cliente y el comando que este envió, escrito en mayúsculas.

El programa del cliente (inet_06)


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

# ------------------------------------------------------ main        
# cliente TCP genérico
# sintaxis de la llamada: argv[0] host puerto
# el cliente se conecta al servicio de eco (servidor, puerto)
# el servidor devuelve las líneas escritas por el cliente

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

#------------------------------------- se verifica la llamada
# debe haber dos argumentos
nbArguments=len(sys.argv)
if nbArguments!=3:
    print syntaxe
    sys.exit(1)

# se recuperan los argumentos
hote=sys.argv[1]
# el puerto debe ser un número
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:
  # conexión del cliente al servidor
  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:
    # bucle de entrada
    ligne=raw_input("Commande (rien pour arreter): ").strip()
    while ligne!="":
        # se envía la línea al servidor
        connexion.send("%s\n" % (ligne))
        # se espera la respuesta
        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:
    # se cierra la conexión
    connexion.close()

Los resultados

El servidor se inicia en una primera ventana de comandos:

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

Se inicia un primer cliente en una segunda ventana:

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

El cliente recibe correctamente, como respuesta al comando que envía al servidor, ese mismo comando escrito en mayúsculas. Se inicia un segundo cliente en una tercera ventana:

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

La consola del servidor se ve así:

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)

Los clientes se atienden correctamente en hilos diferentes. Para detener un cliente, basta con escribir un comando vacío.

11.6. Servidor TCP genérico

Nos proponemos escribir un script en Python que

  • sea un servidor TCP capaz de atender a un cliente a la vez,
  • que acepte líneas de texto enviadas por el cliente,
  • y que acepte líneas de texto provenientes del teclado, las cuales se envían como respuesta al cliente.

De esta manera, es el usuario en el teclado quien actúa como servidor:

  • ve en su consola las líneas de texto enviadas por el cliente;
  • y le responde al cliente al escribir la respuesta en el teclado.

De esta manera, puede adaptarse a cualquier tipo de cliente. Por eso lo llamaremos «servidor TCP genérico». Es una herramienta práctica para explorar los protocolos de comunicación TCP. En el siguiente ejemplo, el cliente TCP será un navegador web, lo que nos permitirá explorar el protocolo HTTP que utilizan los clientes web.


El programa (inet_10)


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

# servidor TCP genérico

# carga de los archivos de encabezado
import re,sys,SocketServer,threading

# servidor TCP genérico
class MyTCPHandler(SocketServer.StreamRequestHandler):
    def handle(self):
        # se muestra el cliente
        print "client %s" % (self.client_address[0])
        # se crea un hilo para leer los comandos del cliente
        thread_lecture = threading.Thread(target=self.lecture)
        thread_lecture.start()
        # se detiene al recibir el comando «bye»
        # lectura del comando ingresado desde el teclado
        cmde=raw_input("--> ")
        while cmde!="bye":
            # envío del comando al cliente. self.wfile es el flujo de escritura hacia el cliente
            self.wfile.write("%s\n" % (cmde))
            # lectura del siguiente comando
            cmde=raw_input("--> ")

    def lecture(self):
        # se muestran todas las líneas enviadas por el cliente hasta recibir el comando «bye»
        ligne=""
        while ligne!="bye":
            # las líneas de texto del cliente se leen con el método readline
            ligne = self.rfile.readline().strip()
            # seguimiento de la consola
            print "<--- %s : %s" % (self.client_address[0], ligne)
    
# ------------------------------------------------------ main        
# sintaxis de llamada: argv[0] puerto
# el servidor se inicia en el puerto indicado

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

#------------------------------------- se verifica la llamada
# debe haber un argumento y solo uno
nbArguments=len(sys.argv)
if nbArguments!=2:
    print syntaxe
    sys.exit(1)

# el puerto debe ser un número
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)

# Inicio del servidor
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()

Notas:

  • línea 57: el servidor TCP será una instancia de la clase SocketServer.TCPServer. Su constructor admite dos parámetros:
    • el primer parámetro es una tupla de dos elementos (host, puerto), donde host es la máquina en la que opera el servicio (generalmente localhost) y port, el puerto en el que el servicio espera (escucha) las solicitudes de los clientes;
    • el segundo parámetro especifica la clase de servicio de un cliente. Cuando un cliente se conecta, se crea una instancia de la clase de servicio y es su método handle el que debe gestionar la conexión con el cliente.

El servidor TCP SocketServer.TCPServer no es multihilo. Atiende a un cliente a la vez;

  • línea 59: se ejecuta el método serve_forever del servidor TCP. Se trata de un bucle infinito de espera de clientes;
  • línea 9: el servidor TCP es aquí una clase derivada de la clase SocketServer.StreamRequestHandler. Esto permite considerar los flujos de datos intercambiados con el cliente como archivos de texto. Ya nos hemos encontrado con esta clase. Contamos con los métodos:
    • readline para leer una línea de texto proveniente del cliente;
    • write para enviarle líneas de texto.
  • línea 12: self.client_address[0] es la dirección IP del cliente;
  • línea 14: el servidor TCP se comunicará con el cliente mediante dos subprocesos
    • un hilo para leer las líneas del cliente;
    • un hilo para escribir líneas al cliente.
  • línea 14: se crea el hilo de lectura de las líneas del cliente. Su parámetro target establece el método que ejecutará el hilo. Es el definido en la línea 25;
  • línea 25: se inicia el hilo de lectura;
  • líneas 25-32: el método ejecutado por el hilo de lectura;
  • línea 28: el hilo de lectura lee todas las líneas de texto enviadas por el cliente hasta recibir la línea «bye»;
  • línea 18: aquí nos encontramos en el hilo de escritura al cliente. El principio consiste en enviar al cliente todas las líneas de texto que el usuario escribe en el teclado.

Los resultados

El servidor

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

El navegador del cliente

 

La solicitud recibida por el servidor

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 :

La respuesta del servidor que el usuario escribió en el teclado (sin el signo -->)

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
  • líneas 1-5: respuesta HTTP enviada al cliente;
  • línea 6: la página HTML enviada al cliente;
  • línea 7: fin del diálogo con el cliente. El servicio al cliente terminará y se cerrará la conexión, lo que interrumpirá abruptamente el hilo de lectura de las líneas de texto enviadas por el cliente;
  • las líneas 1-5 de la respuesta HTTP enviada al cliente tienen el siguiente significado:
  • línea 1: se ha encontrado el recurso solicitado por el cliente;
  • línea 2: identificación del servidor;
  • línea 3: el servidor cerrará la conexión después de enviar el recurso;
  • línea 4: tipo de recurso enviado por el servidor: un documento HTML;
  • línea 5: una línea vacía.

La página mostrada por el navegador [1]:

Si se muestra el código fuente recibido por el navegador [2], se observa que el código HTML recibido por el navegador web es, efectivamente, el que se le había enviado.