Análise do Código-Fonte do Projeto de Código Aberto TinyHttpd em C

TinyHttpd é um servidor HTTP simples escrito em C, frequentemente usado para fins educacionais. Esta análise explora sua estrutura e funções principais, focando na implementação da comunicação via sockets e tratamento de requisições HTTP.

A execução do programa começa na função principal, que configura o socket do servidor e gerencia conexões de clientes. O código-fonte consiste em dois arquivos: httpd.c, que contém o servidor, e simpleclient.c, um cliente de teste. A análise concentra-se em httpd.c.

Função Principal

A função main inicializa o socket do servidor e entra em um loop infinito para aceitar conexões. Cada conexão é tratada em uma nova thread. O código abaixo mostra uma versão simplificada com variáveis renomeadas para maior clareza:

int main(void) {
    int socket_servidor = -1;
    unsigned short porta = 4000;
    int socket_cliente = -1;
    struct sockaddr_in endereco_cliente;
    socklen_t tamanho_endereco = sizeof(endereco_cliente);
    pthread_t thread_id;

    socket_servidor = iniciar_socket(&porta);
    printf("Servidor HTTPd rodando na porta %d\n", porta);

    while (1) {
        socket_cliente = accept(socket_servidor,
                                (struct sockaddr *)&endereco_cliente,
                                &tamanho_endereco);
        if (socket_cliente == -1) {
            perror("Erro ao aceitar conexão");
            continue;
        }
        if (pthread_create(&thread_id, NULL, processar_requisicao, (void *)&socket_cliente) != 0) {
            perror("Erro ao criar thread");
        }
    }

    close(socket_servidor);
    return 0;
}

Função de Inicialização do Socket

A função iniciar_socket configura o socket, vincula-o a uma porta e coloca-o em modo de escuta. Ela utiliza opções para reutilizar endereços e suporta portas dinâmicas. O código adaptado é apresentdao abaixo:

int iniciar_socket(unsigned short *porta) {
    int socket_fd = 0;
    int opcao = 1;
    struct sockaddr_in endereco;

    socket_fd = socket(PF_INET, SOCK_STREAM, 0);
    if (socket_fd == -1) {
        perror("Erro ao criar socket");
        exit(1);
    }

    memset(&endereco, 0, sizeof(endereco));
    endereco.sin_family = AF_INET;
    endereco.sin_port = htons(*porta);
    endereco.sin_addr.s_addr = htonl(INADDR_ANY);

    if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &opcao, sizeof(opcao)) < 0) {
        perror("Erro ao configurar opções do socket");
        close(socket_fd);
        exit(1);
    }

    if (bind(socket_fd, (struct sockaddr *)&endereco, sizeof(endereco)) < 0) {
        perror("Erro ao vincular socket");
        close(socket_fd);
        exit(1);
    }

    if (*porta == 0) {
        socklen_t len = sizeof(endereco);
        if (getsockname(socket_fd, (struct sockaddr *)&endereco, &len) == -1) {
            perror("Erro ao obter porta");
            close(socket_fd);
            exit(1);
        }
        *porta = ntohs(endereco.sin_port);
    }

    if (listen(socket_fd, 5) < 0) {
        perror("Erro ao escutar conexões");
        close(socket_fd);
        exit(1);
    }

    return socket_fd;
}

Função de Processamento de Requisições

A função processar_requisicao lida com requisições HTTP recebidas. Ela analisa o método, URL e cabeçalhos, decide entre servir arquivos estáticos ou executar scripts CGI. A estrutura lógica é mantida, mas com variáveis e fluxo reorganizados:

void *processar_requisicao(void *arg) {
    int cliente_fd = *(int *)arg;
    char buffer[1024];
    size_t bytes_lidos;
    char metodo[255];
    char caminho_url[255];
    char caminho_arquivo[512];
    size_t i, j;
    struct stat info_arquivo;
    int eh_cgi = 0;
    char *parametros = NULL;

    bytes_lidos = ler_linha(cliente_fd, buffer, sizeof(buffer));

    i = 0;
    j = 0;
    while (!isspace((unsigned char)buffer[i]) && i < sizeof(metodo) - 1) {
        metodo[i] = buffer[i];
        i++;
    }
    j = i;
    metodo[i] = '\0';

    if (strcasecmp(metodo, "GET") != 0 && strcasecmp(metodo, "POST") != 0) {
        enviar_resposta_nao_implementado(cliente_fd);
        close(cliente_fd);
        return NULL;
    }

    if (strcasecmp(metodo, "POST") == 0) {
        eh_cgi = 1;
    }

    i = 0;
    while (isspace((unsigned char)buffer[j]) && j < bytes_lidos) {
        j++;
    }
    while (!isspace((unsigned char)buffer[j]) && i < sizeof(caminho_url) - 1 && j < bytes_lidos) {
        caminho_url[i] = buffer[j];
        i++;
        j++;
    }
    caminho_url[i] = '\0';

    if (strcasecmp(metodo, "GET") == 0) {
        parametros = caminho_url;
        while (*parametros != '?' && *parametros != '\0') {
            parametros++;
        }
        if (*parametros == '?') {
            eh_cgi = 1;
            *parametros = '\0';
            parametros++;
        }
    }

    snprintf(caminho_arquivo, sizeof(caminho_arquivo), "htdocs%s", caminho_url);
    if (caminho_arquivo[strlen(caminho_arquivo) - 1] == '/') {
        strcat(caminho_arquivo, "index.html");
    }

    if (stat(caminho_arquivo, &info_arquivo) == -1) {
        while (bytes_lidos > 0 && strcmp("\n", buffer) != 0) {
            bytes_lidos = ler_linha(cliente_fd, buffer, sizeof(buffer));
        }
        enviar_resposta_nao_encontrado(cliente_fd);
    } else {
        if ((info_arquivo.st_mode & S_IFMT) == S_IFDIR) {
            strcat(caminho_arquivo, "/index.html");
        }
        if ((info_arquivo.st_mode & S_IXUSR) || (info_arquivo.st_mode & S_IXGRP) || (info_arquivo.st_mode & S_IXOTH)) {
            eh_cgi = 1;
        }
        if (!eh_cgi) {
            servir_arquivo_estatico(cliente_fd, caminho_arquivo);
        } else {
            executar_cgi(cliente_fd, caminho_arquivo, metodo, parametros);
        }
    }

    close(cliente_fd);
    return NULL;
}

Fundamentos de HTTP/1.1

Para entender o funcionamento do servidor, é essencial conhecer o protocolo HTTP/1.1. As requisições incluem um método (GET ou POST), URL e cabeçalhos. As respostas retornam um código de status e conteúdo. Por exemplo, uma requisição típica pode ser:

GET /index.html HTTP/1.1
Host: exemplo.com
Connection: keep-alive
Accept: text/html

E uma resposta correspondente:

HTTP/1.1 200 OK
Content-Type: text/html
Connection: keep-alive

<html><body>Conteúdo aqui</body></html>

Conexões via Sockets

O servidor utiliza sockets TCP para comunicação. A inicialização envolve criar um socket, vinculá-lo a uma porta e colocá-lo em modo de escuta. Cada conexão aceita gera uma thread para processamento, seguindo o modelo de múltiplas threads. Isso permite tratar múltiplos clientes simultaneamente, embora de forma simples.

Definições e Constantes

O código usa macros para simplificar operações, como verificar espaços em branco ou definir strings do servidor. Exemplos incluem:

#define EH_ESPACO(x) isspace((int)(x))
#define STRING_SERVIDOR "Servidor: httpd-basico/0.1.0\r\n"

Tags: C TinyHttpd sockets HTTP Multithreading

Publicado em 6-25 00:24