Projeto final: implementando um servidor HTTP minimalista em C
1. Fundamentos do protocolo HTTP e arquitetura do servidor
1.1. Visão geral do HTTP/1.0 e HTTP/1.1
O protocolo HTTP (Hypertext Transfer Protocol) opera sobre TCP na porta 80. No HTTP/1.0, cada requisição abre uma nova conexão TCP, enquanto no HTTP/1.1 introduziu-se o conceito de conexões persistentes (keep-alive). Uma requisição HTTP típica contém método (GET, POST), URI, versão do protocolo e cabeçalhos no formato Chave: Valor.
1.2. Estrutura básica de um servidor TCP em C
O ciclo fundamental de um servidor TCP envolve quatro chamadas de sistema:
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
bind(server_fd, (struct sockaddr*)&address, sizeof(address));
listen(server_fd, 10);
while (1) {
int client_fd = accept(server_fd, NULL, NULL);
// processar requisição
close(client_fd);
}
1.3. Modelo de concorrência simplificado
Para este projeto, adotaremos um modelo single-thread com loop de eventos, suficiente para servir arquivos estáticos com baixa concorrência. O servidor aceita uma conexão, processa a requisição, envia a resposta e fecha o socket antes de aceitar a próxima conexão.
2. Parsing da requisição HTTP e tratamento de buffers
2.1. Leitura do socket com buffers de tamanho fixo
Utilizamos um buffer de 4096 bytes para ler dados parciais do socket. A função recv() retorna a quantidade de bytes efetivamente lidos:
#define BUFFER_SIZE 4096
char buffer[BUFFER_SIZE];
int bytes_read = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
if (bytes_read < 0) {
perror("recv failed");
return;
}
buffer[bytes_read] = '\0';
2.2. Análise manual da linha de requisição
Extraímos método, URI e versão do HTTP usando sscanf:
char method[16], uri[256], version[16];
if (sscanf(buffer, "%15s %255s %15s", method, uri, version) != 3) {
send_error(client_fd, 400, "Bad Request");
return;
}
2.3. Validação mínima de campos obrigatórios
Verificamos se o método é suportado (GET) e se a versão é HTTP/1.0 ou HTTP/1.1:
if (strcmp(method, "GET") != 0) {
send_error(client_fd, 501, "Not Implemented");
return;
}
if (strncmp(version, "HTTP/", 5) != 0) {
send_error(client_fd, 400, "Bad Request");
return;
}
3. Implementação do roteador e resposta estática
3.1. Mapeamento de URI para caminhos no sistema de arquivos
Sanitizamos o caminho para evitar ataques de path traversal:
char filepath[512];
snprintf(filepath, sizeof(filepath), "./www%s", uri);
// Prevenir path traversal
if (strstr(filepath, "..") != NULL) {
send_error(client_fd, 400, "Bad Request");
return;
}
3.2. Leitura de arquivos estáticos e construção do corpo da resposta
Abrimos o arquivo solicitado e lemos seu conteúdo:
FILE *file = fopen(filepath, "rb");
if (!file) {
send_error(client_fd, 404, "Not Found");
return;
}
fseek(file, 0, SEEK_END);
long file_size = ftell(file);
rewind(file);
char *body = malloc(file_size + 1);
fread(body, 1, file_size, file);
body[file_size] = '\0';
fclose(file);
3.3. Montagem manual do cabeçalho HTTP
Construímos a resposta completa com cabeçalhos essenciais:
char response[4096];
int header_len = snprintf(response, sizeof(response),
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n"
"\r\n",
file_size);
send(client_fd, response, header_len, 0);
send(client_fd, body, file_size, 0);
free(body);
4. Gerenciamento de conexões e tratamento de erros HTTP
4.1. Controle de keep-alive vs. close
Para simplicidade, nosso servidor sempre fecha a conexão após responder. O cabeçalho Connection: close informa ao cliente que não haverá reutilização do socket.
4.2. Respostas de erro padronizadas
Implementamos funções auxiliares para erros comuns:
void send_error(int client_fd, int status_code, const char *reason) {
char body[256];
int body_len = snprintf(body, sizeof(body),
"<html><body><h1>%d %s</h1></body></html>", status_code, reason);
char response[512];
int header_len = snprintf(response, sizeof(response),
"HTTP/1.1 %d %s\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n"
"\r\n%s",
status_code, reason, body_len, body);
send(client_fd, response, header_len, 0);
}
4.3. Limpeza de recursos
Garantimos que todo recurso alocado seja liberado antes de fechar o socket:
void handle_client(int client_fd) {
char buffer[BUFFER_SIZE];
int bytes_read = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
if (bytes_read <= 0) {
close(client_fd);
return;
}
// processamento da requisição...
close(client_fd);
}
5. Integração com limites de recursos (ulimit e cgroups)
5.1. Ajuste de ulimit para número máximo de arquivos abertos
Antes de executar o servidor, configuramos limites do sistema:
#include <sys/resource.h>
struct rlimit limit;
limit.rlim_cur = 1024;
limit.rlim_max = 4096;
setrlimit(RLIMIT_NOFILE, &limit);
5.2. Uso de cgroups para limitar memória e CPU
Em sistemas Linux, podemos verificar limites impostos por cgroups lendo arquivos em /sys/fs/cgroup/:
FILE *mem_limit = fopen("/sys/fs/cgroup/memory/memory.limit_in_bytes", "r");
if (mem_limit) {
unsigned long limit;
fscanf(mem_limit, "%lu", &limit);
printf("Memory limit: %lu bytes\n", limit);
fclose(mem_limit);
}
5.3. Verificação em tempo de execução
Consultamos limites ativos para ajustar o comportamento do servidor:
long max_fds = sysconf(_SC_OPEN_MAX);
printf("Maximum open file descriptors: %ld\n", max_fds);
6. Portabilidade e detecção de plataforma em compile-time
6.1. Macros condicionais para diferenciar plataformas
Usamos #ifdef para adaptar o código a diferentes sistemas operacionais:
#ifdef __linux__
#include <sys/sendfile.h>
#elif defined(__APPLE__)
#include <sys/uio.h>
#endif
6.2. Adaptação de funções de socket específicas
No Linux, podemos usar sendfile para enviar arquivos eficientemente; em outros sistemas, usamos write com buffer:
#ifdef __linux__
off_t offset = 0;
sendfile(client_fd, file_fd, &offset, file_size);
#else
write(client_fd, body, file_size);
#endif
6.3. Inclusão condicional de cabeçalhos
Garantimos que apenas cabeçalhos disponíveis na plataforma sejam incluídos:
#ifdef __linux__
#include <features.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
7. Compilação, testes e considerações finais
7.1. Makefile simples
CC = gcc
CFLAGS = -Wall -Wextra -O2
LDFLAGS =
all: server
server: server.c
$(CC) $(CFLAGS) -o server server.c $(LDFLAGS)
debug: CFLAGS += -g -DDEBUG
debug: server
clean:
rm -f server
.PHONY: all debug clean
7.2. Testes manuais com curl
# Teste básico
curl -v http://localhost:8080/index.html
# Teste de erro 404
curl -v http://localhost:8080/nao-existe.html
# Teste com cabeçalhos
curl -H "Connection: close" http://localhost:8080/
7.3. Limitações e próximos passos
Este servidor minimalista não suporta:
- Requisições POST e outros métodos HTTP
- Conexões persistentes (keep-alive)
- HTTPS/TLS
- Multiplexação com select/poll/epoll
Para evoluir o projeto, implemente suporte a POST com parsing de corpo, adicione select() para multiplexação e integre OpenSSL para TLS.
Referências
- Beej's Guide to Network Programming — Guia clássico e completo sobre programação de sockets em C, com exemplos práticos de servidores TCP.
- HTTP/1.1: RFC 7230 - Message Syntax and Routing — Especificação oficial do protocolo HTTP/1.1, definindo formato de mensagens e cabeçalhos.
- GNU C Library Manual: Sockets — Documentação oficial da glibc sobre operações de socket, bind, listen e accept.
- Linux man pages: sendfile(2) — Página de manual detalhada sobre a chamada de sistema sendfile para transferência eficiente de arquivos.
- curl manual - Testing HTTP Servers — Documentação oficial do curl com exemplos de requisições HTTP para testes de servidores.