Introdução
Um framework web é uma ferramenta que facilita o desenvolvimento de aplicações. Essa facilidade existe porque ele tem abstrações, ou soluções elegantes e genéricas que permitem nos concentrarmos na aplicação que estamos construindo sem ter que implementar detalhes que são comuns a toda aplicação web. Esse post é um exercício prático para entender de forma intuitiva e gradual como um framework web funciona.
Problema 1 - Comunicação entre duas partes
Imagine que tenhamos uma função em Python que retorne a mensagem "Hey, como vai?".
def main():
return "Hey, como vai?"
Podemos simplesmente invocar a função e obter o resultado. Mas e se quiséssemos algo diferente: deixar a função rodando o tempo todo "escutando" qualquer chamada e só então retornar a mensagem? Como faríamos isso?
Solução
def main():
while True:
print("Escutando...")
if __name__ == "__main__":
main()
Bem, agora temos uma função que roda eternamente. Porém, removemos a parte em que ela retorna a mensagem. O nosso desafio é: fazer a função ficar escutando chamadas de qualquer um e sempre retornar a mensagem "Hey, como vai?". Invocar uma função "parada" é simples. O que queremos invocar é uma "função" que está rodando. Isso é o que chamamos de processo.
Bem, agora que entendemos que chamar uma função rodando (processo) é diferente de chamar uma função parada, entendemos que precisamos de um canal para fazer a chamada. A ideia é bem simples: o processo se conecta nesse canal. Assim, quando queremos um retorno do processo, nos conectamos nesse canal e o obtemos. Vamos usar algo que você já conhece para criar esse canal: um arquivo.
import os
import time
def main():
while True:
try:
with open("channel.txt", "r") as f:
message = f.read()
# Lê chamada
...
# Responde chamada
...
except FileNotFoundError:
pass
time.sleep(0.1)
if __name__ == "__main__":
main()
Agora, nossa função roda eternamente e também abre um arquivo como canal de comunicação. Para permitir essa comunicação, vamos definir que nossa função que responde é um servidor e quem pede a resposta é um cliente. Dessa forma, basta implementar uma regra para ler a chamada do cliente e então responder.
# server.py
import os
import time
def main():
while True:
try:
with open("channel.txt", "r") as f:
message = f.read()
if message.startswith("CLIENT:"):
# Consome a mensagem (remove do arquivo)
os.remove("channel.txt")
# Responde
with open("channel.txt", "w") as f:
f.write("SERVER: Hey, como vai?\n")
except FileNotFoundError:
pass
time.sleep(0.1)
if __name__ == "__main__":
main()
Nossa função que antes era "parada" agora fica escutando ativamente qualquer chamada feita — rodando como um processo. Agora sim, um cliente pode se comunicar com esse processo e receber a resposta.
# client.py
import os
import time
# Escreve mensagem
with open("channel.txt", "w") as f:
f.write("CLIENT: Olá!\n")
# Aguarda resposta
time.sleep(0.2)
# Lê resposta
with open("channel.txt", "r") as f:
response = f.read()
print(response)
# Consome a resposta (remove do arquivo)
os.remove("channel.txt")
Note que na verdade o processo cliente que críamos se "conecta" ao arquivo. Quando ele escreve nesse arquivo, o processo servidor escuta e então escreve novamente no arquivo. O cliente por sua vez, lê novamente o arquivo, obtendo a resposta escrita pelo servidor. Após a comunicação terminar, "fechamos" o arquivo e o destruímos.
Como desafio, tente mudar o código para que o cliente passe dois números e o servidor retorne a soma deles.
Problema 2 - Reaproveitar o canal de comunicação
Criamos um sistema rudimentar, mas funcional que permite conectar dois processos. Agora pense no seguinte: e se quisermos reaproveitar esse sistema para conectar quaisquer processos? Nosso computador roda milhares de processos. Se tivermos que escrever toda vez essa sequência de abre e fecha arquivos, cria loops, remove arquivos, teríamos muito trabalho. Já que essa parte é basicamente a mesma para todos os cenários, podemos esconder isso dos processos e criar uma forma comum para que qualquer processo possa se comunicar com outro.
Solução
Vamos criar uma versão genérica, ou abstrata desse canal. Na nossa versão inicial, usamos um simples arquivo. Porém na prática, esse detalhe não é importante para os nossos processos. Eles precisam de qualquer coisa que permita a comunicação. Portanto, vamos abstrair, ou esconder esses detalhes e deixar exposto apenas o que importa para os processos se comunicarem.
# channel.py
class Channel:
# orquestra a comunicação entre os processos
Dessa forma temos o seguinte:
graph LR
A[Processo Servidor] <--> B[Channel] <--> C[Processo Cliente]
Criamos uma classe Channel para encapsular os detalhes e expor métodos para que os processos possam se comunicar. Lembre-se: nosso objetivo é criar uma forma de comunicação de processos tão genérica que qualquer processo possa usá-la, não importa o que ele faça. Ou seja, em Channel definimos uma interface, ou conjunto de funcionalidades de acordo com o que os processos precisam para se comunicar. Assim, basta o processo usar essas funcionalidades e ele terá o que precisa. Para exemplificar o conceito de interface, é só pensar em um controle de videogame. Ele esconde os circuitos e toda a funcionalidade e expõe ao jogador apenas os botões. Para o jogador (pelo menos para um jogador comum), não importa o que acontece dentro do controle. Ele só precisa saber quais botões apertar.
Vamos usar a mesma ideia na nossa classe Channel. Mas quais "botões", ou métodos nossa classe vai expor? Vamos usar algumas perguntas como base para definir isso.
- Pergunta: Como um processo A encontra um processo B para se conectar, sem se confundir com outros processos?
Pense em uma empresa com vários departamentos. Você quer falar com alguém do departamento de vendas. Imagine que o número da empresa seja 1921681100. Na empresa, ficou definido que o departamento de vendas atenderá no ramal 3000. Então você liga para a empresa e informa o ramal. Sempre que quiser uma informação, ou resposta, do departamento de vendas, vai precisar ligar para esse mesmo número e ramal.
Vamos usar essa mesma ideia. Ao criar uma instância de Channel, vamos precisar de um método para que um processo se ligue ao outro. Vamos chamar esse processo de bind. Esse método vai receber um "número" e um "ramal". Vamos chamar esse número de address e o ramal de port.
# channel.py
class Channel:
def __init__(self):
self._port = None
self._host = None
self._is_server = False
self._channel_file = None
def bind(self, address: str, port: int):
"""Liga o channel a um endereço (host, porta)"""
self._host,