HTTP server minimalista em C

1. Fundamentos do Protocolo HTTP/1.0 e/ou HTTP/1.1

O protocolo HTTP opera sobre TCP e segue um modelo requisição-resposta. Uma requisição HTTP mínima contém:

  • Linha de requisição: método, path e versão (ex: GET /index.html HTTP/1.1)
  • Headers: pares chave-valor separados por CRLF
  • Body: presente apenas em métodos como POST

A resposta HTTP segue estrutura similar:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 13

Hello, World!

HTTP/1.0 fecha a conexão após cada resposta. HTTP/1.1 mantém a conexão aberta (keep-alive) para reutilização, reduzindo overhead. Nosso servidor minimalista implementará HTTP/1.0 para simplificar.

2. Criação do Socket TCP e Loop Principal

O servidor precisa criar um socket TCP, associá-lo a uma porta e escutar por conexões. O código abaixo estabelece a base:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 4096

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address;
    int opt = 1;
    socklen_t addrlen = sizeof(address);

    // Criar socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // Configurar SO_REUSEADDR para evitar "Address already in use"
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 10) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("Servidor rodando na porta %d\n", PORT);

    // Loop principal
    while (1) {
        client_fd = accept(server_fd, (struct sockaddr *)&address, &addrlen);
        if (client_fd < 0) {
            perror("accept");
            continue;
        }

        // Processar requisição (será implementado nas próximas seções)
        handle_client(client_fd);
        close(client_fd);
    }

    close(server_fd);
    return 0;
}

3. Parsing Manual da Requisição HTTP

A função handle_client lê o buffer TCP e extrai os componentes da requisição. Implementamos um parser manual sem dependências externas:

typedef struct {
    char method[16];
    char path[256];
    char version[16];
    int header_count;
    char headers[32][2][256]; // [index][0]=key, [index][1]=value
} http_request;

void parse_request(const char *buffer, http_request *req) {
    // Extrair linha de requisição
    sscanf(buffer, "%s %s %s", req->method, req->path, req->version);

    // Extrair headers (linha por linha)
    const char *ptr = buffer;
    ptr = strstr(ptr, "\r\n") + 2; // Pular linha de requisição
    req->header_count = 0;

    while (ptr && *ptr != '\r' && *(ptr+1) != '\n') {
        char key[256], value[256];
        if (sscanf(ptr, "%[^:]: %[^\r\n]", key, value) == 2) {
            strcpy(req->headers[req->header_count][0], key);
            strcpy(req->headers[req->header_count][1], value);
            req->header_count++;
        }
        ptr = strstr(ptr, "\r\n");
        if (ptr) ptr += 2;
    }
}

void handle_client(int client_fd) {
    char buffer[BUFFER_SIZE] = {0};
    http_request req = {0};

    read(client_fd, buffer, BUFFER_SIZE - 1);
    parse_request(buffer, &req);

    printf("Requisição: %s %s %s\n", req.method, req.path, req.version);

    // Roteamento e resposta serão implementados a seguir
    send_response(client_fd, 200, "text/plain", "Hello, C!\r\n");
}

4. Roteamento de Requisições (Router Caseiro)

Criamos uma estrutura de mapeamento de paths para funções handler:

typedef void (*route_handler)(int client_fd, http_request *req);

typedef struct {
    const char *path;
    route_handler handler;
} route_entry;

void handle_home(int client_fd, http_request *req) {
    send_response(client_fd, 200, "text/html", 
                  "<h1>Bem-vindo ao servidor C</h1>");
}

void handle_api(int client_fd, http_request *req) {
    send_response(client_fd, 200, "application/json", 
                  "{\"status\":\"ok\",\"message\":\"API funcionando\"}");
}

void handle_404(int client_fd, http_request *req) {
    send_response(client_fd, 404, "text/html", 
                  "<h1>404 - Página não encontrada</h1>");
}

route_entry routes[] = {
    {"/", handle_home},
    {"/api", handle_api},
    {"/api/*", handle_api}, // Rota curinga
    {NULL, handle_404}      // Rota padrão
};

void dispatch_request(int client_fd, http_request *req) {
    for (int i = 0; routes[i].path != NULL; i++) {
        if (strcmp(routes[i].path, req->path) == 0) {
            routes[i].handler(client_fd, req);
            return;
        }
        // Verificar rota curinga
        if (strstr(routes[i].path, "/*")) {
            char base[256];
            strncpy(base, routes[i].path, strlen(routes[i].path) - 2);
            base[strlen(routes[i].path) - 2] = '\0';
            if (strncmp(req->path, base, strlen(base)) == 0) {
                routes[i].handler(client_fd, req);
                return;
            }
        }
    }
    handle_404(client_fd, req);
}

5. Construção da Resposta HTTP

Função utilitária para enviar respostas HTTP completas:

void send_response(int client_fd, int status_code, 
                   const char *content_type, const char *body) {
    char response[BUFFER_SIZE];
    int body_len = strlen(body);

    const char *status_text = (status_code == 200) ? "OK" : 
                              (status_code == 404) ? "Not Found" : "Internal Server Error";

    int len = snprintf(response, BUFFER_SIZE,
        "HTTP/1.0 %d %s\r\n"
        "Content-Type: %s\r\n"
        "Content-Length: %d\r\n"
        "Connection: close\r\n"
        "\r\n"
        "%s",
        status_code, status_text,
        content_type,
        body_len,
        body);

    send(client_fd, response, len, 0);
}

void send_file(int client_fd, const char *filepath) {
    FILE *file = fopen(filepath, "rb");
    if (!file) {
        send_response(client_fd, 404, "text/plain", "Arquivo não encontrado");
        return;
    }

    fseek(file, 0, SEEK_END);
    long file_size = ftell(file);
    rewind(file);

    char *file_content = malloc(file_size + 1);
    fread(file_content, 1, file_size, file);
    file_content[file_size] = '\0';
    fclose(file);

    // Determinar content-type pela extensão
    const char *ext = strrchr(filepath, '.');
    const char *content_type = "text/plain";
    if (ext) {
        if (strcmp(ext, ".html") == 0) content_type = "text/html";
        else if (strcmp(ext, ".css") == 0) content_type = "text/css";
        else if (strcmp(ext, ".js") == 0) content_type = "application/javascript";
        else if (strcmp(ext, ".json") == 0) content_type = "application/json";
    }

    send_response(client_fd, 200, content_type, file_content);
    free(file_content);
}

6. Gerenciamento de Conexões e Concorrência Básica

Para atender múltiplos clientes simultaneamente, utilizamos fork():

void handle_client_with_fork(int client_fd) {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        close(client_fd);
        return;
    }

    if (pid == 0) { // Processo filho
        close(server_fd); // Filho não precisa do socket do servidor

        char buffer[BUFFER_SIZE] = {0};
        http_request req = {0};

        read(client_fd, buffer, BUFFER_SIZE - 1);
        parse_request(buffer, &req);
        dispatch_request(client_fd, &req);

        close(client_fd);
        exit(0);
    } else { // Processo pai
        close(client_fd); // Pai não precisa do socket do cliente

        // Evitar processos zumbis (não bloqueante)
        signal(SIGCHLD, SIG_IGN);
        // Ou usar: waitpid(-1, NULL, WNOHANG);
    }
}

No loop principal, substituímos handle_client(client_fd) por handle_client_with_fork(client_fd).

7. Tratamento de Erros e Logging Mínimo

Implementamos verificação de erros e logging no formato Apache-like:

void log_request(const char *ip, const char *method, 
                 const char *path, int status) {
    time_t now = time(NULL);
    struct tm *tm_info = localtime(&now);
    char timestamp[64];
    strftime(timestamp, sizeof(timestamp), "%d/%b/%Y:%H:%M:%S %z", tm_info);

    printf("%s - - [%s] \"%s %s HTTP/1.0\" %d -\n", 
           ip, timestamp, method, path, status);
}

// Na função handle_client_with_fork, após dispatch_request:
// log_request(ip_str, req.method, req.path, status_code);

Para configurar timeout de leitura:

struct timeval tv;
tv.tv_sec = 5;  // 5 segundos
tv.tv_usec = 0;
setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

8. Compilação, Teste e Extensões Possíveis

Compile com:

gcc -Wall -Wextra -o server server.c

Execute e teste:

./server &
curl http://localhost:8080/
curl http://localhost:8080/api
curl http://localhost:8080/nao-existe

Teste com navegador acessando http://localhost:8080/.

Sugestões de melhoria

  • Suporte a POST: ler Content-Length e capturar body da requisição
  • Conexão keep-alive: loop de leitura enquanto Connection: keep-alive
  • Buffer pool: alocação prévia de buffers para performance
  • Thread pool: substituir fork por threads com pthreads
  • Suporte a HTTPS: integrar OpenSSL

Referências