Como criar um servidor em Python

Como criar um servidor em Python

Quando acessamos um site ou algum recurso de um aplicativo que busca informação na internet, estamos falando do modelo de cliente-servidor. Onde o cliente é o dispositivo que está acessando o recurso e servidor é aquele cujas funções incluem ser responsável por prover os dados, verificar se o cliente que está solicitando tem a permissão para acessar os dados, cadastrar usuários, conectar as ações dos usuários, dentre outras. Pode ser descrito basicamente como um intermediário entre os usuários e os dados da aplicação, fazendo parte do que chamamos de backend, que é a parte do software que pode incluir além do servidor, bancos de dados, modelos de inteligência artificial, acesso a recursos de terceiros, etc.

Nesse artigo, vamos ver  como implementar um simples modelo de cliente-servidor utilizando a linguagem python para criar uma sala de bate-papo virtual.

O primeiro passo para construir o nosso programa é definir quais funções nós queremos implementar. Nesse caso, iremos trabalhar com um servidor que é capaz de conectar clientes e processar suas requisições, dando a eles a habilidade de enviar mensagens a outros clientes e também receber as respostas.

Server.py

Agora nós começamos criando um arquivo python para o servidor, chamaremos de “server.py” e importamos as bibliotecas socket e threading (são nativas do python, então você não vai precisar instalar nada :D).

import socket
import threading

Socket

O socket é a biblioteca responsável por se comunicar com a sua máquina, para criar um ponto de conexão para transferir informações para outras máquinas. Existem diferentes tipos de sockets, por exemplo para bluetooth ou a nível de sistema operacional. Mas neste projeto utilizaremos o de internet. Então começamos criando um objeto da classe socket especificando o tipo de socket que vai ser usado e o protocolo:

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

AF_INET

O “AF_INET” representa o tipo de socket, existem dois para internet, o IPV4 e IPV6 que basicamente, fazendo um paralelo, é como se fosse um sistema de endereços de casas, onde para você saber qual máquina vai se comunicar com qual é necessário saber o endereço IP da outra máquina, o IPV4 é mais antigo, ele tem a capacidade de criar endereços para bilhões de máquinas, porém, com o crescimento da internet, se fez necessário a criação de um novo sistema com maior capacidade que é o IPV6.

SOCK_STREAM E SOCK_DGRAM

Já o “SOCK_STREAM” representa o tipo do protocolo, que nesse caso é o TCP (Transmission Control Protocol ou Protocolo de controle de transmissão), mas também existe o “SOCK_DGRAM” que seria para o protocolo UDP (User Datagram Protocol). A diferença entre os dois basicamente é que no TCP existe um sistema para garantir que todos os pacotes de dados sejam enviados, uma conexão é estabelecida antes da transmissão e depois fechada. Enquanto no UDP, não existe um sistema que garanta que todos os dados serão recebidos, o que torna esse tipo de conexão mais leve e rápida. Geralmente utiliza-se o UDP para, por exemplo, chats em tempo real com câmera e compartilhamento de tela como o google meet, o zoom ou o skype (por isso, que às vezes a imagem da outra pessoa “congela”), já um exemplo com o TCP seria um chat por texto, onde é uma prioridade maior que a mensagem chegue completa ao destino do que o tempo de resposta.

E esse conjunto é o que forma o tão famoso protocolo TCP/IP, que será a forma como o nosso servidor vai se comunicar com os clientes.

O próximo passo então será definir as constantes para o endereço do servidor e a porta na qual ele vai estar esperando por comandos. O host, nós podemos colocar o 127.0.0.1 que representa o localhost, ou seja, esse endereço serve para o seu computador direcionar as requisições para ele mesmo (em uma aplicação comercial, você teria que expor o endereço IP da sua rede, se não as outras pessoas não vão conseguir se conectar no seu servidor). E a porta nós podemos escolher arbitrariamente, só não é bom escolher as muito baixas, pois já são utilizadas por outros processos, como a 80 que é http, 22 que é SSH e assim por diante, como exemplo, vou usar a porta 14532:

HOST = '127.0.0.1'
PORT = 14532

Agora é só chamar a função bind do nosso objeto server, passando o host e a porta, depois a função listen, assim o servidor vai saber qual o seu endereço, qual porta deve abrir e começar a “escutar” por comandos:

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

Feito isso, tecnicamente já temos o nosso servidor pronto. Então vamos adicionar mais algumas habilidades para ele ficar mais interessante, primeiro definiremos uma lista para os clients e uma para os seus respectivos nomes de usuário, depois uma função que receberá uma mensagem e enviará para todos os clientes que se conectarem no servidor:

clients = []
usernames = []
def sendMessage(message):
    for client in clients:
        client.send(message)

Agora vamos criar uma função para lidar com as ações dos usuários:

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

Essa função será um loop infinito, que primeiro vai tentar receber as mensagens do usuário (o 1024 representa a quantidade de bytes que vamos receber), decodificar e enviá-las para os outros usuários através da função sendMessage, mas caso ocorra algum erro ou o usuário se desconecte do servidor, ela vai identificar o índice dele na lista de clients, removê-lo, fechar a conexão, achar seu nome de usuário na lista usernames com o respectivo índice, removê-lo da lista, avisar os outros usuários que ele foi desconectado chamando a função sendMessage (lembrando de encriptar a mensagem no formato utf-8) e por fim finalizar o loop infinito desse usuário com a função break.

O próximo passo é criar uma função que vai ficar aceitando a conexão de novos clients:

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

Essa função também será um loop infinito, que primeiro vai chamar a função accept() do nosso objeto server da classe socket, para receber um objeto que refere-se ao client (que nele vai conter as funções para enviar e receber mensagens) e seu endereço IP que passaremos para as variáveis client e address respectivamente.

Então enviaremos a mensagem ‘USERNAME’ para o client que vai interpretar e responder com o nome de usuário que ele escolheu, que nós passaremos para a variável username depois de decodificar e então adicionaremos ele à lista usernames.

O próximo passo é mostrar no terminal do servidor a informação sobre o nome do usuário e o endereço IP do client que se conectou, depois avisar aos outros usuários já conectados (se houver) que ele conectou. Então adicionamos o objeto do client na lista clients e enviamos a esse client a informação de que ele se conectou no servidor.

Por fim, criamos uma nova thread, usando da biblioteca threading que importamos no começo, passando a função handle como target, o client e o username como argumento e inicializamos a thread com o comando start. Esse passo é importante, pois é através das threads que podemos rodar múltiplos processos ao mesmo tempo, em outras palavras, o servidor vai ter a habilidade de simultaneamente lidar com as ações de cada client ao criar uma nova thread e aceitar conexões de novos clients. Depois disso, é só chamar essa função que acabamos de criar e o nosso servidor está pronto.

Client.py

Agora que já temos nosso servidor pronto, precisamos configurar o código para os clients para testá-lo, começaremos criando um arquivo chamado “client.py” e nele o começo é bem parecido com o código que fizemos para o 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))

O endereço e a porta são os mesmos neste caso em específico, pois estamos rodando tanto o servidor quanto o client na mesma máquina, mas se o client tivesse rodando em outra máquina o endereço HOST deve ser o do servidor. Uma pequena diferença entre o código do servidor é que agora nós pedimos para o client informar o nome do usuário antes de iniciar a conexão e em vez de chamar a função bind e listen, agora chamamos a função connect do nosso objeto da classe socket.

Agora vamos criar duas funções para o client, uma para ele receber as mensagens que o servidor mandar e outra para ele enviar mensagens para o servidor. Primeiro a função para receber:

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

Essa função vai ser um loop infinito, no qual vamos tentar receber e decodificar as mensagens vindas do servidor no formato utf-8, depois verificar se a mensagem é igual a ‘USERNAME’, que seria referente a primeira vez que o client se conectou no servidor, então o client envia o nome de usuário que ele escolheu, caso contrário, ele mostra no terminal do client a mensagem que o servidor enviou. Porém, se ocorrer algum erro durante esse processo, a função vai mostrar no terminal do client que um erro ocorreu, fechar a conexão com o servidor e finalizar o loop.

Já a função para enviar mensagens para o servidor é simples:

def write():
    while True:
        try:
            message =  input("")
            client.send(message.encode('utf-8'))
        except KeyboardInterrupt:
            break

Basicamente ela é um loop infinito que vai ficar esperando o client digitar algo no terminal e pressionar enter, então ela encripta a mensagem no formato utf-8 e envia para o servidor. Ela só para quando for pressionado “ctrl+c”.

Por fim, é só chamar as duas funções, a receive em uma thread e a write direto, para que  assim o client tenha a habilidade de receber as mensagens do servidor e enviar ao mesmo tempo:

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

Executando o algoritmo

Agora para executar é simples, basta abrir três terminais, navegar até a pasta do projeto utilizando o comando “cd nome_da_pasta” e inicializar o servidor em um com o seguinte comando:

python server.py

E nos outros dois terminais é preciso fazer a mesma coisa, mas em vez de executar o servidor, você executa os clients:

python client.py

Então agora é só escolher um nome de usuário para cada client conectado no servidor, enviar uma mensagem em cada um e ver se nos outros terminais estão aparecendo, respectivamente, os dados de conexão dos clients no terminal do servidor e as mensagens dos outros clients nos terminais de cada client.

Em uma aplicação comercial, o código do servidor vai rodar na máquina do desenvolvedor, na infraestrutura da sua aplicação, enquanto o código do client vai rodar na máquina do seu usuário, ou seja, esse código vai estar junto da aplicação que o seu usuário vai baixar, seja ela um aplicativo de celular, um site, um aplicativo desktop, enfim.

Conclusão

Neste artigo, vimos através de um exemplo simples como é o procedimento para criar um servidor e conectar usuários nele. Esse projeto poderia ser melhorado adicionando, por exemplo, sistemas para cadastro de usuários em um banco de dados, diferentes tipos de usuários (como por exemplo um administrador, que teria a capacidade de banir outros usuários do chat se eles não estivessem seguindo as regras do servidor), sistemas de login, recuperação de senha, um sistema de DNS (para evitar que os usuários tivessem que digitar o IP e a porta do servidor, em vez disso, eles digitariam algo como: https://www.google.com no navegador que é mais fácil de memorizar e o servidor retornaria um site contendo HTML, CSS e Javascript), tratar algum conflito que possa vir a acontecer (como para o caso de dois clientes tentarem se conectar com o mesmo nome de usuário), etc.

Porém, na prática, dependendo do tipo de aplicação que você está projetando, dificilmente vai ter que chegar a um nível de ter que trabalhar com sockets. Aqui, neste artigo, foi só para exemplificar para que você possa compreender como a dinâmica cliente-servidor funciona em um nível mais fundamental. É comum e até recomendado que se utilize de soluções já prontas de bibliotecas e frameworks como o Flask ou Django (no caso da linguagem python), pois eles já encapsulam grande parte das funcionalidades básicas que a maioria dos sistemas vai precisar, seguindo assim um princípio básico da programação que é o “Don’t repeat yourself” ou “Não repita a si mesmo” que tem como objetivo aumentar a produtividade dos programadores, evitando que nós tenhamos que “reinventar a roda” a cada novo sistema que projetamos. Isso significa que nunca vamos precisar “reinventar a roda”? Não exatamente... às vezes precisamos projetar um rover lunar e esse trabalho se faz necessário, mas esse é um assunto para um próximo capítulo.

🛑
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo. A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.