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-Lengthe 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
- Beej's Guide to Network Programming — Guia clássico e completo sobre sockets em C, essencial para entender a base do servidor HTTP
- RFC 1945 - HTTP/1.0 — Especificação oficial do HTTP/1.0, referência para implementação correta do protocolo
- Implementing an HTTP Server in C — Tutorial prático no livro "C Programming Absolute Beginner's Guide" que aborda servidores HTTP
- libmicrohttpd Documentation — Documentação da biblioteca GNU libmicrohttpd, excelente referência para implementações reais em C
- Simple HTTP Server in C - GitHub Gist — Exemplo funcional e comentado de servidor HTTP minimalista em C disponível no GitHub