Extensão — O Terminal Vira Rede: Um Servidor HTTP Mínimo

Extensão — O Terminal Vira Rede: Um Servidor HTTP Mínimo

Um programa de terminal vira servidor HTTP com uma biblioteca de cabeçalho único e rotas escritas como lambdas. O artigo monta rotas com parâmetros, serve dados vindos do banco e é honesto sobre a distância até produção — onde o problema de expor isso à internet não é desempenho, é segurança.
Linguagem C++

13 min de leitura

No artigo Extensão — Dados que Sobrevivem: Integrando SQLite com RAII demos persistência ao nosso conhecimento, envolvendo o SQLite em RAII. Hoje exploramos o outro território do mapa que traçamos no Extensão: a web. Vamos construir um servidor HTTP mínimo em C++ — um programa que escuta requisições da rede e responde, transformando aquele std::cout que sempre falou com o terminal num serviço que fala com navegadores e outros programas pelo mundo. Usaremos a biblioteca cpp-httplib, escolhida por ser de cabeçalho único (basta incluí-la, sem lutar com o sistema de build) e simples, deixando o foco nos conceitos. Faremos uma pequena API que responde a requisições e, costurando as duas extensões, esboçaremos como servir dados vindos de um banco. Será a materialização concreta da ideia de que "um servidor web em C++ usa tudo do curso".

O servidor mais simples possível

O cpp-httplib reduz a criação de um servidor a poucas linhas. Você cria um objeto Server, registra rotas (associando um caminho de URL a uma função que produz a resposta), e manda escutar numa porta. Eis o "Olá, mundo" da web em C++:

// servidor.cpp — requer o cabeçalho único httplib.h
#include "httplib.h"
#include <iostream>

int main() {
    httplib::Server servidor;   // o objeto servidor

    // Registra uma ROTA: quando alguém acessar GET /, executa esta função.
    // A lambda (Fase 5!) recebe a requisição e escreve na resposta.
    servidor.Get("/", [](const httplib::Request& req, httplib::Response& res) {
        res.set_content("Olá do C++!", "text/plain");   // corpo e tipo da resposta
    });

    std::cout << "servidor rodando em http://localhost:8080\n";
    servidor.listen("localhost", 8080);   // escuta na porta 8080 (bloqueia aqui)
    return 0;
}

Compile (linkando a biblioteca de threads e sockets do sistema) e rode; depois abra http://localhost:8080 no navegador ou use curl http://localhost:8080. Você verá "Olá do C++!". O que aconteceu: seu programa virou um servidor — ele escuta na porta 8080, e quando uma requisição chega para o caminho /, a lambda que você registrou produz a resposta. Aquele res.set_content(...) é o parente web do std::cout: em vez de escrever no terminal, escreve na resposta HTTP que volta ao cliente. O terminal virou rede. E note a lambda (Fase 5) no coração da rota — os conceitos do curso já estão aqui.

Rotas com parâmetros e uma pequena API

Um servidor útil tem várias rotas e responde a dados na URL. Vamos fazer uma pequena API que cumprimenta por nome e faz uma soma — usando parâmetros de rota e devolvendo dados estruturados:

#include "httplib.h"
#include <string>

int main() {
    httplib::Server servidor;

    // Rota com PARÂMETRO na URL: /ola/Ana → captura "Ana".
    servidor.Get(R"(/ola/(\w+))", [](const httplib::Request& req, httplib::Response& res) {
        std::string nome = req.matches[1];   // o grupo capturado da URL
        res.set_content("Olá, " + nome + "!", "text/plain");
    });

    // Rota que lê parâmetros de query: /soma?a=3&b=5 → 8.
    servidor.Get("/soma", [](const httplib::Request& req, httplib::Response& res) {
        // has_param/get_param_value acessam os parâmetros ?a=..&b=..
        if (req.has_param("a") && req.has_param("b")) {
            int a = std::stoi(req.get_param_value("a"));
            int b = std::stoi(req.get_param_value("b"));
            res.set_content(std::to_string(a + b), "text/plain");
        } else {
            res.status = 400;   // Bad Request: faltam parâmetros
            res.set_content("faltam parâmetros a e b", "text/plain");
        }
    });

    servidor.listen("localhost", 8080);
    return 0;
}

Acesse http://localhost:8080/ola/Maria e receberá "Olá, Maria!"; acesse /soma?a=3&b=5 e receberá "8"; acesse /soma sem parâmetros e receberá um erro 400. Note o tratamento de erro (Fase 6) definindo o status HTTP apropriado — o 400 que comunica "requisição malformada". Cada rota é uma lambda que recebe a requisição e monta a resposta; organizar isso em métodos de uma classe (Fase 3) seria o passo natural num servidor maior. Já dá para ver a arquitetura de uma API real emergindo.

Costurando tudo: servindo dados de um banco

Agora unimos as duas extensões, no espírito de "cada artigo reúne o que já foi ensinado". Imagine servir, por HTTP, os usuários guardados no SQLite do artigo Extensão — Dados que Sobrevivem: Integrando SQLite com RAII. Conceitualmente (esboço, unindo BancoSQLite, Consulta e o servidor):

#include "httplib.h"
#include "banco_sqlite.h"   // do Artigo Extensão

int main() {
    httplib::Server servidor;

    // Rota que consulta o banco e devolve os usuários como texto.
    servidor.Get("/usuarios", [](const httplib::Request& req, httplib::Response& res) {
        try {
            BancoSQLite banco("usuarios.db");     // RAII: abre e fechará sozinho (Fase 2)
            Consulta c(banco, "SELECT nome, idade FROM usuarios");  // RAII (Fase 2)

            std::string corpo;
            while (c.proxima_linha()) {            // itera resultados (Artigo Extensão)
                corpo += c.coluna_texto(0) + ": "
                       + std::to_string(c.coluna_int(1)) + " anos\n";
            }
            res.set_content(corpo, "text/plain");
        }
        catch (const std::exception& e) {          // Fase 6: erro vira resposta 500
            res.status = 500;
            res.set_content(std::string("erro no banco: ") + e.what(), "text/plain");
        }
    });

    servidor.listen("localhost", 8080);
    return 0;
}

Contemple a costura: uma requisição HTTP chega, a rota abre o banco SQLite (RAII — Fase 2), consulta os dados (a Consulta do artigo anterior), monta a resposta, e — se algo falhar — o catch transforma a exceção (Fase 6) numa resposta HTTP 500 apropriada, enquanto o RAII garante que a conexão feche mesmo no erro (a aliança RAII-exceção do artigo Quando o Contrato se Quebra — Exceções e a Aliança com o RAII!). Isto é um serviço web de verdade em miniatura: recebe requisições, busca dados persistidos, responde, e trata falhas com elegância. As duas extensões se uniram, e nelas está o curso inteiro.

A honestidade sobre servidores de produção

Prometo a franqueza de sempre, e ela é importante para você não sair daqui com uma impressão falsa. O que construímos é um servidor didático, e um servidor de produção real tem camadas que deliberadamente omiti. Concorrência: o cpp-httplib atende requisições em threads, o que significa que, se suas rotas compartilharem dados mutáveis (um cache, um contador), você precisa de toda a sincronização da Fase 7 — abrir uma nova conexão de banco por requisição, como fiz, evita alguns problemas, mas um servidor sério usaria um pool de conexões gerenciado com cuidado. Segurança: servidores reais precisam de HTTPS (criptografia), validação rigorosa de entrada, proteção contra os prepared statements do artigo anterior, autenticação, e muito mais — o que mostrei não tem nenhuma dessas proteções e jamais deveria ser exposto à internet como está. Robustez e escala: produção envolve balanceamento de carga, limites de taxa, logging, monitoramento. Formato de dados: uma API real devolveria JSON (com uma biblioteca como nlohmann/json), não texto simples. Mostrei o núcleo conceitual — como C++ responde a HTTP e serve dados — porque é isso que conecta ao curso; o resto é um universo próprio. A lição honesta: você agora entende o princípio de um servidor web em C++ e viu seus conceitos se aplicarem, mas construir um servidor de produção seguro e escalável é uma especialização que vai muito além desta introdução.

Servir HTTP a partir de um programa C++ é menos trabalhoso do que parece com uma biblioteca de cabeçalho único, e o roteamento por lambda torna cada rota curta. O que separa esse servidor de um de produção, porém, é longo: concorrência de verdade, limites de tamanho e tempo, tratamento de entrada maliciosa, TLS, logs. Exposto à internet como está, o problema não é desempenho — é segurança.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Compile e rode o servidor "Olá do C++!" desta aula (você precisará do httplib.h e de linkar threads: -lpthread). Acesse-o pelo navegador ou com curl e descreva o que acontece.

Ver resposta

✓ Resposta: Ao compilar (algo como g++ -std=c++20 servidor.cpp -lpthread -o servidor) e rodar, o programa imprime a mensagem de que está rodando e então bloqueia no listen, aguardando requisições — o terminal não retorna ao prompt, pois o servidor está ativo. Acessando http://localhost:8080 no navegador (ou curl http://localhost:8080), a requisição chega ao servidor, a lambda registrada para / executa, e o cliente recebe "Olá do C++!" como resposta (o navegador exibe o texto; o curl o imprime). Cada acesso dispara a lambda novamente. Para encerrar o servidor, interrompe-se o programa (Ctrl+C). O ponto-chave observado: o programa deixou de ser um utilitário que roda e termina, e virou um serviço que fica ativo respondendo a requisições.

Exercício 2

Adicione ao servidor uma rota GET /hora que responda com a data e hora atuais. (Dica: use <chrono> e <ctime> para obter a hora.) Explique como a lambda da rota produz a resposta.

Ver resposta

✓ Resposta: Rota /hora:

#include <chrono>
#include <ctime>
servidor.Get("/hora", [](const httplib::Request& req, httplib::Response& res) {
    auto agora = std::chrono::system_clock::now();
    std::time_t t = std::chrono::system_clock::to_time_t(agora);
    std::string texto = std::ctime(&t);   // representação textual da hora
    res.set_content("Agora: " + texto, "text/plain");
});

A lambda da rota produz a resposta obtendo o instante atual do relógio do sistema (system_clock::now()), convertendo-o para o tipo time_t e depois para texto legível com std::ctime, e escrevendo esse texto no corpo da resposta com res.set_content. Quando uma requisição chega a /hora, a lambda é executada naquele momento, então cada acesso reflete a hora atual — o res é o canal pelo qual a função entrega seu resultado ao cliente, análogo a como uma função comum usaria return, mas aqui "retornando" pela resposta HTTP.

Exercício 3

Na rota /soma, explique o papel do res.status = 400 quando faltam parâmetros. Por que devolver um status HTTP apropriado é importante, e qual seria o status para uma requisição bem-sucedida?

Ver resposta

✓ Resposta: O res.status = 400 define o código de status HTTP da resposta como 400 (Bad Request), que comunica ao cliente que a requisição estava malformada — no caso, faltaram os parâmetros obrigatórios a e b. Devolver o status apropriado é importante porque o código de status é a forma padronizada como o HTTP comunica o resultado de uma requisição: clientes (navegadores, outros programas, bibliotecas) dependem dele para saber se a requisição deu certo e, se não, por quê — um programa que consome a API pode reagir programaticamente a um 400 (corrigir e reenviar) de forma diferente de um 500 (erro do servidor, talvez tentar de novo mais tarde). Devolver 200 (sucesso) com uma mensagem de erro no corpo enganaria o cliente, que pensaria que tudo deu certo. Para uma requisição bem-sucedida, o status seria 200 (OK), que o cpp-httplib define por padrão quando você não o altera — por isso as rotas de sucesso não precisam defini-lo explicitamente.

Exercício 4

No exemplo que serve dados do banco, explique como a aliança RAII-exceção (artigo Quando o Contrato se Quebra — Exceções e a Aliança com o RAII) garante que a conexão SQLite seja fechada mesmo quando uma consulta falha e o catch é acionado. Trace o caminho da conexão desde a abertura até o fechamento no caso de erro.

Ver resposta

✓ Resposta: No caso de erro, o caminho da conexão é: a lambda entra no bloco try e cria o objeto BancoSQLite banco("usuarios.db") — o construtor abre a conexão SQLite (aquisição do recurso, RAII). Em seguida, cria a Consulta e começa a iterar. Se a consulta falhar e lançar uma exceção (por exemplo, a tabela não existe), o fluxo normal é interrompido e inicia-se o desenrolamento da pilha (artigo Quando o Contrato se Quebra — Exceções e a Aliança com o RAII): ao sair do escopo do try por causa da exceção, os objetos locais criados nele são destruídos na ordem inversa — primeiro a Consulta (cujo destrutor finaliza o statement), depois o banco (cujo destrutor chama sqlite3_close, fechando a conexão). Só então o controle chega ao catch, que monta a resposta 500. Ou seja: quando o catch executa, a conexão já foi fechada pelo destrutor de banco, acionado automaticamente durante o desenrolamento. É a aliança RAII-exceção em ação: a limpeza não depende de um código explícito no caminho de erro (não há close no catch), mas do destrutor que roda sozinho ao desenrolar a pilha — garantindo que nenhuma conexão vaze, mesmo quando algo dá errado no meio da consulta.

Exercício 5

A aula lista várias camadas que um servidor de produção tem e o nosso não. Escolha duas (por exemplo, concorrência e segurança) e explique concretamente que problema surgiria se você expusesse nosso servidor didático à internet sem elas, e como cada uma seria abordada.

Ver resposta

✓ Resposta: Duas camadas ausentes e seus problemas: Concorrência — o cpp-httplib atende requisições em múltiplas threads, então se várias requisições chegarem simultaneamente e nossas rotas compartilhassem um dado mutável (digamos, um contador de visitas global sem proteção), teríamos uma corrida de dados (Fase 7): o contador seria corrompido, com incrementos perdidos, exatamente como no exemplo do artigo Fazendo Duas Coisas ao Mesmo Tempo — std::thread. Pior, se compartilhássemos uma única conexão de banco entre threads sem sincronização, poderíamos corromper seu estado. Abordagem: proteger qualquer dado compartilhado com std::mutex/lock_guard (Fase 7), e gerenciar conexões de banco com cuidado — um pool de conexões sincronizado, ou (como fizemos) uma conexão por requisição. Segurança — nosso servidor não valida entrada nem usa prepared statements ao construir consultas; se uma rota montasse SQL concatenando um parâmetro da URL (por exemplo, /usuario/{nome} inserindo nome direto no SQL), um atacante poderia fazer injeção de SQL (artigo Extensão — Dados que Sobrevivem: Integrando SQLite com RAII), lendo ou apagando dados; , sem HTTPS, todo o tráfego (incluindo dados sensíveis) trafegaria em texto claro, interceptável. Abordagem: usar prepared statements com binding para toda entrada externa, validar rigorosamente os parâmetros, adicionar HTTPS (criptografia), e nunca confiar em dados vindos do cliente. Expor nosso servidor didático à internet sem essas camadas seria um convite a corrupção de dados (por corridas) e a invasões (por injeção e interceptação) — por isso ele é para aprendizado local, não para produção.

Comentários

Mais em Linguagem C++

Fazendo Duas Coisas ao Mesmo Tempo — std::thread
Fazendo Duas Coisas ao Mesmo Tempo — std::thread

Criar uma thread é a parte fácil. O C++ obriga a decidir entre join e detach…

Texto como Coleção — std::string Revisitada e o std::string_view
Texto como Coleção — std::string Revisitada e o std::string_view

Enxergar std::string como container muda o que se espera dele: tamanho…

Extensão — Páginas Dinâmicas: HTML Gerado com Templates
Extensão — Páginas Dinâmicas: HTML Gerado com Templates

Montar HTML concatenando string quebra por dois motivos ao mesmo tempo: fica…