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"