Um servidor HTTP em Lua
2024-03-25
HTTP talvez seja o protocolo de aplicação mais ubíquo. Mas você sabe como funciona? Conseguiria escrever seu próprio servidor?
Anatomia de uma mensagem HTTP 1.1
Uma requisição HTTP parece mais ou menos com isso:
GET /about.html HTTP/1.1
Essa linha é uma request line:
GET
é um verbo. Indica qual ação você quer executar no servidor./about.html
é um caminho. Indica qual recurso o cliente deseja acessar no servidor.HTTP/1.1
é a versão que o cliente suporta. Nesse artigo vamos focar na versão 1.1.
Nosso servidor pode responder com algo como:
HTTP/1.1 200 OK
Content-Length: 12
Hello World!
200 OK
é o código de status e indica se houve sucesso ou erro na requisição. HTTP tem muitos status, incluindo o código 418 I'm a teapot.Content-Length: 12
é um cabeçalho, e indica o tamanho do nosso corpo. Nesse caso, 12 bytes.Hello World!
é o corpo da nossa resposta.
Sockets
HTTP utiliza TCP como camada de transporte. Para isso vamos precisar criar um socket e atribuir a uma porta. Como estamos usando Lua, vamos utilizar a biblioteca, adivinha, LuaSocket.
É provável que essa biblioteca estaja incluída no gerenciador de pacotes da sua distro (caso esteja utilizando linux). Se não, é possível instalar utilizando luarocks.
Criando um socket TCP
Precisamos falar pro nosso OS que aceitamos conexões em uma determinada porta. A função bind faz exatamente isso. O primeiro argumento é qual IP da interface. Vamos usar "*" para permitir qualquer interface. O segundo argumento é a porta. Vamos utilizar um número sensível como 3000.
local socket = require "socket"
local server = socket.bind("*", 3000)
Lidando com clientes
Se tudo tiver ido bem obtivemos a porta 3000 para nós. Agora vamos ouvir o que
os clientes tem para nos dizer. Fazemos isso com o método accept
do servidor
que criamos.
Depois de aceitar um cliente, é possível ler o que está sendo transmitido
utilizando o método do cliente receive
. Chamando sem argumentos, vamos ler
linha a linha. Para ler de outras maneiras, da uma olhada na documentação.
while true do
local client = server:accept()
local request_line = client:receive()
print(request_line)
client:close()
end
Rodando o código e enviando uma requisição com curl http://localhost:3000
,
nosso servidor apenas ecoa a request line e imediatamente fecha a conexão.
Respondendo que nem gente
Seria uma maravilha se os servidores se comportassem assim como o nosso. Nosso
tutorial estaria pronto. Mas o cliente espera uma resposta. Vamos utilizar o
método send
do cliente para enviar dados.
client:send("HTTP/1.1 200 OK\r\n\r\n")
Nosso servidor agora responde devidamente os clientes. Mas você deve estar se
perguntando :thinking:, o que são esses \r\n\r\n
no final da mensagem. Isso é
como o protocolo indica que uma mensagem chegou ao fim.
Resultado final
Seria interessante se nosso servidor se importasse com o caminho da requisição, seu conteúdo e cabeçalhos. Mas isso é só uma visão geral de como um servidor HTTP funciona. Num post futuro, vamos explorar como lidar com headers e corpo.
Se quiser dar uma olhada em como isso seria feito, da uma olhada nessa lib que eu fiz: http.lua. Com ~150 linhas de código, é possível criar um servidor HTTP que lida com cabeçalhos, corpo e caminhos.
local socket = require "socket"
local server = socket.bind("*", 3000)
while true do
local client = server:accept()
local request_line = client:receive()
client:send("HTTP/1.1 200 OK\r\n\r\n")
client:close()
end