Cómo crear un servidor en Python, paso a paso

Cómo crear un servidor en Python, paso a paso

Cuando accedemos a un sitio web o a alguna funcionalidad de una aplicación que busca información en internet, hablamos del modelo cliente-servidor, donde el cliente es el dispositivo que accede al recurso y el servidor es aquél cuyas funciones incluyen ser responsable de proporcionar los datos, verificar que el cliente solicitante tenga permiso para acceder a éstos, registrar usuarios, conectar las acciones de los usuarios, entre otras. otros.

Básicamente, se puede describir como un intermediario entre los usuarios y los datos de la aplicación, formando parte de lo que llamamos backend, la parte del software que puede incluir, además del servidor, bases de datos, modelos de inteligencia artificial, acceso a terceros, etc. En este artículo, veremos cómo implementar un modelo cliente-servidor simple usando el lenguaje Python para crear una sala de chat virtual.

El primer paso para construir nuestro programa es definir cuáles funciones queremos implementar. En este caso, trabajaremos con un servidor que sea capaz de conectar clientes y procesar sus solicitudes, dándoles la capacidad de enviar mensajes a otros clientes y también recibir respuestas.

Server.py

Ahora comenzamos creando un archivo python para el servidor, lo llamaremos "server.py" e importamos las bibliotecas de sockets y subprocesos (son nativas de Python, por lo que no necesitarás instalar nada :D).

import socket
import threading

Socket

El socket es la biblioteca encargada de comunicarse con tu máquina, para crear un punto de conexión para transferir información a otras máquinas. Existen diferentes tipos de sockets, por ejemplo para bluetooth o a nivel de sistema operativo. En este proyecto usaremos internet. Entonces comenzamos creando un objeto de clase de socket especificando el tipo de socket que se usará y el protocolo:

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

AF_INET

AF_INET representa el tipo de socket. Hay dos para internet, IPV4 e IPV6, que básicamente, haciendo un paralelo, es como un sistema de direcciones de casas, donde para saber cuál máquina se comunicará con cuál es necesario conocer la dirección IP de la otra máquina. IPV4 es más antiguo; tiene la capacidad de crear direcciones para miles de millones de máquinas. Sin embargo, con el crecimiento de internet, fue necesario crear un nuevo sistema con mayor capacidad, el IPV6.

SOCK_STREAM E SOCK_DGRAM

El SOCK_STREAM representa el tipo de protocolo, que en este caso es TCP (Transmission Control Protocol), pero también existe el SOCK_DGRAM para el protocolo UDP (User Datagram Protocol). La diferencia entre ambos es que en TCP hay un sistema para garantizar que todos los paquetes de datos se envían, se establece una conexión antes de la transmisión y luego se cierra. En UDP, en cambio, no existe un sistema que garantice que se recibirán todos los datos, lo que hace que este tipo de conexión sea más ligera y rápida.

UDP se suele utilizar, por ejemplo, para chats en tiempo real con cámara y pantalla compartida como Google meet, Zoom o Skype (por eso a veces se “congela” la imagen de la otra persona). Un ejemplo con TCP sería un chat de texto, donde es más prioritario que el mensaje llegue completo al destino que el tiempo de respuesta.

Este conjunto integra el famoso protocolo TCP/IP, que será la forma en la que nuestro servidor se comunicará con los clientes.

El siguiente paso será definir las constantes para la dirección del servidor y el puerto en el que esperará los comandos. En el host podemos poner el 127.0.0.1, que representa el localhost. Esta dirección es para que tu computadora dirija las solicitudes a sí misma (en una aplicación comercial, tendría que exponer la dirección IP de tu red, de lo contrario otros la gente no podrá conectarse a tu servidor). El puerto lo podemos elegir arbitrariamente. Sin embargo, no es bueno elegir los muy bajos porque otros procesos ya los usan, como el 80 que es http, el 22 que es SSH y así. Como ejemplo, lo haré use el puerto 14532:

HOST = '127.0.0.1'
PORT = 14532

Ahora, simplemente llama a la función de enlace de nuestro objeto de servidor, pasando el host y el puerto, luego la función de escucha, para que el servidor sepa tu dirección, cuál puerto debe abrir y comience a "escuchar" los comandos:

server.bind((HOST, PORT))
server.listen()
print(f"Server escutando na porta: {PORT}")

Hecho esto, técnicamente ya tenemos listo nuestro servidor, así que agreguemos algunas habilidades más para hacerlo más interesante. Primero, definiremos una lista para los clientes y otra para sus respectivos nombres de usuario. Luego una función que recibirá un mensaje y lo enviará a todos los clientes que se conecten al servidor:

clients = []
usernames = []
def sendMessage(message):

    for client in clients:

        client.send(message)

Ahora vamos a crear una función para manejar las acciones del usuario:

def handle(client, username):

    while True:

        try:

            message = client.recv(1024).decode('utf-8')
            sendMessage(f"[{username}] {message}".encode('utf-8'))
        except:
            index = clients.index(client)
            clients.remove(client)
            client.close()
            username = usernames[index]
            usernames.remove(username)

            sendMessage(f"{username} desconectou do servidor.".encode('utf-8')))
            break

Esta función será un bucle infinito, la cual intentará primero recibir los mensajes del usuario (el 1024 representa la cantidad de bytes que vamos a recibir), decodificarlos y enviarlos a otros usuarios a través de la función sendMessage.

En caso de un error o de que el usuario se desconecte del servidor, identificará su índice en la lista de clientes, lo eliminará, cerrará la conexión, encontrará su nombre de usuario en la lista de nombres de usuario con el índice respectivo, lo eliminará de la lista, advertirá a otros usuarios que se desconectó llamando a la función sendMessage (recordando cifrar el mensaje en formato utf-8) y terminará el bucle infinito de este usuario con la función break.

El siguiente paso es crear una función que acepte la conexión de nuevos clientes:

def receive():

    while True:

        client, address = server.accept()
        client.send("USERNAME".encode('utf-8'))
        username = client.recv(1024).decode('utf-8')
        usernames.append(username)

        print(f"O usuário {username} se conectou no servidor! endereço: {address}")
        sendMessage(f"{username} entrou no chat.".encode('utf-8'))
        clients.append(client)
        client.send('Conectado ao servidor!'.encode('utf-8'))
                   
        thread = threading.Thread(target=handle, args=(client, username, ))
        thread.start()


receive()

Esta función también será un bucle infinito, que primero llamará a la función accept() de nuestro objeto servidor de la clase socket, para recibir un objeto que hace referencia al cliente (que contendrá las funciones de envío y recepción de mensajes) y su Dirección IP que pasaremos a las variables cliente y dirección respectivamente.

Luego, enviaremos el mensaje 'NOMBRE DE USUARIO' al cliente, que interpretará y responderá con el nombre de usuario que eligió, el cual a su vez pasaremos a la variable de nombre de usuario después de decodificarlo y luego lo agregaremos a la lista de nombres de usuario.

El siguiente paso es mostrar la información sobre el nombre de usuario y la dirección IP del cliente que se conectó en la terminal del servidor, luego notificar a los otros usuarios ya conectados (si los hay) que se ha conectado. Luego agregamos el objeto de cliente a la lista de clientes y enviamos a este cliente la información de que se ha conectado al servidor.

Finalmente, creamos un nuevo hilo, usando la biblioteca de hilos que importamos al principio, pasando la función handle como destino, el cliente y el nombre de usuario como argumento, y comenzamos el hilo con el comando de inicio. Este paso es importante porque es a través de hilos que podemos ejecutar múltiples procesos al mismo tiempo, en otras palabras, el servidor tendrá la capacidad de manejar simultáneamente las acciones de cada cliente creando un nuevo hilo y aceptando conexiones de nuevos clientes. Después de eso, simplemente llame a esta función que acabamos de crear y nuestro servidor estará listo.

Client.py

Ahora que tenemos nuestro servidor listo, necesitamos configurar el código para que los clientes lo prueben. Comenzaremos creando un archivo llamado “client.py”, cuyo comienzo es muy similar al código que hicimos para el servidor:

import socket
import threading

HOST = '127.0.0.1'
PORT = 14532

username = input("Digite seu nome de usuário: ")
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOST, PORT))

La dirección y el puerto son los mismos en este caso específico, ya que estamos ejecutando tanto el servidor como el cliente en la misma máquina, pero si el cliente se estaba ejecutando en otra máquina, la dirección del HOST debe ser la del servidor. Una pequeña diferencia con el código del servidor es que ahora le pedimos al cliente que ingrese el nombre de usuario antes de iniciar la conexión y, en lugar de llamar a la función de enlace y escucha, ahora llamamos a la función de conexión de nuestro objeto de clase de socket.

Ahora vamos crearemos dos funciones para el cliente, una para que reciba los mensajes que envía el servidor y otra para que envíe mensajes al servidor. Primero la función a recibir:

def receive():

    while True:

        try:
            message = client.recv(1024).decode('utf-8')
            if message == 'USERNAME':                                                                                            

                client.send(username.encode('utf-8'))
            else:
                print(message)
        except:
            print("Encerrando conexão com o servidor...")
            client.close()

            break

Esta función será un bucle infinito en el que intentaremos recibir y decodificar los mensajes provenientes del servidor en formato utf-8. Luego verificaremos si el mensaje es igual a 'USERNAME', que se referiría a la primera vez que el cliente está conectado al servidor, entonces el cliente envía el nombre de usuario que eligió. De lo contrario, muestra en la terminal del cliente el mensaje que envió el servidor. Sin embargo, si ocurre un error durante este proceso, la función mostrará en la terminal del cliente que ha ocurrido un error, cerrará la conexión con el servidor y terminará el ciclo.

La función para enviar mensajes al servidor es simple:

def write():

    while True:

        try:

            message =  input("")
            client.send(message.encode('utf-8'))
        except KeyboardInterrupt:
            break

Básicamente, es un bucle infinito que esperará a que el cliente escriba algo en la terminal y presione enter, luego encripta el mensaje en formato utf-8 y lo envía al servidor. Solo se detiene cuando se presiona “ctrl+c”.

Finalmente, llama a las dos funciones, receive en un hilo y write directamente, para que el cliente tenga la capacidad de recibir mensajes del servidor y enviarlos al mismo tiempo:

receive_thread = threading.Thread(target=receive)
receive_thread.start()
write()

Ejecutando el algoritmo

Ahora, ejecutarlo es simple. Solo abre tres terminales, navega a la carpeta del proyecto usando el comando "cd folder_name" e inicia el servidor en uno con el siguiente comando:

python server.py

Y en las otras dos terminales haces lo mismo, pero en lugar de ejecutar el servidor, ejecutas los clientes:

python client.py

Entonces, ahora solo elige un nombre de usuario para cada cliente conectado al servidor, envía un mensaje a cada uno y verifica los datos de conexión de los clientes en la terminal del servidor y los mensajes de los otros clientes en las terminales de cada cliente.

En una aplicación comercial, el código del servidor se ejecutará en la máquina del desarrollador, en la infraestructura de su aplicación, mientras que el código del cliente se ejecutará en la máquina de su usuario, es decir, este código estará junto con la aplicación que su usuario estará descargando, ya sea una aplicación móvil, un sitio web, una aplicación de escritorio, etcétera.

Conclusión

En este artículo, vimos a través de un ejemplo simple cómo crear un servidor y conectar usuarios a él. Este proyecto podría mejorarse agregando, por ejemplo, sistemas para registrar usuarios en una base de datos, diferentes tipos de usuarios (como un administrador, que tendría la capacidad de banear a otros usuarios del chat si no estaban siguiendo las reglas del servidor), sistemas de inicio de sesión, recuperación de contraseña, un sistema DNS (para evitar que los usuarios tengan que escribir la IP y el puerto del servidor, en su lugar escribirían algo como: https://www.google.com en el navegador que es más fácil de recordar y el servidor devolvería un sitio que contiene HTML, CSS y Javascript) manejaría cualquier conflicto que pudiera ocurrir (como si dos clientes intentaran conectarse con el mismo nombre de usuario), etc.

Sin embargo, en la práctica, dependiendo del tipo de aplicación que diseñes, difícilmente tendrás que llegar a un nivel tan bajo de trabajar con sockets a un nivel más fundamental, porque es común e incluso recomendado usar soluciones ready-made a partir de bibliotecas y marcos como Flask o Django (en el caso del lenguaje Python) ya que encapsulan la mayor parte de la funcionalidad básica que necesitarán la mayoría de los sistemas, siguiendo así un principio básico de programación de "No repetir usted mismo" o "No te repitas", centrado en  aumentar la productividad de los programadores, evitando que tengamos que "reinventar la rueda" en cada nuevo sistema que diseñamos.

¿Significa esto que nunca tendremos que “reinventar la rueda”? No exactamente. A veces necesitamos diseñar un rover lunar y este trabajo es necesario, pero eso es tema para un próximo capítulo :D.

Para obtener más información, consulta el código fuente completo de este proyecto y un GIF de la aplicación que funciona en mi repositorio de GitHub: https://github.com/ThiagoA20/chat_room

¿Te gustó el contenido?

⚠️
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Revelo.

Revelo Content Network da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.