brennim
17:38
  • 📁
    blog
  • 📁
    games
  • 📁
    photos
  • 📄
    guestbook.txt
×

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