15 - Curso de Python - Sockets en Python

De ChuWiki


Cualquier duda suelo atender en este foro de python

Todo el código de este curso de Python gratuito está en github https://github.com/chuidiang/chuidiang-ejemplos/tree/master/PYTHON/curso-python. En línea comandos python tienes cómo arrancar la consola de comandos de python por si quieres ir probando los ejemplos de sockets en Python.

Anterior: 13 - Curso de Python - Leer y escribir ficheros en python -- Índice: Curso de Python -- Siguiente: Próximamente.

Tienes una introducción a los sockets si no te son familiares conceptos como sockets TCP, UPD o Multicast.

Ejemplo sencillo de sockets TCP/IP con python[editar]

Vamos a hacer un pequeño servidor python y un cliente python que se conectan con socket, se intercambian unas cadenas de texto y cierran la conexión. Aquí tienes los fuentes completos del ejemplo de sockets con python.

Si recuerdas, el servidor es el ejecutable que está a la escucha, esperando que un cliente se le conecte y empiece la conexión.

El servidor[editar]

En el servidor, primero establecemos el socket servidor. Para ello, usamos el módulo socket de python, haciendo el import correspondiente. Damos los siguientes pasos:

  • Llamada a la función socket(), a la que le pasamos el tipo de socket que queremos abrir. en el ejemplo, AF_INET que es el habitual y SOCK_STREAM que es el correspondiente a un socket TCP/IP.
  • Llamada a la función bind(), pasándole una dirección address compuesto por (host, puerto).
    • Veamos qué es host. Imagina que tu ordenador tiene varias tarjetas de red. Quizás tienes una tarjeta de red conectada por cable y la otra es la wifi. Cada tarjeta de red tiene su IP propia, así que tendrás tantas IPs distintas como tarjetas de red tengas. Con este parámetro, host indicas por cual de las tarjetas de red quieres que tu programa escuche. Por ejemplo, puedes querer escuchar a clientes que se conecten por tu tarjeta de red de cable, pero no por la wifi. Así que aquí pones la IP de la tarjeta de red por la que quieres escuchar. Si en este parámetro pones una cadena vacía, tu programa escuchará por todas las tarjetas de red. En nuestro ejemplo pasamos "" como host
    • Como puerto el que queramos que esté libre (8000). La llamada a bind() le indica al sistema operativo que nosotros vamos a atender las conexiones por el puerto 8000. Los puertos del 1 al 1024 suelen estar reservados o en sistemas operativos como linux incluso requieren perimsos de administrador para ponerse a la escucha de ellos. Por eso, elegimos un puerto 1024 o superior.
  • Llamada a listen(). Esta llamada indica al sistema operativo que ya estamos listos para admitir conexiones. El número 1 de parámetro indica cuantos clientes podemos tener encolados en espera simultáneamente. Este número no debería ser grande, puesto que es el número máximo de clientes que quedarán encolados desde que aceptamos un cliente hasta que estamos dispuestos a aceptar el siguiente. Si el código está bien hecho, este tiempo debería ser realmente pequeño, ya que al conectarse un cliente, deberíamos lanzar un hilo para atenderlo y entrar inmediatamente a la espera de otro cliente.

El código de todo esto puede ser

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("", 8000))
server.listen(1)

Ahora nos metemos en un bucle eterno, esperando clientes y atendiéndolos. Para esperar un cliente, la llamada es accept(), que nos devuelve un par (socket_cliente, datos_cliente) del que obtenemos el socket para hablar con el cliente y sus datos (ip, puerto).

   # bucle para atender clientes
   while 1:
      # Se espera a un cliente
      socket_cliente, datos_cliente = server.accept()
      # Se escribe su informacion
      print "conectado "+str(datos_cliente)

Ahora hacemos otro bucle para recibir y atender los datos del cliente. Deberíamos meter este bucle en un hilo, de forma que el hilo principal vuelva al accept() y sea capaz de atender más clientes mientras atendemos al primero, pero no vamos a hacerlo para no liar el código.

Para leer los datos del cliente, usamos recv(), pasando como parámetro el número máximo de bytes que queremos leer de una tacada. La lectura se quedará bloqueada hasta que llegue algo del cliente. En cuanto lleguen datos, la llamada nos devolverá esos datos. Es importante saber que la llamada no esperará hasta el máximo que hemos indicado, así que en nuestro codigo debemos ver si los datos que nos han entregado tienen toda la información que esperamos, o bien tenemos que guardarlos temporalmente y volver a llamar a recv() las veces que sea necesario hasta tener todo lo que necesitamos. El bucle para atender al cliente puede quedar así

      # Bucle indefinido hasta que el cliente envie "adios"
      seguir = True
      while seguir:
         # Espera por datos
         peticion = socket_cliente.recv(1000)
         print ('Recibido '+str(peticion))
         
         # Si recibimos cero bytes, es que el cliente ha cerrado el socket
         if not peticion:
            seguir = false

         # Contestacion a "hola"
         if ("hola"==peticion.decode()):
             print (str(datos_cliente)+ " envia hola: contesto")
             socket_cliente.send("pues hola".encode())
             
         # Contestacion y cierre a "adios"
         if ("adios"==peticion.decode()):
             print (str(datos_cliente)+ " envia adios: contesto y desconecto")
             socket_cliente.send("pues adios".encode())
             socket_cliente.close()
             print ("desconectado "+str(datos_cliente))
             seguir = False

La variable seguir nos hará permanecer en el bucle hasta que la pongamos a False, cosa que haremos cuando cerremos la conexión con el cliente. recv(1000) lee las peticiones del cliente y con los if comprobamos si lo recibido es "hola" o "adios" o bien un cierre de conexión por parte del cliente (recibimos cero bytes). Para contestar usamos send(), enviando la cadena de respuesta. Si es "adios", además de contestar, cerramos el socket y ponemos a False la variable seguir, para terminar el bucle.

La función recv() y la función send() esperan bytes y no cadenas de texto. Por ello, usamos la función decode() de los bytes para convertirlos a cadenas de texto y la función encode() de las cadenas de texto para convertirlas a bytes.

El código completo está en servidor.py

El cliente[editar]

Para el cliente, creamos el socket con la llamada a socket(), igual que en el servidor, pero establecemos la conexión con connect(), indicando el host servidor (localhost en nuestro ejemplo) y el puerto. localhost es equivalente a la IP 127.0.0.1. Todos los ordenadores tienen esta IP, es una IP interna al ordenador, que no sale a la red. Es válida siempre y cuando ambos programas, cliente y servidor, corran en el mismo ordenador. El puerto en el que esté escuchando el servidor, que en nuestro ejemplo es el 8000.

import socket
...
# Se establece la conexion
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("localhost", 8000))

El resto es más sencillo que en el servidor. Envíamos "hola" con send(), esperamos y escribimos la respuesta con recv(), hacemos una espera de 2 segundos con sleep(), enviamos con send() la cadena "adios", esperamos y escribimos respuesta con recv() y finalmente cerramos el socket con close()

import time
...
    # Se envia "hola"
    s.send("hola".encode())
    
    # Se recibe la respuesta y se escribe en pantalla
    datos = s.recv(1000)
    print (datos.decode())
    
    # Espera de 2 segundos
    time.sleep(2)
    
    # Se envia "adios"
    s.send("adios".encode())
    
    # Se espera respuesta, se escribe en pantalla y se cierra la
    # conexion
    datos = s.recv(1000)
    print (datos.decode())
    s.close()

Igual que en el caso del servidor, el socket recibe y envía bytes, no cadenas de texto. Por ello es necesario llamar a la función encode() de las cadenas de texto cuando se quiera enviar por un socket y llamar a decode() de los bytes recibidos para tenerlos como cadenas de texto.

Aquí tienes el código completo de cliente.py

Socket UDP en python[editar]

Como comentamos al principio, un socket UDP no está orientado a conexión. Los servidores deben ponerse a la escucha, pero no necesitan aceptar una conexión accept(). Los clientes pueden enviar sin necesidad de conectarse con un connect().

Servidor socket UDP[editar]

El servidor es el que está a la escucha en espera de que alguien le envíe algo. En el caso de sockets UDP, al no establcerse una conexión, el concepto cliente/servidor no está tan claro. Aunque siempre hay alguien, el servidor, que es el que está permanentemente arrancado y en espera que alguien le envíe o pida algo.

import socket

if __name__ == '__main__':
    # Se abre el socket servidor
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
        # Se le pone a la escucha
        server.bind(("", 8000))

        while True:
            # Se espera un mensaje de cliente
            message, client = server.recvfrom(1000)
            # Se imprime
            print(str(client))
            print(message.decode())
            print("-------")
            # Se le envía respuesta
            server.sendto("Mundo".encode(), client)

El socket se abre igual que en TCP/IP, pero con el segundo parámetro como socket.SOCK_DGRAM. Los paquetes de datos en sockets UDP se conocen como "datagrams", de ahí el nombre de la opción. Una vez abierto el socket, se llama a bind() como en el caso anterior. Puesto que no va a haber conexiones no es necesario llamar ni a accept() ni a listen().

Y ya sin más podemos ponernos a leer mensajes, pero esta vez, en vez de recv() que está pensado para cuando hay conexión, usaremos recvfrom que es para cuando no hay conexión. La diferencia es que recv() devuelve solo los datos leídos mientras que recvfrom() devuelve datos leídos y quién los ha enviado. En el caso de conexión esos datos los teníamos de la llamada a accept().

Escribimos por pantalla tanto la procencia de los datos client como los datos en sí mismos. Al ser btyes, usamos decode() para convertirlos a cadena de texto. Para el envío de una respuesta a ese cliente usamos sendto() que lleva dos parámetros: los datos en sí mismos así como el destinatario (ip y puerto). Como es una contestación al que nos ha enviado datos, usamos client como destinatario.

Cliente Socket UDP[editar]

El cliente no tiene nada especial que no hayamos visto ya. Hace lo mismo que el servidor, pero en distinto orden. Envía un mensaje y se pone a escuchar la respuesta. La única diferencia es que como es el primero que envía, tiene que saber la IP y puerto donde está escuchando el servidor para saber dónde enviar el mensaje. El servidor, como hemos visto, coge estos datos del mismo mensaje que le llega, por lo que no tiene que saber por adelantado las direcciones IP y puertos de los clientes. El código queda así.

import socket

if __name__ == '__main__':
    # Direccion y puerto del servidor
    serverAddress = ('127.0.0.1', 8000)
    # Se abre el socket UDP (SOCK_DGRAM)
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
        # Se envia "Hola".
        client.sendto("Hola".encode(), serverAddress)
        # Se espera respuesta
        message, server = client.recvfrom(1000)
        # Se imprime la respuesta
        print(str(server))
        print(message.decode())

En serverAddress tenemos la IP y puerto donde escucha el servidor. El socket se abre igual que en el lado del servidor, con la opción socket.SOCK_DGRAM. Y en envío se hace con sendTo() con el mensaje y el destinatario (el servidor). Para leer la respuesta, se usa recvfrom() indicando el tamaño máximo de bytes que queremos recibir.

Hay un punto importante a tener aquí en cuenta. Hacemos recvfrom() pero en ningún sitio hemos dicho en qué IP/puerto atender. No hemos llamado a ningún bind(). El motivo es que la primera llamada que hagamos a sendto() hará un bind() por debajo para atender justo en ese puerto.

Socket UDP broadcast[editar]

Ante todo, mencionar que los sockets UDP en broadcast se siguen manteniendo, pero están obsoletos. Las nuevas interfaces de red IPv6 no los soportan. En su lugar, tanto en IPv4 como en IPv6 se prefiere usar los sockets multicast. No obstante, comentamos aquí los sockets broadcast.

Servidor socket UDP broadcast[editar]

En broadcast el concepto de servidor y cliente cambia mucho. Habitualmente, antes de multicast, se usa broadcast para que un servidor divulgue sobre la red una inforamción que pueda ser de interés para muchos cientes. Por ejemplo, imagina un ordenador conectado a un GPS que obtiene del GPS la fecha/hora oficial además de su posición geográfica. Este ordenador puede distribuir por broadcast la fecha/hora oficial obtenida del GPS de forma que el resto de ordenadores de la red, sin necesidad de conectarse al GPS, puedan saberla. En este ejemplo, el servidor no está a la escucha de nada, sólo divulga una información. Son los clientes que tengan interés los que deben ponerse a la escucha.

Así que el concepto cliente/servidor aquí es al revés. El servidor difunde información a la red sin que nadie la pida y los clientes que tengan interés se ponen a la escucha para recogerla. El único concepto cliente/servidor que se mantiene es que los clientes deben saber dónde emite el servidor.

El código del servidor puede ser como este

import socket
import time

if __name__ == '__main__':
    # Se abre el socket servidor
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
        # Se le pone a la escucha
        server.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

        while True:
            # Envio de una info por broadcast
            server.sendto("Hola".encode(), ('<broadcast>', 8000))
            server.sendto("Hola".encode(), ('255.255.255.255', 8000))
            server.sendto("Hola".encode(), ('127.0.0.255', 8000))
            server.sendto("Hola".encode(), ('192.168.0.255', 8000))
            time.sleep(2)

El código manda el mensaje "Hola" por broadcast 4 veces cada 2 segundos. Tras abrir el socket con socket.SOCK_DGRAM puesto que el broadcast es UDP, igual que antes, ponemos una nueva opción para indicar que es broadcast. Es la línea server.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1). La función setsockopt() sirve para mofificar opciones del socket.

  • socket.SOL_SOCKET indica en qué nivel del protocolo está la opción que queremos cambiar. Habitualmente cuando tratamos con sockets y no estamos haciendo cosas "raras", el valor a poner aquí es socket.SOL_SOCKET. Hay otros niveles de protocolo que suelen empezar por socket.SOL_*.
  • socket.SO_BROADCAST es la propiedad que queremos cambiar. Hay más propiedades que a nivel de protocolo de socket suelen ser socket.SO_*.
  • El 1 es el valor que queremos darle a la propiedad. En este caso un uno para habiitar el envío por broadcast.

Las llamadas a setsockopt() en general deben hacerse antes de usar el socket para asegurar que tienen efecto. Veremos más adelante otras opciones de interés.

A la hora de enviar con sendto(), debemos indicar IP y puerto del destinatario. El puerto el que queramos. La IP debería ser una IP de broadcast, En el ejemplo hemos puesto varias opciones:

  • '<broadcast>' es una cadena especial que phyton entiende como dirección de broadcast. El mensaje saldrá por la tarjeta de red por defecto del ordenador en modo broadcast.
  • '255.255.255.255' sería equivalente a lo anterior, el mensaje sale por la interface de red por defecto.
  • '127.0.0.255' es la dirección IP de broadcast correspondiente a la "red" local interna del PC, cuya IP es 127.0.0.1 o "localhost". Esos mensajes de broadcast no salen del ordenador y solo puede recibirlos un cliente que corra en el mismo ordenador.
  • '192.168.0.255' es mi IP de broadcast del ordenador concreto donde estoy haciendo el código de ejemplo. Mi IP es 192.168.0.100, interna en mi casa con mi router. La de broadcast cambia el último número por un 255.

Vamos a ver un poco más de ese 255. En la configuración de la tarjeta de red hay dos cosas de interés. La IP que tiene la tarjeta, que ya hemos comentado, y la "máscara de red" o "mascara de subred". Si en windows ejecutas el comando ipconfig desde una ventana de comandos o ifconfig desde una terminal linux, verás algo como esto

Adaptador de Ethernet Ethernet:

   Sufijo DNS específico para la conexión. . :
   Vínculo: dirección IPv6 local. . . : fe80::e045:8f65:d029:2cb%5
   Dirección IPv4. . . . . . . . . . . . . . : 192.168.0.180
   Máscara de subred . . . . . . . . . . . . : 255.255.255.0
   Puerta de enlace predeterminada . . . . . : 192.168.0.1

Fíjate que sale la "máscara de subred" cuyo valor es 255.255.255.0. Esto quiere decir que todos los ordenadores de la red tendrán los tres primeros números de la IP iguales (192.168.0.x en mi caso) y que el último número es el que puede cambiar de un ordenador a otro de esa red. Este valor de submáscara 255.255.155.0 será el habitual en tu casa o en una oficina pequeña. En grandes redes corporativas puede haber otras máscaras. Pues bien, la dirección de broadcast de la red se obtiene haciendo que el número ese variable dentro de la red (la última cifra en este caso), sea 255. En mi ejemplo, mi dirección de broadcast es 192.168.0.255. Si mi mácara de subred fuera 255.155.0.0, entonces mi dirección de broadcast sería 192.168.255.255

Cliente socket UDP broadcast[editar]

Vamos con el cliente, mucho más sencillo ahora. El código

import socket

if __name__ == '__main__':
    # Se abre el socket UDP (SOCK_DGRAM)
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
        client.bind(("", 8000))
        while True:
            message, server = client.recvfrom(1000)
            # Se imprime la respuesta
            print(str(server))
            print(message.decode())

El socket se abre igual que en el servidor, mismas opciones. Si no vamos a enviar, no necesitamos la llamada a setsockopt() ya que esta opción sólo es para habilitar el envío. Así que nuestro cliente sólo tiene que ponerse a la escucha con bind() en el puerto por el que el servidor emite. Finalmente un bucle para escribir de dónde nos llegan los mensajes y su contenido. La salida para una tanda de 4 mensajes es como esta

('192.168.0.180', 53550)
Hola
('192.168.0.180', 53550)
Hola
('127.0.0.1', 53550)
Hola
('192.168.0.180', 53550)
Hola

Fíjate que recibmos tres de la interface de red cuya IP es 192.168.0.180 (la mía) y que corresponden a '<broadcast>', 255.255.255.255 y 192.169.0.255, mientra que recibimos uno por 127.0.0.1 que corresponde en el servidor al envío por 127.0.0.255.

Socket multicast en python[editar]

Nos falta el útlimo ejemplo, multicast en python. Como comentamos, usando unas direcciones de red específicas que están en el rango 224.0.0.0 a 239.255.255.255 podemos enviar mensajes que todo el que se suscriba puede escuchar. Por cada IP que decidamos usar, decidimos también puertos. Vamos a ello

Tienes los ejemplos completos en multicast_server.py y en multicast_client.py

Servidor socket multicast en python[editar]

La apertura del socket es sencilla, prácticamente igual a un socket UDP.

# Crea socket Datagram. Los multicast son UDP, es decir, Datagram.
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:

Hay un detalle con el envío de mensajes multicast (paquetes de datos). Los routers no los retransmiten de una redes a otras salvo que se lo indiquemos. Lo normal es que los paquetes multicast se queden dentro de nuestra red interna. Si queremos que nuestros paquetes pasen a otras redes, debemos configurar un valor llamado TTL (Time To Live). Es un número entero hasta 255 que va con el paquete de datos. Cada vez que el paquete de datos atraviese un router, el router lo decrementa en 1. Si el valor llega a cero, el router ya no lo retransmite.

La línea para configurar esta opción es

sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1)

No necesitamos configurar este valor si nos vale la opción por defecto que es que el paquete multicast se quede en nuestra red local.

Y ahora solo nos queda que el servidor empiece a enviar mensajes

    message = 'Hello World'
    multicast_group = ('224.3.29.71', 5557)

    while True:
        sock.sendto(message.encode(), multicast_group)
        time.sleep(1)

Hemos definido el mensaje message, un texto "Hello World" y la dirección multicast/puerto multicast_group por el que queremos enviar el mensaje. Se conoce como "grupo multicast" a la dirección multicast y puerto que usamos. Y a partir de ahí, un envío periódico cada segundo con sock.sendto(). Se pasa el mensaje en formato bytes (de ahí el message.encode() y el grupo multicast al que se quiere enviar (dirección y puerto).

Client socket multicast en python[editar]

El cliente es algo más complejo. En primer lugar, se abre el socket Datagram de la forma habitual

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:

Debemos hacer un bind() para ponerlo a la escucha en una tarjeta de red y puerto

    server_address = ('', 5557)
    sock.bind(server_address)

En server_address guardamos IP de la tarjeta de red que queremos usar para ponernos a la escucha y puerto multicast. Como comentamos en ejemplos anteriores, si ponemos '' estamos escuchando en todas las tarjetas de red.

Y ahora nos queda decir qué grupo multicast concreto queremos escuchar

import struct
import socket
...
   multicast_group = '224.3.29.71'
...
   group = socket.inet_aton(multicast_group)
   mreq = struct.pack('4sL', group, socket.INADDR_ANY)
   sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

Bueno, vemos que es un poco lío. Todo esto sólo sirve para decirle al socket que acabamos de abrir que queremos unirnos (escuchar) el grupo multicast de la IP 224.3.29.71. Vamos a explicar las líneas por encima para que se entienda que estamos haciendo, pero sin entrar en mucho detalle.

Para suscribirnos a un grupo, debemos configurar una opción del socket llamando a sock.setsockopt(). Como mencionamos anteriormente, esta función admite tres parámetros:

  • Capa del protocolo dode está la opción que queremos cambiar. Para suscirbirnos a un socket multicast la capa es socket.IPPROTO_IP
  • El nombre de la opción que queremos cambiar. En el caso de querer suscribirnos a un grupo multicast (hacernos miembros de él), la opción se llama socket.IP_ADD_MEMBERSHIP
  • Valor de la opción. El valor en este caso son dos direcciones IP. Una la del grupo multicast al que queremos unirnos, la otra la interface de red por la que queremos escuchar. El problema con este parámetro es que tenemos que pasarlo codificados como 8 bytes, 4 para la primera dirección IP (la del multicast) y otros 4 para la segunda dirección IP (la de la interface de red). De ahí el "lío" con las dos líneas de código anteriores
    • socket.inet_aton() convierte un string con una ip en 4 bytes codificados como necesitamos. Fíjate que estamos llamando a socket y no a sock. El primero es el nombre del módulo python de sockets que estamos usando y inet_aton() es una función de ese módulo. sock es la variable que contiene nuestro socket, no tiene función inet_aton()
    • struc.pack() es una función del módulo struct que agrupa varias variables (en este caso nuestra IP multicast y la IP de la interface de red) en bytes según el formato que necesitemos. No vamos a entrar en detalles, pero admite:
      • Un parámetro string de "formato" con el que queremos los bytes. "4s" indica cuatro bytes y "L" indica un entero de 4 bytes. Tienes todos los detalles en https://docs.python.org/3/library/struct.html Así que obtendremos 8 bytes, los cuatro primeros con el segundo parámetro que pasemos a la función y los cuatro siguientes con el tercer parámetro.
      • Segundo parámetro el group que acabamos de obtener y que ya está en formato de 4 bytes.
      • Tercer parámetro socket.INADDR_ANY que indica que queremos cualquier interface de red.

Un poco lío, pero espero que se entienda el sentido. Unirse al grupo multicast para recibir los mensajes.

Es necesaria la llamada a bind() y también esta configuración. La llamada a bind() básicamente le dice al sistema operativo que estamos interesado en paquetes datagram de cualquier interface de red. Eso hará que nuestros socket reciba todos los paquetes UDP. La segunda llamada a setsockopt() filtra un poco más para decir exactamente qué paquetes nos interesan.


Mejoras en el código[editar]

Hasta ahora hemos puesto lo básico de los sockets, como establecerlos, enviar mensajes y leer mensajes. Pero a la hora de hacer un programa en serio, hay varias cosas que si no se hacen bien pueden darnos dolores de cabeza. Veamos algunas de ellas.

Delimitar mensajes en sockets python[editar]

Un problema con los sockets TCP/IP es la lectura de los mensajes. Como tenemos un flujo contínuo de datos en la conexión, tenemos que leer de la forma adecuada para asegurarnos que tenemos mensajes completos. Esto no sucede en los socket UDP o multicast. En un socket UDP/Multicast, el mensaje puede llegar o no, pero si llega, llega completo. Veamos la problemática y solución de sockets TCP/IP

Hemos comentado que la función recv() lee hasta un máximo de lo que le inidiquemos como parámetro, pero no tenemos garantía de que lea todo eso. Ni siquiera que lea un mensaje completo. En nuestro ejemplo enviamos cadenas de texto "hola" y "adios". No tenemos garantía de que en una sola lectura nos llegue la palabra completa. Igual hacemos un linea = socket.recv(1000) y solo nos llega la mitad "ho".

Por ello, es importante que en la lectura nos aseguremos de tener mensajes completos y que si nos llega parcialmente, sigamos leyendo hasta tenerlo completo. Esto implica que debemos saber cuándo un mensaje está completo. Y esto implica "delimitar" el mensaje de alguna manera, es decir, que enviemos bytes con los que de alguna forma podamos saber cuando está el mensaje completo y podemos, por tanto, tratarlo.

Aunque conceptualmente es lo mismo, delimitar el mensaje de alguna manera, cuando los mensajes son cadenas de texto suele ser más fácil que cuando son bytes sin más. Para cadenas de texto un delimitador habitual es el retorno de carro. Es decir, debemos leer hasta que encontremos un retorno de carro. Para mensajes de datos binarios (bytes) el asunto puede complicarse, ya que quizás no haya ningún byte simple que podamos usar como delimntador del mensaje puesto que cualquier byte podría formar parte también de los datos que queremos transmitir.

Veamos un par de ejemplos, uno para delimitar textos y otro para delimitar mensajes binarios.

Leer líneas de texto en un socket en python[editar]

Afortunadamente y puesto que enviar texto delimitado por retornos de carro es bastante común en cualquier protocolo de comunicación, todos los lenguajes de programación dan soporte para este tipo de lecturas. En el caso de python, podemos encapsular nuestro socket en una variable de tipo fichero de texto. Esto nos da los métodos readline() y write() para leer y escribir cadenas de texto delimitadas por retornos de carro. Si recuerdas de los ejemplos anteriores, leíamos y escribíamos bytes y era necesario llamar a encode() y decode() para convertir de texto a bytes y viceversa.

La forma de encapsular nuestro socket, cliente o servidor, en algo que se parezca a un fichero de texto es llamando a la función makefile() del socket.

socket_file = socket_cliente.makefile("rw")

Admite como parámetros los mismos que en la apertura de ficheros en python. Como en nuestro ejemplo queremos leer y escribir por el socket, ponemos de parámetro "rw" (de read write).

Para escribir en el socket ya no es necesario enviar bytes, podemos enviar una cadena de texto. Debemos poner nosotros en la cadena el retorno de carro. Y un detalle importante. Este "fichero" tieen un buffer interno en memoria donde va acumulando los datos para enviar (nuestras cadenas de texto), pero no las envía realmente hasta que no haya suficientes datos acumulados. Por ello, si es importante que se envíe en el momento, debemos forzarlo llamando a la función flush(). Resumiendo, debemos enviar las cadenas de texto por el socket de la siguiente forma

# Ponemos el retorno de carro al final
socket_file.write("hola\n")
# Forzamos la escritura en el fichero
socket_file.flush()

En cuanto a la lectura, es más sencillo. Basta llamar a la función readline(). Es importante tener en cuenta que la llamada a esta función se quedará bloqueada hasta que llegue una línea completa. Esto era justamente nuestro objetivo, asegurarnos que leemos el mensaje (línea) completa y no se queda a medias. Otro detalle importante a tener en cuenta es que el caracter de retorno de carro \n forma parte de la cadena, al final de ella. Nos interesa en general quitarlo, por lo que llamamos a la función rstrinp(). La función rstrip() sin parámetros elimina todo tipo de caracteres "en blanco" al final de la cadena, como espacios, tabuladores, retornos de carro, etc. Si en nuestros mensajes no hay al final este tipo de caracteres, podemos llamar a rstrip() sin más. Pero si hay espacios o tabuladores al final de la cadena que queramos conservar, es importante poner como parámetro \n asegurándonos que sólo elimina esos caracteres.

line = s_file.readline().rstrip('\n')

Puedes ver el ejemplo completo de servidor y cliente, con estas modificaciones, en servidor_line.py y cliente_line.py

Leer mensajes binarios en un socket en python[editar]

Con un mensaje binario, en bytes, es más complejo. Podemos pensar, por ejemplo, en usar el byte 0x00 (cero) para delimitar mensajes. Pero ¿en nuestros mensajes, como parte de los datos, puede haber bytes 0x00?. Si la respuesta es negativa, que no puede haber bytes 0x00 en nuestros datos, podemos usar el 0x00 para delimitar mensajes. Pero si la respuesta es que sí puede haberlos, el 0x00 no puede ser el delimitador del mensaje. Quien dice 0x00 dice cualquier otro byte o cunjunto de bytes que se nos ocurra.

Así que es habitual buscar algún mecanismo más complicado y más seguro. Cuando se envían datos binarios por sockets, suele enviarse un conjunto de bytes como "cabecera" que nos ayude a identificar el mensaje y otro conjunto de bytes conocidos como "payload" que son los datos reales que queremos enviar e incluso detrás de los datos algunos bytes más como "cola" del mensaje.

La cabecera puede ser todo lo complicada que queramos, pero una forma relativamente estándar consiste en enviar los bytes de la siguiente forma:

  • Un byte "mágico" o número mágico, siempre el mismo, que es el inicio del mensaje. Puede ser el que queramos, pero siempre el mismo. Por ejemplo, 0x40, o bien 0x33 o el que más rabia nos dé. Incluso más de un byte, como por ejemplo 0xCA 0xFE 0xBA 0xBE. (CAFE BABE o cafecito).
  • dos bytes que indiquen la longitud de los datos que van detras, cuántos bytes hay de "payload" o datos útiles. Por supuesto, estos bytes de longitud pueden ser solo uno si sabemos que nuestros mensajes siempre serán menos de 256 bytes o más de dos bytes si prevemos que hay mensajes muy largos. Pero una vez lo decidamos, siempre el mismo número de bytes indpendientemente de la longitud del payload.
  • Los bytes de datos en sí, cuyo número de bytes debe coincidir con lo que dicen los dos bytes anteriores.
  • dos bytes que sean un "checksum" de los datos de payload. Este checksum no es más que una "suma" de los bytes de datos con alguna regla para que esa suma no supere los dos bytes. Igual que con la longitud, podemos hacer un checksum de un solo byte o de más de dos, a nuestro gusto. Pero como antes, una vez elegida la longitud de nuestro checksum, debemos respetarla en todos los mensajes.

En forma de imagen

No vamos a hacer el código de lectura aquí para no complicar demasiado esta lección. Pero básicamente los pasos a seguir serían

  • Leer del socket los bytes uno a uno hasta encontrar el número mágico.
  • Leer los dos bytes de longitud para saber la longitud de los datos, digamos 20 bytes.
  • Leer tantos bytes como nos haya dicho el cambo longitud, es decir, 20 bytes. Esto será nuestro mensaje.
  • Calcular el checksum del payload.
  • Leer el checksum del socket y compararlo con el calculado.

Para realizar este procso correctamente, es importante que todo lo que leamos lo vayamos guardando en un buffer (array) en memoria y lo mantengamos hasta tener el mensaje completo o descartar bytes erróneos. En concreto, teneiendo todo lo leído en memoria, lo que tendríamos que hacer en caso de que la comprobación de checksum coincida o no

    • Si coincide, todo correcto y podemos emepezar a tratar el payload. En el array de memoria eliminamos el byte magico, la longitud, el payload y el checksum, pero conservarmos los bytes que hayamos podido leer después del checksum para poder buscar el siguiente mensaje.
    • Si no coincide, descartar el primer byte número mágico que leímos del buffer de memoria y volver a intentarlo a partir del segundo bytes del buffer de memoria.

Un detalle. Un socket TCP/IP garantiza que los datos llegan y que llegan bien. Así que en principio podríamos fiarnos de que no vamos a perder bytes y que estos llegarían bien. Nos bastaría sólo con los bytes de longitud (para saber cuandos datos leer) y los datos. Como no perdemos bytes y llegarán bien, no es necesario buscar el número mágico ni verificar un checksum. Según abramos el socket y empecemos a leer, lo primero que nos llegará será una longitud, luego los datos, luego otra longitud y así sucesivamente. Cosas como el número mágico y los checksum son necesarios con otras comunicaciones menos fiables que no nos garanticen que los datos llegan o que llegan correctamente, por ejemplo, un puerto serie o un socket UDP en el que un mensaje venga partido en varios paquetes UDP.

Ponemos un hilo al servidor[editar]

Como comentamos antes, la forma correcta de hacer el servidor es haciendo que cree un nuevo hilo para atender a cada cliente. De esta forma, el servidor podrá seguir aceptando clientes mientras atiende simultáneamente a los ya conectados, un hilo para cada cliente.

Para la creación del hilo en python usaremos la clase Thread del módulo threading de python. Nos basta hacer una clase Cliente hija de Thread y ponerle el método run(). En ese método haremos el bucle para atender al cliente. Esta clase necesitará los datos del cliente así como el socket que debe atender. Por ello, metemos ambos parámetros en el constructor de la clase para almacenarlos. El código puede ser este

import threading
...
# Clase con el hilo para atender a los clientes.
# En el constructor recibe el socket con el cliente y los datos del
# cliente para escribir por pantalla
class Client(threading.Thread):
    def __init__(self, socket_cliente, datos_cliente):
        # LLamada al constructor padre, para que se inicialice de forma
        # correcta la clase Thread.
        threading.Thread.__init__(self)
        # Guardamos los parametros recibidos.
        self.socket = socket_cliente
        self.datos = datos_cliente

    # Bucle para atender al cliente.
    def run(self):
        # Bucle indefinido hasta que el cliente envie "adios"
        # Lo hacemos con with socket para que se cierre automaticamente cuando terminemos
        with self.socket:
            seguir = True
            while seguir:
                # Espera por datos
                peticion = self.socket.recv(1000).decode()

                # Si recibimos cadena vacía, el socket ha sido cerrado por el cliente
                if not peticion:
                    seguir = False

                # Contestacion a "hola"
                if ("hola" == peticion):
                    print
                    str(self.datos) + " envia hola: contesto"
                    self.socket.send("pues hola".encode())

                # Contestacion y cierre a "adios"
                if ("adios" == peticion):
                    print
                    str(self.datos) + " envia adios: contesto y desconecto"
                    self.socket.send("pues adios".encode())
                    self.socket.close()
                    print
                    "desconectado " + str(self.datos)
                    seguir = False

Vemos que en el método run() el código es prácticamente igual al que teníamos en el servidor socket de python para atender al cliente. En dicho servidor, ahora solo tenemos que instanciar una clase Cliente cada vez que se conecte un cliente, pasándole como parámetros el socket del cliente y sus datos. Una vez tengamos la instancia, lanzamos el hilo llamando al método start()

   # bucle para atender clientes
   while 1:
      # Se espera a un cliente
      socket_cliente, datos_cliente = server.accept()
      # Se escribe su informacion
      print "conectado "+str(datos_cliente)
      # Se crea la clase con el hilo
      hilo = Cliente(socket_cliente, datos_cliente)
      # y se arranca el hilo
      hilo.start()

Fijate en un detalle. En este bucle llamamos a server.accept() y ahí, cuando se conecta un cliente, es cuando se crea el socket cliente para atender. El detalle es que aquí no hemos puesto la estructura with para asegurarnos que se cierra el socket cuando terminemos, sino que dicha estructura with la hemos puesto dentro de la clase Cliente. ¿Motivo?. En este trozo de código estamos lanzando un hilo y la llamada a hilo.start() lanzará el hilo del cliente y nos devolverá el control inmediatamente. Si hubieramos metido todo esto en un with socket_cliente:, se nos cerraría el socket. Así que la clase Cliente estaría en un bucle con un socket cerrado. Así que el cierre (la estructura with) la hemos metido dentro de la clase Cliente. Esta clase sabrá mejor que nadie cuando hay que cerrar el socket porque ya se ha intercambiado toda la información que requiere nuestra aplicación.

Ahora, si arrancamos el servidor, podemos arrancar a la vez varios clientes y todos ellos serán atendidos simultáneamente. Aquí tienes el código completo de thread_server.py

Sockets no bloqueantes en python[editar]

Hemos visto que las llamadas a accept(), connect(), recv() y send() pueden quedarse bloqueadas hasta que haya algún dato disponible o alguien acepte los datos que estamos intentando enviar. Es posible hacer que estas llamadas no se bloqueen indefinidamente, sino que tras un tiempo de espera, o incluso inmediatamente, salgan con un error en caso de no haber datos disponibles. Esto suele ser útil si esperamos que en el otro lado nos envíen un mensaje cada cierto tiempo y dejamos de recibirlo. O si esperamos que se conecte un cliente y no llega dicha conexión.

Para ello, basta con llamar a socket.setblocking(False) si queremos que las llamadas no sean bloqueantes y vuelvan inmediatamente si no hay datos disponibles, o bien socket.settiemout() si queremos que esté un tiempo bloqueado. En este segundo caso, si no se reciben datos, saltará una excepción TimeoutError.

Veamos el ejemplo del socket servidor TCP/IP con lectura no bloqueante de cliente. El código, que tienes en noblocking_server.py puede ser este

import socket

if __name__ == '__main__':
    # Se prepara el servidor
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(("", 8000))
    server.listen(1)
    print("Esperando clientes...")

    # bucle para atender clientes
    while True:

        # Se espera a un cliente
        socket_cliente, datos_cliente = server.accept()

        # Se escribe su informacion
        print("conectado " + str(datos_cliente))

        # Hacemos que el socket sea no bloqueante
        # socket_cliente.setblocking(False)
        socket_cliente.settimeout(1)

        # Bucle indefinido hasta que el cliente envie "adios"
        seguir = True
        while seguir:
            # Espera por datos
            try:
                peticion = socket_cliente.recv(1000)
            except TimeoutError:
                print ("Timeout. Cerramos socket")
                seguir = False
            else:
                print('Recibido ' + str(peticion))

El accept() se bloquea como siempre, no hemos tocado nada ahí. Si lo hemos hecho en el socket que obtenemos cuando un cliente se conecta. Justo después de la conexión hemos llamado a socket_cliente.settimeout(1) para hacer que las lecturas esperen 1 segundo y salte el error TimeoutError en caso de no llegar datos. Así que la lectura recv() debemos meterla en un bloque Try Exception. Si salta la excepción, es que no nos han llegado datos en un tiempo de espera de un segundo, así que cerramos el socket. En caso de que si lleguen datos, los sacamos por pantalla y continuamos el bucle leyendo el siguiente paquete de datos.

Para probar esto, hacemos un cliente noblocking_client.py cuyo código es

import socket
import time

if __name__ == '__main__':
    # Se establece la conexion
    counter = 0
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect(("localhost", 8000))

        while counter<5:
            # Espera de 2 segundos
            time.sleep(0.5)
            s.send("Message".encode())
            counter += 1

        time.sleep(10)

Estable conexión, pone un contador para enviar cinco mensajes con una espera de 0.5 segundos entre ellos y tras ello no envía más, espera 10 segundos para terminar el programa. Si arrancamos primero el servidor y luego el cliente, veremos que llegan los cinco mensajes espaciados medio segundo y que después de esto, el servidor decide cerrar el socket e ir a atender a otro cliente.

Sockets con select() en python[editar]

Si dejamos los sockets bloqueantes, es decir, las llamadas accept(), connect(), recv() y send() pueden quedarse bloqueadas, si queremos que nuestro código no se pare, debemos lanzar hilos encargados de atender sockets y que sean esos hilos los que se quedan bloqueados. Por ejemplo, suele ser habitual

  • Un hilo para aceptar conexiones. Cuando alguien se conecta, hay que lanzar un hilo separado para atender la conexión.
  • Para el envío depende. Normalmente un envío no se queda bloqueado. El sistema operativo tiene un buffer de memoria y cuando enviamos algo por el socket, realmente se lo estamos dando al sistema operativo para que lo meta en ese buffer y ya lo enviará cuando pueda. Nuestra llamada a send() suele volver inmediatamente. El problema es si el que tiene que recibir ese mensaje no lo lee porque está haciendo otras cosas. En ese caso, el buffer del sistema operativo se irá llenado de mensajes pendientes de receptcionar y finalmente se llenará. En ese momento, nuestra siguiente llamada a send() se quedará bloqueada hasta que alguien retire algún mensaje. Por ello, suele ser conveniente protegerse de esta situación. Es buena idea tener un hilo que haga los send() de forma que sea ese hilo el que se quede bloqueado si nadie lee nuestros mensajes.

Para evitar lanzar tanto hilo, hemos visto la opción de hacer que los socket no sean bloqueantes. Sin embargo, esta opción suele ser bastante engorrosa de programar. Después de cualquiera de estas llamadas tienes que poner código para verificar si se ha realizado con éxito o ha saltado por timeout. Y código para ambos casos. Y reintentos en caso de timeout. Etc, etc, etc.

Hay una tercera opción que es la que vemos en este apartado. Phyton y casi cualquier lenguaje de programación, tiene un módulo select con una función select(). A esta función se le pasan todos los sockets que está manejando nuestro programa y la llamada se queda bloqueada hasta que algún socket tenga algo de interés. La función nos devuelve los sockets en los que ha sucedido algo que requiera nuestra atención. De esta forma, en principio, con un solo hilo podemos tratar todos los sockets.

En python, la llamada select quedaría de la siguiente forma

import select
...
readables, writables, exceptions = select.select(inputs, outputs, all)

Le pasamos tres parámetros, aunque hay un cuarto opcional

  • inputs Es una lista con los sockets de los que esperamos mensajes.
  • outputs Es una lista con los sockets por los que queremos enviar mensajes. Es importante colocarlos aquí sólo cuando tenemos un mensaje para enviar y no colocarlos siempre. Vemos el motivo más abajo.
  • all Es una listga con todos los sockets que tiene nuestro programa en los que puede haber algún error, como un cierre de conexión inesperado.
  • El cuarto parámetro que no hemos puesto sería un timeout opcional. La llamada a select() se quedará bloqueada hasta que en alguno de los sockets suceda algo. Con este timeout si no sucede nada, pasado el timeout, la función select() terminará.

El motivo de no poner en outputs todos los sockets y poner sólo aquellos en los que tenemos algo concreto que enviar, es que la funión select() terminará si se puede escribir en esos socket, independientemente de si tenemos o no algo para enviar. Por ello, lo normal es que select() termine inmeditamente, puesto que salvo que se haya llenado el buffer del sistema oeprativo que comentamos antes, siemper será posible escribir.

Vamos con un ejemplo. Nuestro socket servidor de ejemplo, pero hecho con esta función.

import socket
import select

if __name__ == '__main__':
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setblocking(False)
    server.bind(("", 8000))
    server.listen(1)

    inputs = [server]
    outputs = []
    output_messages = {}

    while True:
        readables, writables, exceptions = select.select(inputs, outputs, inputs)

        for socket in readables:
            if socket is server:
                print("Aceptado cliente")
                client, client_data = server.accept()
                client.setblocking(False)
                inputs.append(client)
            else:
                message = socket.recv(1024).decode()
                if "hola" == message:
                    output_messages[socket] = "Pues Hola"
                    outputs.append(socket)
                if "adios" == message:
                    output_messages[socket] = "Pues Adios"
                    outputs.append(socket)
                if "" == message:
                    inputs.remove(socket)
                    socket.close()

        for socket in writables:
            socket.send(output_messages[socket].encode())
            outputs.remove(socket)

Primero abrimos nuestro socket servidor de la forma habitual: socket.socket(), bind() y listen(). Es importante, para poder usar select() ponerlos en modo no bloqueante, por ello también la llamada a setblocking().

Luego creamos tres listas:

  • inputs para los sockets de los que esperamos leer algo. La inicializamos con el socket servidor server puesto que la llamada a accept() se considera de lectura.
  • outputs para los sockets a los que tenemos algo que enviar, de momento vacío.
  • output_messages un diccionario (no una lista) para los mensajes que queremos usar. Es un diccionario porque usaremos como clave el socket por el queremos enviar el mensaje y el valor será el mensaje en sí.

A continuación un bucle infinito con la llamada a select(). Esta llamada se quedará bloqueada hasta que suceda algo. La primera vez, será hasta que se conecte un cliente. Detrás del select() el código para tratar lo que haya pasado.

En readables nos devolverá una lista de sockets en los que tenemos pendiente algo para leer. Puede ser que se ha conectado un cliente. Puede ser que un cliente ya conectado nos haya enviado un mensaje. Así que un if

  • Si el socket es el socket servidor server, es que se ha conectado un cliente. Así que hacemos el accept() con la seguridad de que no se va a quedar bloqueado. Hacemos que el cliente también sea no bloqueante y lo añadimos a la lista de inputs.
  • Si el socket no es el socket servidor, es que es un cliente que nos ha enviado un mensaje. Así que llamamos a recv() para leer el mensaje y lo analizamos:
    • Si es "hola", ponemos en el diccionario de mensajes a enviar la contestación, usando como clave el socket y como valor el mensaje: output_messages[socket] = "Pues Hola". En vez de esto, podríamos optar aquí por enviar la respuesta directamente, pero entonces no tenemos garantía de que send() no se quede bloqueado, nadie lo ha verificado. Añadimos el socket a la lista de outputs, puesto que ya hay un mensaje para enviar por él.
    • Si es "adios", mismo tratamiento que con "hola", pero con otro mensaje distinto.
    • Si es "", es que el cliente ha cerrado la conexión, así que eliminamos este socket de inputs puesto que ya no tiene interés y lo cerramos.

En writables nos devolverá una lista de sockets en los que tenemos algo que escribir (porque los añadimos a outputs) y podemos hacerlo sin riesgo de quedarnos bloqueados. Así que bucle para recorrer dicha lista, recoger el mensaje de output_messages y enviarlo. Una vez enviado, eliminamos el socket de outputs puesto que ya no tenemos nada más que enviar.

Y listo, nueva vuelta del bucle y nueva llamada a select(), esta vez con los inputs y outputs modificados respecto a la llamada anterior: un nuevo cliente conectado o un nuevo mensaje que enviarle a alguien.

Anterior: 13 - Curso de Python - Leer y escribir ficheros en python -- Índice: Curso de Python -- Siguiente: Próximamente.