domingo, 26 de mayo de 2024

Redes con Python: Programación de Sockets para la Comunicación. 3.- Streaming

El protocolo TCP transmite datos a través de una conexión. Las funciones de socket send y recv sugieren que los mensajes individuales se envían y reciben. Sin embargo, esto no es cierto.

Ahora llegamos al principal problema de los socket, send y recv operan en los buffers de red. Ellos no manejan necesariamente todos los bytes que se les entrega porque su enfoque principal es manejar los buffers de red. En general, ellos devuelven los datos cuando los buffers de red se han llenado (send) o vaciado (recv). Luego nos dicen cuantos bytes han manejado. Es nuestra responsabilidad llamarlos nuevamente hasta que el mensaje haya sido tratado por completo.

Cuando recv retorna 0 bytes significa que el otro lado ha cerrado (o está en el proceso de cerrar) la conexión. No recibirás más datos de esta conexión. Nunca.

Un protocolo como HTTP usa un socket para una sola transferencia. El cliente manda una petición, luego lee la respuesta. Eso es todo. El socket es descartado. Esto significa que un cliente puede detectar el final de la respuesta al recibir 0 bytes.

Pero si planeas reusar el socket para más transferencias, tienes que darte cuenta que no hay una señal de fin de conexión (EOT) en un socket. Repito: si la llamada a send o recv de un socket retorna después de manejar 0 bytes, la conexión se ha interrumpido. 

Enviar y recibir flujos de bytes

Cada vez que un socket envía un mensaje, se envía una serie de bytes como un flujo.


flujo de datos


TCP garantiza que todos los bytes llegarán al socket receptor en el orden correcto.

Sin embargo, no hay garantía de que el número de bytes devueltos por una llamada a recv sea el mismo número de bytes enviados usando send

Podría ser que múltiples mensajes se hayan enviado usando send, los cuales se recuperan mediante una sola llamada a recv.

También podría ser que se necesiten múltiples llamadas a recv para recuperar todos los bytes que se enviaron en un solo mensaje.

Me explico. Cuando el cliente usa s.recv(), es posible que reciba solamente un byte, b"h" del mensaje b"Hola, Gracias por conectarte" enviado por el servidor. El argumento buffersize de 1024 que hemos usado para recibir datos, es la cantidad máxima de datos que se pueden recibir de una sola vez, lo que no significa que se devuelvan exactamente esos 1024 bytes.

Esto crea dos problemas prácticos para los programadores:

1. Cómo determinar el final de un mensaje y el comienzo de otro

2. Cómo reconstruir un solo mensaje a partir de múltiples conjuntos de bytes

Cuando se trata de mensajes de red que contienen solo texto, un enfoque común es incluir un carácter no imprimible al final de cada mensaje, como un retorno de carro \n o un carácter de Fin de Transmisión (EOT) \4.

En las siguientes secciones crearemos dos funciones que hacen uso de un carácter de terminación.

Más adelante veremos otras opciones para tratar estos problemas al enviar datos binarios (no texto).

Terminar un mensaje

Cada mensaje que envíes debe terminar con el carácter de terminación \n.
Revisa la función enviar_texto a continuación y observa que:

  • Espera un socket y el texto a enviar como parámetros.
  • Añade el carácter \n al mensaje antes de enviarlo.

def enviar_texto(client_socket, texto):
    texto = texto + "\n"
    data = texto.encode()
    client_socket.send(data)

Agrega la función enviar_texto a tu programa socket_server.py. Reemplaza la llamada a .send con el código para llamar a la función enviar_texto.

Código antiguo:

mensaje = "Hola, ¡Gracias por conectarte!"
data = mensaje.encode()
client_socket.send(data)

Código nuevo:

mensaje = "Hola, ¡Gracias por conectarte!"
enviar_texto(conexion_socket, mensaje)


Obteniendo mensajes completos

Ahora puedes crear una función complementaria `obtener_texto` que usará el carácter \n para determinar si se ha recibido un mensaje completo.

El proceso de asegurar que se reciban mensajes completos es más complicado, ya que tiene que verificar si:

1. Se han devuelto múltiples mensajes de una sola llamada a `recv`.

2. Se requieren múltiples llamadas a `recv` para obtener un solo mensaje.


La función `obtener_texto` a continuación hace esto mediante:

- Llamadas continuas para recibir datos (`recv`) desde un socket pasado como parámetro.

        - Agregar cualquier dato recibido a un buffer.

        - Verificar si el buffer contiene un carácter \n y, por lo tanto, un mensaje completo.

        - Devolver el mensaje completo.

        - Eliminar el mensaje devuelto del buffer.

        - Verificar si el buffer contiene más mensajes completos.

def obtener_texto(client_socket):
    buffer = ""
    socket_abierto = True
    
    while socket_abierto:
        # Lee cualquier dato desde el socket
        data = client_socket.recv(1024)
        # Si no se reciben más datos el socket debe cerrarse.
        if not data:
            socket_abierto = False
        # Añadir datos al buffer.
        buffer = buffer + data.decode()
        # Si hay un caracter de fin de mensaje en el buffer
        posicion_caracter_fin = buffer.find("\n")
        # Si el valor es más grande que -1, un /n debe existir.
        while posicion_caracter_fin > -1:
            # obtiene el mensaje del buffer
            mensaje = buffer[:posicion_caracter_fin]
            # eliminamos el mensaje del buffer
            buffer = buffer[posicion_caracter_fin + 1:]
            # yield en el mensaje (lo explicamos luego)
            yield mensaje
            # ¿Hay otro caracter de fin de mensaje el buffer
            posicion_caracter_fin = buffer.find("\n")

La función obtener_texto utiliza una función generadora yield para devolver mensajes completos uno a la vez. Hay más información sobre cómo funcionan yield y las funciones generadoras en el anexo al final de este post.

Para usar la función obtener_texto en el programa client_socket.py necesitas reemplazar este código:

client_socket.py

#...
data = client_socket.recv(1024)
mensaje = data.decode()
print(mensaje)

Podrías usar la función obtener texto dentro de un bucle for, que se repetirá hasta que se complete la función del generador obtener_texto cuando el socket esté cerrado:

for mensaje in obtener_texto(client_socket):
    print(mensaje)

O podrías usar next para llamar a la función  obtener_texto una vez y obtener un único mensaje.

mensaje = next(obtener_texto(client_socket))
print(mensaje)

Experimenta con ambas opciones y asegúrate de que entiendes la diferencia entre ambas.


EJERCICIO PRÁCTICO. APLICACIÓN DE CHAT.

En este ejercicio vamos a construir una aplicación de Chat usando lo que hemos visto hasta ahora. Vamos a crear un solo programa que se pueda utilizar a la vez como cliente y servidor y que nos permita crear una conversación entre dos máquinas. 

Puedes llamar al programa como quieras, yo lo llamará server_client_chat.py. Vamos a empezar importando la librería socket y las funciones que hemos visto antes y que nos servirán tanto para enviar como para recibir texto, las funciones obtener_texto y enviar_texto.

server_client_chat.py

import socket

TCP_IP = "0.0.0.0"  # IP address to bind to
TCP_PORT = 8081  # Port to bind to or connect to
BUFFER_SIZE = 1024  # Buffer size for receiving data
END_MESSAGE = "%end%"  # Special message to indicate end of communication

def enviar_texto(sending_socket, texto):
    """Envía texto a través de un socket, añadiendo una nueva línea al final."""
    texto += "\n"
    data = texto.encode()
    sending_socket.send(data)

def obtener_texto(receiving_socket):
    """Generador que recibe texto desde un socket y devuelve mensajes completos."""
    buffer = ""
    while True:
        data = receiving_socket.recv(BUFFER_SIZE)
        if not data:
            break
        buffer += data.decode()
        while "\n" in buffer:
            mensaje, buffer = buffer.split("\n", 1)
            yield mensaje

Creamos el menú para que el usuario pueda elegir entre la opción 1, que el programa actúe como un servidor u opción 2, que se comporte como un cliente. Si se escoge cualquier otra opción el programa volverá a mostrar el menú inicial.

Añade el siguiente código al final del código anterior.

server_client_chat.py

# ...
def main():
    """Función principal que selecciona modo de operación: servidor o cliente."""
    while True:
        ser_cli = input("Elige: iniciar un servidor (1) o conectarte a uno - cliente (2) > ")
        if ser_cli in ('1', '2'):
            break

    if ser_cli == '1':
        iniciar_servidor()
    else:
        conectar_cliente()

    print("¡Programa terminado! Sockets cerrados.")

if __name__ == "__main__":
    main()

Vamos a desarrollar el código para la opción del servidor (conexión, enlace, escucha, aceptación) que se ejecutará a través de la función iniciar_servidor()

- Conexión. Lo primero es crear un objeto socket usando la biblioteca que importamos previamente (Creamos un socket TCP  de flujo).

- Enlace. Cuando estamos intentando configurar un servidor, necesitamos que el socket se asocie a una dirección IP y a un número de puerto especifico para que pueda recibir las conexiones. La función "bind" se utiliza para esta tarea.

- Escucha. Llamamos al método listen del socket. El programa se parará hasta que se conecte un cliente.

- Aceptación. Si se acepta la conexión mostraremos un mensaje en pantalla con la dirección IP y el puerto donde se realiza la conexión.

Crearemos un mensaje para que primero, el cliente sepa que se ha realizado la conexión y luego sepa como finalizar la conexión. 

server_client_chat.py

def iniciar_servidor():
    """Inicializa el servidor y maneja las conexiones entrantes."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as servidor_socket:
        servidor_socket.bind((TCP_IP, TCP_PORT))
        servidor_socket.listen()
        print(f"Servidor vinculado a {TCP_IP}:{TCP_PORT}\nEsperando conexión...")

        conexion, client_address = servidor_socket.accept()
        with conexion:
            print(f"Cliente conectado desde {client_address[0]}:{client_address[1]}")
            enviar_texto(conexion, "¡Bienvenido al servidor! Teclea %end% para finalizar...")
            manejar_comunicacion(conexion)

Una vez que tenemos hecha la parte del servidor pasemos a ver como desarrollar la parte del cliente. Es igual que la hemos usado previamente, con la peculiaridad de que incorporamos la posibilidad de que el usuario introduzca la IP del servidor.

server_client_chat.py

# ...
   def conectar_cliente():
    """Conecta al cliente a un servidor especificado."""
    serverIP = input("Introduce la dirección IP (e.g. 127.0.0.1) o deja en blanco para LOCALHOST: ") or "127.0.0.1"
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as conexion:
        conexion.connect((serverIP, TCP_PORT))
        print(f"Conectado al servidor {serverIP}:{TCP_PORT}")
        manejar_comunicacion(conexion)

Crearemos otra función que maneje las comunicaciones entre el servidor y el cliente. La conexión entre ambos se finalizará cuando el mensaje enviado sea "%end%". 

Luego usaríamos otro conjunto de instrucciones try - except-finally para cerrar las conexiones.

server_client_chat.py

#...
def manejar_comunicacion(conexion):
    """Maneja la comunicación bidireccional entre cliente y servidor."""
    try:
        for mensaje in obtener_texto(conexion):
            print(">> ", mensaje)
            if mensaje == END_MESSAGE:
                break
            nuevo_mensaje = input("Escribe un mensaje > ")
            enviar_texto(conexion, nuevo_mensaje)
    except Exception as e:
        print(f"Error en la comunicación: {e}")
    finally:
        print("Conexión cerrada.")

En resumen:

Descripción del flujo principal

  1. Selección de Modo (Servidor o Cliente):

    • El usuario elige si quiere iniciar un servidor (1) o conectarse a uno como cliente (2).
  2. Servidor:

    • Se crea un socket de servidor (servidor_socket).
    • Se vincula (bind) el socket a la IP y el puerto definidos.
    • Se pone el socket en modo de escucha (listen) para aceptar conexiones entrantes.
    • Cuando un cliente se conecta, se acepta la conexión (accept), creando un nuevo socket (conexion) para la comunicación con el cliente.
    • Se envía un mensaje de bienvenida al cliente.
  3. Cliente:

    • Se crea un socket de cliente (conexion).
    • El usuario ingresa la dirección IP del servidor al que desea conectarse.
    • El socket se conecta al servidor usando la IP y el puerto definidos.
  4. Comunicación:

    • Se entra en un bucle donde el cliente y el servidor intercambian mensajes.
    • Los mensajes recibidos se muestran en pantalla y el usuario puede enviar nuevos mensajes.
    • Si se recibe el mensaje %end%, el bucle se rompe y se cierran los sockets.
  5. Cierre:

    • Se intentan cerrar los sockets tanto en el cliente como en el servidor para asegurar que los recursos se liberen correctamente.
Así quedaría el código final:

server_client_chat.py

import socket

TCP_IP = "0.0.0.0"  # IP address to bind to
TCP_PORT = 8081  # Port to bind to or connect to
BUFFER_SIZE = 1024  # Buffer size for receiving data
END_MESSAGE = "%end%"  # Special message to indicate end of communication

def enviar_texto(sending_socket, texto):
    """Envía texto a través de un socket, añadiendo una nueva línea al final."""
    texto += "\n"
    data = texto.encode()
    sending_socket.send(data)

def obtener_texto(receiving_socket):
    """Generador que recibe texto desde un socket y devuelve mensajes completos."""
    buffer = ""
    while True:
        data = receiving_socket.recv(BUFFER_SIZE)
        if not data:
            break
        buffer += data.decode()
        while "\n" in buffer:
            mensaje, buffer = buffer.split("\n", 1)
            yield mensaje

def iniciar_servidor():
    """Inicializa el servidor y maneja las conexiones entrantes."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as servidor_socket:
        servidor_socket.bind((TCP_IP, TCP_PORT))
        servidor_socket.listen()
        print(f"Servidor vinculado a {TCP_IP}:{TCP_PORT}\nEsperando conexión...")

        conexion, client_address = servidor_socket.accept()
        with conexion:
            print(f"Cliente conectado desde {client_address[0]}:{client_address[1]}")
            enviar_texto(conexion, "¡Bienvenido al servidor! Teclea %end% para finalizar...")
            manejar_comunicacion(conexion)

def conectar_cliente():
    """Conecta al cliente a un servidor especificado."""
    serverIP = input("Introduce la dirección IP (e.g. 127.0.0.1) o deja en blanco para LOCALHOST: ") or "127.0.0.1"
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as conexion:
        conexion.connect((serverIP, TCP_PORT))
        print(f"Conectado al servidor {serverIP}:{TCP_PORT}")
        manejar_comunicacion(conexion)

def manejar_comunicacion(conexion):
    """Maneja la comunicación bidireccional entre cliente y servidor."""
    try:
        for mensaje in obtener_texto(conexion):
            print(">> ", mensaje)
            if mensaje == END_MESSAGE:
                break
            nuevo_mensaje = input("Escribe un mensaje > ")
            enviar_texto(conexion, nuevo_mensaje)
    except Exception as e:
        print(f"Error en la comunicación: {e}")
    finally:
        print("Conexión cerrada.")

def main():
    """Función principal que selecciona modo de operación: servidor o cliente."""
    while True:
        ser_cli = input("Elige: iniciar un servidor (1) o conectarte a uno - cliente (2) > ")
        if ser_cli in ('1', '2'):
            break

    if ser_cli == '1':
        iniciar_servidor()
    else:
        conectar_cliente()

    print("¡Programa terminado! Sockets cerrados.")

if __name__ == "__main__":
    main()

Vamos a probarlo. Abre dos ventanas del terminal. En una, ejecuta el archivo server_client_chat.py con la opción 1 y en la otra el mismo archivo con la opción 2. Prueba a enviar mensajes. Primero en uno y luego en el otro. Cuando quieras terminar envia el mensaje %end%.

comunicación cliente servidor


Anexo:

YIELD MENSAJE

Se usa "yield" para devolver los datos desde una función especial denominada generador. 

Los generadores son muy útiles cuando necesitas devolver una gran cantidad de datos o simplemente desconoces cuantos datos debes devolver. Hemos usado un generador en la función obtener_texto porque desconocemos cuantos mensajes están disponibles para obtener.

Hay dos diferencias fundamentales entre las funciones normales y los generadores:

- Un generador siempre devuelve un iterador en vez de un valor individual, por ejemplo una lista de uno o más elementos.

- Cuando se utiliza yield, la función no termina sino que recuerda donde se ha detenido. La siguiente vez que se la llama, continuará en el punto donde termino sin tener que volver a empezar desde el principio. 

Los generadores deben llamarse como parte de un bucle for o usando el comando next. 

Puedes encontrar más información y ejemplos en el siguiente enlace.

El código de este post se encuentra en este enlace de GITHUB.

Próximo Post:

4.- Manejando múltiples conexiones.


viernes, 24 de mayo de 2024

Redes con Python: Programación de Sockets para la Comunicación. 2.- Enviar y Recibir datos.

En el post anterior hemos conseguido conectar dos sockets, así que en este nos vamos a centrar en como pueden comunicarse, enviando y recibiendo datos. Lo vamos a conseguir utilizando las funciones del modulo socket "send" (enviar) y "recv" (recibir).

Para comprender mejor como funciona esto, en el siguiente gráfico podemos ver las llamadas API del socket y el flujo de datos para TCP:

TCP socket flow


Ya estamos listos para crear una implementación simple de lo anterior. Empezaremos viéndolo desde el punto de vista del servidor y del cliente.

SERVIDOR.

El servidor escuchará las conexiones entrantes y los mensajes que envíe el cliente. Lo primero que haremos será crear un nuevo servidor, que será muy parecido al del post anterior. Lo llamaré socket_server.py. De momento lo único que hará será esperar una conexión y devolver un mensaje de bienvenida al cliente.

socket_server.py

import socket

HOST = '127.0.0.1' # Standard loopback interface address (localhost)
PORT = 8081 # Port to listen on (non-privileged ports are > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:

    # Vincula el socket a una dirección y puerto específicos
    s.bind((HOST, PORT))

    # Escucha conexiones entrantes (el argumento especifica el tamaño máximo de la cola de conexiones)
    s.listen()

    print("Servidor escuchando en el puerto 8081...")

    # Aceptar una conexión
    client_socket, client_address = s.accept()
    with client_socket:
        print(f"Conexión aceptada de {client_address}")
        
        mensaje = "Hola,¡Gracias por conectarte!"
        data = mensaje.encode()
        client_socket.send(data)

Vamos a pararnos un momento a comentar algunas cosas.

1.- Nuestro programa necesita codificar el mensaje para poder ser enviado. 

    data = mensaje.encode()

    La función encode() utiliza el estándar de codificación de caracteres utf-8 para convertir cada carácter, es decir cada letra o símbolo, que son datos de tipo string, en una serie de bytes que son los que se pueden enviar a través de la red. Puedes codificar los datos a enviar utilizando otro tipo de codificación. Por ejemplo:

    data = mensaje.encode("base64")

Es útil para enviar datos que no sean texto.

2.- Una vez que los datos han sido codificados por el programa, pueden ser enviados usando el client_socket.

    client_socket.send(data)

    client_socket es el socket que está emparejado y concectado con el socket del cliente. Ten en cuenta que esto es diferente del server_socket, que se usa para escuchar y aceptar conexiones, pero no para usarlas.

3.- Hemos usado with para que las conexiones se cierren automáticamente y no tener que hacer al final del programa.

client_socket.close()
server_socket.close()


CLIENTE.

Crea un nuevo archivo de Python y llámalo socket_client.py, por ejemplo. Añade el siguiente código.

client_server.py

import socket

HOST = "127.0.0.1"  # El nombre del servidor o IP del mismo
PORT = 8081  # El puerto usado por el servidor

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    client_socket.connect((HOST, PORT))
    data = client_socket.recv(1024)
    mensaje = data.decode()
    print(mensaje)

El código para recibir los datos es muy parecido al de enviarlos.

1.- Una vez establecida la conexión, los datos se reciben desde el client_socket. 

data = client_socket.recv(1024)

Los datos se reciben o leen desde el client_socket usando client_socket.recv(1024). El valor 1024 es el número máximo de bytes que se deberían leer de una sola vez. Es decir creamos un buffer, si más de 1024 bytes han sido enviados, las llamadas posteriores a recv deberían recibir el resto de los datos.


2.- Los datos recibidos son decodificados e impresos.

    mensaje = data.decode()
    print(mensaje)

Los datos recibidos por el socket son un flujo de bits y deben decodificarse en una cadena usando decode() para que puedan ser impresos por pantalla.

En comparación con el servidor el código del cliente es muy sencillo. Crea un objeto socket, usa .connect() para conectarse al servidor y .recv() para recibir el mensaje.

Vamos a probarlo todo. Abre dos terminales y ejecuta el programa servidor y después el programa cliente. Si todo ha ido bien verás una imagen parecida a esta.

servidor y cliente comunicandose

Un error común que te puedes encontrar es cuando una conexión intenta conectarse a un puerto en el que no esta escuchando ningún socket. Si ejecutas client_socket.py sin haber ejecutado previamente server_socket.py tendrás este bonito error:

$ python3 client_socket.py 
Traceback (most recent call last):
  File "/home/chema/Escritorio/socket/2.- Enviando y Recibiendo datos/client_socket.py", line 7, in <module>
    client_socket.connect((HOST, PORT))
ConnectionRefusedError: [Errno 111] Connection refused
Si el puerto usado es erróneo, el servidor no se está ejecutando o si hay un firewall en el camino que bloquee la conexión te encontrarás con este error.

DESAFIO.

Actualmente en el programa del servidor y del cliente solamente se envían datos en una dirección, del servidor al cliente. ¿Puedes modificar el programa de forma que el cliente responda con un mensaje después de recibir el mensaje "Hola,¡Gracias por conectarte!" y el servidor lo reciba y lo muestre por pantalla?

Pista. Puedes encontrar el código tanto del servidor, como del cliente en Pastebin.

Puedes encontrar el código de este post en este enlace de GITHUB

Próximo capitulo 

Redes con Python: Programación de Sockets para la Comunicación. 3.- Streaming


lunes, 20 de mayo de 2024

Redes con Python: Programación de Sockets para la Comunicación. 1.- Usando Sockets con Python.

Python tiene una API que nos permite comunicarnos con otras programas a través de una red. Vamos a empezar viendo como usar esta API para crear un servidor y usar tu navegador para conectarnos a él. Lo primero que vamos a hacer es crear un nuevo programa de Python al que llamaremos server.py

Para poder usar el API tenemos que importar el módulo socket, de esta forma:

server.py

import socket
Esta línea importa el módulo "socket" que nos proporciona acceso a la interfaz de sockets de red de bajo nivel. Nos va a permitir realizar operaciones de red, como establecer conexiones y trasferir datos entre sistemas.

Luego crearemos el socket. Lo llamaré server_socket y tendrá unos parámetros que van a determinar el tipo de protocolo utilizado.

server.py

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

  • socket.socket(): Es la función que crea un nuevo socket.
  • socket.AF_INET: Especifica la familia de direcciones que el socket puede usar. AF_INET significa que el socket utilizará direcciones IPv4. Si quisieras usar direcciones IPv6, usarías AF_INET6.
  • socket.SOCK_STREAM: Especifica el tipo de socket. SOCK_STREAM indica que se va a usar un socket de flujo, lo que significa que se trata de un socket orientado a la conexión y basado en TCP (Transmission Control Protocol). TCP es un protocolo que proporciona una conexión confiable y orientada a la conexión para el intercambio de datos.

En conclusión, hemos creado un socket TCP/IP. A continuación tenemos que vincular ese socket con una dirección y un puerto especifico. 

server.py

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM
server_socket.bind(('0.0.0.0', 8081))
Usaremos la dirección 0.0.0.0 y el puerto 8081. 

La dirección 0.0.0.0 se utiliza en la función bind() de los sockets para indicar que el socket debe aceptar conexiones en todas las interfaces de red disponibles del sistema. Es una dirección especial que no se refiere a una sola dirección IP específica, sino a todas las direcciones IP configuradas en el dispositivo. Vamos a explorar esto con más detalle:

Contexto de Uso de 0.0.0.0

  • 0.0.0.0: Acepta conexiones en todas las interfaces de red del sistema. Esto significa que el servidor será accesible desde cualquier dirección IP que tenga el sistema (por ejemplo, desde localhost, direcciones privadas, y direcciones públicas, si están configuradas). También es equivalente a dejar este argumento en blanco "".
  • 127.0.0.1: Acepta conexiones solo desde la interfaz de loopback (localhost). Esto significa que el servidor solo será accesible desde el mismo sistema u ordenador donde está ejecutándose y no desde otras máquinas.
  • Dirección IP específica (como 192.168.1.10): Acepta conexiones solo en esa dirección IP específica. Esto puede ser útil si solo quieres que tu servidor esté accesible a través de una interfaz de red particular.
Es decir cualquier conexión que se solicite a nuestro ordenador activará el socket en el puerto 8081.

Por otra parte, los puertos se utilizan normalmente para identificar el propósito de la conexión. Se puede utilizar cualquier número entre el 0 y el 65535. No obstante ten en cuenta que los puertos entre el 0 y el 1023 se les llama "Puertos Bien Conocidos" y suelen estar restringidos a usos específicos. Por ejemplo:

puertos en un ordenador


Podríamos haber usado cualquier otro puerto, pero hemos optado por el 8081 que se suele utilizar para testear.

Preparemos al servidor para que se ponga a escuchar las peticiones entrantes. Vamos también a poner un mensaje en la pantalla, que indique que el servidor está esperando la conexión y podemos también limitar el número de conexiones entrantes. (5 en este caso)

server.py

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Vincula el socket a una dirección y puerto específicos
server_socket.bind(('0.0.0.0', 8081))

# Escucha conexiones entrantes (el argumento especifica el tamaño máximo de la cola de conexiones)
server_socket.listen(5)

print("Servidor escuchando en el puerto 8080...")
Ya tenemos al servidor esperando a las conexiones. Una vez que tenga una petición tiene que aceptarlas. Así que vamos a prepararlo todo:

server.py

import socket

# Creamos el socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Vincula el socket a una dirección y puerto específicos
server_socket.bind(('0.0.0.0', 8081))

# Escucha conexiones entrantes (el argumento especifica el 
# número máximo de solicitudes de conexión en cola)
server_socket.listen(5)

print("Servidor escuchando en el puerto 8081...")

# Aceptar una conexión
client_socket, client_address = server_socket.accept()
print(f"Conexión aceptada de {client_address}")

El programa está esperando que se realice una conexión y cuando esto se produce, se devuelven dos parámetros. 

El primero "client_socketes un nuevo objeto de socket que representa la conexión específica con el cliente que ha hecho la solicitud de conexión. Este socket se usa para enviar y recibir datos a y desde el cliente. Cada vez que un cliente se conecta al servidor, el método accept() crea un nuevo socket para manejar esa conexión individual, permitiendo que el servidor continúe escuchando nuevas conexiones en el socket original.

Para que quede claro. Un socket_server no manda ningún dato. No recibe ningún dato. Solo produce sockets clientes. Cada client_socket es creado en respuesta a algún otro socket cliente que hace connect al host y puerto al que estamos vinculado. Tan pronto como hemos creado ese cliente_socket volvemos a escuchar por más conexiones. Los dos "clientes" son libres de conversar entre ellos - están usando algún puerto asignado dinámicamente que será reciclado cuando la conversación termine.

Cualquier software que sea capaz de conectarse mediante TCP podrá establecer una conexión con nuestro programa de servidor, como un navegador web o un terminal. Ahora, en el siguiente paso, vamos a crear tu propio cliente, pero antes vamos a probar que nuestro programa funciona usando tu navegador web para conectarse. Así que ejecutemos nuestro programa.

El segundo "client_address" es una tupla que contiene la dirección IP y el número de puerto del cliente que ha establecido la conexión con el servidor. Este parámetro proporciona información sobre la ubicación de red del cliente.

Pondremos un mensaje en pantalla para indicar que se ha realizado la conexión.


>>> python3 server.py
Servidor escuchando en el puerto 8081...

Podemos ver que está esperando una conexión. Abre un navegador web y ve a la dirección 127.0.0.1, que es la dirección local de tu ordenador. Usa el puerto 8081 porque ese es el puerto en el que programa está escuchando. (http://127.0.0.1:8081/)

>>> python3 server.py
Servidor escuchando en el puerto 8081...
Conexión aceptada de ('127.0.0.1', 45438)
Aunque el navegador web no muestra ninguna información, podemos ver que está conectado. ¿Por que el navegador no muestra nada? ¿Qué tendrías que hacer para que el navegador responda?  Lo veremos en el próximo post.

Puedes encontrar el código de este post en el siguiente enlace.